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 9e83deaf fix(row): guard timestamp conversion against overflow from 
corrupt data (#545)
9e83deaf is described below

commit 9e83deafcae03e9e51c70e53e55cc3ae73fc0a11
Author: hechao <[email protected]>
AuthorDate: Tue Jul 21 12:18:06 2026 +0800

    fix(row): guard timestamp conversion against overflow from corrupt data 
(#545)
---
 crates/paimon/src/arrow/format/row.rs | 112 +++++++++++++++++++++++++++++++++-
 1 file changed, 110 insertions(+), 2 deletions(-)

diff --git a/crates/paimon/src/arrow/format/row.rs 
b/crates/paimon/src/arrow/format/row.rs
index a3fbd9d9..3060f5ee 100644
--- a/crates/paimon/src/arrow/format/row.rs
+++ b/crates/paimon/src/arrow/format/row.rs
@@ -1778,11 +1778,23 @@ fn read_timestamp_value(input: &mut BlockInput<'_>, 
unit: TimeUnit) -> crate::Re
         TimeUnit::Millisecond => millis,
         TimeUnit::Microsecond => {
             let nanos = i64::from(input.read_var_u32()?);
-            millis * 1_000 + nanos / 1_000
+            let value = millis as i128 * 1_000i128 + (nanos / 1_000) as i128;
+            i64::try_from(value).map_err(|_| Error::DataInvalid {
+                message: format!(
+                    ".row timestamp microsecond conversion overflow: 
millis={millis}, nanos={nanos}"
+                ),
+                source: None,
+            })?
         }
         TimeUnit::Nanosecond => {
             let nanos = i64::from(input.read_var_u32()?);
-            millis * 1_000_000 + nanos
+            let value = millis as i128 * 1_000_000i128 + nanos as i128;
+            i64::try_from(value).map_err(|_| Error::DataInvalid {
+                message: format!(
+                    ".row timestamp nanosecond conversion overflow: 
millis={millis}, nanos={nanos}"
+                ),
+                source: None,
+            })?
         }
         TimeUnit::Second => millis / 1_000,
     })
@@ -3508,4 +3520,100 @@ mod tests {
         ])
         .unwrap();
     }
+
+    #[test]
+    fn read_timestamp_rejects_overflow() {
+        // Millis = i64::MAX triggers overflow in final microsecond/nanosecond 
conversion
+        let mut buf = Vec::new();
+        buf.extend_from_slice(&i64::MAX.to_le_bytes()); // millis (8 bytes LE)
+        buf.push(0); // nanos_of_milli = 0 (varint 1 byte)
+
+        let new_input = || BlockInput {
+            data: &buf,
+            position: 0,
+            data_end: buf.len(),
+        };
+
+        let result = read_timestamp_value(&mut new_input(), 
TimeUnit::Microsecond);
+        assert!(matches!(result, Err(Error::DataInvalid { .. })));
+
+        let result = read_timestamp_value(&mut new_input(), 
TimeUnit::Nanosecond);
+        assert!(matches!(result, Err(Error::DataInvalid { .. })));
+    }
+
+    #[test]
+    fn read_timestamp_i64_min_negative_boundary_converts() {
+        // A writer can encode i64::MIN microseconds using Euclidean division:
+        //   millis = floor(i64::MIN / 1000)  = -9_223_372_036_854_776
+        //   nanos  = rem_euclid(1000) * 1000  = 192_000
+        // The intermediate product millis*1000 = -9_223_372_036_854_776_000
+        // which temporarily falls below i64::MIN, but the final value
+        // (-9_223_372_036_854_776_000 + 192) = i64::MIN fits in i64.
+        // Before the i128 fix, checked_mul would reject this valid roundtrip.
+
+        // Microsecond path: millis = floor(i64::MIN / 1000), nanos = 192_000
+        let millis = i64::MIN.div_euclid(1_000); // floor = 
-9_223_372_036_854_776
+        let nanos = (i64::MIN.rem_euclid(1_000) * 1_000) as u32; // 192_000
+
+        let mut buf = Vec::new();
+        buf.extend_from_slice(&millis.to_le_bytes());
+        write_var_u32(&mut buf, nanos);
+
+        let mut input = BlockInput {
+            data: &buf,
+            position: 0,
+            data_end: buf.len(),
+        };
+        let result = read_timestamp_value(&mut input, 
TimeUnit::Microsecond).unwrap();
+        assert_eq!(result, i64::MIN);
+
+        // Nanosecond path: millis = floor(i64::MIN / 1_000_000), nanos = 
224_192
+        let millis = i64::MIN.div_euclid(1_000_000); // floor = 
-9_223_372_036_855
+        let nanos = i64::MIN.rem_euclid(1_000_000) as u32; // 224_192
+
+        let mut buf = Vec::new();
+        buf.extend_from_slice(&millis.to_le_bytes());
+        write_var_u32(&mut buf, nanos);
+
+        let mut input = BlockInput {
+            data: &buf,
+            position: 0,
+            data_end: buf.len(),
+        };
+        let result = read_timestamp_value(&mut input, 
TimeUnit::Nanosecond).unwrap();
+        assert_eq!(result, i64::MIN);
+    }
+
+    #[test]
+    fn read_timestamp_accepts_boundary_values() {
+        // Maximum millis that still fits into microsecond precision
+        let max_safe_micros = i64::MAX / 1_000;
+
+        let mut buf = Vec::new();
+        buf.extend_from_slice(&max_safe_micros.to_le_bytes());
+        buf.push(0);
+
+        let mut input = BlockInput {
+            data: &buf,
+            position: 0,
+            data_end: buf.len(),
+        };
+        let result = read_timestamp_value(&mut input, TimeUnit::Microsecond);
+        assert!(result.is_ok());
+
+        // Maximum millis that still fits into nanosecond precision
+        let max_safe_nanos = i64::MAX / 1_000_000;
+
+        let mut buf = Vec::new();
+        buf.extend_from_slice(&max_safe_nanos.to_le_bytes());
+        buf.push(0);
+
+        let mut input = BlockInput {
+            data: &buf,
+            position: 0,
+            data_end: buf.len(),
+        };
+        let result = read_timestamp_value(&mut input, TimeUnit::Nanosecond);
+        assert!(result.is_ok());
+    }
 }

Reply via email to