Apache ORC C++ Reader β Unbounded Allocation & Integer Overflow Report
Overview
Four (4) security findings in the Apache ORC C++ core reader (orc::createReader() / RowReader::next()) discovered via 6.5-hour AFL++ fuzzing campaign and manual PoC development.
Target: Apache ORC C++ v2.3.1 (commit a6f12fd, 2026-07-17)
Toolchain: clang-18 + ASan + UBSan + trace-pc-guard
Fuzzer: 3 AFL++ workers, 1M+ execs, 23.81% coverage
Reproducer: Any program calling orc::createReader() + RowReader::next() on the crafted .orc files
Finding 1: DictionaryLoader β Integer Wraparound (CWE-190)
File: c++/src/DictionaryLoader.cc:67
PoC: dictionary_wraparound_poc.orc
uint32_t dictSize = encoding.dictionary_size() β no upper-bound validation. When dictSize = 0xFFFFFFFF, dictSize + 1 wraps to 0 in uint32 arithmetic. dictionaryOffset.resize(0) allocates a zero-capacity buffer, and lengthDecoder->next() writes ~4.29 billion int64_t values past it β heap-buffer-overflow.
Confirmation: ASan heap-buffer-overflow WRITE (standalone harness + real ORC file) Prior Art: β EnigmaConsultant/orc-dictionary-overflow (same root cause, included for completeness) Severity: High (OOB write from crafted column encoding metadata)
Finding 2: DictionaryLoader β Unbounded Allocation (CWE-789)
File: c++/src/DictionaryLoader.cc:67
PoC: dictionary_unbounded_alloc_poc.orc
Same missing upper bound on dictSize, but with a large positive value (e.g., 0xfffff9cd = 4294965709) instead of the exact wraparound boundary. This causes resize(4294965710) β ~34GB allocation β ASan OOM / allocation-size-too-big crash.
Confirmation: ASan allocation-size-too-big (fuzz crash from 6.5h campaign) Novelty: β Not covered by EnigmaConsultant (whose PoC only tests the exact 0xFFFFFFFF wraparound) Severity: Medium-High (OOM / resource exhaustion via uncontrolled allocation)
Finding 3: ColumnReader β Unbounded Allocation (CWE-789)
File: c++/src/ColumnReader.cc:743 (StringVectorBatch / StringDirectColumnReader)
PoCs: string_vectorbatch_resize_poc.orc, string_direct_reader_resize_poc.orc
Two crash files from the fuzzer that trigger DataBuffer<char>::resize(huge) via:
StringVectorBatch::resize()called fromStringDictionaryColumnReader::next()(745B crash)StringDirectColumnReader::next()direct blob resize (373KB crash)
Both paths decode untrusted stream lengths and allocate without upper-bound validation.
Confirmation: ASan heap-buffer-overflow / allocation-size-too-big (fuzz-confirmed) Novelty: β Novel β separate code path from DictionaryLoader Severity: Medium (OOB write / OOM)
Finding 4: StripeStream β Integer Overflow β OOB Read (CWE-190 β CWE-125)
File: c++/src/StripeStream.cc:94
PoC: stripestream_offset_overflow_poc.orc
Confirmation: Manually-confirmed (protobuf-valid PoC)
Root Cause
// StripeStream.cc:82-126
uint64_t offset = stripeStart_;
uint64_t dataEnd = stripeInfo_.offset() + stripeInfo_.index_length() + stripeInfo_.data_length();
for (int i = 0; i < footer_.streams_size(); ++i) {
const proto::Stream& stream = footer_.streams(i);
if (stream.has_kind() && stream.kind() == kind && stream.column() == columnId) {
if (offset + stream.length() > dataEnd) { /* overflow-unsafe addition */ ... }
...
}
offset += stream.length(); /* cumulative, overflow-unsafe */
}
offset += stream.length() is applied to ALL streams regardless of whether they match the search. If a non-matching stream has length = UINT64_MAX, the cumulative offset wraps. A subsequent matching stream then bypasses the offset + streamLength > dataEnd check because the wrapped value is small.
ASan Output (from PoC)
READ of size 200 at 0x... (1 byte before buffer)
UBSan: addition of unsigned offset to 0x... overflowed to 0x...
Stack: MemoryInputStream::read β SeekableFileInputStream::Next
β RleDecoderV2::readByte β StringDirectColumnReader::next
Impact
Heap-buffer-overflow READ (not WRITE). The wrapped offset causes the reader to read from memory before the allocated buffer, leaking or crashing.
Novelty: β Novel β different mechanism from DictionaryLoader, manually confirmed Severity: Medium (OOB read, potential info leak / crash)
Fix Recommendations
DictionaryLoader.cc:67 β Add upper-bound check on
dictionary_size:if (dictSize > MAX_DICT_ENTRIES) throw ParseError("..."); dictionary->dictionaryOffset.resize(static_cast<uint64_t>(dictSize) + 1);ColumnReader.cc β Add upper-bound check on decoded lengths before
resize():if (totalLength > MAX_BLOB_SIZE) throw ParseError("..."); byteBatch.blob.resize(totalLength);StripeStream.cc:94 β Use overflow-safe addition for cumulative offset and data checks:
if (addOverflow(offset, stream.length(), &offset)) throw ParseError("...");
Timeline
| Date | Event |
|---|---|
| 2026-07-19 | Fuzzing campaign starts (3 AFL++ workers) |
| 2026-07-19 | 7 crashes captured, all traced to missing upper bounds |
| 2026-07-19 | StripeStream overflow manually confirmed with protobuf-valid PoC |
| 2026-07-19 | Report finalized |
Credits
- @drogba771 β Discovery, fuzzing, PoC, analysis