fix(arrow-avro): bound untrusted OCF block size and item counts#10237
fix(arrow-avro): bound untrusted OCF block size and item counts#10237miniex wants to merge 4 commits into
Conversation
|
run benchmarks avro_reader decoder |
|
🤖 Arrow criterion benchmark running (GKE) | trigger CPU Details (lscpu)Comparing fix/avro-untrusted-block-bounds (5907f95) to 7616e10 (merge-base) diff File an issue against this benchmark runner |
|
🤖 Arrow criterion benchmark running (GKE) | trigger CPU Details (lscpu)Comparing fix/avro-untrusted-block-bounds (5907f95) to 7616e10 (merge-base) diff File an issue against this benchmark runner |
|
🤖 Arrow criterion benchmark completed (GKE) | trigger Instance: CPU Details (lscpu)Details
Resource Usagebase (merge-base)
branch
File an issue against this benchmark runner |
|
🤖 Arrow criterion benchmark completed (GKE) | trigger Instance: CPU Details (lscpu)Details
Resource Usagebase (merge-base)
branch
File an issue against this benchmark runner |
A crafted OCF block size of i64::MAX hit Vec::reserve before any payload was read, aborting the process. Reserve only what the input backs. Closes apache#10234
A block count of i64::MAX spun the item loop forever for zero-byte items like null, and i64::MIN overflowed the negation. Reject counts above the bytes remaining and negate with unsigned_abs. Closes apache#10235
Co-authored-by: Jeffrey Vo <jeffrey.vo.australia@gmail.com>
The old check rejected any block with more items than bytes remaining, wrongly refusing valid `array<null>` (zero-byte items). Cap the item total at `i32::MAX` instead; oversized counts still error before the loop.
605a62c to
83f3909
Compare
| /// Decode `count` items, capping the running total at `i32::MAX` (the largest index | ||
| /// an Arrow list/map offset holds). Otherwise a crafted `i64::MAX` count of a zero-byte | ||
| /// item like `null` spins the loop forever (#10235); byte-consuming items self-terminate | ||
| /// on cursor exhaustion, so valid blocks (including `array<null>`) are unaffected. | ||
| #[inline] | ||
| fn process_block_items( | ||
| buf: &mut AvroCursor, | ||
| count: usize, | ||
| total: usize, | ||
| on_item: &mut impl FnMut(&mut AvroCursor) -> Result<(), AvroError>, | ||
| ) -> Result<usize, AvroError> { | ||
| let new_total = total.checked_add(count).filter(|&t| t <= i32::MAX as usize); |
There was a problem hiding this comment.
i'm not sure about hard limiting to i32::MAX either considering we have LargeLists which have i64 offsets; i'll refer to my previous question, in wondering how other avro implementations deal with this issue?
Which issue does this PR close?
Rationale for this change
The Avro OCF reader trusts two length fields straight from the input, so a small crafted file can take down a process that parses untrusted Avro. A block size of
i64::MAXreachesVec::reservebefore any payload is read and aborts the process on a huge allocation (#10234). A block count ofi64::MAXspins the array/map item loop forever for a zero-byte item type likenull, andi64::MINoverflows the negative-count negation (#10235).What changes are included in this PR?
block.rs: reserve only what the current input buffer backs, and let the rest grow as data arrives.record.rs: reject a block item count larger than the bytes left to decode, and take the negative-count magnitude withunsigned_abs.cursor.rs: addAvroCursor::remaining(), used by that bound.A count past the remaining bytes can only describe items that are not there. Items that read input each need at least one byte, so only the zero-byte case is rejected and valid blocks keep working.
Are these changes tested?
Yes. The new tests hang or abort before this change and pass after. In
reader::block, ani64::MAXblock size stays bounded instead of aborting, a negative size errors, and a well-formed block still round-trips. Inreader::record,i64::MAXandi64::MINblock counts on anarray<null>now error instead of spinning the item loop.fmt --checkandclippyare clean.Are there any user-facing changes?
Malformed Avro OCF input that used to abort or hang now returns a clean
AvroError. There are no public API changes.I'm Korean, so sorry if any wording reads a little awkward.