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

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


The following commit(s) were added to refs/heads/main by this push:
     new b7fe7d1  fix: reject truncated BinaryRow serialized bytes instead of 
panicking (#364)
b7fe7d1 is described below

commit b7fe7d1e987327efd93a385995454839b779b001
Author: tonghuaroot (童话) <[email protected]>
AuthorDate: Tue Jun 16 13:54:37 2026 +0800

    fix: reject truncated BinaryRow serialized bytes instead of panicking (#364)
---
 crates/paimon/src/spec/binary_row.rs | 76 +++++++++++++++++++++++++++++++++++-
 1 file changed, 74 insertions(+), 2 deletions(-)

diff --git a/crates/paimon/src/spec/binary_row.rs 
b/crates/paimon/src/spec/binary_row.rs
index 8b7feb9..104e343 100644
--- a/crates/paimon/src/spec/binary_row.rs
+++ b/crates/paimon/src/spec/binary_row.rs
@@ -92,7 +92,29 @@ impl BinaryRow {
             });
         }
         let arity = i32::from_be_bytes([data[0], data[1], data[2], data[3]]);
-        Ok(Self::from_bytes(arity, data[4..].to_vec()))
+        if arity < 0 {
+            return Err(crate::Error::UnexpectedError {
+                message: format!("BinaryRow: serialized data has negative 
arity: {arity}"),
+                source: None,
+            });
+        }
+        let body = &data[4..];
+        // The body must hold at least the null bitmap and the fixed part
+        // (8 bytes per field); reject truncated input rather than panicking
+        // later when reading the null bitmap or a field. The size is computed
+        // in i64 so an absurd arity in malformed input cannot overflow.
+        let bit_set_width = ((arity as i64 + 63 + Self::HEADER_SIZE_IN_BYTES 
as i64) / 64) * 8;
+        let fix_part_size = bit_set_width + 8 * arity as i64;
+        if (body.len() as i64) < fix_part_size {
+            return Err(crate::Error::UnexpectedError {
+                message: format!(
+                    "BinaryRow: serialized body too short for arity {arity}: 
{} bytes, need at least {fix_part_size}",
+                    body.len()
+                ),
+                source: None,
+            });
+        }
+        Ok(Self::from_bytes(arity, body.to_vec()))
     }
 
     /// Serialize this BinaryRow to bytes (arity prefix + data), the inverse 
of `from_serialized_bytes`.
@@ -119,7 +141,13 @@ impl BinaryRow {
         let bit_index = pos + Self::HEADER_SIZE_IN_BYTES as usize;
         let byte_index = bit_index / 8;
         let bit_offset = bit_index % 8;
-        (self.data[byte_index] & (1 << bit_offset)) != 0
+        // Index defensively: a truncated buffer that lacks the null bitmap
+        // byte is reported as not-null so the typed field readers can return
+        // a graceful error instead of this method panicking.
+        match self.data.get(byte_index) {
+            Some(byte) => (byte & (1 << bit_offset)) != 0,
+            None => false,
+        }
     }
 
     fn field_offset(&self, pos: usize) -> usize {
@@ -1186,6 +1214,50 @@ mod tests {
         assert!(BinaryRow::from_serialized_bytes(&[0, 0]).is_err());
     }
 
+    #[test]
+    fn test_from_serialized_bytes_truncated_body() {
+        // Valid 4-byte arity prefix (arity = 1) but the body is empty, so it
+        // cannot hold the null bitmap. This must be rejected gracefully rather
+        // than panicking when the null bitmap is later read.
+        let truncated = [0u8, 0, 0, 1];
+        assert!(BinaryRow::from_serialized_bytes(&truncated).is_err());
+
+        // Body present but still shorter than the fixed part (null bitmap of 8
+        // bytes + one 8-byte field = 16 bytes for arity 1).
+        let mut short_body = vec![0u8, 0, 0, 1];
+        short_body.extend_from_slice(&[0u8; 4]);
+        assert!(BinaryRow::from_serialized_bytes(&short_body).is_err());
+    }
+
+    #[test]
+    fn test_from_serialized_bytes_negative_arity() {
+        // arity = -1 (0xFFFFFFFF) must be rejected, not used in size math.
+        let data = [0xFFu8, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0];
+        assert!(BinaryRow::from_serialized_bytes(&data).is_err());
+    }
+
+    #[test]
+    fn test_from_serialized_bytes_well_formed_decodes() {
+        // Negative control: a correctly sized body decodes and reads back 
fine.
+        let mut builder = BinaryRowBuilder::new(1);
+        builder.write_int(0, 7);
+        let serialized = builder.build_serialized();
+        let row = BinaryRow::from_serialized_bytes(&serialized).unwrap();
+        assert_eq!(row.arity(), 1);
+        assert!(!row.is_null_at(0));
+        assert_eq!(row.get_int(0).unwrap(), 7);
+    }
+
+    #[test]
+    fn test_is_null_at_short_buffer_does_not_panic() {
+        // A row whose backing buffer lacks the null bitmap byte must not panic
+        // in is_null_at; the position is reported as not-null and the typed
+        // reader then returns a graceful error.
+        let row = BinaryRow::from_bytes(1, Vec::new());
+        assert!(!row.is_null_at(0));
+        assert!(row.get_int(0).is_err());
+    }
+
     #[test]
     fn test_get_int() {
         let mut builder = BinaryRowBuilder::new(2);

Reply via email to