parquet: panic (index out of bounds) reading FIXED_LEN_BYTE_ARRAY dictionary page with out-of-range index
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 83/100
Research direction
Start in parquet/src/arrow/array_reader/fixed_len_byte_array.rs, at ValueDecoder::read, where the issue shows the dictionary buffer is sliced using the decoded key. Read the related out-of-range handling in RleDecoder::get_batch_with_dict from #9434, then run or adapt the reproduction in the issue. Done when an out-of-range dictionary index returns a ParquetError instead of panicking.
Written by the indexing model from the issue text.
Description
Describe the bug
Reading a FIXED_LEN_BYTE_ARRAY column whose data page contains a dictionary index that is out of range for the dictionary page panics instead of returning an error.
ValueDecoder::read in parquet/src/arrow/array_reader/fixed_len_byte_array.rs slices the dictionary buffer with key * byte_length without checking the key against the number of dictionary entries:
let offset = *key as usize * self.byte_length;
let val = &dict.as_ref()[offset..offset + self.byte_length];
RleDecoder::get_batch_with_dict returns an error for out-of-range indices (#9434), but this path indexes the dictionary directly.
To Reproduce
With parquet = "60.0.0", arrow-array = "60.0.0", arrow-schema = "60.0.0", bytes = "1":
use std::sync::Arc;
use arrow_array::{FixedSizeBinaryArray, RecordBatch};
use arrow_schema::{DataType, Field, Schema};
use bytes::Bytes;
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use parquet::arrow::ArrowWriter;
use parquet::file::properties::{WriterProperties, WriterVersion};
fn main() {
// A FIXED_LEN_BYTE_ARRAY(4) column: 50 x "aaaa" then 50 x "bbbb", so the
// data page holds dictionary indices as two RLE runs (0 then 1).
let values: Vec<&[u8]> = (0..100).map(|i| if i < 50 { b"aaaa" } else { b"bbbb" } as &[u8]).collect();
let array = FixedSizeBinaryArray::try_from_iter(values.into_iter()).unwrap();
let schema = Arc::new(Schema::new(vec![Field::new("c", DataType::FixedSizeBinary(4), false)]));
let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(array)]).unwrap();
let mut buf = Vec::new();
let props = WriterProperties::builder()
.set_writer_version(WriterVersion::PARQUET_2_0) // dictionary for FLBA
.set_dictionary_enabled(true)
.build();
let mut writer = ArrowWriter::try_new(&mut buf, schema, Some(props)).unwrap();
writer.write(&batch).unwrap();
writer.close().unwrap();
// Data page values: bit width 1, RLE run (50 x 0), RLE run (50 x 1).
// Change the bit width to 8 and the second run's index to 255; the
// dictionary only has 2 entries.
let pattern = [0x01, 0x64, 0x00, 0x64, 0x01];
let pos = buf.windows(pattern.len()).position(|w| w == pattern).expect("pattern");
buf[pos] = 8;
buf[pos + 4] = 0xFF;
let reader = ParquetRecordBatchReaderBuilder::try_new(Bytes::from(buf))
.unwrap()
.build()
.unwrap();
for batch in reader {
println!("{:?}", batch.map(|b| b.num_rows()));
}
}
Output (debug and release):
thread 'main' panicked at parquet-60.0.0/src/arrow/array_reader/fixed_len_byte_array.rs:497:49:
range start index 1020 out of range for slice of length 8
Backtrace (abridged):
<parquet::arrow::array_reader::fixed_len_byte_array::ValueDecoder as ColumnValueDecoder>::read::{{closure}}
parquet::arrow::decoder::dictionary_index::DictIndexDecoder::read
<parquet::arrow::array_reader::fixed_len_byte_array::ValueDecoder as ColumnValueDecoder>::read
parquet::column::reader::GenericColumnReader<R,D,V>::read_records_with_reservation
...
Expected behavior
The reader returns a ParquetError (e.g. "dictionary index out of bounds") instead of panicking.
Additional context
- Reproduces with
parquet60.0.0 and onmainat 33d338ddf (panic atfixed_len_byte_array.rs:432). - Found while fuzzing a Parquet reader built on arrow-rs. Related: #9434, #9742.
- Dominant language
- Rust
- Stars
- 3.6k
- Forks
- 1.4k
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 253
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from apache/arrow-rs
-
Add coalesce benchmarks for Boolean, Dictionary, and REEPossibly taken @Rich-T-kid claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 Under an hour Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
`substring` errors when the bytes under a null `Utf8` slot would be cut inside a characterPossibly taken @cakeni claimed this 3 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
apache/arrow-rs#11590 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
-
enhancement parquet
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
home-assistant/installer#308 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
swc-project/swc-next#1053 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
nautechsystems/nautilus_trader#5387 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
NomicFoundation/edr#1872 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
linebender/resvg#1179 · 1 comment ·
Maintainers usually reply within 2 days