PoC: Unbounded allocation from Parquet footer row_groups list count (apache/arrow-rs parquet crate)
Status: gated, access-controlled proof-of-concept for authorized security research / bug-bounty triage only.
Summary
The parquet crate in apache/arrow-rs (current
main, crate version 59.1.0, commit 6035e287adcc3f1a574c6438b4b610faded2d9b6)
aborts the process with an unrecoverable allocation failure when parsing a
maliciously crafted Parquet file footer. The custom Thrift-compact decoder used
for footer/metadata parsing reads the declared element count of the
row_groups list directly from attacker-controlled bytes and immediately calls
Vec::with_capacity(list_ident.size as usize) before validating that the
declared count is remotely plausible given the actual size of the input.
A 490-byte crafted .parquet file (poc_row_groups_alloc_bomb.parquet in this
repo) declares row_groups.size = 195,932,266. Because RowGroupMetaData is
~96 bytes, this drives an immediate ~18.8 GB allocation request, which fails
and aborts the process via Rust's global allocator error handler
(std::alloc::handle_alloc_error β abort()).
This is a memory-exhaustion / denial-of-service vulnerability: opening a
tiny, untrusted Parquet file (as any service that accepts user-uploaded
Parquet data would do) crashes the process outright, or β on a host with
enough RAM/overcommit to satisfy the allocation β reserves tens of gigabytes
from a few hundred bytes of input, which can OOM-kill co-located
processes/containers. Because this happens inside Rust's allocator error
hook rather than as a panic!, it cannot be caught with
std::panic::catch_unwind, regardless of the consuming application's panic
strategy.
Where
- Repo: https://github.com/apache/arrow-rs
- File:
parquet/src/file/metadata/thrift/mod.rs - Function:
parquet_metadata_from_bytes(field id4,row_groups) - Vulnerable line:
let list_ident = prot.read_list_begin()?; // check for list of struct validate_list_type(ElementType::Struct, &list_ident)?; let mut rg_vec = Vec::with_capacity(list_ident.size as usize); // <-- unvalidated list_ident.sizeis ani32read straight off the wire inparquet/src/parquet_thrift.rs::read_list_begin()(varint-decoded, capped only ati32::MAX, i.e. up to ~2.1 billion) β there is no check against the remaining bytes in the buffer before the count is used to size an allocation.- The exact same unvalidated pattern (
Vec::with_capacity(list_ident.size as usize)immediately afterread_list_begin()/validate_list_type(), with no bound check) also appears in:parquet/src/parquet_thrift.rs::read_thrift_vec()β the generic Thrift list reader used forschema,key_value_metadata,column_orders, and other Thrift lists throughout metadata parsing.parquet/src/file/page_index/offset_index.rs:94βpage_locationslist when decoding the Parquet page index (offset index). These two additional call sites were not independently fuzzed/PoC'd in this pass (only therow_groupspath was confirmed with a live crash) but they share the identical unbounded-allocation-from-attacker-controlled-count pattern and are flagged here for the maintainers' awareness as the same root-cause class.
How it was found
Coverage-guided fuzzing with AFL++ (cargo-afl / afl.rs), 3 parallel
instances (-M main + 2x -S), ~10-20 min wall-clock, driving the
real, unmodified parquet crate straight from a fresh clone of
apache/arrow-rs main via a Cargo path dependency (no source patching).
Build flags: overflow-checks = true, debug-assertions = true, panic = "abort", release opt-level 1, so any panic or allocator abort is caught by
AFL as a crash. Seeds: a handful of small valid .parquet files from
parquet-testing and the arrow-rs test suite. The campaign found this same
allocation-failure root cause independently 5 times across all 3 fuzzer
instances (different declared sizes: ~18.8GB, ~22.8GB, ~25.4GB, ~25.8GB,
~45.6GB β same code path, same bug).
The harness (harness_main.rs in this repo) calls
ParquetMetaDataReader::new().with_page_index_policy(PageIndexPolicy::Optional).try_parse(&bytes)
directly on the fuzzer input, i.e. it exercises exactly the footer + row-group
- column-chunk metadata parsing path.
Real-world confirmation (not just the harness)
The crash was independently reproduced against the crate's own shipped,
unmodified parquet-read CLI binary (built with cargo build --release --bin parquet-read --features cli, no fuzzing harness or AFL instrumentation
involved):
$ ./target/release/parquet-read poc_row_groups_alloc_bomb.parquet
memory allocation of 18809497536 bytes failed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
[process aborts, exit code 134 / SIGABRT]
This confirms the bug is reachable through the crate's standard, documented
API surface (ParquetMetaDataReader / SerializedFileReader), not an
artifact of the fuzz harness.
Reproduction
git clone --depth 1 https://github.com/apache/arrow-rs.git
cd arrow-rs/parquet
cargo build --release --bin parquet-read --features cli
./target/release/parquet-read /path/to/poc_row_groups_alloc_bomb.parquet
# -> "memory allocation of 18809497536 bytes failed", process aborts (SIGABRT)
Or directly against the metadata reader:
let bytes = std::fs::read("poc_row_groups_alloc_bomb.parquet").unwrap();
let bytes = bytes::Bytes::from(bytes);
let _ = parquet::file::metadata::ParquetMetaDataReader::new()
.parse_and_finish(&bytes); // aborts the process
ASan note
AFL++'s Rust bindings (afl.rs) do not currently support combining the
bundled AFL LLVM coverage runtime with -Z sanitizer=address + -Z build-std in this toolchain without a duplicate lang item link
conflict, so this campaign relied on AFL coverage instrumentation plus
overflow-checks/debug-assertions/panic=abort rather than ASan. The
crash reported here is a deterministic, 100%-reproducible allocator abort
(std::alloc::handle_alloc_error), independently confirmed via the crate's
own shipped CLI binary, so the lack of ASan does not weaken the evidence β
there is no memory-corruption claim being made here, only unbounded resource
consumption from an unvalidated attacker-controlled length field.
Dedup / prior-art check
Searched GitHub issues/PRs on apache/arrow-rs for prior reports of this
class of bug before finalizing:
- Issue #9705 / PR
#9725 (merged 2026-04-16):
fixed a different panic β an
assert!(end <= remainder.len())inParquetMetaDataReadertriggered by a truncated file during page index reading. Different code path, different trigger condition (an assertion on a byte range, not an unboundedVec::with_capacityfrom a raw Thrift list count), already fixed in the commit this PoC was built against. - Issue #9742: tracks a
plan to add a corruption/mutation fuzzer for the parquet crate (not yet
implemented at the time of this campaign β confirmed by the absence of any
fuzz/directory orcargo-fuzz/libFuzzer/AFL harness anywhere in theapache/arrow-rstree as of this clone). - No CVE/GHSA advisory, issue, or PR found describing an unbounded
allocation from the
row_groups,schema,key_value_metadata,column_orders, or page-indexpage_locationsThrift list counts. - The custom Thrift decoder itself (
parquet/src/parquet_thrift.rs,parquet/src/file/metadata/thrift/) is relatively new, written for performance (see issue #5854 / PR #8530, "3x-9x Faster Apache Parquet Footer Metadata Using a Custom Thrift Parser in Rust", Oct 2025) and replaced use of the general-purposethriftcrate β consistent with no prior disclosure existing for this specific implementation's input validation gap.
Files in this repo
poc_row_groups_alloc_bomb.parquetβ 490-byte minimized (viaafl-tmin) crash input. Declaresrow_groups.size = 195,932,266in the footer, driving an ~18.8 GBVec::with_capacityrequest.harness_main.rsβ the exact AFL++ (afl.rs) fuzz harness used, callingParquetMetaDataReader::try_parse(footer + row-group + column-chunk + page-index metadata parsing) on unmodified crate source via a Cargo path dependency.harness_Cargo.tomlβ harness manifest (path dependency onapache/arrow-rs'sparquetcrate,default-features = false,features = ["base64"],panic = "abort",overflow-checks = true,debug-assertions = true).
Access
Gated β granted to protectai-bot and Enigma Partners Global bug-bounty
reviewers for triage purposes. Not for public redistribution while the
issue is unpatched upstream.