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

mjsax pushed a commit to branch 4.3
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/4.3 by this push:
     new caf7eb30537 KAFKA-20769: ListDeserializer can silently deserialize a 
corrupted entry when the input is truncated mid-entry (#22755)
caf7eb30537 is described below

commit caf7eb305378816a733506d282b66e307bc92d82
Author: Sepuri Sai Krishna <[email protected]>
AuthorDate: Fri Jul 24 11:01:20 2026 +0530

    KAFKA-20769: ListDeserializer can silently deserialize a corrupted entry 
when the input is truncated mid-entry (#22755)
    
    ListDeserializer.deserialize is using DataInputStream.read(byte[]) which
    may return "early" if the stream is shorter than expected (ie, cannot
    fill  the provided `byte[]` array) w/o error. This can lead to a
    corrupted  deserialization result.
    
    This PR switches to DataInputStream.readFully, which throws EOFException
    when the buffer cannot be filled.
    
    Reviewers: Matthias J. Sax <[email protected]>
---
 .../common/serialization/ListDeserializer.java     |  8 ++++-
 .../common/serialization/ListDeserializerTest.java | 34 ++++++++++++++++++++++
 2 files changed, 41 insertions(+), 1 deletion(-)

diff --git 
a/clients/src/main/java/org/apache/kafka/common/serialization/ListDeserializer.java
 
b/clients/src/main/java/org/apache/kafka/common/serialization/ListDeserializer.java
index 42e7c48dde8..d22b76741a2 100644
--- 
a/clients/src/main/java/org/apache/kafka/common/serialization/ListDeserializer.java
+++ 
b/clients/src/main/java/org/apache/kafka/common/serialization/ListDeserializer.java
@@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
 
 import java.io.ByteArrayInputStream;
 import java.io.DataInputStream;
+import java.io.EOFException;
 import java.io.IOException;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
@@ -194,7 +195,12 @@ public class ListDeserializer<Inner> implements 
Deserializer<List<Inner>> {
                     continue;
                 }
                 byte[] payload = new byte[entrySize];
-                if (dis.read(payload) == -1) {
+                try {
+                    // Use readFully (not read) so a stream truncated 
mid-entry is detected: read(byte[]) may
+                    // return a partial count without reaching EOF, which 
would silently leave the tail of the
+                    // payload zero-filled and hand a corrupted entry to the 
inner deserializer.
+                    dis.readFully(payload);
+                } catch (EOFException e) {
                     log.error("Ran out of bytes in serialized list");
                     log.trace("Deserialized list so far: {}", 
deserializedList); // avoid logging actual data above TRACE level since it may 
contain sensitive information
                     throw new SerializationException("End of the stream was 
reached prematurely");
diff --git 
a/clients/src/test/java/org/apache/kafka/common/serialization/ListDeserializerTest.java
 
b/clients/src/test/java/org/apache/kafka/common/serialization/ListDeserializerTest.java
index 95da416af4b..6643ccd3b96 100644
--- 
a/clients/src/test/java/org/apache/kafka/common/serialization/ListDeserializerTest.java
+++ 
b/clients/src/test/java/org/apache/kafka/common/serialization/ListDeserializerTest.java
@@ -354,6 +354,40 @@ public class ListDeserializerTest {
         );
     }
 
+    @Test
+    public void shouldThrowOnEntryTruncatedMidStream() {
+        // entrySize (10) is within data.length (14) so it passes the bounds 
check, but only 5 payload bytes
+        // actually follow, so deserialization must fail rather than return a 
truncated entry.
+        final byte[] corruptedData = new byte[] {
+            (byte) 
Serdes.ListSerde.SerializationStrategy.VARIABLE_SIZE.ordinal(),
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, // encodes 
length == 1
+            (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x0A, // encodes 
entrySize == 10
+            (byte) 'h', (byte) 'e', (byte) 'l', (byte) 'l', (byte) 'o' // only 
5 of the 10 declared bytes
+        };
+        final ListDeserializer<String> testDeserializer = new 
ListDeserializer<>(ArrayList.class, new StringDeserializer());
+        final SerializationException exception = assertThrows(
+            SerializationException.class,
+            () -> testDeserializer.deserialize(null, corruptedData)
+        );
+        assertEquals(
+            "End of the stream was reached prematurely",
+            exception.getMessage()
+        );
+    }
+
+    @Test
+    public void shouldDeserializeListEndingInEmptyEntry() {
+        // A list whose last element serializes to zero bytes leaves the 
stream at EOF when that final
+        // (empty) entry is read: the deserializer must handle this correctly 
and should not fail.
+        final String topic = "topic";
+        final List<String> data = List.of("a", "");
+        final byte[] serializedData = new ListSerializer<>(new 
StringSerializer()).serialize(topic, data);
+
+        final ListDeserializer<String> testDeserializer = new 
ListDeserializer<>(ArrayList.class, new StringDeserializer());
+
+        assertEquals(data, testDeserializer.deserialize(topic, 
serializedData));
+    }
+
     @Test
     public void shouldThrowOnNegativeNullEntryLength() {
         final byte[] corruptedData = new byte[] {

Reply via email to