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