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:
```cpp
// 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.:
```cpp
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