From 77ce23d9ff50e15b2bba651e87b422a0865034d5 Mon Sep 17 00:00:00 2001 From: joszamama Date: Thu, 18 Jun 2026 12:26:32 +0200 Subject: [PATCH 01/13] Fixed unbounded allocation when reading primitive arrays with a corrupt length (#1282) When reading from a fixed buffer (no InputStream), a primitive array or byte[] was allocated from the declared length before checking that the input held enough data, so a tiny corrupt payload declaring a multi-billion element length triggered an OutOfMemoryError. The declared length is now validated against the remaining bytes (every element needs at least one byte) before allocating, in Input, ByteBufferInput, UnsafeInput and UnsafeByteBufferInput. --- .../kryo/io/ByteBufferInput.java | 16 +++---- src/com/esotericsoftware/kryo/io/Input.java | 25 ++++++----- .../kryo/unsafe/UnsafeByteBufferInput.java | 14 +++--- .../kryo/unsafe/UnsafeInput.java | 14 +++--- .../kryo/io/InputOutputTest.java | 43 +++++++++++++++++++ .../kryo/io/UnsafeInputOutputTest.java | 14 ++++++ 6 files changed, 94 insertions(+), 32 deletions(-) diff --git a/src/com/esotericsoftware/kryo/io/ByteBufferInput.java b/src/com/esotericsoftware/kryo/io/ByteBufferInput.java index fc2c58f65..3d8c36c20 100644 --- a/src/com/esotericsoftware/kryo/io/ByteBufferInput.java +++ b/src/com/esotericsoftware/kryo/io/ByteBufferInput.java @@ -333,7 +333,7 @@ public int readByteUnsigned () throws KryoException { } public byte[] readBytes (int length) throws KryoException { - byte[] bytes = new byte[length]; + byte[] bytes = new byte[validateArrayLength(length)]; readBytes(bytes, 0, length); return bytes; } @@ -858,7 +858,7 @@ private String readAscii_slow (int charCount) { // Primitive arrays: public int[] readInts (int length) throws KryoException { - int[] array = new int[length]; + int[] array = new int[validateArrayLength(length)]; if (optional(length << 2) == length << 2) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { @@ -876,7 +876,7 @@ public int[] readInts (int length) throws KryoException { } public long[] readLongs (int length) throws KryoException { - long[] array = new long[length]; + long[] array = new long[validateArrayLength(length)]; if (optional(length << 3) == length << 3) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { @@ -898,7 +898,7 @@ public long[] readLongs (int length) throws KryoException { } public float[] readFloats (int length) throws KryoException { - float[] array = new float[length]; + float[] array = new float[validateArrayLength(length)]; if (optional(length << 2) == length << 2) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { @@ -916,7 +916,7 @@ public float[] readFloats (int length) throws KryoException { } public double[] readDoubles (int length) throws KryoException { - double[] array = new double[length]; + double[] array = new double[validateArrayLength(length)]; if (optional(length << 3) == length << 3) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { @@ -938,7 +938,7 @@ public double[] readDoubles (int length) throws KryoException { } public short[] readShorts (int length) throws KryoException { - short[] array = new short[length]; + short[] array = new short[validateArrayLength(length)]; if (optional(length << 1) == length << 1) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) @@ -952,7 +952,7 @@ public short[] readShorts (int length) throws KryoException { } public char[] readChars (int length) throws KryoException { - char[] array = new char[length]; + char[] array = new char[validateArrayLength(length)]; if (optional(length << 1) == length << 1) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) @@ -966,7 +966,7 @@ public char[] readChars (int length) throws KryoException { } public boolean[] readBooleans (int length) throws KryoException { - boolean[] array = new boolean[length]; + boolean[] array = new boolean[validateArrayLength(length)]; if (optional(length) == length) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) diff --git a/src/com/esotericsoftware/kryo/io/Input.java b/src/com/esotericsoftware/kryo/io/Input.java index 6b83334b2..6bc44c2b1 100644 --- a/src/com/esotericsoftware/kryo/io/Input.java +++ b/src/com/esotericsoftware/kryo/io/Input.java @@ -348,7 +348,7 @@ public int readByteUnsigned () throws KryoException { /** Reads the specified number of bytes into a new byte[]. */ public byte[] readBytes (int length) throws KryoException { - byte[] bytes = new byte[length]; + byte[] bytes = new byte[validateArrayLength(length)]; readBytes(bytes, 0, length); return bytes; } @@ -941,9 +941,14 @@ private String readAscii_slow (int charCount) { // Primitive arrays: + protected int validateArrayLength (int length) { + if (inputStream == null && length > limit - position) throw new KryoBufferUnderflowException("Buffer underflow."); + return length; + } + /** Reads an int array in bulk. This may be more efficient than reading them individually. */ public int[] readInts (int length) throws KryoException { - int[] array = new int[length]; + int[] array = new int[validateArrayLength(length)]; if (optional(length << 2) == length << 2) { byte[] buffer = this.buffer; int p = this.position; @@ -965,7 +970,7 @@ public int[] readInts (int length) throws KryoException { * {@link #setVariableLengthEncoding(boolean)}. This may be more efficient than reading them individually. */ public int[] readInts (int length, boolean optimizePositive) throws KryoException { if (varEncoding) { - int[] array = new int[length]; + int[] array = new int[validateArrayLength(length)]; for (int i = 0; i < length; i++) array[i] = readVarInt(optimizePositive); return array; @@ -975,7 +980,7 @@ public int[] readInts (int length, boolean optimizePositive) throws KryoExceptio /** Reads a long array in bulk. This may be more efficient than reading them individually. */ public long[] readLongs (int length) throws KryoException { - long[] array = new long[length]; + long[] array = new long[validateArrayLength(length)]; if (optional(length << 3) == length << 3) { byte[] buffer = this.buffer; int p = this.position; @@ -1001,7 +1006,7 @@ public long[] readLongs (int length) throws KryoException { * {@link #setVariableLengthEncoding(boolean)}. This may be more efficient than reading them individually. */ public long[] readLongs (int length, boolean optimizePositive) throws KryoException { if (varEncoding) { - long[] array = new long[length]; + long[] array = new long[validateArrayLength(length)]; for (int i = 0; i < length; i++) array[i] = readVarLong(optimizePositive); return array; @@ -1011,7 +1016,7 @@ public long[] readLongs (int length, boolean optimizePositive) throws KryoExcept /** Reads a float array in bulk. This may be more efficient than reading them individually. */ public float[] readFloats (int length) throws KryoException { - float[] array = new float[length]; + float[] array = new float[validateArrayLength(length)]; if (optional(length << 2) == length << 2) { byte[] buffer = this.buffer; int p = this.position; @@ -1031,7 +1036,7 @@ public float[] readFloats (int length) throws KryoException { /** Reads a double array in bulk. This may be more efficient than reading them individually. */ public double[] readDoubles (int length) throws KryoException { - double[] array = new double[length]; + double[] array = new double[validateArrayLength(length)]; if (optional(length << 3) == length << 3) { byte[] buffer = this.buffer; int p = this.position; @@ -1055,7 +1060,7 @@ public double[] readDoubles (int length) throws KryoException { /** Reads a short array in bulk. This may be more efficient than reading them individually. */ public short[] readShorts (int length) throws KryoException { - short[] array = new short[length]; + short[] array = new short[validateArrayLength(length)]; if (optional(length << 1) == length << 1) { byte[] buffer = this.buffer; int p = this.position; @@ -1071,7 +1076,7 @@ public short[] readShorts (int length) throws KryoException { /** Reads a char array in bulk. This may be more efficient than reading them individually. */ public char[] readChars (int length) throws KryoException { - char[] array = new char[length]; + char[] array = new char[validateArrayLength(length)]; if (optional(length << 1) == length << 1) { byte[] buffer = this.buffer; int p = this.position; @@ -1087,7 +1092,7 @@ public char[] readChars (int length) throws KryoException { /** Reads a boolean array in bulk. This may be more efficient than reading them individually. */ public boolean[] readBooleans (int length) throws KryoException { - boolean[] array = new boolean[length]; + boolean[] array = new boolean[validateArrayLength(length)]; if (optional(length) == length) { byte[] buffer = this.buffer; int p = this.position; diff --git a/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java b/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java index 2e5945694..faafedbf2 100644 --- a/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java +++ b/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java @@ -185,43 +185,43 @@ public boolean readBoolean () throws KryoException { } public int[] readInts (int length) throws KryoException { - int[] array = new int[length]; + int[] array = new int[validateArrayLength(length)]; readBytes(array, intArrayBaseOffset, length << 2); return array; } public long[] readLongs (int length) throws KryoException { - long[] array = new long[length]; + long[] array = new long[validateArrayLength(length)]; readBytes(array, longArrayBaseOffset, length << 3); return array; } public float[] readFloats (int length) throws KryoException { - float[] array = new float[length]; + float[] array = new float[validateArrayLength(length)]; readBytes(array, floatArrayBaseOffset, length << 2); return array; } public double[] readDoubles (int length) throws KryoException { - double[] array = new double[length]; + double[] array = new double[validateArrayLength(length)]; readBytes(array, doubleArrayBaseOffset, length << 3); return array; } public short[] readShorts (int length) throws KryoException { - short[] array = new short[length]; + short[] array = new short[validateArrayLength(length)]; readBytes(array, shortArrayBaseOffset, length << 1); return array; } public char[] readChars (int length) throws KryoException { - char[] array = new char[length]; + char[] array = new char[validateArrayLength(length)]; readBytes(array, charArrayBaseOffset, length << 1); return array; } public boolean[] readBooleans (int length) throws KryoException { - boolean[] array = new boolean[length]; + boolean[] array = new boolean[validateArrayLength(length)]; readBytes(array, booleanArrayBaseOffset, length); return array; } diff --git a/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java b/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java index 79f673b45..e8c1ccd78 100644 --- a/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java +++ b/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java @@ -136,43 +136,43 @@ public boolean readBoolean () throws KryoException { } public int[] readInts (int length) throws KryoException { - int[] array = new int[length]; + int[] array = new int[validateArrayLength(length)]; readBytes(array, intArrayBaseOffset, length << 2); return array; } public long[] readLongs (int length) throws KryoException { - long[] array = new long[length]; + long[] array = new long[validateArrayLength(length)]; readBytes(array, longArrayBaseOffset, length << 3); return array; } public float[] readFloats (int length) throws KryoException { - float[] array = new float[length]; + float[] array = new float[validateArrayLength(length)]; readBytes(array, floatArrayBaseOffset, length << 2); return array; } public double[] readDoubles (int length) throws KryoException { - double[] array = new double[length]; + double[] array = new double[validateArrayLength(length)]; readBytes(array, doubleArrayBaseOffset, length << 3); return array; } public short[] readShorts (int length) throws KryoException { - short[] array = new short[length]; + short[] array = new short[validateArrayLength(length)]; readBytes(array, shortArrayBaseOffset, length << 1); return array; } public char[] readChars (int length) throws KryoException { - char[] array = new char[length]; + char[] array = new char[validateArrayLength(length)]; readBytes(array, charArrayBaseOffset, length << 1); return array; } public boolean[] readBooleans (int length) throws KryoException { - boolean[] array = new boolean[length]; + boolean[] array = new boolean[validateArrayLength(length)]; readBytes(array, booleanArrayBaseOffset, length); return array; } diff --git a/test/com/esotericsoftware/kryo/io/InputOutputTest.java b/test/com/esotericsoftware/kryo/io/InputOutputTest.java index b0e5a19f3..8f7edec18 100644 --- a/test/com/esotericsoftware/kryo/io/InputOutputTest.java +++ b/test/com/esotericsoftware/kryo/io/InputOutputTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.*; import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.KryoTestCase; import com.esotericsoftware.kryo.Registration; import com.esotericsoftware.kryo.io.KryoBufferUnderflowException; @@ -139,6 +140,48 @@ void testUnderflow () throws IOException { assertTrue(thrown.getMessage().equals("Buffer underflow.")); } + @Test + void testArrayLengthValidation () throws IOException { + int hugeLength = 2000000000; + + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[5]).readInts(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[5]).readInts(hugeLength, true)); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[5]).readLongs(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[5]).readLongs(hugeLength, true)); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[5]).readFloats(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[5]).readDoubles(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[5]).readShorts(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[5]).readChars(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[5]).readBooleans(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[5]).readBytes(hugeLength)); + + assertThrows(KryoBufferUnderflowException.class, () -> new ByteBufferInput(new byte[5]).readInts(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new ByteBufferInput(new byte[5]).readDoubles(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new ByteBufferInput(new byte[5]).readBytes(hugeLength)); + + int[] ints = {1, 2, 3, 4, 5}; + Output output = new Output(64); + output.writeInts(ints, 0, ints.length, false); + output.flush(); + assertArrayEquals(ints, new Input(output.toBytes()).readInts(ints.length, false)); + } + + @Test + void testMaliciousArrayLength () throws IOException { + kryo.register(int[].class); + + Output declared = new Output(8); + declared.writeVarInt(2000000001, true); + declared.flush(); + assertThrows(KryoException.class, () -> kryo.readObject(new Input(declared.toBytes()), int[].class)); + + int[] valid = {1, 2, 3, 4, 5}; + Output output = new Output(64); + kryo.writeObject(output, valid); + output.flush(); + assertArrayEquals(valid, kryo.readObject(new Input(output.toBytes()), int[].class)); + } + @Test void testStrings () throws IOException { runStringTest(new Output(4096)); diff --git a/test/com/esotericsoftware/kryo/io/UnsafeInputOutputTest.java b/test/com/esotericsoftware/kryo/io/UnsafeInputOutputTest.java index 2d6256d33..8236c2716 100644 --- a/test/com/esotericsoftware/kryo/io/UnsafeInputOutputTest.java +++ b/test/com/esotericsoftware/kryo/io/UnsafeInputOutputTest.java @@ -23,6 +23,8 @@ import static org.junit.jupiter.api.Assertions.*; import com.esotericsoftware.kryo.Unsafe; +import com.esotericsoftware.kryo.io.KryoBufferUnderflowException; +import com.esotericsoftware.kryo.unsafe.UnsafeByteBufferInput; import com.esotericsoftware.kryo.unsafe.UnsafeInput; import com.esotericsoftware.kryo.unsafe.UnsafeOutput; @@ -35,6 +37,18 @@ /** @author Nathan Sweet */ @Unsafe class UnsafeInputOutputTest { + @Test + void testArrayLengthValidation () { + int hugeLength = 2000000000; + + assertThrows(KryoBufferUnderflowException.class, () -> new UnsafeInput(new byte[5]).readInts(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new UnsafeInput(new byte[5]).readLongs(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new UnsafeInput(new byte[5]).readDoubles(hugeLength)); + + assertThrows(KryoBufferUnderflowException.class, () -> new UnsafeByteBufferInput(new byte[5]).readInts(hugeLength)); + assertThrows(KryoBufferUnderflowException.class, () -> new UnsafeByteBufferInput(new byte[5]).readDoubles(hugeLength)); + } + @Test void testOutputStream () { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); From 63816ebfdbb1cd800e02ff2fb13d564e42c0698f Mon Sep 17 00:00:00 2001 From: joszamama Date: Thu, 18 Jun 2026 17:31:08 +0200 Subject: [PATCH 02/13] fix: bound declared string length before allocating --- .../esotericsoftware/kryo/io/ByteBufferInput.java | 2 +- src/com/esotericsoftware/kryo/io/Input.java | 4 ++-- .../esotericsoftware/kryo/io/InputOutputTest.java | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/com/esotericsoftware/kryo/io/ByteBufferInput.java b/src/com/esotericsoftware/kryo/io/ByteBufferInput.java index 3d8c36c20..51a3ddf61 100644 --- a/src/com/esotericsoftware/kryo/io/ByteBufferInput.java +++ b/src/com/esotericsoftware/kryo/io/ByteBufferInput.java @@ -762,7 +762,7 @@ public StringBuilder readStringBuilder () { } private void readUtf8Chars (int charCount) { - if (chars.length < charCount) chars = new char[charCount]; + if (chars.length < charCount) chars = new char[validateArrayLength(charCount)]; char[] chars = this.chars; // Try to read 7 bit ASCII chars. ByteBuffer byteBuffer = this.byteBuffer; diff --git a/src/com/esotericsoftware/kryo/io/Input.java b/src/com/esotericsoftware/kryo/io/Input.java index 6bc44c2b1..c5c41f068 100644 --- a/src/com/esotericsoftware/kryo/io/Input.java +++ b/src/com/esotericsoftware/kryo/io/Input.java @@ -848,7 +848,7 @@ public StringBuilder readStringBuilder () { } private void readUtf8Chars (int charCount) { - if (chars.length < charCount) chars = new char[charCount]; + if (chars.length < charCount) chars = new char[validateArrayLength(charCount)]; byte[] buffer = this.buffer; char[] chars = this.chars; // Try to read 7 bit ASCII chars. @@ -941,7 +941,7 @@ private String readAscii_slow (int charCount) { // Primitive arrays: - protected int validateArrayLength (int length) { + public int validateArrayLength (int length) { if (inputStream == null && length > limit - position) throw new KryoBufferUnderflowException("Buffer underflow."); return length; } diff --git a/test/com/esotericsoftware/kryo/io/InputOutputTest.java b/test/com/esotericsoftware/kryo/io/InputOutputTest.java index 8f7edec18..30584efd9 100644 --- a/test/com/esotericsoftware/kryo/io/InputOutputTest.java +++ b/test/com/esotericsoftware/kryo/io/InputOutputTest.java @@ -182,6 +182,21 @@ void testMaliciousArrayLength () throws IOException { assertArrayEquals(valid, kryo.readObject(new Input(output.toBytes()), int[].class)); } + @Test + void testMaliciousStringLength () throws IOException { + Output declared = new Output(8); + declared.writeVarIntFlag(true, 2000000001, true); + declared.flush(); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(declared.toBytes()).readString()); + assertThrows(KryoBufferUnderflowException.class, () -> new ByteBufferInput(declared.toBytes()).readString()); + + String value = "kryo \u00e9\u00fc\u1234 unicode string"; + Output output = new Output(64); + output.writeString(value); + output.flush(); + assertEquals(value, new Input(output.toBytes()).readString()); + } + @Test void testStrings () throws IOException { runStringTest(new Output(4096)); From 73f3a444f9c5ddf40395225b103c8f7548eb2953 Mon Sep 17 00:00:00 2001 From: joszamama Date: Thu, 18 Jun 2026 17:31:08 +0200 Subject: [PATCH 03/13] fix: clamp collection and map capacity to remaining input --- .../kryo/serializers/CollectionSerializer.java | 8 ++++++-- .../kryo/serializers/MapSerializer.java | 6 +++++- .../kryo/serializers/CollectionSerializerTest.java | 14 ++++++++++++++ .../kryo/serializers/MapSerializerTest.java | 12 ++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java b/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java index 299a72b2c..eb81b40f8 100644 --- a/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java @@ -179,6 +179,10 @@ protected T create (Kryo kryo, Input input, Class type, int size) { return collection; } + private static int clampSize (Input input, int size) { + return input.getInputStream() == null ? Math.min(size, input.limit() - input.position()) : size; + } + public T read (Kryo kryo, Input input, Class type) { Class elementClass = this.elementClass; Serializer elementSerializer = this.elementSerializer; @@ -203,7 +207,7 @@ public T read (Kryo kryo, Input input, Class type) { if (length == 0) return null; length--; - collection = create(kryo, input, type, length); + collection = create(kryo, input, type, clampSize(input, length)); kryo.reference(collection); if (length == 0) return collection; @@ -213,7 +217,7 @@ public T read (Kryo kryo, Input input, Class type) { if (length == 0) return null; length--; - collection = create(kryo, input, type, length); + collection = create(kryo, input, type, clampSize(input, length)); kryo.reference(collection); if (length == 0) return collection; diff --git a/src/com/esotericsoftware/kryo/serializers/MapSerializer.java b/src/com/esotericsoftware/kryo/serializers/MapSerializer.java index 2a0edf843..865cf4fd3 100644 --- a/src/com/esotericsoftware/kryo/serializers/MapSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/MapSerializer.java @@ -172,6 +172,10 @@ protected void writeHeader (Kryo kryo, Output output, T map) { /** Used by {@link #read(Kryo, Input, Class)} to create the new object. This can be overridden to customize object creation, eg * to call a constructor with arguments. The default implementation uses {@link Kryo#newInstance(Class)} with a special case * for HashMap. */ + private static int clampSize (Input input, int size) { + return input.getInputStream() == null ? Math.min(size, input.limit() - input.position()) : size; + } + protected T create (Kryo kryo, Input input, Class type, int size) { if (type == HashMap.class) { if (size < 3) @@ -188,7 +192,7 @@ public T read (Kryo kryo, Input input, Class type) { if (length == 0) return null; length--; - T map = create(kryo, input, type, length); + T map = create(kryo, input, type, clampSize(input, length)); kryo.reference(map); if (length == 0) return map; diff --git a/test/com/esotericsoftware/kryo/serializers/CollectionSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/CollectionSerializerTest.java index a98650948..78cd6d57c 100644 --- a/test/com/esotericsoftware/kryo/serializers/CollectionSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/CollectionSerializerTest.java @@ -22,7 +22,10 @@ import static org.junit.jupiter.api.Assertions.*; import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.KryoTestCase; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; import com.esotericsoftware.kryo.serializers.DefaultSerializers.StringSerializer; import com.esotericsoftware.kryo.serializers.MapSerializerTest.KeyComparator; import com.esotericsoftware.kryo.serializers.MapSerializerTest.KeyThatIsntComparable; @@ -43,6 +46,17 @@ class CollectionSerializerTest extends KryoTestCase { supportsCopy = true; } + @Test + void testMaliciousCollectionSize () { + kryo.setReferences(false); + kryo.register(ArrayList.class); + Output declared = new Output(8); + kryo.writeClass(declared, ArrayList.class); + declared.writeVarIntFlag(false, 2000000001, true); + declared.flush(); + assertThrows(KryoException.class, () -> kryo.readClassAndObject(new Input(declared.toBytes()))); + } + @Test void testCollections () { kryo.register(ArrayList.class); diff --git a/test/com/esotericsoftware/kryo/serializers/MapSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/MapSerializerTest.java index 648785559..3a7a05805 100644 --- a/test/com/esotericsoftware/kryo/serializers/MapSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/MapSerializerTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.*; import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.KryoTestCase; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; @@ -50,6 +51,17 @@ class MapSerializerTest extends KryoTestCase { supportsCopy = true; } + @Test + void testMaliciousMapSize () { + kryo.setReferences(false); + kryo.register(HashMap.class); + Output declared = new Output(8); + kryo.writeClass(declared, HashMap.class); + declared.writeVarInt(2000000001, true); + declared.flush(); + assertThrows(KryoException.class, () -> kryo.readClassAndObject(new Input(declared.toBytes()))); + } + @Test void testMaps () { kryo.register(HashMap.class); From 0c1f930059bdf6c27a4a2825742d110851ea8d83 Mon Sep 17 00:00:00 2001 From: joszamama Date: Thu, 18 Jun 2026 17:31:08 +0200 Subject: [PATCH 04/13] fix: guard string-array, closure and field-count allocation --- .../esotericsoftware/kryo/serializers/ClosureSerializer.java | 2 +- .../kryo/serializers/CompatibleFieldSerializer.java | 2 +- .../kryo/serializers/DefaultArraySerializers.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/com/esotericsoftware/kryo/serializers/ClosureSerializer.java b/src/com/esotericsoftware/kryo/serializers/ClosureSerializer.java index 5061e64a8..aa2282f1a 100644 --- a/src/com/esotericsoftware/kryo/serializers/ClosureSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/ClosureSerializer.java @@ -98,7 +98,7 @@ public void write (Kryo kryo, Output output, Object object) { public Object read (Kryo kryo, Input input, Class type) { int count = input.readVarInt(true); - Object[] capturedArgs = new Object[count]; + Object[] capturedArgs = new Object[input.validateArrayLength(count)]; for (int i = 0; i < count; i++) capturedArgs[i] = kryo.readClassAndObject(input); Class capturingClass = kryo.readClass(input).getType(); diff --git a/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java index 75874321d..c3708c36c 100644 --- a/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java @@ -193,7 +193,7 @@ public T read (Kryo kryo, Input input, Class type) { private CachedField[] readFields (Kryo kryo, Input input) { if (TRACE) trace("kryo", "Read fields for class: " + type.getName()); - int length = input.readVarInt(true); + int length = input.validateArrayLength(input.readVarInt(true)); String[] names = new String[length]; for (int i = 0; i < length; i++) { names[i] = input.readString(); diff --git a/src/com/esotericsoftware/kryo/serializers/DefaultArraySerializers.java b/src/com/esotericsoftware/kryo/serializers/DefaultArraySerializers.java index fd3c261af..97b88137b 100644 --- a/src/com/esotericsoftware/kryo/serializers/DefaultArraySerializers.java +++ b/src/com/esotericsoftware/kryo/serializers/DefaultArraySerializers.java @@ -277,7 +277,7 @@ public void write (Kryo kryo, Output output, String[] object) { public String[] read (Kryo kryo, Input input, Class type) { int length = input.readVarInt(true); if (length == NULL) return null; - String[] array = new String[--length]; + String[] array = new String[input.validateArrayLength(--length)]; if (kryo.getReferences() && kryo.getReferenceResolver().useReferences(String.class)) { Serializer serializer = kryo.getSerializer(String.class); for (int i = 0; i < length; i++) From d02d8c4a96d624a12e22689c6001591b2b635847 Mon Sep 17 00:00:00 2001 From: joszamama Date: Thu, 18 Jun 2026 18:24:45 +0200 Subject: [PATCH 05/13] feat: add configurable maxArraySize to bound deserialized sizes --- src/com/esotericsoftware/kryo/io/Input.java | 10 ++++++++++ .../kryo/serializers/CollectionSerializer.java | 3 +++ .../kryo/serializers/MapSerializer.java | 3 +++ .../esotericsoftware/kryo/io/InputOutputTest.java | 10 ++++++++++ .../kryo/serializers/CollectionSerializerTest.java | 14 ++++++++++++++ .../kryo/serializers/MapSerializerTest.java | 13 +++++++++++++ 6 files changed, 53 insertions(+) diff --git a/src/com/esotericsoftware/kryo/io/Input.java b/src/com/esotericsoftware/kryo/io/Input.java index c5c41f068..b1bf18148 100644 --- a/src/com/esotericsoftware/kryo/io/Input.java +++ b/src/com/esotericsoftware/kryo/io/Input.java @@ -39,6 +39,7 @@ public class Input extends InputStream implements Poolable { protected char[] chars = new char[32]; protected InputStream inputStream; protected boolean varEncoding = true; + protected int maxArraySize = Integer.MAX_VALUE; /** Creates an uninitialized Input, {@link #setBuffer(byte[])} must be called before the Input is used. */ public Input () { @@ -126,6 +127,14 @@ public void setVariableLengthEncoding (boolean varEncoding) { this.varEncoding = varEncoding; } + public int getMaxArraySize () { + return maxArraySize; + } + + public void setMaxArraySize (int maxArraySize) { + this.maxArraySize = maxArraySize; + } + /** Returns the total number of bytes read. */ public long total () { return total + position; @@ -942,6 +951,7 @@ private String readAscii_slow (int charCount) { // Primitive arrays: public int validateArrayLength (int length) { + if (length > maxArraySize) throw new KryoException("Array size is larger than maxArraySize: " + length + " > " + maxArraySize); if (inputStream == null && length > limit - position) throw new KryoBufferUnderflowException("Buffer underflow."); return length; } diff --git a/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java b/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java index eb81b40f8..1c6cfb4bc 100644 --- a/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java @@ -30,6 +30,7 @@ import java.util.HashSet; import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.Registration; import com.esotericsoftware.kryo.Serializer; import com.esotericsoftware.kryo.SerializerFactory; @@ -180,6 +181,8 @@ protected T create (Kryo kryo, Input input, Class type, int size) { } private static int clampSize (Input input, int size) { + if (size > input.getMaxArraySize()) + throw new KryoException("Array size is larger than maxArraySize: " + size + " > " + input.getMaxArraySize()); return input.getInputStream() == null ? Math.min(size, input.limit() - input.position()) : size; } diff --git a/src/com/esotericsoftware/kryo/serializers/MapSerializer.java b/src/com/esotericsoftware/kryo/serializers/MapSerializer.java index 865cf4fd3..55f916c77 100644 --- a/src/com/esotericsoftware/kryo/serializers/MapSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/MapSerializer.java @@ -20,6 +20,7 @@ package com.esotericsoftware.kryo.serializers; import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.Serializer; import com.esotericsoftware.kryo.SerializerFactory; import com.esotericsoftware.kryo.io.Input; @@ -173,6 +174,8 @@ protected void writeHeader (Kryo kryo, Output output, T map) { * to call a constructor with arguments. The default implementation uses {@link Kryo#newInstance(Class)} with a special case * for HashMap. */ private static int clampSize (Input input, int size) { + if (size > input.getMaxArraySize()) + throw new KryoException("Array size is larger than maxArraySize: " + size + " > " + input.getMaxArraySize()); return input.getInputStream() == null ? Math.min(size, input.limit() - input.position()) : size; } diff --git a/test/com/esotericsoftware/kryo/io/InputOutputTest.java b/test/com/esotericsoftware/kryo/io/InputOutputTest.java index 30584efd9..3d8c406af 100644 --- a/test/com/esotericsoftware/kryo/io/InputOutputTest.java +++ b/test/com/esotericsoftware/kryo/io/InputOutputTest.java @@ -197,6 +197,16 @@ void testMaliciousStringLength () throws IOException { assertEquals(value, new Input(output.toBytes()).readString()); } + @Test + void testMaxArraySize () throws IOException { + Input stream = new Input(new ByteArrayInputStream(new byte[16])); + stream.setMaxArraySize(1024); + assertEquals(1024, stream.getMaxArraySize()); + assertThrows(KryoException.class, () -> stream.readInts(2000000)); + assertThrows(KryoException.class, () -> stream.readBytes(2000000)); + assertThrows(KryoException.class, () -> stream.readLongs(2000000, true)); + } + @Test void testStrings () throws IOException { runStringTest(new Output(4096)); diff --git a/test/com/esotericsoftware/kryo/serializers/CollectionSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/CollectionSerializerTest.java index 78cd6d57c..9c6615308 100644 --- a/test/com/esotericsoftware/kryo/serializers/CollectionSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/CollectionSerializerTest.java @@ -30,6 +30,7 @@ import com.esotericsoftware.kryo.serializers.MapSerializerTest.KeyComparator; import com.esotericsoftware.kryo.serializers.MapSerializerTest.KeyThatIsntComparable; +import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -57,6 +58,19 @@ void testMaliciousCollectionSize () { assertThrows(KryoException.class, () -> kryo.readClassAndObject(new Input(declared.toBytes()))); } + @Test + void testMaxCollectionSize () { + kryo.setReferences(false); + kryo.register(ArrayList.class); + Output declared = new Output(8); + kryo.writeClass(declared, ArrayList.class); + declared.writeVarIntFlag(false, 2000000001, true); + declared.flush(); + Input stream = new Input(new ByteArrayInputStream(declared.toBytes())); + stream.setMaxArraySize(1024); + assertThrows(KryoException.class, () -> kryo.readClassAndObject(stream)); + } + @Test void testCollections () { kryo.register(ArrayList.class); diff --git a/test/com/esotericsoftware/kryo/serializers/MapSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/MapSerializerTest.java index 3a7a05805..6faa71e82 100644 --- a/test/com/esotericsoftware/kryo/serializers/MapSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/MapSerializerTest.java @@ -62,6 +62,19 @@ void testMaliciousMapSize () { assertThrows(KryoException.class, () -> kryo.readClassAndObject(new Input(declared.toBytes()))); } + @Test + void testMaxMapSize () { + kryo.setReferences(false); + kryo.register(HashMap.class); + Output declared = new Output(8); + kryo.writeClass(declared, HashMap.class); + declared.writeVarInt(2000000001, true); + declared.flush(); + Input stream = new Input(new ByteArrayInputStream(declared.toBytes())); + stream.setMaxArraySize(1024); + assertThrows(KryoException.class, () -> kryo.readClassAndObject(stream)); + } + @Test void testMaps () { kryo.register(HashMap.class); From 0827a7ed37200e040358956d1fdfe1b13230883f Mon Sep 17 00:00:00 2001 From: joszamama Date: Fri, 19 Jun 2026 17:16:32 +0200 Subject: [PATCH 06/13] fix: validate array length by byte width Account for element width and reject sizes whose byte count overflows an int, preventing over-allocation and the length<= 0. */ public void setMaxArraySize (int maxArraySize) { + if (maxArraySize < 0) throw new IllegalArgumentException("maxArraySize must be >= 0: " + maxArraySize); this.maxArraySize = maxArraySize; } @@ -950,15 +960,37 @@ private String readAscii_slow (int charCount) { // Primitive arrays: + /** Validates a declared array length read from the input, treating each element as occupying at least one byte. Equivalent to + * {@link #validateArrayLength(int, int)} with a bytes per element of 1. */ public int validateArrayLength (int length) { - if (length > maxArraySize) throw new KryoException("Array size is larger than maxArraySize: " + length + " > " + maxArraySize); - if (inputStream == null && length > limit - position) throw new KryoBufferUnderflowException("Buffer underflow."); + return validateArrayLength(length, 1); + } + + /** Validates a declared array length read from the input before it is used to allocate. Throws if {@code length} exceeds + * {@link #setMaxArraySize(int) maxArraySize}, if the total size {@code length * bytesPerElement} would overflow an int, or, for + * a buffer-backed input (no {@link InputStream}), if it exceeds the bytes remaining. A buffer-backed input can never hold more + * elements than it has bytes remaining, since every element occupies at least {@code bytesPerElement} bytes, so a larger + * declared length is provably malformed and is rejected before allocating. Stream-backed input cannot be checked this way and + * is bounded only by {@code maxArraySize} and the overflow check. + * @param bytesPerElement the minimum number of bytes each element occupies in the input. */ + public int validateArrayLength (int length, int bytesPerElement) { + if (length > maxArraySize) + throw new KryoException("Declared size larger than maxArraySize: " + length + " > " + maxArraySize); + long bytes = (long)length * bytesPerElement; + if (inputStream == null) { + // A buffer-backed input cannot supply more bytes than it has remaining, so a larger declared size is malformed. + if (bytes > limit - position) throw new KryoBufferUnderflowException("Buffer underflow."); + } else if (bytes > Integer.MAX_VALUE) { + // Stream-backed input cannot be checked against the buffered window, but a size whose byte count overflows an int + // can never be read and would overflow the length << n shifts in the bulk readers, so reject it. + throw new KryoException("Declared size is too large to read: " + length + " elements of " + bytesPerElement + " bytes"); + } return length; } /** Reads an int array in bulk. This may be more efficient than reading them individually. */ public int[] readInts (int length) throws KryoException { - int[] array = new int[validateArrayLength(length)]; + int[] array = new int[validateArrayLength(length, 4)]; if (optional(length << 2) == length << 2) { byte[] buffer = this.buffer; int p = this.position; @@ -990,7 +1022,7 @@ public int[] readInts (int length, boolean optimizePositive) throws KryoExceptio /** Reads a long array in bulk. This may be more efficient than reading them individually. */ public long[] readLongs (int length) throws KryoException { - long[] array = new long[validateArrayLength(length)]; + long[] array = new long[validateArrayLength(length, 8)]; if (optional(length << 3) == length << 3) { byte[] buffer = this.buffer; int p = this.position; @@ -1026,7 +1058,7 @@ public long[] readLongs (int length, boolean optimizePositive) throws KryoExcept /** Reads a float array in bulk. This may be more efficient than reading them individually. */ public float[] readFloats (int length) throws KryoException { - float[] array = new float[validateArrayLength(length)]; + float[] array = new float[validateArrayLength(length, 4)]; if (optional(length << 2) == length << 2) { byte[] buffer = this.buffer; int p = this.position; @@ -1046,7 +1078,7 @@ public float[] readFloats (int length) throws KryoException { /** Reads a double array in bulk. This may be more efficient than reading them individually. */ public double[] readDoubles (int length) throws KryoException { - double[] array = new double[validateArrayLength(length)]; + double[] array = new double[validateArrayLength(length, 8)]; if (optional(length << 3) == length << 3) { byte[] buffer = this.buffer; int p = this.position; @@ -1070,7 +1102,7 @@ public double[] readDoubles (int length) throws KryoException { /** Reads a short array in bulk. This may be more efficient than reading them individually. */ public short[] readShorts (int length) throws KryoException { - short[] array = new short[validateArrayLength(length)]; + short[] array = new short[validateArrayLength(length, 2)]; if (optional(length << 1) == length << 1) { byte[] buffer = this.buffer; int p = this.position; @@ -1086,7 +1118,7 @@ public short[] readShorts (int length) throws KryoException { /** Reads a char array in bulk. This may be more efficient than reading them individually. */ public char[] readChars (int length) throws KryoException { - char[] array = new char[validateArrayLength(length)]; + char[] array = new char[validateArrayLength(length, 2)]; if (optional(length << 1) == length << 1) { byte[] buffer = this.buffer; int p = this.position; diff --git a/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java b/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java index faafedbf2..11f4e0b0f 100644 --- a/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java +++ b/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java @@ -185,37 +185,37 @@ public boolean readBoolean () throws KryoException { } public int[] readInts (int length) throws KryoException { - int[] array = new int[validateArrayLength(length)]; + int[] array = new int[validateArrayLength(length, 4)]; readBytes(array, intArrayBaseOffset, length << 2); return array; } public long[] readLongs (int length) throws KryoException { - long[] array = new long[validateArrayLength(length)]; + long[] array = new long[validateArrayLength(length, 8)]; readBytes(array, longArrayBaseOffset, length << 3); return array; } public float[] readFloats (int length) throws KryoException { - float[] array = new float[validateArrayLength(length)]; + float[] array = new float[validateArrayLength(length, 4)]; readBytes(array, floatArrayBaseOffset, length << 2); return array; } public double[] readDoubles (int length) throws KryoException { - double[] array = new double[validateArrayLength(length)]; + double[] array = new double[validateArrayLength(length, 8)]; readBytes(array, doubleArrayBaseOffset, length << 3); return array; } public short[] readShorts (int length) throws KryoException { - short[] array = new short[validateArrayLength(length)]; + short[] array = new short[validateArrayLength(length, 2)]; readBytes(array, shortArrayBaseOffset, length << 1); return array; } public char[] readChars (int length) throws KryoException { - char[] array = new char[validateArrayLength(length)]; + char[] array = new char[validateArrayLength(length, 2)]; readBytes(array, charArrayBaseOffset, length << 1); return array; } diff --git a/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java b/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java index e8c1ccd78..39bb6b309 100644 --- a/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java +++ b/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java @@ -136,37 +136,37 @@ public boolean readBoolean () throws KryoException { } public int[] readInts (int length) throws KryoException { - int[] array = new int[validateArrayLength(length)]; + int[] array = new int[validateArrayLength(length, 4)]; readBytes(array, intArrayBaseOffset, length << 2); return array; } public long[] readLongs (int length) throws KryoException { - long[] array = new long[validateArrayLength(length)]; + long[] array = new long[validateArrayLength(length, 8)]; readBytes(array, longArrayBaseOffset, length << 3); return array; } public float[] readFloats (int length) throws KryoException { - float[] array = new float[validateArrayLength(length)]; + float[] array = new float[validateArrayLength(length, 4)]; readBytes(array, floatArrayBaseOffset, length << 2); return array; } public double[] readDoubles (int length) throws KryoException { - double[] array = new double[validateArrayLength(length)]; + double[] array = new double[validateArrayLength(length, 8)]; readBytes(array, doubleArrayBaseOffset, length << 3); return array; } public short[] readShorts (int length) throws KryoException { - short[] array = new short[validateArrayLength(length)]; + short[] array = new short[validateArrayLength(length, 2)]; readBytes(array, shortArrayBaseOffset, length << 1); return array; } public char[] readChars (int length) throws KryoException { - char[] array = new char[validateArrayLength(length)]; + char[] array = new char[validateArrayLength(length, 2)]; readBytes(array, charArrayBaseOffset, length << 1); return array; } From 81c59aa3eb7fa104dae9654b658c0f7e5c8afb05 Mon Sep 17 00:00:00 2001 From: joszamama Date: Fri, 19 Jun 2026 17:16:32 +0200 Subject: [PATCH 07/13] refactor: align collection and map size error message --- .../esotericsoftware/kryo/serializers/CollectionSerializer.java | 2 +- src/com/esotericsoftware/kryo/serializers/MapSerializer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java b/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java index 1c6cfb4bc..496747cdf 100644 --- a/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java @@ -182,7 +182,7 @@ protected T create (Kryo kryo, Input input, Class type, int size) { private static int clampSize (Input input, int size) { if (size > input.getMaxArraySize()) - throw new KryoException("Array size is larger than maxArraySize: " + size + " > " + input.getMaxArraySize()); + throw new KryoException("Declared size larger than maxArraySize: " + size + " > " + input.getMaxArraySize()); return input.getInputStream() == null ? Math.min(size, input.limit() - input.position()) : size; } diff --git a/src/com/esotericsoftware/kryo/serializers/MapSerializer.java b/src/com/esotericsoftware/kryo/serializers/MapSerializer.java index 55f916c77..f9a136daf 100644 --- a/src/com/esotericsoftware/kryo/serializers/MapSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/MapSerializer.java @@ -175,7 +175,7 @@ protected void writeHeader (Kryo kryo, Output output, T map) { * for HashMap. */ private static int clampSize (Input input, int size) { if (size > input.getMaxArraySize()) - throw new KryoException("Array size is larger than maxArraySize: " + size + " > " + input.getMaxArraySize()); + throw new KryoException("Declared size larger than maxArraySize: " + size + " > " + input.getMaxArraySize()); return input.getInputStream() == null ? Math.min(size, input.limit() - input.position()) : size; } From 18ed4a865b46baee06a83f93262c99ae83799867 Mon Sep 17 00:00:00 2001 From: joszamama Date: Fri, 19 Jun 2026 17:16:32 +0200 Subject: [PATCH 08/13] test: cover byte-width array length validation --- .../kryo/io/InputOutputTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/com/esotericsoftware/kryo/io/InputOutputTest.java b/test/com/esotericsoftware/kryo/io/InputOutputTest.java index 3d8c406af..9540fab16 100644 --- a/test/com/esotericsoftware/kryo/io/InputOutputTest.java +++ b/test/com/esotericsoftware/kryo/io/InputOutputTest.java @@ -207,6 +207,31 @@ void testMaxArraySize () throws IOException { assertThrows(KryoException.class, () -> stream.readLongs(2000000, true)); } + @Test + void testArrayLengthByteWidthValidation () throws IOException { + // A declared element count that fits the buffer by count but not once element width is counted is rejected up front, + // before the (potentially much larger) array is allocated. Eight ints need 32 bytes, not 8. + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[8]).readInts(8)); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[8]).readLongs(8)); + assertThrows(KryoBufferUnderflowException.class, () -> new Input(new byte[8]).readDoubles(8)); + assertThrows(KryoBufferUnderflowException.class, () -> new ByteBufferInput(new byte[8]).readInts(8)); + + // Stream-backed: a declared size whose total byte count overflows an int can never be read and previously overflowed the + // length << n shifts (risking ArrayIndexOutOfBoundsException or a huge allocation). It is now rejected cleanly. + assertThrows(KryoException.class, () -> new Input(new ByteArrayInputStream(new byte[16])).readInts(600000000)); + assertThrows(KryoException.class, () -> new Input(new ByteArrayInputStream(new byte[16])).readLongs(300000000)); + + // setMaxArraySize rejects negative limits. + assertThrows(IllegalArgumentException.class, () -> new Input(new byte[1]).setMaxArraySize(-1)); + + // Valid fixed-width arrays still round-trip through the guard. + long[] longs = {1, 2, 3, 4, 5}; + Output output = new Output(64); + output.writeLongs(longs, 0, longs.length, false); + output.flush(); + assertArrayEquals(longs, new Input(output.toBytes()).readLongs(longs.length, false)); + } + @Test void testStrings () throws IOException { runStringTest(new Output(4096)); From 493a4c041118ce48b6c970d56b353ac2dd601351 Mon Sep 17 00:00:00 2001 From: joszamama Date: Fri, 19 Jun 2026 17:16:32 +0200 Subject: [PATCH 09/13] docs: document maxArraySize option --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 117416768..4e966eda0 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Kryo maintenance and development is sponsored by the [Gecko fund](https://geckof - [IO](#io) * [Output](#output) * [Input](#input) + * [Limiting deserialized size](#limiting-deserialized-size) * [ByteBuffers](#bytebuffers) * [Unsafe buffers](#unsafe-buffers) * [Variable length encoding](#variable-length-encoding) @@ -226,6 +227,21 @@ If the Input `close` is called, the Input's InputStream is closed, if any. If no The zero argument Input constructor creates an uninitialized Input. Input `setBuffer` must be called before the Input can be used. +### Limiting deserialized size + +When reading an array, string, collection, or map, Kryo first reads a declared size and uses it to allocate before reading any elements. A corrupt or malicious message can declare a size of billions, triggering a large allocation from only a few bytes. + +For an Input backed by a byte array (no InputStream), this is guarded automatically: the declared size cannot exceed the bytes remaining, since every element occupies at least one byte, so an impossible size is rejected with a `KryoException` before allocating. No configuration is needed and valid input is never affected. + +An Input reading from an InputStream cannot be checked this way, because the buffered window is not the total size. For that case, `setMaxArraySize` bounds the declared size: + +```java +Input input = new Input(inputStream); +input.setMaxArraySize(1024 * 1024); // reject any declared array/string/collection/map size above 1M elements +``` + +The default is `Integer.MAX_VALUE`, ie no limit, so by default behavior is unchanged and Kryo's trusted-source assumption is preserved: a valid payload never declares more elements than the input can supply, so the limit never fires on valid input. A declared size above the limit throws a `KryoException` before allocating. Callers that decode untrusted input, especially from a stream, should set a limit suited to their application. + ### ByteBuffers The ByteBufferOutput and ByteBufferInput classes work exactly like Output and Input, except they use a ByteBuffer rather than a byte array. From 7842723eeaa5f06619bf077df22dc4bc6db44933 Mon Sep 17 00:00:00 2001 From: joszamama Date: Fri, 19 Jun 2026 17:16:32 +0200 Subject: [PATCH 10/13] test: add double array benchmarks --- .../kryo/benchmarks/io/ArrayBenchmark.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/io/ArrayBenchmark.java b/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/io/ArrayBenchmark.java index 6649310c6..08ed6fa5d 100644 --- a/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/io/ArrayBenchmark.java +++ b/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/io/ArrayBenchmark.java @@ -77,6 +77,18 @@ public void readVarLongs (ReadLongsState state) { state.input.readLongs(state.longs.length, true); } + @Benchmark + public void writeDoubles (WriteDoublesState state) { + state.reset(); + state.output.writeDoubles(state.doubles, 0, state.doubles.length); + } + + @Benchmark + public void readDoubles (ReadDoublesState state) { + state.reset(); + state.input.readDoubles(state.doubles.length); + } + // @State(Scope.Thread) @@ -111,4 +123,18 @@ public void setup () { new ArrayBenchmark().writeLongs(this); } } + + @State(Scope.Thread) + static public class WriteDoublesState extends InputOutputState { + public double[] doubles = {0, 1, 2, 3, 4, 5, 63, 64, 65, 127, 128, 129, 4000, 5000, 6000, 16000, 32000, 256000, 1024000, + -1, -2, -3, -4, Double.MIN_VALUE, Double.MAX_VALUE, 0.5, -0.5, 3.14159}; + } + + @State(Scope.Thread) + static public class ReadDoublesState extends WriteDoublesState { + public void setup () { + super.setup(); + new ArrayBenchmark().writeDoubles(this); + } + } } From 51a169f77737f6128cba0075e8437581e07026d3 Mon Sep 17 00:00:00 2001 From: joszamama Date: Mon, 22 Jun 2026 16:37:19 +0200 Subject: [PATCH 11/13] refactor: address review feedback on allocation guards - Move clampSize into Input as an instance method and reuse it from MapSerializer and CollectionSerializer (removes the duplicated copies) - Use Integer.BYTES/Long.BYTES/Float.BYTES/Double.BYTES/Short.BYTES/ Character.BYTES for the validateArrayLength element widths and the unsafe bulk-read byte counts, instead of numeric literals and shifts (also resolves the Copilot byte-width note) - Move the create() JavaDoc onto the create method in MapSerializer - Remove the now-unused KryoException imports from the serializers --- .../kryo/io/ByteBufferInput.java | 12 +++++----- src/com/esotericsoftware/kryo/io/Input.java | 23 +++++++++++++----- .../serializers/CollectionSerializer.java | 11 ++------- .../kryo/serializers/MapSerializer.java | 9 +------ .../kryo/unsafe/UnsafeByteBufferInput.java | 24 +++++++++---------- .../kryo/unsafe/UnsafeInput.java | 24 +++++++++---------- 6 files changed, 50 insertions(+), 53 deletions(-) diff --git a/src/com/esotericsoftware/kryo/io/ByteBufferInput.java b/src/com/esotericsoftware/kryo/io/ByteBufferInput.java index 4f2e09134..9bdff5b28 100644 --- a/src/com/esotericsoftware/kryo/io/ByteBufferInput.java +++ b/src/com/esotericsoftware/kryo/io/ByteBufferInput.java @@ -858,7 +858,7 @@ private String readAscii_slow (int charCount) { // Primitive arrays: public int[] readInts (int length) throws KryoException { - int[] array = new int[validateArrayLength(length, 4)]; + int[] array = new int[validateArrayLength(length, Integer.BYTES)]; if (optional(length << 2) == length << 2) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { @@ -876,7 +876,7 @@ public int[] readInts (int length) throws KryoException { } public long[] readLongs (int length) throws KryoException { - long[] array = new long[validateArrayLength(length, 8)]; + long[] array = new long[validateArrayLength(length, Long.BYTES)]; if (optional(length << 3) == length << 3) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { @@ -898,7 +898,7 @@ public long[] readLongs (int length) throws KryoException { } public float[] readFloats (int length) throws KryoException { - float[] array = new float[validateArrayLength(length, 4)]; + float[] array = new float[validateArrayLength(length, Float.BYTES)]; if (optional(length << 2) == length << 2) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { @@ -916,7 +916,7 @@ public float[] readFloats (int length) throws KryoException { } public double[] readDoubles (int length) throws KryoException { - double[] array = new double[validateArrayLength(length, 8)]; + double[] array = new double[validateArrayLength(length, Double.BYTES)]; if (optional(length << 3) == length << 3) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { @@ -938,7 +938,7 @@ public double[] readDoubles (int length) throws KryoException { } public short[] readShorts (int length) throws KryoException { - short[] array = new short[validateArrayLength(length, 2)]; + short[] array = new short[validateArrayLength(length, Short.BYTES)]; if (optional(length << 1) == length << 1) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) @@ -952,7 +952,7 @@ public short[] readShorts (int length) throws KryoException { } public char[] readChars (int length) throws KryoException { - char[] array = new char[validateArrayLength(length, 2)]; + char[] array = new char[validateArrayLength(length, Character.BYTES)]; if (optional(length << 1) == length << 1) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) diff --git a/src/com/esotericsoftware/kryo/io/Input.java b/src/com/esotericsoftware/kryo/io/Input.java index c518b1f1b..c199794fd 100644 --- a/src/com/esotericsoftware/kryo/io/Input.java +++ b/src/com/esotericsoftware/kryo/io/Input.java @@ -988,9 +988,20 @@ public int validateArrayLength (int length, int bytesPerElement) { return length; } + /** Clamps a declared collection or map size before it is used as an initial capacity. Throws if {@code size} exceeds + * {@link #setMaxArraySize(int) maxArraySize}; otherwise, for a buffer-backed input (no {@link InputStream}), limits it to the + * bytes remaining, since a collection or map element can occupy as little as a single byte. A stream-backed input is returned + * unchanged (the remaining size is unknown). Unlike {@link #validateArrayLength(int, int)} the size is clamped rather than + * rejected, because the capacity is only a hint and elements may occupy zero bytes. */ + public int clampSize (int size) { + if (size > maxArraySize) + throw new KryoException("Declared size larger than maxArraySize: " + size + " > " + maxArraySize); + return inputStream == null ? Math.min(size, limit - position) : size; + } + /** Reads an int array in bulk. This may be more efficient than reading them individually. */ public int[] readInts (int length) throws KryoException { - int[] array = new int[validateArrayLength(length, 4)]; + int[] array = new int[validateArrayLength(length, Integer.BYTES)]; if (optional(length << 2) == length << 2) { byte[] buffer = this.buffer; int p = this.position; @@ -1022,7 +1033,7 @@ public int[] readInts (int length, boolean optimizePositive) throws KryoExceptio /** Reads a long array in bulk. This may be more efficient than reading them individually. */ public long[] readLongs (int length) throws KryoException { - long[] array = new long[validateArrayLength(length, 8)]; + long[] array = new long[validateArrayLength(length, Long.BYTES)]; if (optional(length << 3) == length << 3) { byte[] buffer = this.buffer; int p = this.position; @@ -1058,7 +1069,7 @@ public long[] readLongs (int length, boolean optimizePositive) throws KryoExcept /** Reads a float array in bulk. This may be more efficient than reading them individually. */ public float[] readFloats (int length) throws KryoException { - float[] array = new float[validateArrayLength(length, 4)]; + float[] array = new float[validateArrayLength(length, Float.BYTES)]; if (optional(length << 2) == length << 2) { byte[] buffer = this.buffer; int p = this.position; @@ -1078,7 +1089,7 @@ public float[] readFloats (int length) throws KryoException { /** Reads a double array in bulk. This may be more efficient than reading them individually. */ public double[] readDoubles (int length) throws KryoException { - double[] array = new double[validateArrayLength(length, 8)]; + double[] array = new double[validateArrayLength(length, Double.BYTES)]; if (optional(length << 3) == length << 3) { byte[] buffer = this.buffer; int p = this.position; @@ -1102,7 +1113,7 @@ public double[] readDoubles (int length) throws KryoException { /** Reads a short array in bulk. This may be more efficient than reading them individually. */ public short[] readShorts (int length) throws KryoException { - short[] array = new short[validateArrayLength(length, 2)]; + short[] array = new short[validateArrayLength(length, Short.BYTES)]; if (optional(length << 1) == length << 1) { byte[] buffer = this.buffer; int p = this.position; @@ -1118,7 +1129,7 @@ public short[] readShorts (int length) throws KryoException { /** Reads a char array in bulk. This may be more efficient than reading them individually. */ public char[] readChars (int length) throws KryoException { - char[] array = new char[validateArrayLength(length, 2)]; + char[] array = new char[validateArrayLength(length, Character.BYTES)]; if (optional(length << 1) == length << 1) { byte[] buffer = this.buffer; int p = this.position; diff --git a/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java b/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java index 496747cdf..17e8cd3e8 100644 --- a/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/CollectionSerializer.java @@ -30,7 +30,6 @@ import java.util.HashSet; import com.esotericsoftware.kryo.Kryo; -import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.Registration; import com.esotericsoftware.kryo.Serializer; import com.esotericsoftware.kryo.SerializerFactory; @@ -180,12 +179,6 @@ protected T create (Kryo kryo, Input input, Class type, int size) { return collection; } - private static int clampSize (Input input, int size) { - if (size > input.getMaxArraySize()) - throw new KryoException("Declared size larger than maxArraySize: " + size + " > " + input.getMaxArraySize()); - return input.getInputStream() == null ? Math.min(size, input.limit() - input.position()) : size; - } - public T read (Kryo kryo, Input input, Class type) { Class elementClass = this.elementClass; Serializer elementSerializer = this.elementSerializer; @@ -210,7 +203,7 @@ public T read (Kryo kryo, Input input, Class type) { if (length == 0) return null; length--; - collection = create(kryo, input, type, clampSize(input, length)); + collection = create(kryo, input, type, input.clampSize(length)); kryo.reference(collection); if (length == 0) return collection; @@ -220,7 +213,7 @@ public T read (Kryo kryo, Input input, Class type) { if (length == 0) return null; length--; - collection = create(kryo, input, type, clampSize(input, length)); + collection = create(kryo, input, type, input.clampSize(length)); kryo.reference(collection); if (length == 0) return collection; diff --git a/src/com/esotericsoftware/kryo/serializers/MapSerializer.java b/src/com/esotericsoftware/kryo/serializers/MapSerializer.java index f9a136daf..bc5085347 100644 --- a/src/com/esotericsoftware/kryo/serializers/MapSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/MapSerializer.java @@ -20,7 +20,6 @@ package com.esotericsoftware.kryo.serializers; import com.esotericsoftware.kryo.Kryo; -import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.Serializer; import com.esotericsoftware.kryo.SerializerFactory; import com.esotericsoftware.kryo.io.Input; @@ -173,12 +172,6 @@ protected void writeHeader (Kryo kryo, Output output, T map) { /** Used by {@link #read(Kryo, Input, Class)} to create the new object. This can be overridden to customize object creation, eg * to call a constructor with arguments. The default implementation uses {@link Kryo#newInstance(Class)} with a special case * for HashMap. */ - private static int clampSize (Input input, int size) { - if (size > input.getMaxArraySize()) - throw new KryoException("Declared size larger than maxArraySize: " + size + " > " + input.getMaxArraySize()); - return input.getInputStream() == null ? Math.min(size, input.limit() - input.position()) : size; - } - protected T create (Kryo kryo, Input input, Class type, int size) { if (type == HashMap.class) { if (size < 3) @@ -195,7 +188,7 @@ public T read (Kryo kryo, Input input, Class type) { if (length == 0) return null; length--; - T map = create(kryo, input, type, clampSize(input, length)); + T map = create(kryo, input, type, input.clampSize(length)); kryo.reference(map); if (length == 0) return map; diff --git a/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java b/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java index 11f4e0b0f..6eb78eea9 100644 --- a/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java +++ b/src/com/esotericsoftware/kryo/unsafe/UnsafeByteBufferInput.java @@ -185,38 +185,38 @@ public boolean readBoolean () throws KryoException { } public int[] readInts (int length) throws KryoException { - int[] array = new int[validateArrayLength(length, 4)]; - readBytes(array, intArrayBaseOffset, length << 2); + int[] array = new int[validateArrayLength(length, Integer.BYTES)]; + readBytes(array, intArrayBaseOffset, length * Integer.BYTES); return array; } public long[] readLongs (int length) throws KryoException { - long[] array = new long[validateArrayLength(length, 8)]; - readBytes(array, longArrayBaseOffset, length << 3); + long[] array = new long[validateArrayLength(length, Long.BYTES)]; + readBytes(array, longArrayBaseOffset, length * Long.BYTES); return array; } public float[] readFloats (int length) throws KryoException { - float[] array = new float[validateArrayLength(length, 4)]; - readBytes(array, floatArrayBaseOffset, length << 2); + float[] array = new float[validateArrayLength(length, Float.BYTES)]; + readBytes(array, floatArrayBaseOffset, length * Float.BYTES); return array; } public double[] readDoubles (int length) throws KryoException { - double[] array = new double[validateArrayLength(length, 8)]; - readBytes(array, doubleArrayBaseOffset, length << 3); + double[] array = new double[validateArrayLength(length, Double.BYTES)]; + readBytes(array, doubleArrayBaseOffset, length * Double.BYTES); return array; } public short[] readShorts (int length) throws KryoException { - short[] array = new short[validateArrayLength(length, 2)]; - readBytes(array, shortArrayBaseOffset, length << 1); + short[] array = new short[validateArrayLength(length, Short.BYTES)]; + readBytes(array, shortArrayBaseOffset, length * Short.BYTES); return array; } public char[] readChars (int length) throws KryoException { - char[] array = new char[validateArrayLength(length, 2)]; - readBytes(array, charArrayBaseOffset, length << 1); + char[] array = new char[validateArrayLength(length, Character.BYTES)]; + readBytes(array, charArrayBaseOffset, length * Character.BYTES); return array; } diff --git a/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java b/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java index 39bb6b309..d98ca680c 100644 --- a/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java +++ b/src/com/esotericsoftware/kryo/unsafe/UnsafeInput.java @@ -136,38 +136,38 @@ public boolean readBoolean () throws KryoException { } public int[] readInts (int length) throws KryoException { - int[] array = new int[validateArrayLength(length, 4)]; - readBytes(array, intArrayBaseOffset, length << 2); + int[] array = new int[validateArrayLength(length, Integer.BYTES)]; + readBytes(array, intArrayBaseOffset, length * Integer.BYTES); return array; } public long[] readLongs (int length) throws KryoException { - long[] array = new long[validateArrayLength(length, 8)]; - readBytes(array, longArrayBaseOffset, length << 3); + long[] array = new long[validateArrayLength(length, Long.BYTES)]; + readBytes(array, longArrayBaseOffset, length * Long.BYTES); return array; } public float[] readFloats (int length) throws KryoException { - float[] array = new float[validateArrayLength(length, 4)]; - readBytes(array, floatArrayBaseOffset, length << 2); + float[] array = new float[validateArrayLength(length, Float.BYTES)]; + readBytes(array, floatArrayBaseOffset, length * Float.BYTES); return array; } public double[] readDoubles (int length) throws KryoException { - double[] array = new double[validateArrayLength(length, 8)]; - readBytes(array, doubleArrayBaseOffset, length << 3); + double[] array = new double[validateArrayLength(length, Double.BYTES)]; + readBytes(array, doubleArrayBaseOffset, length * Double.BYTES); return array; } public short[] readShorts (int length) throws KryoException { - short[] array = new short[validateArrayLength(length, 2)]; - readBytes(array, shortArrayBaseOffset, length << 1); + short[] array = new short[validateArrayLength(length, Short.BYTES)]; + readBytes(array, shortArrayBaseOffset, length * Short.BYTES); return array; } public char[] readChars (int length) throws KryoException { - char[] array = new char[validateArrayLength(length, 2)]; - readBytes(array, charArrayBaseOffset, length << 1); + char[] array = new char[validateArrayLength(length, Character.BYTES)]; + readBytes(array, charArrayBaseOffset, length * Character.BYTES); return array; } From dd8146309b6290e444435db648f5345cc8998568 Mon Sep 17 00:00:00 2001 From: joszamama Date: Mon, 22 Jun 2026 17:20:14 +0200 Subject: [PATCH 12/13] refactor: use byte constants in the remaining bulk-reader length math Convert the optional(length << n) buffer-availability checks in Input and ByteBufferInput to length * Integer.BYTES / Long.BYTES / etc., matching the allocation and unsafe bulk-read paths so no element-width literals remain in the readers. Also update the validateArrayLength comment that referenced the old shifts. Behavior is unchanged (length * X.BYTES is identical to length << n). --- .../esotericsoftware/kryo/io/ByteBufferInput.java | 12 ++++++------ src/com/esotericsoftware/kryo/io/Input.java | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/com/esotericsoftware/kryo/io/ByteBufferInput.java b/src/com/esotericsoftware/kryo/io/ByteBufferInput.java index 9bdff5b28..e0130b116 100644 --- a/src/com/esotericsoftware/kryo/io/ByteBufferInput.java +++ b/src/com/esotericsoftware/kryo/io/ByteBufferInput.java @@ -859,7 +859,7 @@ private String readAscii_slow (int charCount) { public int[] readInts (int length) throws KryoException { int[] array = new int[validateArrayLength(length, Integer.BYTES)]; - if (optional(length << 2) == length << 2) { + if (optional(length * Integer.BYTES) == length * Integer.BYTES) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { array[i] = byteBuffer.get() & 0xFF // @@ -877,7 +877,7 @@ public int[] readInts (int length) throws KryoException { public long[] readLongs (int length) throws KryoException { long[] array = new long[validateArrayLength(length, Long.BYTES)]; - if (optional(length << 3) == length << 3) { + if (optional(length * Long.BYTES) == length * Long.BYTES) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { array[i] = byteBuffer.get() & 0xFF// @@ -899,7 +899,7 @@ public long[] readLongs (int length) throws KryoException { public float[] readFloats (int length) throws KryoException { float[] array = new float[validateArrayLength(length, Float.BYTES)]; - if (optional(length << 2) == length << 2) { + if (optional(length * Float.BYTES) == length * Float.BYTES) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { array[i] = Float.intBitsToFloat(byteBuffer.get() & 0xFF // @@ -917,7 +917,7 @@ public float[] readFloats (int length) throws KryoException { public double[] readDoubles (int length) throws KryoException { double[] array = new double[validateArrayLength(length, Double.BYTES)]; - if (optional(length << 3) == length << 3) { + if (optional(length * Double.BYTES) == length * Double.BYTES) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) { array[i] = Double.longBitsToDouble(byteBuffer.get() & 0xFF // @@ -939,7 +939,7 @@ public double[] readDoubles (int length) throws KryoException { public short[] readShorts (int length) throws KryoException { short[] array = new short[validateArrayLength(length, Short.BYTES)]; - if (optional(length << 1) == length << 1) { + if (optional(length * Short.BYTES) == length * Short.BYTES) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) array[i] = (short)((byteBuffer.get() & 0xFF) | ((byteBuffer.get() & 0xFF) << 8)); @@ -953,7 +953,7 @@ public short[] readShorts (int length) throws KryoException { public char[] readChars (int length) throws KryoException { char[] array = new char[validateArrayLength(length, Character.BYTES)]; - if (optional(length << 1) == length << 1) { + if (optional(length * Character.BYTES) == length * Character.BYTES) { ByteBuffer byteBuffer = this.byteBuffer; for (int i = 0; i < length; i++) array[i] = (char)((byteBuffer.get() & 0xFF) | ((byteBuffer.get() & 0xFF) << 8)); diff --git a/src/com/esotericsoftware/kryo/io/Input.java b/src/com/esotericsoftware/kryo/io/Input.java index c199794fd..3b16fb71b 100644 --- a/src/com/esotericsoftware/kryo/io/Input.java +++ b/src/com/esotericsoftware/kryo/io/Input.java @@ -982,7 +982,7 @@ public int validateArrayLength (int length, int bytesPerElement) { if (bytes > limit - position) throw new KryoBufferUnderflowException("Buffer underflow."); } else if (bytes > Integer.MAX_VALUE) { // Stream-backed input cannot be checked against the buffered window, but a size whose byte count overflows an int - // can never be read and would overflow the length << n shifts in the bulk readers, so reject it. + // can never be read and would overflow the length * bytesPerElement computations in the bulk readers, so reject it. throw new KryoException("Declared size is too large to read: " + length + " elements of " + bytesPerElement + " bytes"); } return length; @@ -1002,7 +1002,7 @@ public int clampSize (int size) { /** Reads an int array in bulk. This may be more efficient than reading them individually. */ public int[] readInts (int length) throws KryoException { int[] array = new int[validateArrayLength(length, Integer.BYTES)]; - if (optional(length << 2) == length << 2) { + if (optional(length * Integer.BYTES) == length * Integer.BYTES) { byte[] buffer = this.buffer; int p = this.position; for (int i = 0; i < length; i++, p += 4) { @@ -1034,7 +1034,7 @@ public int[] readInts (int length, boolean optimizePositive) throws KryoExceptio /** Reads a long array in bulk. This may be more efficient than reading them individually. */ public long[] readLongs (int length) throws KryoException { long[] array = new long[validateArrayLength(length, Long.BYTES)]; - if (optional(length << 3) == length << 3) { + if (optional(length * Long.BYTES) == length * Long.BYTES) { byte[] buffer = this.buffer; int p = this.position; for (int i = 0; i < length; i++, p += 8) { @@ -1070,7 +1070,7 @@ public long[] readLongs (int length, boolean optimizePositive) throws KryoExcept /** Reads a float array in bulk. This may be more efficient than reading them individually. */ public float[] readFloats (int length) throws KryoException { float[] array = new float[validateArrayLength(length, Float.BYTES)]; - if (optional(length << 2) == length << 2) { + if (optional(length * Float.BYTES) == length * Float.BYTES) { byte[] buffer = this.buffer; int p = this.position; for (int i = 0; i < length; i++, p += 4) { @@ -1090,7 +1090,7 @@ public float[] readFloats (int length) throws KryoException { /** Reads a double array in bulk. This may be more efficient than reading them individually. */ public double[] readDoubles (int length) throws KryoException { double[] array = new double[validateArrayLength(length, Double.BYTES)]; - if (optional(length << 3) == length << 3) { + if (optional(length * Double.BYTES) == length * Double.BYTES) { byte[] buffer = this.buffer; int p = this.position; for (int i = 0; i < length; i++, p += 8) { @@ -1114,7 +1114,7 @@ public double[] readDoubles (int length) throws KryoException { /** Reads a short array in bulk. This may be more efficient than reading them individually. */ public short[] readShorts (int length) throws KryoException { short[] array = new short[validateArrayLength(length, Short.BYTES)]; - if (optional(length << 1) == length << 1) { + if (optional(length * Short.BYTES) == length * Short.BYTES) { byte[] buffer = this.buffer; int p = this.position; for (int i = 0; i < length; i++, p += 2) @@ -1130,7 +1130,7 @@ public short[] readShorts (int length) throws KryoException { /** Reads a char array in bulk. This may be more efficient than reading them individually. */ public char[] readChars (int length) throws KryoException { char[] array = new char[validateArrayLength(length, Character.BYTES)]; - if (optional(length << 1) == length << 1) { + if (optional(length * Character.BYTES) == length * Character.BYTES) { byte[] buffer = this.buffer; int p = this.position; for (int i = 0; i < length; i++, p += 2) From 9dbbf5f35e7b7c4648f67aa178048b7e00139086 Mon Sep 17 00:00:00 2001 From: joszamama Date: Mon, 22 Jun 2026 17:24:05 +0200 Subject: [PATCH 13/13] docs: clarify clampSize JavaDoc Describe clampSize as an initial-capacity hint that is capped at the bytes remaining (collection/map still grows), rather than the earlier wording that mixed "single byte" and "zero byte" element sizes. --- src/com/esotericsoftware/kryo/io/Input.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/com/esotericsoftware/kryo/io/Input.java b/src/com/esotericsoftware/kryo/io/Input.java index 3b16fb71b..8f50649f5 100644 --- a/src/com/esotericsoftware/kryo/io/Input.java +++ b/src/com/esotericsoftware/kryo/io/Input.java @@ -989,10 +989,11 @@ public int validateArrayLength (int length, int bytesPerElement) { } /** Clamps a declared collection or map size before it is used as an initial capacity. Throws if {@code size} exceeds - * {@link #setMaxArraySize(int) maxArraySize}; otherwise, for a buffer-backed input (no {@link InputStream}), limits it to the - * bytes remaining, since a collection or map element can occupy as little as a single byte. A stream-backed input is returned - * unchanged (the remaining size is unknown). Unlike {@link #validateArrayLength(int, int)} the size is clamped rather than - * rejected, because the capacity is only a hint and elements may occupy zero bytes. */ + * {@link #setMaxArraySize(int) maxArraySize}. Otherwise, for a buffer-backed input (no {@link InputStream}), caps it at the + * bytes remaining so a corrupt size cannot force a large pre-allocation; the collection or map still grows as elements are + * read. A stream-backed input is returned unchanged (the remaining size is unknown). Unlike + * {@link #validateArrayLength(int, int)} the size is only clamped, not rejected, because it is an initial-capacity hint rather + * than an exact element count. */ public int clampSize (int size) { if (size > maxArraySize) throw new KryoException("Declared size larger than maxArraySize: " + size + " > " + maxArraySize);