trevdatastreams's picture
Add Apache ORC LIST length corruption PoC
7753f48 verified
|
Raw
History Blame Contribute Delete
5.29 kB

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 totalChildren and 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.