This is an automated email from the ASF dual-hosted git repository.

kenhuuu pushed a commit to branch 3.7-dev
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/3.7-dev by this push:
     new 5afbaffa50 TINKERPOP-3281 Reject malformed GraphBinary length prefixes 
(#3610)
5afbaffa50 is described below

commit 5afbaffa503756cd6799c18c4284b87ea358033a
Author: Guian Gumpac <[email protected]>
AuthorDate: Tue Aug 11 12:02:19 2026 -0700

    TINKERPOP-3281 Reject malformed GraphBinary length prefixes (#3610)
    
    Assisted-by: Kiro: Claude Opus 4.8
---
 CHANGELOG.asciidoc                                 |   1 +
 .../io/binary/types/BigIntegerSerializer.java      |   2 +-
 .../io/binary/types/BulkSetSerializer.java         |  10 +-
 .../io/binary/types/ByteBufferSerializer.java      |   2 +-
 .../io/binary/types/ByteCodeSerializer.java        |   6 +-
 .../io/binary/types/CollectionSerializer.java      |   4 +-
 .../structure/io/binary/types/GraphSerializer.java |   6 +-
 .../io/binary/types/InetAddressSerializer.java     |   2 +-
 .../structure/io/binary/types/MapSerializer.java   |   4 +-
 .../structure/io/binary/types/PSerializer.java     |   2 +-
 .../io/binary/types/SimpleTypeSerializer.java      |  34 ++
 .../io/binary/types/StringSerializer.java          |   6 +-
 .../structure/io/binary/types/TreeSerializer.java  |   2 +-
 .../ser/binary/GraphBinaryLengthPrefixTest.java    | 361 +++++++++++++++++++++
 14 files changed, 423 insertions(+), 19 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index ef4250fa9f..55be83bfc1 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -25,6 +25,7 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 [[release-3-7-7]]
 === TinkerPop 3.7.7 (Release Date: NOT OFFICIALLY RELEASED YET)
 
+* Fixed GraphBinary deserialization to reject invalid length and count values 
with an `IOException`.
 * Disabled unsafe Java deserialization on the Gryo IO paths and added 
`GryoMapper.Builder.javaSerializationAllowed(boolean)` to control it.
 * Fixed `subgraph()` to throw a descriptive error identifying the required 
`Edge` input instead of an internal `ClassCastException` when the traversal 
produces a non-edge value.
 * Fixed `where(P)` to throw a descriptive error identifying the required 
String scope key (and suggesting `is(P)` for value comparisons) instead of an 
internal `ClassCastException` when given a non-String predicate value.
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/BigIntegerSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/BigIntegerSerializer.java
index e3d45a6211..851cd1583a 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/BigIntegerSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/BigIntegerSerializer.java
@@ -37,7 +37,7 @@ public class BigIntegerSerializer extends 
SimpleTypeSerializer<BigInteger> {
 
     @Override
     protected BigInteger readValue(final Buffer buffer, final 
GraphBinaryReader context) throws IOException {
-        final byte[] bigIntBytes = new byte[buffer.readInt()];
+        final byte[] bigIntBytes = new byte[readSizePrefix(buffer)];
         buffer.readBytes(bigIntBytes);
         return new BigInteger(bigIntBytes);
     }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/BulkSetSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/BulkSetSerializer.java
index e15eaf691d..b4494f0c6b 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/BulkSetSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/BulkSetSerializer.java
@@ -37,11 +37,17 @@ public class BulkSetSerializer extends 
SimpleTypeSerializer<BulkSet> {
 
     @Override
     protected BulkSet readValue(final Buffer buffer, final GraphBinaryReader 
context) throws IOException {
-        final int length = buffer.readInt();
+        final int length = readSizePrefix(buffer);
 
         final BulkSet result = new BulkSet();
         for (int i = 0; i < length; i++) {
-            result.add(context.read(buffer), buffer.readLong());
+            final Object element = context.read(buffer);
+            final long bulk = buffer.readLong();
+            // A large positive bulk needs no bound here because it is stored 
as a single long weight rather than
+            // expanded, so it drives no allocation on this read path.
+            if (bulk < 0)
+                throw new IOException(String.format("Invalid GraphBinary 
BulkSet bulk: %d", bulk));
+            result.add(element, bulk);
         }
 
         return result;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ByteBufferSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ByteBufferSerializer.java
index 2037e142b1..a1bc218377 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ByteBufferSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ByteBufferSerializer.java
@@ -37,7 +37,7 @@ public class ByteBufferSerializer extends 
SimpleTypeSerializer<ByteBuffer> {
 
     @Override
     protected ByteBuffer readValue(final Buffer buffer, final 
GraphBinaryReader context) throws IOException {
-        final ByteBuffer bb = ByteBuffer.allocate(buffer.readInt());
+        final ByteBuffer bb = ByteBuffer.allocate(readSizePrefix(buffer));
         buffer.readBytes(bb);
         bb.rewind();
         return bb;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ByteCodeSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ByteCodeSerializer.java
index 7028f83b6c..74e6a20546 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ByteCodeSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/ByteCodeSerializer.java
@@ -36,12 +36,12 @@ public class ByteCodeSerializer extends 
SimpleTypeSerializer<Bytecode> {
     protected Bytecode readValue(final Buffer buffer, final GraphBinaryReader 
context) throws IOException {
         final Bytecode result = new Bytecode();
 
-        final int stepsLength = buffer.readInt();
+        final int stepsLength = readSizePrefix(buffer);
         for (int i = 0; i < stepsLength; i++) {
             result.addStep(context.readValue(buffer, String.class, false), 
getInstructionArguments(buffer, context));
         }
 
-        final int sourcesLength = buffer.readInt();
+        final int sourcesLength = readSizePrefix(buffer);
         for (int i = 0; i < sourcesLength; i++) {
             result.addSource(context.readValue(buffer, String.class, false), 
getInstructionArguments(buffer, context));
         }
@@ -50,7 +50,7 @@ public class ByteCodeSerializer extends 
SimpleTypeSerializer<Bytecode> {
     }
 
     private static Object[] getInstructionArguments(final Buffer buffer, final 
GraphBinaryReader context) throws IOException {
-        final int valuesLength = buffer.readInt();
+        final int valuesLength = readSizePrefix(buffer);
         final Object[] values = new Object[valuesLength];
         for (int j = 0; j < valuesLength; j++) {
             values[j] = context.read(buffer);
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CollectionSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CollectionSerializer.java
index 92a6292c0a..868f057087 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CollectionSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/CollectionSerializer.java
@@ -34,9 +34,9 @@ class CollectionSerializer extends 
SimpleTypeSerializer<Collection> {
 
     @Override
     protected Collection readValue(final Buffer buffer, final 
GraphBinaryReader context) throws IOException {
-        final int length = buffer.readInt();
+        final int length = readSizePrefix(buffer);
 
-        final ArrayList result = new ArrayList(length);
+        final ArrayList result = new ArrayList(cappedInitialCapacity(length));
         for (int i = 0; i < length; i++) {
             result.add(context.read(buffer));
         }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/GraphSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/GraphSerializer.java
index 0fe5dd76a9..c7bbc76963 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/GraphSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/GraphSerializer.java
@@ -61,10 +61,10 @@ public class GraphSerializer extends 
SimpleTypeSerializer<Graph> {
 
         try {
             final Graph graph = (Graph) openMethod.invoke(null, conf);
-            final int vertexCount = context.readValue(buffer, Integer.class, 
false);
+            final int vertexCount = readSizePrefix(buffer);
             for (int ix = 0; ix < vertexCount; ix++) {
                 final Vertex v = graph.addVertex(T.id, context.read(buffer), 
T.label, context.readValue(buffer, String.class, false));
-                final int vertexPropertyCount = context.readValue(buffer, 
Integer.class, false);
+                final int vertexPropertyCount = readSizePrefix(buffer);
 
                 for (int iy = 0; iy < vertexPropertyCount; iy++) {
                     final Object id = context.read(buffer);
@@ -80,7 +80,7 @@ public class GraphSerializer extends 
SimpleTypeSerializer<Graph> {
                 }
             }
 
-            final int edgeCount = context.readValue(buffer, Integer.class, 
false);
+            final int edgeCount = readSizePrefix(buffer);
             for (int ix = 0; ix < edgeCount; ix++) {
                 final Object id = context.read(buffer);
                 final String label = context.readValue(buffer, String.class, 
false);
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/InetAddressSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/InetAddressSerializer.java
index 7e42c6e1de..e347aee04b 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/InetAddressSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/InetAddressSerializer.java
@@ -37,7 +37,7 @@ public class InetAddressSerializer<T extends InetAddress> 
extends SimpleTypeSeri
 
     @Override
     protected T readValue(final Buffer buffer, final GraphBinaryReader 
context) throws IOException {
-        final int length = buffer.readInt();
+        final int length = readSizePrefix(buffer);
         final byte[] bytes = new byte[length];
         buffer.readBytes(bytes);
 
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/MapSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/MapSerializer.java
index 0821a0a959..f6fc6188ea 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/MapSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/MapSerializer.java
@@ -35,9 +35,9 @@ public class MapSerializer extends SimpleTypeSerializer<Map> {
 
     @Override
     protected Map readValue(final Buffer buffer, final GraphBinaryReader 
context) throws IOException {
-        final int length = buffer.readInt();
+        final int length = readSizePrefix(buffer);
 
-        final Map<Object,Object> result = new LinkedHashMap<>(length);
+        final Map<Object,Object> result = new 
LinkedHashMap<>(cappedInitialCapacity(length));
         for (int i = 0; i < length; i++) {
             result.put(context.read(buffer), context.read(buffer));
         }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/PSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/PSerializer.java
index 689c518c63..1f945791ea 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/PSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/PSerializer.java
@@ -55,7 +55,7 @@ public class PSerializer<T extends P> extends 
SimpleTypeSerializer<T> {
     @Override
     protected T readValue(final Buffer buffer, final GraphBinaryReader 
context) throws IOException {
         final String predicateName = context.readValue(buffer, String.class, 
false);
-        final int length = context.readValue(buffer, Integer.class, false);
+        final int length = readSizePrefix(buffer);
         final Object[] args = new Object[length];
         final Class<?>[] argumentClasses = new Class[length];
         for (int i = 0; i < length; i++) {
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/SimpleTypeSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/SimpleTypeSerializer.java
index 568243cc36..79f3ff32c5 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/SimpleTypeSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/SimpleTypeSerializer.java
@@ -33,6 +33,13 @@ import java.io.IOException;
 public abstract class SimpleTypeSerializer<T> implements TypeSerializer<T> {
     private final DataType dataType;
 
+    /**
+     * Upper bound on the capacity pre-allocated for a growable container from 
a wire count. The full count is still
+     * honored while reading; this only limits speculative pre-allocation so 
an in-frame but large count cannot force
+     * a large backing array up front. The container grows as elements are 
added.
+     */
+    private static final int MAX_PREALLOC_CAPACITY = 16384;
+
     public DataType getDataType() {
         return dataType;
     }
@@ -68,6 +75,33 @@ public abstract class SimpleTypeSerializer<T> implements 
TypeSerializer<T> {
      */
     protected abstract T readValue(final Buffer buffer, final 
GraphBinaryReader context) throws IOException;
 
+    /**
+     * Reads a length or element-count prefix and validates it before it is 
used to size an allocation. The same
+     * check serves both kinds of prefix: a byte length, where each counted 
unit is one wire byte, and an element
+     * count, where each counted element occupies at least one wire byte. A 
negative value, or one larger than the
+     * number of bytes actually remaining in the buffer, therefore cannot be 
legitimate, so it is rejected rather
+     * than allowed to drive a large allocation from a small message.
+     */
+    protected static int readSizePrefix(final Buffer buffer) throws 
IOException {
+        if (buffer.readableBytes() < Integer.BYTES)
+            throw new IOException(String.format(
+                    "Incomplete GraphBinary length prefix: %d byte(s) 
available", buffer.readableBytes()));
+        final int size = buffer.readInt();
+        if (size < 0 || size > buffer.readableBytes())
+            throw new IOException(String.format("Invalid GraphBinary length 
prefix: %d (readable bytes: %d)",
+                    size, buffer.readableBytes()));
+        return size;
+    }
+
+    /**
+     * Caps the initial capacity used to pre-size a growable container ({@code 
ArrayList}, {@code HashMap}) from a
+     * validated wire count. Safe to use only for containers that grow on 
demand, never for a fixed-size array or a
+     * byte buffer that must hold exactly {@code size} entries.
+     */
+    protected static int cappedInitialCapacity(final int size) {
+        return Math.min(size, MAX_PREALLOC_CAPACITY);
+    }
+
     @Override
     public void write(final T value, final Buffer buffer, final 
GraphBinaryWriter context) throws IOException {
         writeValue(value, buffer, context, true);
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/StringSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/StringSerializer.java
index 6a5d096dab..2f8cd839a3 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/StringSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/StringSerializer.java
@@ -23,6 +23,8 @@ import 
org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
 import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
 import org.apache.tinkerpop.gremlin.structure.io.Buffer;
 
+import java.io.IOException;
+
 import java.nio.charset.StandardCharsets;
 
 public class StringSerializer extends SimpleTypeSerializer<String> {
@@ -31,9 +33,9 @@ public class StringSerializer extends 
SimpleTypeSerializer<String> {
     }
 
     @Override
-    protected String readValue(final Buffer buffer, final GraphBinaryReader 
context) {
+    protected String readValue(final Buffer buffer, final GraphBinaryReader 
context) throws IOException {
         // Use Netty 4.0 API (avoid ByteBuf#readCharSequence() method) to 
maximize compatibility
-        final byte[] bytes = new byte[buffer.readInt()];
+        final byte[] bytes = new byte[readSizePrefix(buffer)];
         buffer.readBytes(bytes);
         return new String(bytes, StandardCharsets.UTF_8);
     }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/TreeSerializer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/TreeSerializer.java
index 5aab638812..8833583bab 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/TreeSerializer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/io/binary/types/TreeSerializer.java
@@ -33,7 +33,7 @@ public class TreeSerializer extends 
SimpleTypeSerializer<Tree> {
 
     @Override
     protected Tree readValue(final Buffer buffer, final GraphBinaryReader 
context) throws IOException {
-        final int length = buffer.readInt();
+        final int length = readSizePrefix(buffer);
 
         final Tree result = new Tree();
         for (int i = 0; i < length; i++) {
diff --git 
a/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/GraphBinaryLengthPrefixTest.java
 
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/GraphBinaryLengthPrefixTest.java
new file mode 100644
index 0000000000..6968de3fd2
--- /dev/null
+++ 
b/gremlin-util/src/test/java/org/apache/tinkerpop/gremlin/util/ser/binary/GraphBinaryLengthPrefixTest.java
@@ -0,0 +1,361 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.util.ser.binary;
+
+import io.netty.buffer.ByteBufAllocator;
+import org.apache.tinkerpop.gremlin.structure.io.Buffer;
+import org.apache.tinkerpop.gremlin.structure.io.binary.DataType;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
+import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
+import org.apache.tinkerpop.gremlin.util.ser.NettyBufferFactory;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * A GraphBinary value that carries a length or element-count prefix must not 
size an allocation from that prefix
+ * without validating it. A crafted prefix (negative, or larger than the bytes 
actually present) is otherwise a
+ * pre-auth memory-amplification denial of service: a tiny message drives a 
multi-gigabyte allocation. These tests
+ * feed such prefixes with no following elements and assert the reader refuses 
them rather than attempting the
+ * allocation.
+ */
+public class GraphBinaryLengthPrefixTest {
+
+    private final GraphBinaryReader reader = new GraphBinaryReader();
+    private final GraphBinaryWriter writer = new GraphBinaryWriter();
+    private static final NettyBufferFactory bufferFactory = new 
NettyBufferFactory();
+
+    // distinct sentinels below Integer.MAX_VALUE, so each Bytecode prefix is 
identified by its own value in the
+    // rejection message. These counts drive a read loop rather than sizing an 
array, so they need no headroom.
+    private static final int OVERSIZED_STEPS_LENGTH = 1_000_000_007;
+    private static final int OVERSIZED_SOURCES_LENGTH = 2_000_000_011;
+
+    // distinct sentinels for the three Graph counts, each far beyond any 
readable byte count and far above the small
+    // legitimate counts that precede them in these frames, so the value 
reported back identifies which count failed
+    private static final int OVERSIZED_GRAPH_VERTEX_COUNT = 1_100_000_009;
+    private static final int OVERSIZED_GRAPH_EDGE_COUNT = 1_200_000_017;
+    private static final int OVERSIZED_GRAPH_VERTEX_PROPERTY_COUNT = 
1_300_000_027;
+
+    private void assertRejectsLengthPrefix(final DataType type, final int 
declaredLength) {
+        final Buffer buffer = 
bufferFactory.create(ByteBufAllocator.DEFAULT.buffer());
+        try {
+            buffer.writeByte(type.getCodeByte());  // {type_code}
+            buffer.writeByte(0);                    // {value_flag} = non-null
+            buffer.writeInt(declaredLength);        // {length} with no 
elements following
+            reader.read(buffer);
+            fail(String.format("read of %s with declared length %d must be 
refused", type, declaredLength));
+        } catch (IOException expected) {
+            assertTrue(expected.getMessage(), 
expected.getMessage().contains("length prefix"));
+        } finally {
+            buffer.release();
+        }
+    }
+
+    @Test
+    public void shouldRejectOversizedListLengthPrefix() {
+        assertRejectsLengthPrefix(DataType.LIST, Integer.MAX_VALUE);
+    }
+
+    @Test
+    public void shouldRejectOversizedSetLengthPrefix() {
+        assertRejectsLengthPrefix(DataType.SET, Integer.MAX_VALUE);
+    }
+
+    @Test
+    public void shouldRejectOversizedMapLengthPrefix() {
+        assertRejectsLengthPrefix(DataType.MAP, Integer.MAX_VALUE);
+    }
+
+    @Test
+    public void shouldRejectOversizedStringLengthPrefix() {
+        assertRejectsLengthPrefix(DataType.STRING, Integer.MAX_VALUE);
+    }
+
+    @Test
+    public void shouldRejectOversizedByteBufferLengthPrefix() {
+        assertRejectsLengthPrefix(DataType.BYTEBUFFER, Integer.MAX_VALUE);
+    }
+
+    @Test
+    public void shouldRejectOversizedTreeLengthPrefix() {
+        assertRejectsLengthPrefix(DataType.TREE, Integer.MAX_VALUE);
+    }
+
+    @Test
+    public void shouldRejectNegativeListLengthPrefix() {
+        assertRejectsLengthPrefix(DataType.LIST, -1);
+    }
+
+    @Test
+    public void shouldRejectNegativeByteBufferLengthPrefix() {
+        assertRejectsLengthPrefix(DataType.BYTEBUFFER, -1);
+    }
+
+    @Test
+    public void shouldRejectNegativeTreeLengthPrefix() {
+        assertRejectsLengthPrefix(DataType.TREE, -1);
+    }
+
+    @Test
+    public void shouldRejectTruncatedListLengthPrefix() {
+        assertRejectsTruncatedLengthPrefix(DataType.LIST);
+    }
+
+    @Test
+    public void shouldRejectTruncatedStringLengthPrefix() {
+        assertRejectsTruncatedLengthPrefix(DataType.STRING);
+    }
+
+    @Test
+    public void shouldRejectNegativeBulkSetBulk() {
+        final Buffer buffer = 
bufferFactory.create(ByteBufAllocator.DEFAULT.buffer());
+        try {
+            buffer.writeByte(DataType.BULKSET.getCodeByte());  // {type_code}
+            buffer.writeByte(0);                                // 
{value_flag} = non-null
+            buffer.writeInt(1);                                 // element 
count = 1
+            buffer.writeByte(DataType.INT.getCodeByte());       // one 
fully-qualified INT element
+            buffer.writeByte(0);
+            buffer.writeInt(42);
+            buffer.writeLong(-1L);                              // negative 
per-element bulk
+            reader.read(buffer);
+            fail("read of a BulkSet with a negative bulk must be refused");
+        } catch (IOException expected) {
+            assertTrue(expected.getMessage(), 
expected.getMessage().contains("bulk"));
+        } finally {
+            buffer.release();
+        }
+    }
+
+    private void assertRejectsTruncatedLengthPrefix(final DataType type) {
+        final Buffer buffer = 
bufferFactory.create(ByteBufAllocator.DEFAULT.buffer());
+        try {
+            buffer.writeByte(type.getCodeByte());   // {type_code}
+            buffer.writeByte(0);                     // {value_flag} = non-null
+            buffer.writeByte(0);                     // only 2 of the 4 
length-prefix bytes present
+            buffer.writeByte(0);
+            reader.read(buffer);
+            fail(String.format("read of %s with a truncated length prefix must 
be refused", type));
+        } catch (IOException expected) {
+            assertTrue(expected.getMessage(), 
expected.getMessage().contains("length prefix"));
+        } finally {
+            buffer.release();
+        }
+    }
+
+    @Test
+    public void shouldRejectOversizedPArgumentCount() {
+        // the argument count immediately sizes fixed Object[]/Class[] arrays, 
so Integer.MAX_VALUE is a safe
+        // sentinel: without the guard the JVM array-size limit fails fast 
instead of attempting a legal allocation
+        final Buffer buffer = 
bufferFactory.create(ByteBufAllocator.DEFAULT.buffer());
+        try {
+            buffer.writeByte(DataType.P.getCodeByte());  // {type_code}
+            buffer.writeByte(0);                         // {value_flag} = 
non-null
+            writeNonNullableString(buffer, "eq");        // a valid predicate 
name precedes the count
+            buffer.writeInt(Integer.MAX_VALUE);          // {length} with no 
arguments following
+            assertRejectsDeclaredCount(buffer, "P argument count", 
Integer.MAX_VALUE);
+        } finally {
+            buffer.release();
+        }
+    }
+
+    @Test
+    public void shouldRejectOversizedBytecodeStepsLength() {
+        final Buffer buffer = 
bufferFactory.create(ByteBufAllocator.DEFAULT.buffer());
+        try {
+            buffer.writeByte(DataType.BYTECODE.getCodeByte());  // {type_code}
+            buffer.writeByte(0);                                 // 
{value_flag} = non-null
+            buffer.writeInt(OVERSIZED_STEPS_LENGTH);             // 
{steps_length} with no steps following
+            assertRejectsDeclaredCount(buffer, "Bytecode steps length", 
OVERSIZED_STEPS_LENGTH);
+        } finally {
+            buffer.release();
+        }
+    }
+
+    @Test
+    public void shouldRejectOversizedBytecodeSourcesLength() {
+        final Buffer buffer = 
bufferFactory.create(ByteBufAllocator.DEFAULT.buffer());
+        try {
+            buffer.writeByte(DataType.BYTECODE.getCodeByte());  // {type_code}
+            buffer.writeByte(0);                                 // 
{value_flag} = non-null
+            buffer.writeInt(0);                                  // a valid, 
empty {steps_length}
+            buffer.writeInt(OVERSIZED_SOURCES_LENGTH);           // 
{sources_length} with no sources following
+            assertRejectsDeclaredCount(buffer, "Bytecode sources length", 
OVERSIZED_SOURCES_LENGTH);
+        } finally {
+            buffer.release();
+        }
+    }
+
+    @Test
+    public void shouldRejectOversizedBytecodeInstructionValuesLength() {
+        // the instruction argument count immediately sizes a fixed Object[], 
so Integer.MAX_VALUE fails fast at the
+        // JVM array-size limit if the guard is absent
+        final Buffer buffer = 
bufferFactory.create(ByteBufAllocator.DEFAULT.buffer());
+        try {
+            buffer.writeByte(DataType.BYTECODE.getCodeByte());  // {type_code}
+            buffer.writeByte(0);                                 // 
{value_flag} = non-null
+            buffer.writeInt(1);                                  // one step 
follows
+            writeNonNullableString(buffer, "V");                 // a valid 
step operator
+            buffer.writeInt(Integer.MAX_VALUE);                  // 
{values_length} with no arguments following
+            assertRejectsDeclaredCount(buffer, "Bytecode instruction values 
length", Integer.MAX_VALUE);
+        } finally {
+            buffer.release();
+        }
+    }
+
+    @Test
+    public void shouldRejectOversizedGraphVertexCountPrefix() {
+        final Buffer buffer = 
bufferFactory.create(ByteBufAllocator.DEFAULT.buffer());
+        try {
+            buffer.writeByte(DataType.GRAPH.getCodeByte());     // {type_code}
+            buffer.writeByte(0);                                 // 
{value_flag} = non-null
+            buffer.writeInt(OVERSIZED_GRAPH_VERTEX_COUNT);       // 
{vertex_count} with no vertices following
+            assertRejectsDeclaredCount(buffer, "Graph vertex count", 
OVERSIZED_GRAPH_VERTEX_COUNT);
+        } finally {
+            buffer.release();
+        }
+    }
+
+    @Test
+    public void shouldRejectOversizedGraphEdgeCountPrefix() {
+        // zero vertices is a legitimate count, so the vertex loop is skipped 
and the edge count is what gets refused
+        final Buffer buffer = 
bufferFactory.create(ByteBufAllocator.DEFAULT.buffer());
+        try {
+            buffer.writeByte(DataType.GRAPH.getCodeByte());     // {type_code}
+            buffer.writeByte(0);                                 // 
{value_flag} = non-null
+            buffer.writeInt(0);                                  // a valid, 
empty {vertex_count}
+            buffer.writeInt(OVERSIZED_GRAPH_EDGE_COUNT);         // 
{edge_count} with no edges following
+            assertRejectsDeclaredCount(buffer, "Graph edge count", 
OVERSIZED_GRAPH_EDGE_COUNT);
+        } finally {
+            buffer.release();
+        }
+    }
+
+    @Test
+    public void shouldRejectOversizedGraphVertexPropertyCountPrefix() {
+        // one complete, valid vertex is read first, so the reader is well 
past the outer count when the per-vertex
+        // vertex-property count is refused
+        final Buffer buffer = 
bufferFactory.create(ByteBufAllocator.DEFAULT.buffer());
+        try {
+            buffer.writeByte(DataType.GRAPH.getCodeByte());     // {type_code}
+            buffer.writeByte(0);                                 // 
{value_flag} = non-null
+            buffer.writeInt(1);                                  // one vertex 
follows
+            buffer.writeByte(DataType.INT.getCodeByte());        // the vertex 
id, fully qualified
+            buffer.writeByte(0);
+            buffer.writeInt(1);
+            writeNonNullableString(buffer, "v");                 // the vertex 
label
+            buffer.writeInt(OVERSIZED_GRAPH_VERTEX_PROPERTY_COUNT);  // 
{vp_count} with no properties following
+            assertRejectsDeclaredCount(buffer, "Graph vertex property count",
+                    OVERSIZED_GRAPH_VERTEX_PROPERTY_COUNT);
+        } finally {
+            buffer.release();
+        }
+    }
+
+    /**
+     * Reads a crafted message that is expected to be refused because of 
{@code declaredCount}, and requires the
+     * failure to name that specific prefix. Matching on the declared count as 
well as the "length prefix" wording
+     * keeps these tests from passing on an unrelated downstream failure: a 
message truncated after the count is also
+     * refused for lacking a length prefix, but only the validated count is 
reported back with its own value.
+     */
+    private void assertRejectsDeclaredCount(final Buffer buffer, final String 
prefixName, final int declaredCount) {
+        try {
+            final Object result = reader.read(buffer);
+            fail(String.format("read of a %s of %d must be refused, but 
returned %s", prefixName, declaredCount,
+                    result));
+        } catch (IOException expected) {
+            assertThat(String.format("%s of %d was refused, but not by the 
length prefix validation: %s", prefixName,
+                    declaredCount, describeCausalChain(expected)),
+                    reportsLengthPrefix(expected, declaredCount), is(true));
+        }
+    }
+
+    /**
+     * Looks for a length-prefix rejection naming {@code declaredCount} 
anywhere in the causal chain, since a nested
+     * read failure may be wrapped by the serializer that requested it.
+     */
+    private static boolean reportsLengthPrefix(final Throwable thrown, final 
int declaredCount) {
+        final String declared = Integer.toString(declaredCount);
+        for (Throwable current = thrown; current != null; current = 
current.getCause()) {
+            final String message = current.getMessage();
+            if (message != null && message.contains("length prefix") && 
message.contains(declared))
+                return true;
+        }
+        return false;
+    }
+
+    private static String describeCausalChain(final Throwable thrown) {
+        final StringBuilder sb = new StringBuilder();
+        for (Throwable current = thrown; current != null; current = 
current.getCause()) {
+            if (sb.length() > 0) sb.append(" caused by ");
+            sb.append(current);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Writes a {@code String} in the non-nullable form the GraphBinary string 
serializer expects, which is the UTF-8
+     * byte length as an int followed by those bytes, with no type code or 
value flag.
+     */
+    private static void writeNonNullableString(final Buffer buffer, final 
String value) {
+        final byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
+        buffer.writeInt(bytes.length);
+        buffer.writeBytes(bytes);
+    }
+
+    @Test
+    public void shouldRoundTripListLargerThanPreallocCap() throws IOException {
+        // more elements than the internal pre-allocation cap, to prove the 
cap bounds only the initial capacity
+        // and the container still grows to hold every element (the cap must 
never be used as the read-loop bound)
+        assertRoundTripsFully(buildIntList(20000));
+    }
+
+    @Test
+    public void shouldRoundTripMapLargerThanPreallocCap() throws IOException {
+        final Map<Integer, Integer> map = new LinkedHashMap<>();
+        for (int i = 0; i < 20000; i++) map.put(i, i);
+        assertRoundTripsFully(map);
+    }
+
+    private List<Integer> buildIntList(final int count) {
+        final List<Integer> list = new ArrayList<>();
+        for (int i = 0; i < count; i++) list.add(i);
+        return list;
+    }
+
+    private void assertRoundTripsFully(final Object value) throws IOException {
+        final Buffer buffer = 
bufferFactory.create(ByteBufAllocator.DEFAULT.buffer());
+        try {
+            writer.write(value, buffer);
+            assertEquals(value, reader.read(buffer));
+        } finally {
+            buffer.release();
+        }
+    }
+}

Reply via email to