EnigmaConsultant's picture
Upload folder using huggingface_hub
c939b29 verified
|
Raw
History Blame Contribute Delete
6.17 kB

apache/orc C++ reader: integer overflow in DictionaryLoader leads to heap out-of-bounds write

Integer overflow (CWE-190) of the attacker-controlled dictionary_size field in apache/orc's C++ core reader leads to a zero-capacity heap allocation followed by an out-of-bounds write (CWE-787) while loading a string-dictionary-encoded column from a crafted .orc file.

  • Component: apache/orc, C++ core reader (liborc)
  • File: c++/src/DictionaryLoader.cc, function orc::loadStringDictionary
  • Format: ORC (Optimized Row Columnar)
  • Class: CWE-190 (Integer Overflow or Wraparound) leading to CWE-787 (Out-of-bounds Write)
  • Impact: heap out-of-bounds write when a consumer opens/reads an attacker-supplied .orc file

Root cause

loadStringDictionary (added 2025-11-05, commit cf85da12, JIRA ORC-2022) reads the dictionary size straight from the stripe-footer ColumnEncoding protobuf message with no upper-bound validation:

// c++/src/DictionaryLoader.cc  (main)
uint32_t dictSize = encoding.dictionary_size();          // line 50  attacker-controlled uint32

std::unique_ptr<SeekableInputStream> stream =
    stripe.getStream(columnId, proto::Stream_Kind_LENGTH, false);
if (dictSize > 0 && stream == nullptr) { ... }           // line 58  only a null-stream check

dictionary->dictionaryOffset.resize(dictSize + 1);       // line 67  dictSize + 1 wraps at 0xFFFFFFFF
int64_t* lengthArray = dictionary->dictionaryOffset.data();
lengthDecoder->next(lengthArray + 1, dictSize, nullptr); // line 69  writes dictSize int64 values

dictionaryOffset is orc::DataBuffer<int64_t> (c++/include/orc/Vector.hh:237).

dictSize is uint32_t. When an attacker sets ColumnEncoding.dictionary_size (proto field 2) to 0xFFFFFFFF, the expression dictSize + 1 is computed in uint32_t and wraps around to 0. dictionaryOffset.resize(0) therefore allocates a zero-capacity buffer. Line 69 then decodes dictSize (~4.29 billion) int64_t values from the LENGTH stream into lengthArray + 1, writing past the zero-capacity allocation on the very first decoded value.

The caller StringDictionaryColumnReader (c++/src/ColumnReader.cc:547) passes the untrusted dictSize through with no validation of its own.

Reachability

dictionary_size is a field of the ColumnEncoding message stored in each stripe's footer. Any consumer that opens a crafted .orc file and reads a StringDictionary (DICTIONARY / DICTIONARY_V2) encoded string column reaches loadStringDictionary, which is triggered during normal column-reader initialization. No special API use is required beyond reading the file.

malicious_column_encoding.bin in this repo is the exact proto2 wire encoding of the malicious ColumnEncoding (kind = DICTIONARY_V2, dictionary_size = 0xFFFFFFFF, hex 0803 10ff ffff ff0f) that a crafted .orc embeds in its stripe footer. craft_encoding.py regenerates it and documents the field layout.

Proof of concept

Because a full apache/orc build vendors protobuf/snappy/zstd/zlib via ExternalProject, this PoC uses a faithful standalone AddressSanitizer harness that compiles the real apache/orc allocator verbatim (c++/src/MemoryPool.cc at main, providing the genuine orc::DataBuffer<int64_t> resize/reserve/MemoryPoolImpl::malloc) and executes the exact vulnerable statements of loadStringDictionary (lines 50, 67-69) driven by the attacker-controlled dictionary_size = 0xFFFFFFFF.

build/harness.cc reproduces resize(dictSize + 1) on a real DataBuffer<int64_t> and then calls the length-stream decode (next(lengthArray + 1, dictSize, ...)). The RLE next() stand-in writes numValues sequential int64 values exactly as ORC's RleDecoderV1::next does when filling a caller-provided buffer; only the first write is needed to prove the overflow.

Build and run

cd build
./build_and_run.sh

(Requires clang++ or g++ with AddressSanitizer and curl. The script fetches the three real ORC source files it compiles verbatim.)

Observed AddressSanitizer output

See asan_output.txt. Key lines:

[*] attacker dictionary_size = 0xffffffff (4294967295)
[*] dictSize + 1 (uint32_t) = 0  <-- integer overflow to 0
[*] dictionaryOffset.size()=0 capacity=0 (zero-capacity buffer)

==ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 8 ...
0x... is located 7 bytes after 1-byte region
allocated by thread T0 here:
    #1 orc::MemoryPoolImpl::malloc(unsigned long)  MemoryPool.cc:44
    #2 orc::DataBuffer<long>::reserve(unsigned long) MemoryPool.cc:122
    #3 orc::DataBuffer<long>::DataBuffer(...)        MemoryPool.cc:59

The write of size 8 (one int64_t) lands past a heap region allocated by ORC's own MemoryPoolImpl::malloc / DataBuffer<int64_t> code, confirming the heap out-of-bounds write in the real allocation path.

Distinctness

This is a different file and different mechanism from the separately reported apache/orc RleDecoderV2::adjustGapAndPatch RLEv2 out-of-bounds READ. This report is a DictionaryLoader.cc dictionary_size integer-overflow leading to an out-of-bounds WRITE.

The only published ORC CVEs are CVE-2018-8015 (type-parsing recursion / DoS) and CVE-2025-47436 (LZO decompression). Neither touches DictionaryLoader.cc, which was added on 2025-11-05 (ORC-2022) and is not in OSS-Fuzz.

Suggested fix

Validate dictSize against a sane bound before use, and compute the + 1 in a 64-bit type, e.g.:

uint64_t dictSize = encoding.dictionary_size();
// reject implausible sizes and avoid 32-bit wraparound
dictionary->dictionaryOffset.resize(dictSize + 1);

plus an upper-bound check tied to the available LENGTH-stream data.

Files

  • build/harness.cc - faithful ASan harness driving the real vulnerable path
  • build/MemoryPool.cc, build/orc/*.hh - real apache/orc allocator, compiled verbatim
  • build/build_and_run.sh - build + run script
  • asan_output.txt - captured AddressSanitizer heap-buffer-overflow WRITE
  • craft_encoding.py - generates the malicious ColumnEncoding payload
  • malicious_column_encoding.bin - the attacker-controlled stripe-footer bytes