From 98dae2bf0a30a4fac4ab8af948ee0d0387d32d5c Mon Sep 17 00:00:00 2001 From: kasemir Date: Fri, 10 Jul 2026 13:46:01 -0400 Subject: [PATCH 1/9] PVA: harden handling of protocol sizes Protocol handling was prone to memory exhaustion because of this pattern: ``` int count = buffer.getInteger(); Whatever[] data = new Whatever[count]; .. then read data from buffer ``` A misconfigured package can contain a huge `count`. This update checks the `count` against the remaining package size, refusing packets that are obviously too small or rather the `count` is obviously too large for the packet size. Along the same lines, one could introduce an upper packet size limit similar to `EPICS_CA_MAX_ARRAY_BYTES` in channel access, but at this time, confirmed in a 2026-07-10 EPICS core telecon, PVA does not impose any such size limits, using all available memory. --- .../epics/pva/client/ClientUDPHandler.java | 16 +++--- .../java/org/epics/pva/common/PVAHeader.java | 11 ++++ .../org/epics/pva/common/SearchRequest.java | 54 ++++++++++++------- .../org/epics/pva/common/SearchResponse.java | 3 ++ .../java/org/epics/pva/data/PVAAnyArray.java | 3 ++ .../java/org/epics/pva/data/PVABitSet.java | 5 +- .../java/org/epics/pva/data/PVABoolArray.java | 2 + .../java/org/epics/pva/data/PVAByteArray.java | 2 + .../org/epics/pva/data/PVADoubleArray.java | 2 + .../org/epics/pva/data/PVAFloatArray.java | 2 + .../java/org/epics/pva/data/PVAIntArray.java | 2 + .../java/org/epics/pva/data/PVALongArray.java | 2 + .../org/epics/pva/data/PVAShortArray.java | 2 + .../main/java/org/epics/pva/data/PVASize.java | 3 ++ .../java/org/epics/pva/data/PVAStatus.java | 3 +- .../java/org/epics/pva/data/PVAString.java | 6 ++- .../org/epics/pva/data/PVAStringArray.java | 3 ++ .../java/org/epics/pva/data/PVAStructure.java | 4 ++ .../org/epics/pva/data/PVAStructureArray.java | 5 ++ .../java/org/epics/pva/data/BitSetTest.java | 2 +- 20 files changed, 101 insertions(+), 31 deletions(-) diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java b/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java index 8205f4e61d..a48653b2fb 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java @@ -61,7 +61,7 @@ public interface SearchResponseHandler * @param server Server that replied to a search request * @param version Server version * @param guid Globally unique ID of the server - * @param tcp Does server require TLS? + * @param tls Does server require TLS? */ void handleSearchResponse(int channel_id, InetSocketAddress server, int version, Guid guid, boolean tls); } @@ -250,15 +250,15 @@ private boolean handleBeacon(final InetSocketAddress from, final byte version, else server = new InetSocketAddress(addr, port); - final String protocol = PVAString.decodeString(buffer); - if (! "tcp".equals(protocol)) - { - logger.log(Level.WARNING, "PVA Server " + from + " sent beacon for protocol '" + protocol + "'"); - return false; - } - try { + final String protocol = PVAString.decodeString(buffer); + if (! "tcp".equals(protocol)) + { + logger.log(Level.WARNING, "PVA Server " + from + " sent beacon for protocol '" + protocol + "'"); + return false; + } + // Decode optional server status (likely null) final PVATypeRegistry types = new PVATypeRegistry(); final PVAData server_status = types.decodeType("", buffer); diff --git a/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java b/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java index 7a868ff1be..d8f78d8686 100644 --- a/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java +++ b/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java @@ -213,6 +213,17 @@ public static int checkMessageAndGetSize(final ByteBuffer buffer, final boolean // Application messages are followed by this number of data bytes final int payload = buffer.getInt(PVAHeader.HEADER_OFFSET_PAYLOAD_SIZE); + // Java implementation for now does not handle large 'unsigned' sizes, + // limited to the positive range of a signed int. + // Could use `Integer.toUnsignedLong(payload)`, but JDK API + // like buffer buffer.remaining() or buffer.get(10) is using int, + // so us updating to long would be of limited use + if (payload < 0) + throw new Exception("Payload size " + payload + + " exceeds max signed integer " + Integer.toHexString(Integer.MAX_VALUE)); + // Could check against a PVA variant of EPICS_CA_MAX_ARRAY_BYTES, + // but PVA design specifically aims to use all available memory + // without self-enforced limitations (confirmed in 2026-07-10 EPICS code telecon) // Total message size: Header followed by data return PVAHeader.HEADER_SIZE + payload; diff --git a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java index 970a449a3b..8edcd3db4d 100644 --- a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java +++ b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java @@ -173,15 +173,23 @@ public static SearchRequest decode(final OriginTag origin, final InetSocketAddre boolean tcp = search.tls = false; int count = Byte.toUnsignedInt(buffer.get()); String unknown_protocol = ""; - for (int i=0; i(count); - for (int i=0; i "PVA Client " + from + " sent search #" + search.seq + " for " + name + " [cid " + cid + "]" - + ", reply addr " + orig_response_addr - + (orig_response_addr.equals(search.client) ? "" : ", using " + search.client) - + (search.tls ? " (TLS)" : "") - + (search.unicast ? " (unicast)" : "") - + (search.reply_required ? " (reply required)" : "") - + (search.reply_to_src_port ? (origin == null ? " (reply to source port)" : " (reply to source port ignored because of origin tag)") : "")); - search.channels.add(new Channel(cid, name)); + for (int i=0; i "PVA Client " + from + " sent search #" + search.seq + " for " + name + " [cid " + cid + "]" + + ", reply addr " + orig_response_addr + + (orig_response_addr.equals(search.client) ? "" : ", using " + search.client) + + (search.tls ? " (TLS)" : "") + + (search.unicast ? " (unicast)" : "") + + (search.reply_required ? " (reply required)" : "") + + (search.reply_to_src_port ? (origin == null ? " (reply to source port)" : " (reply to source port ignored because of origin tag)") : "")); + search.channels.add(new Channel(cid, name)); + } + } + catch (Exception ex) + { + logger.log(Level.WARNING, "PVA Client " + from + " sent damaged search #" + search.seq, ex); + return null; } } diff --git a/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java b/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java index 1367d34626..967ab76dcf 100644 --- a/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java +++ b/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java @@ -97,6 +97,9 @@ public static SearchResponse decode(final int payload, final ByteBuffer buffer) result.found = PVABool.decodeBoolean(buffer); final int count = Short.toUnsignedInt(buffer.getShort()); + if (count*Integer.BYTES > buffer.remaining()) + throw new Exception("PVA Server sent search reply #" + result.seq + " for " + count + " CIDs " + + " with only " + buffer.remaining() + " bytes in buffer"); result.cid = new int[count]; for (int i=0; i buffer.remaining()) + throw new Exception("Array size " + count + " with only " + buffer.remaining() + " bytes in buffer"); // Try to re-use elements PVAny[] new_elements = elements; if (new_elements == null || new_elements.length != count) diff --git a/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java b/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java index 28e17094ef..36fd5d28b4 100644 --- a/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java +++ b/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java @@ -27,10 +27,13 @@ public static void encodeBitSet(final BitSet bits, final ByteBuffer buffer) /** @param buffer Source buffer * @return Decoded bits + * @throws Exception on error */ - public static BitSet decodeBitSet(final ByteBuffer buffer) + public static BitSet decodeBitSet(final ByteBuffer buffer) throws Exception { final int size = PVASize.decodeSize(buffer); + if (size < 0 || size > buffer.remaining()) + throw new Exception("Bitset size " + size + " with only " + buffer.remaining() + " bytes in buffer"); final byte[] bytes = new byte[size]; buffer.get(bytes); return BitSet.valueOf(bytes); diff --git a/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java b/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java index 15b1df5fc0..e1db85412b 100644 --- a/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java +++ b/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java @@ -95,6 +95,8 @@ public void encodeType(ByteBuffer buffer, BitSet described) throws Exception public void decode(final PVATypeRegistry types, final ByteBuffer buffer) throws Exception { final int size = PVASize.decodeSize(buffer); + if (size < 0 || size > buffer.remaining()) + throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer"); final boolean[] new_value = new boolean[size]; for (int i=0; i buffer.remaining()) + throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer"); final byte[] new_value = new byte[size]; buffer.get(new_value); value = new_value; diff --git a/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java b/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java index 9e6dce6a5b..05516eae75 100644 --- a/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java +++ b/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java @@ -98,6 +98,8 @@ public void encodeType(final ByteBuffer buffer, final BitSet described) throws E public void decode(final PVATypeRegistry types, final ByteBuffer buffer) throws Exception { final int size = PVASize.decodeSize(buffer); + if (size < 0 || size*Double.BYTES > buffer.remaining()) + throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer"); final double[] new_value = new double[size]; for (int i=0; i buffer.remaining()) + throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer"); final float[] new_value = new float[size]; for (int i=0; i buffer.remaining()) + throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer"); final int[] new_value = new int[size]; for (int i=0; i buffer.remaining()) + throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer"); final long[] new_value = new long[size]; for (int i=0; i buffer.remaining()) + throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer"); // Try to re-use existing array final short[] new_value = new short[size]; // Considered using diff --git a/core/pva/src/main/java/org/epics/pva/data/PVASize.java b/core/pva/src/main/java/org/epics/pva/data/PVASize.java index 9ad204796a..9e96c72154 100644 --- a/core/pva/src/main/java/org/epics/pva/data/PVASize.java +++ b/core/pva/src/main/java/org/epics/pva/data/PVASize.java @@ -54,6 +54,9 @@ else if (size < 254) */ public static final int decodeSize(final ByteBuffer buffer) { + // XXXX Update to long, using Integer.toUnsignedLong(..)? + // JDK api like buffer.remaining() is limited to int, + // so this would have limited effect... byte b = buffer.get(); if (b == -1) return -1; diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAStatus.java b/core/pva/src/main/java/org/epics/pva/data/PVAStatus.java index 90f1e3bf35..1c17ed43d5 100644 --- a/core/pva/src/main/java/org/epics/pva/data/PVAStatus.java +++ b/core/pva/src/main/java/org/epics/pva/data/PVAStatus.java @@ -72,8 +72,9 @@ public void encode(final ByteBuffer buffer) /** @param buffer Source buffer * @return Decoded status + * @throws Exception on error */ - public static PVAStatus decode(final ByteBuffer buffer) + public static PVAStatus decode(final ByteBuffer buffer) throws Exception { final byte b = buffer.get(); if (b == -1) diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAString.java b/core/pva/src/main/java/org/epics/pva/data/PVAString.java index a8651f5b85..f4706c9a52 100644 --- a/core/pva/src/main/java/org/epics/pva/data/PVAString.java +++ b/core/pva/src/main/java/org/epics/pva/data/PVAString.java @@ -49,12 +49,16 @@ public static void encodeString(final String string, final ByteBuffer buffer) /** @param buffer Buffer from which to decode string * @return Decoded string + * @throws Exception on error */ - public static String decodeString(final ByteBuffer buffer) + public static String decodeString(final ByteBuffer buffer) throws Exception { final int size = PVASize.decodeSize(buffer); if (size >= 0) { + if (size > buffer.remaining()) + throw new Exception("PVAString size " + size + + " exceeds remaining buffer size " + buffer.remaining()); byte[] bytes = new byte[size]; buffer.get(bytes); return new String(bytes); diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java b/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java index d01d2b2e9d..3aca078273 100644 --- a/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java +++ b/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java @@ -89,6 +89,9 @@ public void encodeType(ByteBuffer buffer, BitSet described) throws Exception public void decode(final PVATypeRegistry types, final ByteBuffer buffer) throws Exception { final int size = PVASize.decodeSize(buffer); + // Each array element needs to contain at least a byte for the string size + if (size < 0 || size > buffer.remaining()) + throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer"); String[] new_value = value; if (new_value == null || new_value.length != size) new_value = new String[size]; diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java b/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java index 88bb0ac667..177a6a9395 100644 --- a/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java +++ b/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java @@ -55,6 +55,10 @@ static PVAStructure decodeType(final PVATypeRegistry types, final String name, f // number of elements final int size = PVASize.decodeSize(buffer); + // Each element needs name (at least byte for length) and value (at least byte) + if (size < 0 || size*2 > buffer.remaining()) + throw new Exception("Structure with " + size + " elements but only " + buffer.remaining() + " bytes in buffer"); + // (name, FieldDesc)[] final List values = new ArrayList<>(size); for (int i=0; i buffer.remaining()) + throw new Exception("Structure element count " + count + " with only " + buffer.remaining() + " bytes in buffer"); + // Try to re-use elements PVAStructure[] new_elements = elements; if (new_elements == null || new_elements.length != count) diff --git a/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java b/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java index 64892be4b0..af0c7bfa8e 100644 --- a/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java +++ b/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java @@ -18,7 +18,7 @@ public class BitSetTest { @Test - public void testBitSet() + public void testBitSet() throws Exception { final ByteBuffer buffer = ByteBuffer.allocate(100); BitSet bits = new BitSet(); From d13aae4e76cbc731f9d9d030a56c7061c5f017d4 Mon Sep 17 00:00:00 2001 From: kasemir Date: Fri, 10 Jul 2026 14:57:03 -0400 Subject: [PATCH 2/9] Log using lambda --- .../pva/src/main/java/org/epics/pva/common/SearchRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java index 8edcd3db4d..1557728577 100644 --- a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java +++ b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java @@ -188,7 +188,7 @@ else if ("tcp".equals(protocol)) } catch (Exception ex) { - logger.log(Level.WARNING, "PVA Client " + from + " sent search #" + search.seq + " with invalid protocol", ex); + logger.log(Level.WARNING, ex, () -> "PVA Client " + from + " sent search #" + search.seq + " with invalid protocol"); return null; } @@ -226,7 +226,7 @@ else if ("tcp".equals(protocol)) } catch (Exception ex) { - logger.log(Level.WARNING, "PVA Client " + from + " sent damaged search #" + search.seq, ex); + logger.log(Level.WARNING, ex, () -> "PVA Client " + from + " sent damaged search #" + search.seq); return null; } } From 7d87fc4a7d53b48d9796e565d8e9c6a143aeacde Mon Sep 17 00:00:00 2001 From: shroffk Date: Fri, 21 Aug 2026 14:18:40 -0400 Subject: [PATCH 3/9] replace frequently used strings with constants --- .../java/org/epics/pva/common/SearchRequest.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java index 1557728577..67401bce71 100644 --- a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java +++ b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java @@ -26,6 +26,9 @@ @SuppressWarnings("nls") public class SearchRequest { + private static final String LOG_PVA_CLIENT = "PVA Client "; + private static final String LOG_SENT_SEARCH = " sent search #"; + /** Channel with CID to be searched */ public static class Channel { @@ -151,7 +154,7 @@ public static SearchRequest decode(final OriginTag origin, final InetSocketAddre } catch (Exception ex) { - logger.log(Level.WARNING, "PVA Client " + from + " sent search #" + search.seq + " with invalid address"); + logger.log(Level.WARNING, LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " with invalid address"); return null; } int port = Short.toUnsignedInt(buffer.getShort()); @@ -188,7 +191,7 @@ else if ("tcp".equals(protocol)) } catch (Exception ex) { - logger.log(Level.WARNING, ex, () -> "PVA Client " + from + " sent search #" + search.seq + " with invalid protocol"); + logger.log(Level.WARNING, ex, () -> LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " with invalid protocol"); return null; } @@ -198,13 +201,13 @@ else if ("tcp".equals(protocol)) if (count == 0) { // pvlist request search.channels = null; - logger.log(Level.FINER, () -> "PVA Client " + from + " sent search #" + search.seq + " to list servers"); + logger.log(Level.FINER, () -> LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " to list servers"); } else { // Channel search request if (! (tcp || search.tls)) { - logger.log(Level.WARNING, "PVA Client " + from + " sent search #" + search.seq + " for protocol '" + unknown_protocol + "', need 'tcp' or 'tls'"); + logger.log(Level.WARNING, LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " for protocol '" + unknown_protocol + "', need 'tcp' or 'tls'"); return null; } search.channels = new ArrayList<>(count); @@ -214,7 +217,7 @@ else if ("tcp".equals(protocol)) { final int cid = buffer.getInt(); final String name = PVAString.decodeString(buffer); - logger.log(Level.FINER, () -> "PVA Client " + from + " sent search #" + search.seq + " for " + name + " [cid " + cid + "]" + logger.log(Level.FINER, () -> LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " for " + name + " [cid " + cid + "]" + ", reply addr " + orig_response_addr + (orig_response_addr.equals(search.client) ? "" : ", using " + search.client) + (search.tls ? " (TLS)" : "") @@ -226,7 +229,7 @@ else if ("tcp".equals(protocol)) } catch (Exception ex) { - logger.log(Level.WARNING, ex, () -> "PVA Client " + from + " sent damaged search #" + search.seq); + logger.log(Level.WARNING, ex, () -> LOG_PVA_CLIENT + from + " sent damaged search #" + search.seq); return null; } } From 53d4f222bc4861c37058e2ad699feab2481abb72 Mon Sep 17 00:00:00 2001 From: shroffk Date: Fri, 21 Aug 2026 16:09:43 -0400 Subject: [PATCH 4/9] Create the basis for structured pva exceptions --- .../pva/exceptions/PVAArraySizeException.java | 35 +++++++++++++++++++ .../pva/exceptions/PVAProtocolException.java | 12 +++++++ 2 files changed, 47 insertions(+) create mode 100644 core/pva/src/main/java/org/epics/pva/exceptions/PVAArraySizeException.java create mode 100644 core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java diff --git a/core/pva/src/main/java/org/epics/pva/exceptions/PVAArraySizeException.java b/core/pva/src/main/java/org/epics/pva/exceptions/PVAArraySizeException.java new file mode 100644 index 0000000000..8bc6128a65 --- /dev/null +++ b/core/pva/src/main/java/org/epics/pva/exceptions/PVAArraySizeException.java @@ -0,0 +1,35 @@ +package org.epics.pva.exceptions; + +/** Protocol exception for malformed array payloads. */ +@SuppressWarnings("nls") +public class PVAArraySizeException extends PVAProtocolException +{ + /** + * @param size Decoded array element count + * @param remaining Number of bytes left in buffer + */ + public PVAArraySizeException(final int size, final int remaining) + { + this(size, remaining, 1); + } + + /** + * @param size Decoded array element count + * @param remaining Number of bytes left in buffer + * @param bytesPerElement Number of bytes required per array element + */ + public PVAArraySizeException(final int size, final int remaining, final int bytesPerElement) + { + super(createMessage(size, remaining, bytesPerElement)); + } + + private static String createMessage(final int size, final int remaining, final int bytesPerElement) + { + if (size < 0) + return "Negative array size " + size; + + final long needed = (long) size * bytesPerElement; + return "Array size " + size + " needs " + needed + " bytes with element size " + bytesPerElement + + " but buffer has only " + remaining + " bytes"; + } +} diff --git a/core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java b/core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java new file mode 100644 index 0000000000..906f5a7f85 --- /dev/null +++ b/core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java @@ -0,0 +1,12 @@ +package org.epics.pva.exceptions; + +/** Protocol violation while decoding a PVA message. */ +@SuppressWarnings("nls") +public class PVAProtocolException extends Exception +{ + public PVAProtocolException(final String message) + { + super(message); + } +} + From c0a9c67115c3365aef9e84585b772a1476a8fdcb Mon Sep 17 00:00:00 2001 From: shroffk Date: Fri, 21 Aug 2026 16:10:29 -0400 Subject: [PATCH 5/9] refactoring an existing exception --- .../pva/{data => exceptions}/ElementTypeException.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) rename core/pva/src/main/java/org/epics/pva/{data => exceptions}/ElementTypeException.java (90%) diff --git a/core/pva/src/main/java/org/epics/pva/data/ElementTypeException.java b/core/pva/src/main/java/org/epics/pva/exceptions/ElementTypeException.java similarity index 90% rename from core/pva/src/main/java/org/epics/pva/data/ElementTypeException.java rename to core/pva/src/main/java/org/epics/pva/exceptions/ElementTypeException.java index 3a3bcfe1e8..b983a6edd4 100644 --- a/core/pva/src/main/java/org/epics/pva/data/ElementTypeException.java +++ b/core/pva/src/main/java/org/epics/pva/exceptions/ElementTypeException.java @@ -17,13 +17,15 @@ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -package org.epics.pva.data; +package org.epics.pva.exceptions; + +import org.epics.pva.data.PVAStructure; /** * Exception for when updating a PVAStructure array with a value that * includes a PVAStructure which does not match the element type of the array. */ -public class ElementTypeException extends Exception { +public class ElementTypeException extends PVAProtocolException { /** * Constructor returns an exception with a message based on the From f431319ac77873efef74e8c25607e34781b4e7e2 Mon Sep 17 00:00:00 2001 From: shroffk Date: Fri, 21 Aug 2026 16:11:03 -0400 Subject: [PATCH 6/9] Using the new PVA protocol array exception --- .../main/java/org/epics/pva/common/PVAHeader.java | 14 ++++++++------ .../main/java/org/epics/pva/data/PVABoolArray.java | 3 ++- .../main/java/org/epics/pva/data/PVAByteArray.java | 3 ++- .../java/org/epics/pva/data/PVADoubleArray.java | 5 +++-- .../java/org/epics/pva/data/PVAFloatArray.java | 5 +++-- .../main/java/org/epics/pva/data/PVAIntArray.java | 5 +++-- .../main/java/org/epics/pva/data/PVALongArray.java | 5 +++-- .../java/org/epics/pva/data/PVAShortArray.java | 5 +++-- .../java/org/epics/pva/data/PVAStringArray.java | 3 ++- .../main/java/org/epics/pva/data/PVAStructure.java | 4 +++- .../java/org/epics/pva/data/PVAStructureArray.java | 5 ++++- 11 files changed, 36 insertions(+), 21 deletions(-) diff --git a/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java b/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java index d8f78d8686..71baa61031 100644 --- a/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java +++ b/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java @@ -10,6 +10,8 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; +import org.epics.pva.exceptions.PVAProtocolException; + /** PVA Message Header * *
@@ -174,9 +176,9 @@ public static void encodeMessageHeader(final ByteBuffer buffer, byte flags, fina
      *  @param buffer Buffer as start of protocol header
      *  @param expect_server Expect a server message? Else client message
      *  @return Expected total message size (header + payload)
-     *  @throws Exception on protocol violation
+     *  @throws PVAProtocolException on protocol violation
      */
-    public static int checkMessageAndGetSize(final ByteBuffer buffer, final boolean expect_server) throws Exception
+    public static int checkMessageAndGetSize(final ByteBuffer buffer, final boolean expect_server) throws PVAProtocolException
     {
         if (buffer.position() < PVAHeader.HEADER_SIZE)
             return PVAHeader.HEADER_SIZE;
@@ -185,11 +187,11 @@ public static int checkMessageAndGetSize(final ByteBuffer buffer, final boolean
         // parsing the initial set of bytes
         final byte magic = buffer.get(0);
         if (magic != PVAHeader.PVA_MAGIC)
-            throw new Exception(String.format("Message lacks magic 0x%02X, got 0x%02X", PVAHeader.PVA_MAGIC, magic));
+            throw new PVAProtocolException(String.format("Message lacks magic 0x%02X, got 0x%02X", PVAHeader.PVA_MAGIC, magic));
 
         final byte version = buffer.get(1);
         if (version < PVAHeader.REQUIRED_PVA_PROTOCOL_REVISION)
-            throw new Exception("Cannot handle protocol version " + version +
+            throw new PVAProtocolException("Cannot handle protocol version " + version +
                                 ", expect version " +
                                 PVAHeader.REQUIRED_PVA_PROTOCOL_REVISION +
                                 " or higher");
@@ -197,7 +199,7 @@ public static int checkMessageAndGetSize(final ByteBuffer buffer, final boolean
         final byte flags = buffer.get(2);
         final boolean is_server = (flags & PVAHeader.FLAG_SERVER) != 0;
         if (is_server != expect_server)
-                throw new Exception(expect_server ? "Expected server message" : "Expected client message");
+                throw new PVAProtocolException(expect_server ? "Expected server message" : "Expected client message");
 
         // With each received message, check the byte order
         // and adjust buffer to read further content which usually
@@ -219,7 +221,7 @@ public static int checkMessageAndGetSize(final ByteBuffer buffer, final boolean
         // like buffer buffer.remaining() or buffer.get(10) is using int,
         // so us updating to long would be of limited use
         if (payload < 0)
-            throw new Exception("Payload size " + payload +
+            throw new PVAProtocolException("Payload size " + payload +
                                 " exceeds max signed integer " + Integer.toHexString(Integer.MAX_VALUE));
         // Could check against a PVA variant of EPICS_CA_MAX_ARRAY_BYTES,
         // but PVA design specifically aims to use all available memory
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java b/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java
index e1db85412b..7d46fdc0a8 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java
@@ -13,6 +13,7 @@
 import java.util.List;
 
 import org.epics.pva.PVASettings;
+import org.epics.pva.exceptions.PVAArraySizeException;
 
 /** 'Primitive' PV Access data type
  *   @author Kay Kasemir
@@ -96,7 +97,7 @@ public void decode(final PVATypeRegistry types, final ByteBuffer buffer) throws
     {
         final int size = PVASize.decodeSize(buffer);
         if (size < 0  ||  size > buffer.remaining())
-            throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer");
+            throw new PVAArraySizeException(size, buffer.remaining());
         final boolean[] new_value = new boolean[size];
         for (int i=0; i buffer.remaining())
-            throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer");
+            throw new PVAArraySizeException(size, buffer.remaining());
         final byte[] new_value = new byte[size];
         buffer.get(new_value);
         value = new_value;
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java b/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java
index 05516eae75..c5ac4be0e8 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java
@@ -13,6 +13,7 @@
 import java.util.List;
 
 import org.epics.pva.PVASettings;
+import org.epics.pva.exceptions.PVAArraySizeException;
 
 /** 'Primitive' PV Access data type
  *   @author Kay Kasemir
@@ -98,8 +99,8 @@ public void encodeType(final ByteBuffer buffer, final BitSet described) throws E
     public void decode(final PVATypeRegistry types, final ByteBuffer buffer) throws Exception
     {
         final int size = PVASize.decodeSize(buffer);
-        if (size < 0  ||  size*Double.BYTES > buffer.remaining())
-            throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer");
+        if (size < 0  ||  (long) size * Double.BYTES > buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining(), Double.BYTES);
         final double[] new_value = new double[size];
         for (int i=0; i buffer.remaining())
-            throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer");
+        if (size < 0  ||  (long) size * Float.BYTES > buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining(), Float.BYTES);
         final float[] new_value = new float[size];
         for (int i=0; i buffer.remaining())
-            throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer");
+        if (size < 0  ||  (long) size * Integer.BYTES > buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining(), Integer.BYTES);
         final int[] new_value = new int[size];
         for (int i=0; i buffer.remaining())
-            throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer");
+        if (size < 0  ||  (long) size * Long.BYTES > buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining(), Long.BYTES);
         final long[] new_value = new long[size];
         for (int i=0; i buffer.remaining())
-            throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer");
+        if (size < 0  ||  (long) size * Short.BYTES > buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining(), Short.BYTES);
         // Try to re-use existing array
         final short[] new_value = new short[size];
         // Considered using
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java b/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java
index 3aca078273..157eacd298 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java
@@ -14,6 +14,7 @@
 import java.util.Objects;
 
 import org.epics.pva.PVASettings;
+import org.epics.pva.exceptions.PVAArraySizeException;
 
 /** 'Primitive' PV Access data type
  *   @author Kay Kasemir
@@ -91,7 +92,7 @@ public void decode(final PVATypeRegistry types, final ByteBuffer buffer) throws
         final int size = PVASize.decodeSize(buffer);
         // Each array element needs to contain at least a byte for the string size
         if (size < 0  ||  size > buffer.remaining())
-            throw new Exception("Array size " + size + " with only " + buffer.remaining() + " bytes in buffer");
+            throw new PVAArraySizeException(size, buffer.remaining());
         String[] new_value = value;
         if (new_value == null  ||  new_value.length != size)
             new_value = new String[size];
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java b/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java
index 177a6a9395..ea2d22ee27 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java
@@ -7,6 +7,8 @@
  ******************************************************************************/
 package org.epics.pva.data;
 
+import org.epics.pva.exceptions.PVAProtocolException;
+
 import static org.epics.pva.PVASettings.logger;
 
 import java.nio.ByteBuffer;
@@ -57,7 +59,7 @@ static PVAStructure decodeType(final PVATypeRegistry types, final String name, f
 
         // Each element needs name (at least byte for length) and value (at least byte)
         if (size < 0  ||  size*2 > buffer.remaining())
-            throw new Exception("Structure with " + size + " elements but only " + buffer.remaining() + " bytes in buffer");
+            throw new PVAProtocolException("Structure with " + size + " elements but only " + buffer.remaining() + " bytes in buffer");
 
         // (name, FieldDesc)[]
         final List values = new ArrayList<>(size);
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAStructureArray.java b/core/pva/src/main/java/org/epics/pva/data/PVAStructureArray.java
index e3d4ac54e5..379db596af 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVAStructureArray.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVAStructureArray.java
@@ -7,6 +7,9 @@
  ******************************************************************************/
 package org.epics.pva.data;
 
+import org.epics.pva.exceptions.ElementTypeException;
+import org.epics.pva.exceptions.PVAProtocolException;
+
 import static org.epics.pva.PVASettings.logger;
 
 import java.nio.ByteBuffer;
@@ -161,7 +164,7 @@ public void decode(final PVATypeRegistry types, final ByteBuffer buffer) throws
 
         // Each element needs at least the 'non null' info byte
         if (count < 0  ||  count > buffer.remaining())
-            throw new Exception("Structure element count " + count + " with only " + buffer.remaining() + " bytes in buffer");
+            throw new PVAProtocolException("Structure element count " + count + " with only " + buffer.remaining() + " bytes in buffer");
 
         // Try to re-use elements
         PVAStructure[] new_elements = elements;

From 9ec22e021334297ab58ace29bc53635eeacb9427 Mon Sep 17 00:00:00 2001
From: shroffk 
Date: Fri, 21 Aug 2026 16:11:22 -0400
Subject: [PATCH 7/9] fix missing new line at EOF

---
 core/pva/src/test/java/org/epics/pva/data/BitSetTest.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java b/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java
index af0c7bfa8e..b478c91390 100644
--- a/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java
+++ b/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java
@@ -68,4 +68,4 @@ public void testBitSet() throws Exception
         final BitSet copy = PVABitSet.decodeBitSet(buffer);
         assertThat(copy, equalTo(bits));
     }
-}
\ No newline at end of file
+}

From b702fda4636567291e2cc4786027cb5b7ffe822e Mon Sep 17 00:00:00 2001
From: shroffk 
Date: Fri, 21 Aug 2026 16:15:45 -0400
Subject: [PATCH 8/9] fix exception imports in the pva tests

---
 .../java/org/epics/pva/exceptions/PVAProtocolException.java    | 1 -
 .../test/java/org/epics/pva/data/PVAStructureArrayTest.java    | 3 ++-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java b/core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java
index 906f5a7f85..bc73e8ffa0 100644
--- a/core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java
+++ b/core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java
@@ -9,4 +9,3 @@ public PVAProtocolException(final String message)
         super(message);
     }
 }
-
diff --git a/core/pva/src/test/java/org/epics/pva/data/PVAStructureArrayTest.java b/core/pva/src/test/java/org/epics/pva/data/PVAStructureArrayTest.java
index 54ba39bfe1..12061ed29c 100644
--- a/core/pva/src/test/java/org/epics/pva/data/PVAStructureArrayTest.java
+++ b/core/pva/src/test/java/org/epics/pva/data/PVAStructureArrayTest.java
@@ -19,6 +19,7 @@
 
 package org.epics.pva.data;
 
+import org.epics.pva.exceptions.ElementTypeException;
 import org.junit.jupiter.api.Test;
 
 import java.util.BitSet;
@@ -112,4 +113,4 @@ void set() throws Exception {
                 new PVAStructure("different", "diff"));
         assertThrows(ElementTypeException.class, () -> structureArray.set(diffTypeArray.get()));
     }
-}
\ No newline at end of file
+}

From 7002a4b8df2bcbed3dc792d1b3f77bcc646e5289 Mon Sep 17 00:00:00 2001
From: shroffk 
Date: Fri, 21 Aug 2026 16:42:16 -0400
Subject: [PATCH 9/9] fixing more sonarcube  issues

---
 .../main/java/org/epics/pva/common/SearchRequest.java    | 9 +++++----
 .../src/main/java/org/epics/pva/data/PVAAnyArray.java    | 4 +++-
 2 files changed, 8 insertions(+), 5 deletions(-)

diff --git a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java
index 67401bce71..d89dbf1a2e 100644
--- a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java
+++ b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java
@@ -64,7 +64,7 @@ public String toString()
         {
             return "'" + name + "' [CID " + cid + "]";
         }
-    };
+    }
 
     /** Server should reply with its GUID and empty CID list
      *  even if it does not host any of the searched channels
@@ -128,7 +128,7 @@ public static SearchRequest decode(final OriginTag origin, final InetSocketAddre
         // plus the list of names.
         if (payload < 4+1+3+16+2+1+2)
         {
-            logger.log(Level.WARNING, "PVA client " + from + " sent only " + payload + " bytes for search request");
+            logger.log(Level.WARNING, () -> "PVA client " + from + " sent only " + payload + " bytes for search request");
             return null;
         }
         final SearchRequest search = new SearchRequest();
@@ -154,7 +154,7 @@ public static SearchRequest decode(final OriginTag origin, final InetSocketAddre
         }
         catch (Exception ex)
         {
-            logger.log(Level.WARNING, LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " with invalid address");
+            logger.log(Level.WARNING, () -> LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " with invalid address");
             return null;
         }
         int port = Short.toUnsignedInt(buffer.getShort());
@@ -207,7 +207,8 @@ else if ("tcp".equals(protocol))
         {   // Channel search request
             if (! (tcp || search.tls))
             {
-                logger.log(Level.WARNING, LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " for protocol '" + unknown_protocol + "', need 'tcp' or 'tls'");
+                final String unsupported_protocol = unknown_protocol;
+                logger.log(Level.WARNING, () -> LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " for protocol '" + unsupported_protocol + "', need 'tcp' or 'tls'");
                 return null;
             }
             search.channels = new ArrayList<>(count);
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAAnyArray.java b/core/pva/src/main/java/org/epics/pva/data/PVAAnyArray.java
index 36d9588a84..bc5a7e44d8 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVAAnyArray.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVAAnyArray.java
@@ -19,6 +19,8 @@
 
 package org.epics.pva.data;
 
+import org.epics.pva.exceptions.PVAArraySizeException;
+
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.BitSet;
@@ -130,7 +132,7 @@ public void decode(PVATypeRegistry types, ByteBuffer buffer) throws Exception {
         final int count = PVASize.decodeSize(buffer);
         // Each array element needs at least the 'non_null' byte
         if (count < 0  ||  count > buffer.remaining())
-            throw new Exception("Array size " + count + " with only " + buffer.remaining() + " bytes in buffer");
+            throw new PVAArraySizeException(count, buffer.remaining());
         // Try to re-use elements
         PVAny[] new_elements = elements;
         if (new_elements == null  ||  new_elements.length != count)