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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new f696ddd5ec [common] Reject a serialized row shorter than its 
fixed-length part (#9991)
f696ddd5ec is described below

commit f696ddd5ec8807e46c2f9b4f78afba3d7a8ec6e5
Author: jackylee <[email protected]>
AuthorDate: Sun Sep 20 13:44:05 2026 +0800

    [common] Reject a serialized row shorter than its fixed-length part (#9991)
---
 .../data/serializer/BinaryRowSerializer.java       | 16 ++++
 .../serializer/BinaryRowSerializerLengthTest.java  | 96 ++++++++++++++++++++++
 2 files changed, 112 insertions(+)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/data/serializer/BinaryRowSerializer.java
 
b/paimon-common/src/main/java/org/apache/paimon/data/serializer/BinaryRowSerializer.java
index 3b576b029c..7586e2f708 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/data/serializer/BinaryRowSerializer.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/data/serializer/BinaryRowSerializer.java
@@ -75,6 +75,7 @@ public class BinaryRowSerializer extends 
AbstractRowDataSerializer<BinaryRow> {
     public BinaryRow deserialize(DataInputView source) throws IOException {
         BinaryRow row = new BinaryRow(numFields);
         int length = source.readInt();
+        checkLength(length);
         byte[] bytes = new byte[length];
         source.readFully(bytes);
         row.pointTo(MemorySegment.wrap(bytes), 0, length);
@@ -88,6 +89,7 @@ public class BinaryRowSerializer extends 
AbstractRowDataSerializer<BinaryRow> {
                 "Reuse BinaryRow should have no segments or only one segment 
and offset start at 0.");
 
         int length = source.readInt();
+        checkLength(length);
         if (segments == null || segments[0].size() < length) {
             // Need a larger buffer
             segments = new MemorySegment[] {MemorySegment.wrap(new 
byte[length])};
@@ -105,6 +107,20 @@ public class BinaryRowSerializer extends 
AbstractRowDataSerializer<BinaryRow> {
         return numFields;
     }
 
+    /**
+     * A serialized row of {@code numFields} fields is at least its 
fixed-length part: the null bits
+     * plus one 8-byte word per field. A shorter length leaves fields outside 
the buffer, and
+     * reading them is an unchecked {@code UNSAFE} access that returns 
whatever lies beyond it.
+     */
+    private void checkLength(int length) throws IOException {
+        if (length < fixedLengthPartSize) {
+            throw new IOException(
+                    String.format(
+                            "Read an invalid row length %d, a row of %d fields 
needs at least %d bytes.",
+                            length, numFields, fixedLengthPartSize));
+        }
+    }
+
     @Override
     public BinaryRow toBinaryRow(BinaryRow rowData) throws IOException {
         return rowData;
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/data/serializer/BinaryRowSerializerLengthTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/data/serializer/BinaryRowSerializerLengthTest.java
new file mode 100644
index 0000000000..77a9b4e93f
--- /dev/null
+++ 
b/paimon-common/src/test/java/org/apache/paimon/data/serializer/BinaryRowSerializerLengthTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.paimon.data.serializer;
+
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.BinaryRowWriter;
+import org.apache.paimon.io.DataInputDeserializer;
+import org.apache.paimon.io.DataOutputSerializer;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for the row length validation in {@link 
BinaryRowSerializer#deserialize}. A length below
+ * the fixed-length part leaves fields outside the buffer, and reading one of 
those is an unchecked
+ * {@code UNSAFE} access: until it was checked, a row truncated below its 
fixed part deserialized
+ * without error and returned a value for a field that lay past the end of the 
buffer.
+ */
+class BinaryRowSerializerLengthTest {
+
+    private static final int NUM_FIELDS = 3;
+    private static final int FIXED_PART = 
BinaryRow.calculateFixPartSizeInBytes(NUM_FIELDS);
+
+    @Test
+    void testNegativeLengthIsRejected() {
+        byte[] negativeLength = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 
0xFF};
+        assertThatThrownBy(
+                        () ->
+                                new BinaryRowSerializer(NUM_FIELDS)
+                                        .deserialize(new 
DataInputDeserializer(negativeLength)))
+                .isInstanceOf(IOException.class)
+                .hasMessageContaining("-1");
+    }
+
+    @ParameterizedTest(name = "reuse = {0}")
+    @ValueSource(booleans = {false, true})
+    void testLengthBelowFixedPartIsRejected(boolean reuse) throws IOException {
+        BinaryRowSerializer serializer = new BinaryRowSerializer(NUM_FIELDS);
+        int shortened = FIXED_PART - 8;
+        byte[] bytes = rowWithLengthPrefix(serializer, shortened);
+
+        assertThatThrownBy(
+                        () -> {
+                            DataInputDeserializer in = new 
DataInputDeserializer(bytes);
+                            if (reuse) {
+                                serializer.deserialize(new 
BinaryRow(NUM_FIELDS), in);
+                            } else {
+                                serializer.deserialize(in);
+                            }
+                        })
+                .isInstanceOf(IOException.class)
+                .hasMessageContaining(String.valueOf(shortened))
+                .hasMessageContaining(String.valueOf(FIXED_PART));
+    }
+
+    /** Serializes a valid row, then overwrites its length prefix with {@code 
length}. */
+    private static byte[] rowWithLengthPrefix(BinaryRowSerializer serializer, 
int length)
+            throws IOException {
+        BinaryRow row = new BinaryRow(NUM_FIELDS);
+        BinaryRowWriter writer = new BinaryRowWriter(row);
+        writer.writeInt(0, 42);
+        writer.writeLong(1, 999L);
+        writer.writeInt(2, -7);
+        writer.complete();
+
+        DataOutputSerializer out = new DataOutputSerializer(128);
+        serializer.serialize(row, out);
+        byte[] bytes = out.getCopyOfBuffer();
+        bytes[0] = (byte) (length >>> 24);
+        bytes[1] = (byte) (length >>> 16);
+        bytes[2] = (byte) (length >>> 8);
+        bytes[3] = (byte) length;
+        return bytes;
+    }
+}

Reply via email to