| # Apache ORC C++ RLEv2 patch-list heap-buffer-overflow PoC |
|
|
| Tested against Apache ORC commit: |
|
|
| `af4cbf36b051c176f0d13ae5a7ac436ada8aeccf` |
|
|
| ## Summary |
|
|
| Apache ORC's C++ RLEv2 decoder accepts a PATCHED_BASE patch-list entry that |
| means “continue the gap in the next entry” even when that entry is the last |
| element of the attacker-controlled patch list. |
| |
| `RleDecoderV2::adjustGapAndPatch()` increments the patch index and immediately |
| dereferences it without checking the vector length. A one-entry patch list |
| whose entry encodes `gap == 255 && patch == 0` therefore causes an |
| eight-byte heap-buffer-overflow read. |
| |
| The complete 277-byte `.orc` trigger reaches the flaw through the public |
| `createReader()` / `RowReader::next()` path. The corresponding control differs |
| in one patch-gap byte and parses one row normally. |
| |
| ## Reproduce |
| |
| Build prerequisites are the same as Apache ORC C++. Then run: |
| |
| ```bash |
| ORC_SRC=/path/to/orc bash reproduce.sh |
| ``` |
| |
| The script builds current ORC with AddressSanitizer and |
| UndefinedBehaviorSanitizer, tests the raw decoder control/trigger, and then |
| tests the complete ORC control/trigger. |
| |
| Expected complete-file control: |
| |
| ```text |
| rows=1 |
| ``` |
| |
| Expected complete-file trigger: |
| |
| ```text |
| ERROR: AddressSanitizer: heap-buffer-overflow |
| READ of size 8 |
| RleDecoderV2.cc:265 in orc::RleDecoderV2::adjustGapAndPatch(...) |
| RleDecoderV2.cc:346 in orc::RleDecoderV2::nextPatched<long long>(...) |
| ColumnReader.cc:966 in orc::ListColumnReader::nextInternal<false>(...) |
| Reader.cc in orc::RowReaderImpl::next(...) |
| ``` |
| |
| ## Fixture integrity |
| |
| ```text |
| 098d1f9d7b64afa2cc15c257b4e8d5b57707f19402dd241ef7b8b26ffb94224d control-valid-patch-list.orc |
| 9fbbd8fdc5c991f55fede3823193f439e1843146d6f5d44e0e4f7ca80859fba2 trigger-unterminated-gap-continuation.orc |
| ``` |
| |
| The two generated ORC files are identical except for byte offset 53: |
| |
| - control: patch-list bits decode to `gap == 1` |
| - trigger: patch-list bits decode to `gap == 255 && patch == 0` |
| |
| Both files have internally consistent stream and stripe lengths. |
| |
| ## Root cause |
| |
| At `c++/src/RleDecoderV2.cc:253-266`: |
| |
| ```cpp |
| uint64_t idx = *patchIdx; |
| uint64_t gap = |
| static_cast<uint64_t>(unpackedPatch_[idx]) >> patchBitSize; |
| int64_t patch = unpackedPatch_[idx] & patchMask; |
| |
| while (gap == 255 && patch == 0) { |
| actualGap += 255; |
| ++idx; |
| gap = static_cast<uint64_t>(unpackedPatch_[idx]) >> patchBitSize; |
| patch = unpackedPatch_[idx] & patchMask; |
| } |
| ``` |
| |
| The loop does not check `idx < unpackedPatch_.size()` after incrementing it. |
|
|
| ## Prior-art distinction |
|
|
| Fresh searches of Apache ORC issues and pull requests for |
| `adjustGapAndPatch`, PATCHED_BASE patch-list corruption, `gap == 255`, and |
| RLEv2 heap-buffer-overflow returned no matching disclosure. |
| |
| ORC-2214 hardens the compressed input cursor in `readByte()`. It does not |
| validate the decoded patch-list index and does not prevent this heap read. |
| |
| ## Suggested fix |
| |
| Pass the patch-list length into `adjustGapAndPatch()` and reject an |
| unterminated gap-continuation entry before the next dereference: |
| |
| ```cpp |
| ++idx; |
| if (idx >= unpackedPatch_.size()) { |
| throw ParseError("Corrupt PATCHED_BASE patch list"); |
| } |
| ``` |
| |
| Add a regression test for a one-entry list ending in |
| `gap == 255 && patch == 0`. |
| |