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 120b10bb6d [fix] guard against BinaryRow fixed part exceeding page 
size (#8840)
120b10bb6d is described below

commit 120b10bb6dce01a8c17dc8c5cdbee010c02067c3
Author: Yujiang Zhong <[email protected]>
AuthorDate: Tue Jul 28 21:16:56 2026 +0800

    [fix] guard against BinaryRow fixed part exceeding page size (#8840)
---
 .../data/serializer/BinaryRowSerializer.java       | 21 ++++-
 .../BinaryRowSerializerPageSizeGuardTest.java      | 98 ++++++++++++++++++++++
 .../paimon/flink/sink/WriterOperatorTest.java      |  6 +-
 3 files changed, 120 insertions(+), 5 deletions(-)

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 dbf37f520f..3b576b029c 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
@@ -261,9 +261,26 @@ public class BinaryRowSerializer extends 
AbstractRowDataSerializer<BinaryRow> {
      * binary row fixed part. See {@link BinaryRow}.
      */
     private int checkSkipWriteForFixLengthPart(AbstractPagedOutputView out) 
throws IOException {
+        // The fixed-length part of a BinaryRow must reside within a single 
memory segment,
+        // because random field access (e.g. getLong/getString) always reads 
from segments[0]
+        // without bounds checking. If it is larger than the page size, 
advancing to the next
+        // segment cannot help and the row would silently span segments, 
leading to corrupted
+        // field offsets and eventually a NegativeArraySizeException. Fail 
fast instead.
+        int fixedPartLength = getSerializedRowFixedPartLength();
+        int segmentSize = out.getSegmentSize();
+        if (fixedPartLength > segmentSize) {
+            String msg =
+                    String.format(
+                            "Cannot serialize BinaryRow to pages: the 
serialized fixed-length part "
+                                    + "(%d bytes, numFields=%d) is larger than 
the page size (%d bytes). "
+                                    + "The fixed-length part must fit within a 
single page. Please increase "
+                                    + "the 'page-size' option to at least %d 
bytes.",
+                            fixedPartLength, numFields, segmentSize, 
fixedPartLength);
+            throw new IllegalArgumentException(msg);
+        }
         // skip if there is no enough size.
-        int available = out.getSegmentSize() - 
out.getCurrentPositionInSegment();
-        if (available < getSerializedRowFixedPartLength()) {
+        int available = segmentSize - out.getCurrentPositionInSegment();
+        if (available < fixedPartLength) {
             out.advance();
             return available;
         }
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/data/serializer/BinaryRowSerializerPageSizeGuardTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/data/serializer/BinaryRowSerializerPageSizeGuardTest.java
new file mode 100644
index 0000000000..44f15da20f
--- /dev/null
+++ 
b/paimon-common/src/test/java/org/apache/paimon/data/serializer/BinaryRowSerializerPageSizeGuardTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.data.RandomAccessInputView;
+import org.apache.paimon.data.SimpleCollectingOutputView;
+import org.apache.paimon.memory.CachelessSegmentPool;
+import org.apache.paimon.memory.MemorySegment;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for the page-size guard in {@link 
BinaryRowSerializer#serializeToPages}. When a row's
+ * fixed-length part is larger than the page size, serialization must fail 
fast with a clear error
+ * instead of silently spanning segments and corrupting field access (which 
previously surfaced as a
+ * cryptic {@link NegativeArraySizeException}).
+ */
+class BinaryRowSerializerPageSizeGuardTest {
+
+    // 200 INT columns -> fixed-length part 1632 bytes + 4 length bytes = 1636 
bytes.
+    private static final int NUM_FIELDS = 200;
+
+    @Test
+    void testThrowsWhenFixedPartExceedsPageSize() {
+        BinaryRowSerializer serializer = new BinaryRowSerializer(NUM_FIELDS);
+        int fixedPartLength = serializer.getSerializedRowFixedPartLength();
+        // Page size (power of two) smaller than the fixed-length part.
+        int pageSize = 1024;
+        assertThat(fixedPartLength).isGreaterThan(pageSize);
+
+        SimpleCollectingOutputView out =
+                new SimpleCollectingOutputView(
+                        new CachelessSegmentPool(pageSize, pageSize), 
pageSize);
+
+        BinaryRow row = newIntRow(NUM_FIELDS);
+        assertThatThrownBy(() -> serializer.serializeToPages(row, out))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("page size")
+                .hasMessageContaining(Integer.toString(fixedPartLength))
+                .hasMessageContaining(Integer.toString(pageSize))
+                .hasMessageContaining(Integer.toString(NUM_FIELDS));
+    }
+
+    @Test
+    void testSerializeSucceedsWhenPageLargeEnough() throws Exception {
+        BinaryRowSerializer serializer = new BinaryRowSerializer(NUM_FIELDS);
+        int pageSize = 4096;
+        
assertThat(serializer.getSerializedRowFixedPartLength()).isLessThanOrEqualTo(pageSize);
+
+        SimpleCollectingOutputView out =
+                new SimpleCollectingOutputView(
+                        new CachelessSegmentPool((long) pageSize * 4, 
pageSize), pageSize);
+
+        BinaryRow row = newIntRow(NUM_FIELDS);
+        serializer.serializeToPages(row, out);
+
+        // Round-trip the row back from the written pages.
+        ArrayList<MemorySegment> segments = out.fullSegments();
+        RandomAccessInputView in = new RandomAccessInputView(segments, 
pageSize);
+        BinaryRow deserialized = 
serializer.deserializeFromPages(serializer.createInstance(), in);
+        for (int i = 0; i < NUM_FIELDS; i++) {
+            assertThat(deserialized.getInt(i)).isEqualTo(i);
+        }
+    }
+
+    private static BinaryRow newIntRow(int numFields) {
+        BinaryRow row = new BinaryRow(numFields);
+        BinaryRowWriter writer = new BinaryRowWriter(row);
+        for (int i = 0; i < numFields; i++) {
+            writer.writeInt(i, i);
+        }
+        writer.complete();
+        return row;
+    }
+}
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/WriterOperatorTest.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/WriterOperatorTest.java
index c7cca8eceb..6c48b9379b 100644
--- 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/WriterOperatorTest.java
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/sink/WriterOperatorTest.java
@@ -92,7 +92,7 @@ public class WriterOperatorTest {
         options.set("bucket", "1");
         options.set("write-buffer-size", "256 b");
         options.set("write-buffer-spillable", "false");
-        options.set("page-size", "32 b");
+        options.set("page-size", "64 b");
 
         FileStoreTable table =
                 createFileStoreTable(
@@ -109,7 +109,7 @@ public class WriterOperatorTest {
         Options options = new Options();
         options.set("write-buffer-for-append", "true");
         options.set("write-buffer-size", "256 b");
-        options.set("page-size", "32 b");
+        options.set("page-size", "64 b");
         options.set("write-buffer-spillable", "false");
 
         FileStoreTable table =
@@ -446,7 +446,7 @@ public class WriterOperatorTest {
         options.set("bucket", "1");
         options.set("write-buffer-size", "256 b");
         options.set("write-buffer-spillable", "false");
-        options.set("page-size", "32 b");
+        options.set("page-size", "64 b");
 
         FileStoreTable fileStoreTable =
                 createFileStoreTable(

Reply via email to