YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Apache ORC stripe stream column-id heap-buffer-overflow
Status: reproduced on current main; prior-art gate open.
Apache ORC's public C++ Reader::getStripeStatistics() path trusts the
attacker-controlled column id of every ROW_INDEX stream in a stripe footer.
It uses that id with unchecked std::vector::operator[], then calls
push_back() through the out-of-bounds vector object.
A 272-byte ORC file with three valid columns and one mutated stream column id reliably produces an AddressSanitizer heap-buffer-overflow. The control differs by one byte and exits normally.
Tested at Apache ORC commit
af4cbf36b051c176f0d13ae5a7ac436ada8aeccf (2026-07-29).
Root cause
ReaderImpl::getStripeStatistics() sizes indexStats from the number of
column-statistics entries:
size_t num_cols = contents_->metadata->stripe_stats(stripeIndex).col_stats_size();
std::vector<std::vector<proto::ColumnStatistics>> indexStats(num_cols);
getRowIndexStatistics(..., &indexStats);
getRowIndexStatistics() then trusts stream.column() without checking it
against indexStats->size():
size_t column = static_cast<size_t>(stream.column());
for (int j = 0; j < num_entries; j++) {
const proto::RowIndexEntry& entry = rowIndex.entry(j);
(*indexStats)[column].push_back(entry.statistics());
}
The same attacker-controlled field is also used without bounds checks in
extractReadRangesForStripe() and RowReaderImpl::loadStripeIndex():
selectedColumns[stream.column()]
selectedColumns_[colId]
Differential proof
| Fixture | Third ROW_INDEX stream column | Result |
|---|---|---|
control-valid-column.orc |
2 | Prints all statistics; exit 0 |
trigger-column-3.orc |
3 | ASan heap-buffer-overflow; abort |
Both fixtures are 272 bytes. They differ at byte 74 only. Column 3 is exactly one past the three valid columns 0, 1, and 2.
AddressSanitizer reports an 8-byte read eight bytes after the 72-byte
vector<vector<ColumnStatistics>> allocation. The stack reaches
Reader.cc:834 through the official orc-statistics --withIndex tool.
Controls exited 0 and triggers aborted in three of three runs.
Reproduce
Build current Apache ORC with explicit sanitizer flags:
cmake -S orc -B orc-sanitized \
-DCMAKE_BUILD_TYPE=Debug \
-DBUILD_JAVA=OFF \
-DBUILD_CPP_TESTS=OFF \
-DBUILD_TOOLS=ON \
-DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
cmake --build orc-sanitized --target orc-statistics --parallel 4
Then:
ORC_STATISTICS=/path/to/orc-sanitized/tools/src/orc-statistics ./reproduce.sh
generate_fixture.py materializes both deterministic files and asserts their
SHA-256 hashes.
Impact
An application or service that extracts row-index statistics from an untrusted ORC file can be crashed by a one-byte column-id mutation. This is a native out-of-bounds access in a file parser, not allocation pressure.
The adjacent ordinary row-reader paths trust the same field, broadening the set of potentially affected consumers.
Demonstrated impact: deterministic denial of service and native heap out-of-bounds access. No code-execution claim is made.
Suggested remediation
Before every use of a stripe stream's column id:
- reject
stream.column() >= indexStats->size()ingetRowIndexStatistics(); - reject
stream.column() >= selectedColumns.size()inextractReadRangesForStripe(); - reject
colId >= selectedColumns_.size()and validate the parallelcurrentStripeFooter_.columns()lookup inloadStripeIndex().
Return ParseError for malformed stripe metadata and add regression fixtures
for column == size, very large column ids, and missing column fields.
Novelty
The exact scan found no Hugging Face or local matches. Apache ORC issue #1475 concerns writer stream ordering, PR #2144 introduced the row-index statistics API, and ORC-2198/ORC-2200 validate byte extents. None reports or fixes attacker-controlled stripe stream column ids indexing these vectors.