File size: 5,458 Bytes
626931b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# 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

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