| # 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: |
|
|
| ```cpp |
| 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()`: |
|
|
| ```cpp |
| 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()`: |
|
|
| ```cpp |
| 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: |
| |
| ```bash |
| 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: |
| |
| ```bash |
| 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()` in |
| `getRowIndexStatistics()`; |
| - reject `stream.column() >= selectedColumns.size()` in |
| `extractReadRangesForStripe()`; |
| - reject `colId >= selectedColumns_.size()` and validate the parallel |
| `currentStripeFooter_.columns()` lookup in `loadStripeIndex()`. |
|
|
| 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. |
|
|