YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Apache ORC C++ LIST length child-count corruption
Status: reproduced on current main; prior-art gate open.
Apache ORC's C++ ListColumnReader decodes the untrusted LIST LENGTH
stream as unsigned integers into int64_t slots, then casts each slot to
uint64_t and accumulates it without validating the value or the sum.
A 289-byte ORC file can therefore drive either an impossible child allocation
or wrapped, negative collection offsets.
Tested at Apache ORC commit
af4cbf36b051c176f0d13ae5a7ac436ada8aeccf (2026-07-29).
Root cause
The LIST reader selects unsigned RLE:
rle_ = createRleDecoder(std::move(stream), false, vers, memoryPool, metrics);
The decoded bits are stored in int64_t, cast back to uint64_t, and added
without range or overflow checks:
rle_->next(offsets, numValues, notNull);
uint64_t totalChildren = 0;
for (size_t i = 0; i < numValues; ++i) {
uint64_t tmp = static_cast<uint64_t>(offsets[i]);
offsets[i] = static_cast<int64_t>(totalChildren);
totalChildren += tmp;
}
offsets[numValues] = static_cast<int64_t>(totalChildren);
childReader->next(*(listBatch.elements.get()), totalChildren, nullptr);
DataBuffer<T>::reserve() then trusts the impossible capacity, does not check
sizeof(T) * newCapacity, and does not check malloc() before copying:
buf_ = reinterpret_cast<T*>(
memoryPool_.malloc(sizeof(T) * newCapacity));
memcpy(buf_, buf_old, sizeof(T) * currentSize_);
The same unchecked accumulation exists in ListColumnReader::skip() and in
the parallel Map reader.
Differential proof
| Fixture | LIST lengths | Result with official orc-contents |
|---|---|---|
control-one-row.orc |
[1] |
Prints {"xs": [7]}; exit 0 |
trigger-huge-child-count.orc |
[2^63] |
SIGSEGV in child-batch resize |
control-two-rows.orc |
[1, 1] |
Prints both rows; exit 0 |
trigger-wrapped-offsets.orc |
[2^63, 2^63] |
Wrapped offsets; downstream OOB read/SIGBUS |
The one-row ASan/UBSan build crashes with EXC_BAD_ACCESS at a null write:
_platform_memmove
orc::DataBuffer<char>::reserve(unsigned long long) MemoryPool.cc:119
orc::DataBuffer<char>::resize(unsigned long long) MemoryPool.cc:167
orc::ColumnVectorBatch::resize(unsigned long long) Vector.cc:49
orc::IntegerVectorBatch<long long>::resize(...) Vector.hh:119
orc::ColumnReader::next(...) ColumnReader.cc:83
orc::ListColumnReader::nextInternal<false>(...) ColumnReader.cc:991
The two-row trigger makes totalChildren wrap to zero and returns offsets
[0, INT64_MIN, 0]. The official printer later starts the second row at
INT64_MIN, casts the index to uint64_t, and crashes reading the child
vector:
orc::LongColumnPrinter::printRow(unsigned long long) ColumnPrinter.cc:329
orc::ListColumnPrinter::printRow(unsigned long long) ColumnPrinter.cc:481
This is not merely an allocation-pressure test: the second fixture demonstrates parser-generated invalid offsets and a native out-of-bounds read in an ordinary consumer.
Reproduce
Build the pinned current source with the project sanitizer options:
cmake -S orc -B orc-asan \
-DCMAKE_BUILD_TYPE=DEBUG \
-DBUILD_JAVA=OFF \
-DBUILD_CPP_TESTS=OFF \
-DBUILD_TOOLS=ON \
-DORC_ENABLE_ASAN=ON \
-DORC_ENABLE_UBSAN=ON
cmake --build orc-asan --target orc-contents --parallel 4
Then:
ORC_CONTENTS=/path/to/orc-asan/tools/src/orc-contents ./reproduce.sh
generate_fixtures.py materializes the controls and applies the exact LIST
stream mutations. It asserts all four SHA-256 hashes before testing.
Impact
Applications that deserialize attacker-supplied ORC LIST or MAP columns can crash during ordinary row reads. Depending on the decoded lengths, the C++ reader can:
- pass an attacker-controlled impossible capacity into native buffer resizing, causing a null-pointer write after allocation failure;
- wrap
totalChildrenand return negative/non-monotonic offsets to callers; - induce out-of-bounds reads in consumers that trust those offsets.
The demonstrated impact is deterministic denial of service and native out-of-bounds read. This report does not claim code execution.
Suggested remediation
Reject any decoded collection length that is negative when represented as
int64_t, and reject every addition that would overflow uint64_t or exceed
the reader's practical allocation limit. Keep offsets representable and
monotonically nondecreasing in int64_t. Apply the checks to LIST and MAP
next, nextEncoded, and skip, then add malformed unsigned RLEv1/RLEv2
regression fixtures.
Separately, harden DataBuffer<T>::reserve() against multiplication overflow
and allocation failure.
Novelty
The fresh exact scan found no Hugging Face repository, Apache ORC issue/PR, or local report for LIST/MAP child-count accumulation. Public adjacent findings are different occurrences:
- ORC-2192: direct STRING byte-length handling in
StringDirectColumnReader::computeSize; poc-orc-rlev2-patchedbase-oob-cwe125: PATCHED_BASE decoder OOB;- ORC-414: malformed footer protobuf indexing;
- CVE-2018-8015: recursively nested schemas.
None covers ListColumnReader/MapColumnReader, totalChildren wrap, or
collection offsets.