orc-alloc-poc / report.md
drogba771's picture
Upload report.md with huggingface_hub
626931b verified
|
Raw
History Blame Contribute Delete
5.46 kB

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 from StringDictionaryColumnReader::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

  1. 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);
    
  2. ColumnReader.cc β€” Add upper-bound check on decoded lengths before resize():

    if (totalLength > MAX_BLOB_SIZE) throw ParseError("...");
    byteBatch.blob.resize(totalLength);
    
  3. 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