YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Apache ORC C++ row-index entry-count PoC
Tested against Apache ORC commit:
af4cbf36b051c176f0d13ae5a7ac436ada8aeccf
Summary
The C++ row reader accepts a syntactically valid RowIndex protobuf without
checking that it contains an entry for every row group. A subsequent public
RowReader::seekToRow(0) call indexes entry zero unconditionally.
The 272-byte trigger changes one byte in an otherwise valid ORC file. It
changes column zero's repeated RowIndex.entry tag (0x0a) into an unknown
length-delimited field (0x12). Protobuf accepts the stream, but the parsed
RowIndex has zero entries.
In a release-style sanitizer build, seeking the trigger produces a
null-reference UBSan error followed by an AddressSanitizer invalid read at
address 0x18. The unmodified 272-byte control seeks and reads one row
normally.
Reproduce
Build prerequisites are the same as Apache ORC C++. Then run:
ORC_SRC=/path/to/orc ./reproduce.sh
The script builds current ORC with RelWithDebInfo, AddressSanitizer, and
UndefinedBehaviorSanitizer before running both fixtures.
Expected control:
seeking row 0
seek completed
next=1 rows=1
Expected trigger:
repeated_ptr_field.h:292:12: runtime error: reference binding to null pointer
ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018
The signal is caused by a READ memory access.
A debug build instead terminates at Protobuf's bounds assertion:
Check failed: index < current_size_ (0 vs. 0)
Fixtures
94024b8a77d819409445e6664affa64950003509a0366ef8c19aa8d3ea1e92f8 control-one-row-index-entry.orc
e9d4947a068c6b6643cd22f2016a19ec654fd873bedcec929b9d1ed7a63c8b38 trigger-zero-row-index-entries.orc
The generator changes only byte offset 3. Stream lengths, stripe offsets, file length, and all other metadata remain identical.
Root cause
At c++/src/Reader.cc:542-547, a parsed row index is stored without validating
its entry count:
proto::RowIndex rowIndex;
if (!parseProtobufFromStream(&rowIndex, inStream.get())) {
throw ParseError("Failed to parse the row index");
}
rowIndexes_[colId] = rowIndex;
At c++/src/Reader.cc:573-576, the row-group ID is used without checking
rowGroupEntryId < rowIndex->second.entry_size():
const proto::RowIndexEntry& entry =
rowIndex->second.entry(static_cast<int32_t>(rowGroupEntryId));
Prior-art distinction
ORC-990 / PR #903 also mentions seekToRowGroup, but it fixes MSVC iterator
invalidation while building position providers. It does not validate
attacker-controlled RowIndex.entry counts.
The separate row-index column-ID issue indexes the rowIndexes_ destination
with an unvalidated stream column. This PoC uses a valid column ID and instead
demonstrates an empty entry array consumed without a bounds check.
Suggested fix
Reject a parsed row index whose entry count does not cover the stripe's
declared row groups. As defense in depth, check the requested row-group ID
against every loaded row index before calling entry(), and throw
ParseError for a malformed file.