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

Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new b8b59fb38d avro: bound VLQDecoder::long against overlong varints 
(#10407)
b8b59fb38d is described below

commit b8b59fb38dcc5930d7a087bc3e17bd955a6c9f21
Author: Hill Patel <[email protected]>
AuthorDate: Sat Jul 25 08:45:53 2026 +0530

    avro: bound VLQDecoder::long against overlong varints (#10407)
    
    # Which issue does this PR close?
    
    - Closes #10290.
    
    # Rationale for this change
    
    A malformed or malicious Avro file with an unterminated run of varint
    continuation bytes drives `VLQDecoder::long`'s `self.shift` past 63,
    panicking inside `<< self.shift` ("attempt to shift left with overflow")
    in debug builds. In release builds without overflow-checks the shift is
    silently masked instead — arguably worse, since the decoder keeps
    running and can produce a wrong value rather than fail loudly. Any
    service that reads attacker-controlled Avro bytes through
    `ReaderBuilder::build` can hit this from the public API.
    
    There's a prior attempt at this in #9887 (closed by the stale bot, not
    rejected — no activity for 60 days). That PR used
    `checked_shl(self.shift).unwrap_or(0)` to silently drop overflowing
    contribution bits, which stops the panic but places no bound on how many
    continuation bytes it will consume, and @alamb
    [asked](https://github.com/apache/arrow-rs/pull/9887#discussion_r3189310679)
    whether malformed input should return an error instead of being silently
    absorbed — that question was never answered before the PR went stale.
    
    # What changes are included in this PR?
    
    - `VLQDecoder::long` now bounds the varint to 10 bytes, mirroring the
    bound already enforced by `read_varint_array`'s handling of its 10th
    byte a few lines below in the same file, and returns
    `AvroError::ParseError` on an overlong/malformed varint instead of
    panicking or silently truncating. The decoder's internal state is reset
    on error so a subsequent call decodes a fresh varint cleanly.
    - This changes `long`'s signature from `Option<i64>` to
    `Result<Option<i64>, AvroError>`, updating its 6 call sites in
    `block.rs`/`header.rs`. All of them already sit inside functions
    returning `Result<_, AvroError>` and already propagate sibling parse
    errors via `?`, so each call site only needed a `?` added. This is a
    purely internal, non-breaking change — `vlq`, `block`, and `header` are
    all private (non-`pub`) modules within `arrow-avro`, not part of the
    crate's public API.
    
    # Are these changes tested?
    
    - Added `test_long_overlong_varint_returns_error` reproducing the exact
    19-byte input from the issue's cargo-fuzz repro (Avro magic + 13 `0xFF`
    continuation bytes), asserting the decoder now errors instead of
    panicking, and that it resets cleanly for the next call.
    - Added `test_long_roundtrip`, a direct unit test for
    `VLQDecoder::long`'s happy path (zig-zag decode), which had no dedicated
    test before this change — only the unsigned
    `read_varint`/`read_varint_array` siblings were covered by
    `test_varint`.
    - Verified the exact fuzzer input from the issue no longer panics
    end-to-end through the public `ReaderBuilder::build` API (ad-hoc
    integration test, not committed).
    - `cargo test -p arrow-avro --lib` (402 passed), `cargo clippy -p
    arrow-avro --all-targets -- -D warnings`, and `cargo fmt -p arrow-avro
    -- --check` all clean.
    
    # Are there any user-facing changes?
    
    No public API changes (`vlq`/`block`/`header` are private modules).
    Behaviorally, malformed/malicious Avro input that previously panicked
    (or silently produced garbage in a release build) now returns a
    `AvroError::ParseError` from the public `decode`/`ReaderBuilder::build`
    path, which is the intended, documented behavior for malformed input
    elsewhere in these same decoders.
    
    ---
    
    This PR was prepared with AI assistance (Claude Code). I reviewed the
    root cause in the source, traced every call site of `VLQDecoder::long`
    to confirm the signature change is safe and non-breaking, verified the
    module-privacy claim before choosing this approach, and ran the fuzzer
    repro from the issue against the built fix myself before opening this
    PR.
    
    ---------
    
    Co-authored-by: Claude Sonnet 5 <[email protected]>
    Co-authored-by: Jeffrey Vo <[email protected]>
---
 arrow-avro/src/reader/block.rs  |  4 +--
 arrow-avro/src/reader/header.rs |  8 +++---
 arrow-avro/src/reader/vlq.rs    | 62 +++++++++++++++++++++++++++++++++++++++--
 3 files changed, 65 insertions(+), 9 deletions(-)

diff --git a/arrow-avro/src/reader/block.rs b/arrow-avro/src/reader/block.rs
index 8df76c4b15..3ec6e507ea 100644
--- a/arrow-avro/src/reader/block.rs
+++ b/arrow-avro/src/reader/block.rs
@@ -80,7 +80,7 @@ impl BlockDecoder {
         while !buf.is_empty() {
             match self.state {
                 BlockDecoderState::Count => {
-                    if let Some(c) = self.vlq_decoder.long(&mut buf) {
+                    if let Some(c) = self.vlq_decoder.long(&mut buf)? {
                         self.in_progress.count = c.try_into().map_err(|_| {
                             AvroError::ParseError(format!(
                                 "Block count cannot be negative, got {c}"
@@ -91,7 +91,7 @@ impl BlockDecoder {
                     }
                 }
                 BlockDecoderState::Size => {
-                    if let Some(c) = self.vlq_decoder.long(&mut buf) {
+                    if let Some(c) = self.vlq_decoder.long(&mut buf)? {
                         self.bytes_remaining = c.try_into().map_err(|_| {
                             AvroError::ParseError(format!("Block size cannot 
be negative, got {c}"))
                         })?;
diff --git a/arrow-avro/src/reader/header.rs b/arrow-avro/src/reader/header.rs
index c5593ba0ad..235166bb76 100644
--- a/arrow-avro/src/reader/header.rs
+++ b/arrow-avro/src/reader/header.rs
@@ -253,7 +253,7 @@ impl HeaderDecoder {
                     }
                 }
                 HeaderDecoderState::BlockCount => {
-                    if let Some(block_count) = self.vlq_decoder.long(&mut buf) 
{
+                    if let Some(block_count) = self.vlq_decoder.long(&mut 
buf)? {
                         match block_count.try_into() {
                             Ok(0) => {
                                 self.state = HeaderDecoderState::Sync;
@@ -271,7 +271,7 @@ impl HeaderDecoder {
                     }
                 }
                 HeaderDecoderState::BlockLen => {
-                    if self.vlq_decoder.long(&mut buf).is_some() {
+                    if self.vlq_decoder.long(&mut buf)?.is_some() {
                         self.state = HeaderDecoderState::KeyLen
                     }
                 }
@@ -301,13 +301,13 @@ impl HeaderDecoder {
                     }
                 }
                 HeaderDecoderState::KeyLen => {
-                    if let Some(len) = self.vlq_decoder.long(&mut buf) {
+                    if let Some(len) = self.vlq_decoder.long(&mut buf)? {
                         self.bytes_remaining = len as _;
                         self.state = HeaderDecoderState::Key;
                     }
                 }
                 HeaderDecoderState::ValueLen => {
-                    if let Some(len) = self.vlq_decoder.long(&mut buf) {
+                    if let Some(len) = self.vlq_decoder.long(&mut buf)? {
                         self.bytes_remaining = len as _;
                         self.state = HeaderDecoderState::Value;
                     }
diff --git a/arrow-avro/src/reader/vlq.rs b/arrow-avro/src/reader/vlq.rs
index 26bf656159..cc028458ed 100644
--- a/arrow-avro/src/reader/vlq.rs
+++ b/arrow-avro/src/reader/vlq.rs
@@ -15,6 +15,8 @@
 // specific language governing permissions and limitations
 // under the License.
 
+use crate::errors::AvroError;
+
 /// Decoder for zig-zag encoded variable length (VLW) integers
 ///
 /// See also:
@@ -29,8 +31,18 @@ pub struct VLQDecoder {
 
 impl VLQDecoder {
     /// Decode a signed long from `buf`
-    pub fn long(&mut self, buf: &mut &[u8]) -> Option<i64> {
+    ///
+    /// Returns `Err` if more than 10 continuation bytes accumulate across 
calls without a
+    /// terminator, which would otherwise overflow the accumulated `i64`.
+    pub fn long(&mut self, buf: &mut &[u8]) -> Result<Option<i64>, AvroError> {
         while let Some(byte) = buf.first().copied() {
+            if self.shift == 63 && byte >= 0x02 {
+                self.in_progress = 0;
+                self.shift = 0;
+                return Err(AvroError::ParseError(
+                    "Malformed Avro varint: too many continuation 
bytes".to_string(),
+                ));
+            }
             *buf = &buf[1..];
             self.in_progress |= ((byte & 0x7F) as u64) << self.shift;
             self.shift += 7;
@@ -38,10 +50,10 @@ impl VLQDecoder {
                 let val = self.in_progress;
                 self.in_progress = 0;
                 self.shift = 0;
-                return Some((val >> 1) as i64 ^ -((val & 1) as i64));
+                return Ok(Some((val >> 1) as i64 ^ -((val & 1) as i64)));
             }
         }
-        None
+        Ok(None)
     }
 }
 
@@ -163,4 +175,48 @@ mod tests {
             varint_test(rand::random());
         }
     }
+
+    fn zigzag_encode(n: i64) -> u64 {
+        ((n << 1) ^ (n >> 63)) as u64
+    }
+
+    fn long_test(n: i64) {
+        let mut buf = [0_u8; 10];
+        let len = encode_var(zigzag_encode(n), &mut buf);
+        let mut decoder = VLQDecoder::default();
+        let mut slice = &buf[..len];
+        assert_eq!(decoder.long(&mut slice).unwrap(), Some(n));
+        assert!(slice.is_empty());
+    }
+
+    #[test]
+    fn test_long_roundtrip() {
+        long_test(0);
+        long_test(1);
+        long_test(-1);
+        long_test(4395932);
+        long_test(-4395932);
+        long_test(i64::MAX);
+        long_test(i64::MIN);
+
+        for _ in 0..1000 {
+            long_test(rand::random());
+        }
+    }
+
+    #[test]
+    fn test_long_overlong_varint_returns_error() {
+        // The Avro file magic followed by 13 continuation bytes and a 
terminator, as
+        // reported against #10290: previously drove `self.shift` past 63 and 
panicked
+        // with "attempt to shift left with overflow" inside `<< self.shift`.
+        let overlong = [0xFFu8; 13];
+        let mut decoder = VLQDecoder::default();
+        let mut buf = &overlong[..];
+        assert!(decoder.long(&mut buf).is_err());
+
+        // The decoder's state must be reset after a malformed varint, so a 
subsequent
+        // call decodes a fresh varint cleanly rather than staying stuck 
mid-decode.
+        let mut fresh = &[0x02u8][..]; // zigzag-encoded 1
+        assert_eq!(decoder.long(&mut fresh).unwrap(), Some(1));
+    }
 }

Reply via email to