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();
+ }
+ }
+}