nlebovits opened a new issue, #12100:
URL: https://github.com/apache/arrow-rs/issues/12100

   ### Describe the bug
   
   `ThriftReadInputProtocol::read_bytes_owned` (parquet/src/parquet_thrift.rs) 
reads a varint length from the file and calls `Vec::with_capacity(len)` before 
reading any data. The length is not compared with the remaining input. A page 
header whose statistics declare a very large `binary` value aborts the process 
with an allocation failure instead of returning an error. This is reachable 
from the public API when page statistics are enabled 
(`ReaderPropertiesBuilder::set_read_page_statistics(true)`), for example 
through `SerializedPageReader`. A smaller length (1 GB) allocates 1 GB for a 
file of a few hundred bytes.
   
   Found while fuzzing a downstream GeoParquet reader. This is the `Read`-based 
counterpart of the checks added for `read_thrift_vec` in #10979.
   
   ### To Reproduce
   
   `Cargo.toml`:
   ```toml
   [dependencies]
   parquet = { version = "60.1.0", default-features = false }
   bytes = "1"
   ```
   
   `main.rs` (the argument is the declared length):
   ```rust
   use bytes::Bytes;
   use parquet::basic::Type;
   use parquet::column::writer::ColumnWriter;
   use parquet::file::properties::WriterProperties;
   use parquet::file::properties::ReaderProperties;
   use parquet::file::reader::{FileReader, SerializedFileReader};
   use parquet::file::serialized_reader::ReadOptionsBuilder;
   use parquet::file::writer::SerializedFileWriter;
   use parquet::schema::types::Type as SchemaType;
   use std::sync::Arc;
   use std::time::Instant;
   
   fn valid_file() -> Vec<u8> {
       let schema = SchemaType::group_type_builder("s")
           .with_fields(vec![Arc::new(
               SchemaType::primitive_type_builder("a", 
Type::INT32).with_repetition(parquet::basic::Repetition::REQUIRED).build().unwrap(),
           )])
           .build()
           .unwrap();
       let mut out = Vec::new();
       let props = Arc::new(WriterProperties::builder().build());
       let mut w = SerializedFileWriter::new(&mut out, Arc::new(schema), 
props).unwrap();
       let mut rg = w.next_row_group().unwrap();
       let mut col = rg.next_column().unwrap().unwrap();
       if let ColumnWriter::Int32ColumnWriter(c) = col.untyped() {
           c.write_batch(&[1, 2, 3], None, None).unwrap();
       }
       col.close().unwrap();
       rg.close().unwrap();
       w.close().unwrap();
       out
   }
   
   fn main() {
       let n: u64 = std::env::args().nth(1).unwrap().parse().unwrap();
       let v = valid_file();
       // footer = thrift metadata + 4-byte length + "PAR1"
       let meta_len = u32::from_le_bytes(v[v.len() - 8..v.len() - 
4].try_into().unwrap()) as usize;
       let footer = &v[v.len() - 8 - meta_len..];
       // Page header (type=0, sizes=100) with data_page_header { num_values, 
encoding,
       // def/rep level encodings, statistics { field 1 (binary) with declared 
length `n` } }
       let mut f = b"PAR1".to_vec();
       f.extend_from_slice(&[0x15, 0x00, 0x15, 0xc8, 0x01, 0x15, 0xc8, 0x01]);
       f.extend_from_slice(&[0x2c, 0x15, 0x06, 0x15, 0x00, 0x15, 0x00, 0x15, 
0x00, 0x1c, 0x18]);
       let mut n_ = n;
       while n_ >= 0x80 {
           f.push((n_ & 0x7f) as u8 | 0x80);
           n_ >>= 7;
       }
       f.push(n_ as u8);
       f.extend_from_slice(footer);
   
       let t = Instant::now();
       let r = SerializedFileReader::new_with_options(
           Bytes::from(f),
           ReadOptionsBuilder::new()
               
.with_reader_properties(ReaderProperties::builder().set_read_page_statistics(true).build())
               .build(),
       )
       .unwrap();
       let rg = r.get_row_group(0).unwrap();
       let mut pr = rg.get_column_page_reader(0).unwrap();
       println!("n={n}: {:?}", pr.get_next_page().map(|p| p.map(|p| 
p.page_type())));
       println!("elapsed {:?}", t.elapsed());
   }
   ```
   
   Output on parquet 60.1.0 (`ulimit -v 2000000; timeout 60 ./repro-3 
1099511627776`):
   ```
   memory allocation of 1099511627776 bytes failed
   timeout: the monitored command dumped core   (exit 134)
   ```
   With `1000` or `1000000000` the call returns `Err(External(... UnexpectedEof 
...))`, after reserving that much memory. Same behaviour on current main 
(bb47f80).
   
   ### Expected behavior
   
   `Err(ParquetError)`, without a large up-front allocation.
   
   ### Additional context
   
   Suggested fix: do not trust `len` for the reservation. Either read through 
`Read::take(len).read_to_end(&mut v)` into an empty `Vec` (it grows as data 
arrives), or cap the reserve (for example `Vec::with_capacity(len.min(8192))`), 
and return `Eof` if fewer than `len` bytes were read. Environment: parquet 
60.1.0 and main bb47f80, rustc 1.99.1, Linux aarch64.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to