trevdatastreams's picture
Add reproducible ORC dictionary length overflow PoC
c7c2307 verified
|
Raw
History Blame Contribute Delete
3.18 kB
# Apache ORC C++ dictionary-length cumulative-overflow PoC
Tested against Apache ORC commit:
```text
af4cbf36b051c176f0d13ae5a7ac436ada8aeccf
```
## Summary
The C++ string-dictionary loader validates each decoded dictionary entry length
for negativity, but it adds the individually valid values into signed
`int64_t` prefix sums without checking overflow.
The 251-byte trigger contains three non-negative dictionary lengths:
```text
INT64_MAX, INT64_MAX, 2
```
Their mathematical sum is `2^64`. The prefix sums overflow signed `int64_t`,
the final dictionary blob size wraps to zero, and the row reader returns a
`StringVectorBatch` whose first row advertises length `INT64_MAX` over a
one-byte backing allocation. A consumer that reads the returned string data
causes a heap-buffer-overflow.
The valid 228-byte control contains the strings `a`, `bb`, `a`, `bb` and exits
normally.
## Reproduce
Build prerequisites are the same as Apache ORC C++. Then run:
```bash
ORC_SRC=/path/to/orc ./reproduce.sh
```
The script configures current ORC with AddressSanitizer and
UndefinedBehaviorSanitizer, builds the standalone reader harness, and runs both
fixtures.
Expected control:
```text
row=0 length=1
checksum=97
value=a
row=1 length=2
checksum=0
value=bb
...
rows=4
```
Expected trigger:
```text
row=0 length=9223372036854775807
DictionaryLoader.cc:79:22: runtime error: signed integer overflow
ColumnReader.cc:591:57: runtime error: signed integer overflow
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
0x... is located 0 bytes after 1-byte region
```
The harness limits downstream inspection to 1 MiB. It does not attempt to
materialize the advertised `INT64_MAX` bytes.
## Fixtures
```text
88477314865f2fdf594489fdd08a9996447a549245be27cddc303444d2cf5068 control-two-entry-dictionary.orc
0f3625588531b294bfebb5c9f19b3099dbc62815ce31487cf8c102f0de4f1034 trigger-wrapped-zero-offsets.orc
```
The generator embeds the Apache ORC 2.2.2 control and changes only:
- the dictionary LENGTH stream;
- that stream's serialized byte length;
- the stripe data length; and
- the dictionary entry count.
## Root cause
At `c++/src/DictionaryLoader.cc:73-80`, each raw entry passes the existing
negative-value check before the unchecked signed addition:
```cpp
if (lengthArray[i] < 0) {
throw ParseError(...);
}
lengthArray[i] += lengthArray[i - 1];
```
At `c++/src/ColumnReader.cc:588-593`, the corrupted offsets are subtracted
again and exposed through `StringVectorBatch::length` and
`StringVectorBatch::data`.
## Prior-art distinction
ORC-312 / PR #224 fixed the case where one decoded dictionary length was
already negative before accumulation. This trigger uses three individually
non-negative lengths and overflows only during the prefix-sum addition after
that check.
ORC-2192 / PR #2673 covers direct-encoded strings, not dictionary prefix sums.
## Suggested fix
Before adding each entry, reject:
```cpp
lengthArray[i] >
std::numeric_limits<int64_t>::max() - lengthArray[i - 1]
```
Also validate that every cumulative offset is monotonic, fits the backing
dictionary blob, and can be safely subtracted before exposing string lengths.