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..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
@@ -213,6 +215,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 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
+ // 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..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
@@ -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
{
@@ -61,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
@@ -125,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();
@@ -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());
@@ -173,15 +176,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 LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " with invalid protocol");
+ return null;
}
// Loop over searched channels
@@ -190,28 +201,37 @@ 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'");
+ 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);
- 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 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)" : "")
+ + (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, ex, () -> LOG_PVA_CLIENT + from + " sent damaged search #" + search.seq);
+ 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 PVAArraySizeException(count, buffer.remaining());
// 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..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
@@ -95,6 +96,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 PVAArraySizeException(size, buffer.remaining());
final boolean[] new_value = new boolean[size];
for (int i=0; i buffer.remaining())
+ 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 9e6dce6a5b..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,6 +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 || (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 PVAArraySizeException(size, buffer.remaining(), Float.BYTES);
final float[] new_value = new float[size];
for (int i=0; i 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 PVAArraySizeException(size, buffer.remaining(), Long.BYTES);
final long[] new_value = new long[size];
for (int i=0; i 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/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..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
@@ -89,6 +90,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 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 88bb0ac667..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;
@@ -55,6 +57,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 PVAProtocolException("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 PVAProtocolException("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/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
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..bc73e8ffa0
--- /dev/null
+++ b/core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java
@@ -0,0 +1,11 @@
+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);
+ }
+}
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..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
@@ -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();
@@ -68,4 +68,4 @@ public void testBitSet()
final BitSet copy = PVABitSet.decodeBitSet(buffer);
assertThat(copy, equalTo(bits));
}
-}
\ No newline at end of file
+}
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
+}