Harden RLE integer decoders against buffer cursor overshoot#2691
Open
Roman-Da-Ripper wants to merge 1 commit into
Open
Harden RLE integer decoders against buffer cursor overshoot#2691Roman-Da-Ripper wants to merge 1 commit into
Roman-Da-Ripper wants to merge 1 commit into
Conversation
When decoding a malformed RLE integer stream, the read cursor bufferStart_ can be advanced past the buffer sentinel bufferEnd_. Two spots then turn that past-the-end cursor into an out-of-bounds heap read instead of a clean error: * RleDecoderV2::bufLength() returns bufferEnd_ - bufferStart_ as an unsigned uint64_t. Once bufferStart_ > bufferEnd_ this underflows to a value near 2^64, defeating the std::min clamps in the UnpackDefault fast loops (unrolledUnpack16/32/40/56/64) and causing them to read past the buffer. Return 0 when the cursor has overshot so the fast-loop bufLength()/width term can no longer inflate. * RleDecoderV2::readByte() and RleDecoderV1::readByte() gate their refill on strict equality (bufferStart_ == bufferEnd_). An already-overshot cursor fails that test and dereferences *bufferStart_ out of bounds. Use >= so an overshot cursor still triggers a refill. Both changes are behavior-preserving for valid inputs (bufferStart_ never exceeds bufferEnd_ there); they only bound the pathological overshoot path. Reported to security@orc.apache.org and, per the project security model, redirected here as a hardening improvement by Arnout Engelen (ASF Security). Authored-by: Roman Arce Brán <arce.roman.b@gmail.com> (Cyfra Tech Solutions) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes were proposed in this pull request?
This is a small defense-in-depth hardening of the C++ RLE integer decoders so that a malformed / truncated RLE stream can no longer walk the read cursor past the end of the internal stream buffer.
Two minimal changes:
RleDecoderV2::bufLength()(c++/src/RLEv2.hh) now returns0when the cursor has overshot the buffer (bufferStart_ > bufferEnd_) instead of the raw pointer difference. The difference is returned as an unsigneduint64_t; oncebufferStart_ > bufferEnd_it underflows to a value near 2^64, which defeats thestd::minclamps in theUnpackDefaultbit-unpacking fast loops (unrolledUnpack16/32/40/56/64inBpackingDefault.cc) and lets them readvaluesRemaining * widthbytes off the end of the heap buffer.RleDecoderV2::readByte()(c++/src/RleDecoderV2.cc) andRleDecoderV1::readByte()(c++/src/RLEv1.cc) now gate their buffer refill onbufferStart_ >= bufferEnd_instead ofbufferStart_ == bufferEnd_. A cursor that has already overshot fails the strict-equality test and dereferences*bufferStart_out of bounds;>=makes an overshot cursor refill (and cleanly hitParseErrorat EOF) instead.Both changes are behavior-neutral on valid data — for a well-formed stream
bufferStart_never exceedsbufferEnd_, sobufLength()returns the same value and the refill guard fires at exactly the same point. They only bound the pathological overshoot path.Why are the changes needed?
Reading a crafted/corrupt ORC file can currently drive the RLE integer decoders to read one or more bytes past the end of a heap stream buffer (AddressSanitizer:
heap-buffer-overflow READ of size 1) via the public reader API (orc::createReader/RowReader::next). The effect is out-of-bounds reads / a crash of the reading process (no write / no code-execution demonstrated).The underlying cause is that the decode cursor
bufferStart_is allowed to advance past thebufferEnd_sentinel when a run length / bit width taken from the file implies consuming more bytes than the current stream buffer holds; neitherbufLength()nor thereadByte()refill guard bounds the cursor once it has overshot.This was reported privately to
security@orc.apache.org. Per the project's published security model (https://orc.apache.org/security/), a crash while reading an untrusted/malformed file is out of scope as a vulnerability, and Arnout Engelen (ASF Security) kindly invited us to submit the fix through the normal contribution channel as a hardening improvement — hence this PR.How was this patch tested?
orc-test,RelWithDebInfo), including the 123 RLE / integer / byte-RLE / writer round-trip tests — no regressions.readLocalFile+createReader+RowReader::next) built with AddressSanitizer, onmain:heap-buffer-overflowinRleDecoderV2::readByte,RleDecoderV1::readByte,UnpackDefault::unrolledUnpack32andunrolledUnpack64.ParseError/ decompression EOF) or decodes cleanly. No ASAN report.Representative before/after (
unrolledUnpack32case):Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Anthropic Claude Opus 4.8)
Contributed by Roman Arce Brán — Cyfra Tech Solutions (
arce.roman.b@gmail.com).Note on issue tracking: I don't have an Apache JIRA account to file the
ORC-xxxxticket that the PR template asks for, so this PR intentionally omits theORC-xxxxprefix. Happy to add the reference (and reword the commit) once a JIRA is created/assigned — just let me know the number.🤖 Generated with Claude Code