Repository navigation
fix(rocDecode): Bound the shared NAL unit and SEI parsing in the base parser - #12673
Conversation
…cket. - GetNalUnit() scanned with curr_byte_offset_ < pic_data_size_ - 2. Both are uint32_t, so a one byte packet made the bound 0xFFFFFFFF and the loop read pic_data_buffer_ptr_ three bytes at a time far past the end of the buffer. - AVC and HEVC are the only callers. Both guard payload && payload_size, so a zero length packet cannot arrive, but a one byte packet can. A two byte packet was already safe by accident, since 2 - 2 is 0 and the loop does not run. - Test the size separately instead. Rewriting the condition as curr_byte_offset_ + 2 < pic_data_size_ would have removed the underflow but added an overflow: the body advances the offset by 3, so on re-test it can equal pic_data_size_, and for a payload near 4 GB that sum wraps and the loop continues with an offset past the end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…signed. - curr_start_code_offset_, next_start_code_offset_ and nal_unit_size_ were int while pic_data_size_ and curr_byte_offset_ are uint32_t, so the arithmetic between them converted signedness. nal_unit_size_ is now uint32_t along with the two offsets. - The int type was load bearing, so it is replaced rather than simply dropped. nal_unit_size_ was assigned from pic_data_size_ - curr_start_code_offset_, which already evaluates unsigned, so an offset past the end of the picture gave a huge value that became negative on the way into the int and was then caught by the nal_unit_size_ >= 4 and >= 5 floors in the callers. Widening the member alone would have let that value through the floor and into a memcpy of RBSP_BUF_SIZE bytes from a source past the end of the packet. GetNalUnit() now range checks the two offsets before deriving the size from them, which replaces the accidental backstop with a deliberate one. - ebsp_size and sei_ebsp_size were int for the same reason: with nal_unit_size_ unsigned, a NAL unit larger than INT_MAX would have made them negative, and sei_ebsp_size reaches new uint8_t[] and memcpy() directly. Both are uint32_t now, which also removes the int to unsigned comparison against sei_rbsp_buf_size_. EbspToRbsp() takes size_t, so those calls widen cleanly. - The callers' nal_unit_size_ floors and the RBSP_BUF_SIZE clamp read the same under unsigned, and the build is clean with no new sign compare warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- ParseSeiMessage() took the NAL unit size but used it only in the loop
condition at the bottom. Nothing else consulted it, so for AVC and HEVC,
which share this base, a malformed SEI NAL unit read past its end in four
ways:
- the two ff_byte scans for payload_type and payload_size ran while
nalu[offset] was 0xFF with no bound, so a run of 0xFF walked off the end;
- the terminating byte after each scan was read without checking the offset;
- payload_size came straight from the bitstream and was never compared with
what was left of the NAL unit, so the memcpy that appends the payload read
an attacker chosen length past the end. This is the serious one;
- payload_type and payload_size accumulated 255 at a time into an int, so a
long enough run overflowed a signed type before anything caught it.
- Bound every read by size, require payload_size to fit the bytes left after
the headers, and accumulate in size_t so the running total cannot wrap before
it is checked.
- sei_payload_buf_size_ was not updated when the payload buffer grew, leaving
every later message to compare against a stale bound. Writes stayed in range
because the buffer is never smaller than the recorded size, so this was
wasted reallocation rather than an overflow, but the bookkeeping was wrong.
- The function now returns ParserResult so the condition is on the record.
SEI is supplemental and does not affect the decode, so it stops at the first
bad message, keeps the ones already parsed, and the callers carry on with the
picture rather than failing it. The contract is written on the declaration.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tead of returning a sentinel. - EbspToRbsp() returned (size_t)-1 when 0x000003 was followed by a byte above 0x03, and none of its nine callers in the AVC and HEVC parsers checked for it. They assigned the result straight into rbsp_size_ and passed that to the parameter set, slice header and SEI parse functions. - rbsp_size_ was an int, so the sentinel landed there as -1, and every one of those parse functions takes size_t, so it arrived as SIZE_MAX. That turns the size they are given into no bound at all, which defeats the length checks inside them. The bounds just added to ParseSeiMessage() are an example: with size equal to SIZE_MAX every offset < size test passes and all four of them are inert. - Return a ParserResult and hand the size back through an out parameter, so the failure cannot be read as a length. All nine call sites now log and skip the NAL unit, which is what they already did for a failed parameter set or slice header parse. - rbsp_size_ becomes size_t to match the functions it is passed to, so there is no conversion left on that path either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ All Policy Checks Passed
📖 Need help? See the Policy FAQ for details on every check and how to fix failures. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The converted RBSP length incorrectly adds removed bytes, allowing parsing beyond the valid buffer.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Bounds AVC/HEVC NAL and SEI parsing against malformed or truncated packets.
Changes:
- Adds NAL offset and SEI payload bounds checks.
- Replaces the unchecked RBSP size sentinel with
ParserResult. - Corrects parser size types and SEI buffer bookkeeping.
| File | Description |
|---|---|
roc_video_parser.h |
Updates parser types and method contracts. |
roc_video_parser.cpp |
Adds shared NAL, RBSP, and SEI validation. |
hevc_parser.cpp |
Handles RBSP conversion failures for HEVC. |
avc_parser.cpp |
Handles RBSP conversion failures for AVC. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…m the RBSP size. - EbspToRbsp() reported end_bytepos - begin_bytepos + reduce_count. Every byte counted in reduce_count has been removed from the buffer, so adding it gave a length larger than the EBSP ever held: 00 00 03 01 is 3 bytes of RBSP but was reported as 5. - The callers pass that straight to the parameter set, slice header and SEI parse functions as the size of rbsp_buf_, which is a fixed 1024 bytes that ebsp_size already fills in the worst case. An over-reported size therefore tells those functions the buffer is larger than it is, which is the same class of problem as the sentinel fixed in the previous commit and undercuts the bounds added across this branch. - The cabac_zero_word path breaks with the trailing 0x03 still in the buffer, and the spec discards it, so count it in reduce_count before breaking. That keeps reduce_count meaning the number of discarded bytes and makes the one subtraction correct for both exits. - This predates the branch. Conformance did not catch it because the parse functions stop at the end of the syntax they need rather than at the end of the buffer, so an over-reported size changes nothing for a well formed stream. All 532 streams still pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ignature. - The comment still described the old contract, that the function returns the converted size directly. Document the p_rbsp_size out parameter and the ParserResult return instead. - Record the two things a caller needs: the size is the converted range less the discarded emulation prevention bytes, and it is set to 0 on failure so it cannot be read as a length, which is the whole point of replacing the previous (size_t)-1 sentinel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Fitting the capacity to exactly what the current message needs leaves sei_payload_buf_size_ equal to sei_payload_size_ once that payload is appended, so the next message carrying any payload at all re-enters the grow branch and copies the whole accumulated payload again. - The number of SEI messages in one picture is bounded only by the packet size, and so is the total payload, which makes the copying quadratic in the packet size. A picture with one large payload followed by many small messages is enough to reach it. - Double the capacity until it covers what is needed instead, falling back to the exact requirement if the doubling overshoots uint32_t. sei_payload_size_ is reset per picture and the payloads come from a single packet, so the requirement itself always fits. - The exact fit came in with the previous commit on this branch, which set sei_payload_buf_size_ so it would stop being stale. Before that the recorded size stayed at INIT_SEI_PAYLOAD_BUF_SIZE, which had the same quadratic behaviour once the accumulated payload passed 1 MB, so this is a fix for both shapes rather than only for the new one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t(). - a1253a1 stopped the start code scan underflowing by testing pic_data_size_ >= 3 in the loop condition. That keeps the scan from running, but it leaves the function returning through the start_code_num_ == 0 check below, which only fires because of an invariant held by the callers: both reset start_code_num_ before the loop, pic_data_size_ cannot change within a picture, and the caller returns as soon as GetNalUnit() reports PARSER_NOT_FOUND. So the first call is the only one that can see a packet shorter than a start code. - That chain holds today, but none of it is enforced here, and a caller change would break it quietly. Return PARSER_NOT_FOUND from the top instead, so the function covers the case on its own and the loop condition goes back to the simple form. - Behaviour is unchanged for every input: a packet shorter than three bytes cannot hold a start code, and both paths report PARSER_NOT_FOUND. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixed type. Co-authored-by: Aryan Salmanpour <aryan.salmanpour@amd.com>
…capacity. - Review feedback: the overflow fallback checked nothing. When the doubled size passed 0xFFFFFFFF it fell back to needed, which is itself an unchecked size_t, so the cast truncated exactly as silently as the value it was guarding. - The consequence is a write, not a wrong number. sei_payload_buf_size_ is what the allocation on the next line is sized from and what both copies are bounded by, so a truncated capacity under allocates and the copy of the existing payload and the append of the new one both run past the new buffer. - Take the larger of the doubled size and the requirement, and fail with PARSER_OUT_OF_RANGE if that cannot be recorded in the uint32_t size field. The old comment argued the requirement always fits; arguing it in a comment was the problem, since nothing enforced it. - The two other narrowings in the same function are now explicit, as the widening to size_t in this branch made them implicit. The first payload case is bounded by what was checked against the NAL unit, and the running total by the capacity the branches above just confirmed, so both are casts with the bound stated rather than further checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rrawther
left a comment
There was a problem hiding this comment.
I see some revew comments are not resolved. After that it can be merged
- Review feedback: sei_message_size is uint32_t in the public RocdecSeiMessage, so storing payload_size there narrowed from size_t implicitly. The previous commit made the other narrowings in this function explicit and missed this one. - sei_message_type on the line above was in the same state: commented but not cast. Both are spelled out now, since the comment was describing a conversion the code did not show. - The comment distinguishes them, because they are not the same kind of conversion. sei_message_type is uint8_t, so a payload type above 255 is truncated and the value stored is not the one parsed. sei_message_size is exact, because payload_size was bounded against what is left of the NAL unit a few lines above. - No behaviour change: these are the conversions the compiler was already performing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rrawther
left a comment
There was a problem hiding this comment.
Also please take a look at this
3. Issues found
None blocking. I re-audited the delta for new problems and found none:
All 9 EbspToRbsp call sites check the result and skip the NAL unit; I enumerated them and confirmed there is no 10th.
reduce_count cannot exceed end − begin — each increment is paired with one distinct discarded byte in the span (the memmove branch also decrements streamBuffer_end) — so the new size_t subtraction at roc_video_parser.cpp:234 cannot underflow.
The nal_payload_size hoist is arithmetically identical to what it replaced, just evaluated where the floor is visible.
No public header or dispatch-table change → no ABI version bump required, correctly omitted.
4. Suggestions
CHANGELOG.md:17 — "HVEC" is a typo for "HEVC". This is user-facing release-note text; worth fixing before merge. The same line also credits AV1 and VP9, which this PR does not touch (VP9 landed via #12624) — defensible as a consolidated entry, but if it's meant to describe this PR it overstates its scope.
roc_video_parser.h:231-232 — the doc comment names only the malformed-emulation-sequence cause of PARSER_INVALID_ARG; end_bytepos < begin_bytepos now returns it too. One clause would make the contract complete.
Unit tests — I agree with @AryanSalmanpour's request. EbspToRbsp is a pure function over a byte buffer and the corrected end − (begin + reduce_count) formula is exactly the kind of thing that silently regresses. My ad-hoc harness covers these cases today but lives outside the repo.
Follow-up, out of scope: es_reader.cpp:840 still carries a near-duplicate EbspToRbsp with the same return -1 sentinel that neither caller checks, and its callers at lines 611 and 713 memcpy a fixed 256 bytes out of the stream buffer unconditionally. Same bug class, separate PR.
Minor: roc_video_parser.cpp:188 is missing a space after the comma in the signature, and the header's parameter name is stream_buffer where the definition uses streamBuffer.
- Review feedback on (sei_message_count_ + 1) > sei_message_list_.size(): signed against std::vector::size_type, which is the mixing this branch has been removing. Took the preferred option and changed the member rather than casting at the comparison. num_slices_ has the same shape and is included. - Both are counters that are only reset, incremented, compared against zero, used as an index, and handed to a uint32_t field of the public structures (RocdecSeiMessageInfo::sei_message_count, RocdecPicParams::num_slices). Nothing assigns them a negative value or relies on one. - The four loops of the form for (int slice_index = 0; slice_index < num_slices_; ...) take uint32_t indices now, so the change removes comparisons rather than moving them from one line to another. - num_slices_ - 1 in hevc_parser.cpp sits inside one of those loops, so it only runs when num_slices_ is at least 1 and cannot wrap now that the type is unsigned. - Counted with -Wsign-compare before and after, on the three files this touches: 5, 84 and 100 warnings become 4, 82 and 98. Five fewer, none added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…unsigned. - Review feedback: the macro was an unparenthesized signed expression, 1024 * 1024, used on the right of unsigned comparisons in four places. - The parentheses are the part that matters. Without them the macro expands into its use site and any operator of higher precedence binds wrong: b / INIT_SEI_PAYLOAD_BUF_SIZE becomes b / 1024 * 1024, which for b of 3000000 gives 2999296 instead of 2. None of the four current uses do that, but nothing stops the next one. - The u suffixes change no diagnostic. Counted -Wsign-compare on the three files that use the macro, before and after: 4, 82 and 98 both times. The operand was already a constant the compiler knows is non negative, so the comparisons never warned and never converted wrongly. Worth saying, since the comment this came from expects the signedness to be the problem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Review feedback: it was the last signed member in the chain. Every expression assigned to it is already unsigned arithmetic over members this branch widened, pic_data_start_offset_ in avc_parser.h being uint32_t as well, so only the destination was signed and each assignment converted. - Everything it feeds is unsigned too: RocdecPicParams::bitstream_data_len and the slice_data_size fields are all uint32_t. Checked all thirteen uses: nothing compares it, assigns it a negative, or tests for one, so unlike nal_unit_size_ earlier on this branch the type was doing no work. - Counted the effect with -Wsign-conversion, which is the diagnostic that applies here. -Wsign-compare does not move, because these are conversions on assignment rather than comparisons: avc 294 to 290, hevc 254 to 252, av1 182 to 180, vp9 66 to 63. Eleven fewer, none added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…st why it returns. - Review feedback: the comment explained why the branch has to return and not what returning costs. It exits before the end of packet finalization, so slices already accumulated for the current picture would be dropped rather than decoded. - Checked whether any can be: neither condition that reports PARSER_INVALID_ FORMAT can leave slices accumulated. The short packet one tests pic_data_size_, which both parsers set once before the loop and never change within a packet, so it can only fire on the first pass, when num_slices_ is still 0 from the reset a few lines above it. The offset ordering one is the defensive check that is not reachable with the current callers. - So the drop costs nothing today, but that rests on where those two conditions can fire rather than on anything local. Record it, along with the note that a third condition able to fire mid packet would have to decide whether to finalize first. - Comments only, in both parsers, which have the same shape here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ntract. - Review feedback. The definition took streamBuffer where the declaration takes stream_buffer, and had no space after the first comma. The definition follows the declaration now, which is also the snake_case the rest of the file uses. The two derived locals keep their names; renaming them is noise in a diff this branch is already long enough. - The doc comment named only the malformed emulation prevention sequence as a cause of PARSER_INVALID_ARG. An end before the start returns it too, since the previous commit stopped that case reporting a length. Both are listed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t hold. - An extended payload type, written as one or more ff_bytes, is valid syntax but does not fit RocdecSeiMessage::sei_message_type, which is uint8_t. Type 261 reached the callback as type 5, and the SEI consumers in utils read 5 as SEI_TYPE_USER_DATA_UNREGISTERED and parse the payload as that, so the callback was told a message was something it was not. - This predates the branch; the implicit conversion did the same. Making the cast explicit a few commits ago only made it visible. - Skip the message instead. The payload size was bounded just above, so the offset can step over it and the messages either side are still delivered. Neither the message nor its payload is recorded, which keeps sei_message_list_ and the concatenated sei_payload_buf_ in step for a caller walking the two together. - Widening the public field was the other option offered in review and is not taken here. It would repurpose the three reserved bytes, change what a consumer reading the field as uint8_t sees, and make this the only public header change in the branch. That is a decision for the API, not for a parser fix, and extended types can be delivered later without this dropping them wrongly in the meantime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ithub.com:ROCm/rocm-systems into users/jeffqjiangNew/rocDecode_parser_error_handle
…bit limit. - Review feedback: once the capacity is at least 2 GiB, one more doubling takes it past what sei_payload_buf_size_ can hold, so the check below rejected the growth even where the requirement itself was still representable, and the remaining SEI messages of the picture were dropped. - Cap the doubling at half the 32 bit limit. The capacity line below, which was written as defence, is what makes this work: when the loop stops early new_size is less than needed, so the capacity falls back to the exact requirement. - Checked the boundaries against the old code. A 3 GiB capacity needing 3.4 GiB was rejected and now allocates 3.4 GiB. A 2 GiB capacity needing exactly UINT32_MAX was rejected, the doubling having produced UINT32_MAX + 1, and now allocates exactly UINT32_MAX. A requirement of 5 GB is still rejected, which is right, and the ordinary growth paths are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AryanSalmanpour
left a comment
There was a problem hiding this comment.
Re-review at fe19a15
Re-reviewed against 3d0a553, the commit of my earlier review. Every point raised there has been addressed — seven in code, one in comments — and I found no new defects in the changes.
Worth calling out as better than what I suggested:
- The SEI buffer growth is now correct rather than merely guarded. Capping the doubling at
0xFFFFFFFF/2and falling back to the exact requirement means the capacity check can no longer reject a growth whose requirement would itself have fit, and the >4 GB case fails loudly instead of narrowing. I ran the loop through its edges, includingneededjust below and just above0xFFFFFFFF; it never produces a capacity that overflows theuint32_tfield. - Retyping
sei_message_count_andnum_slices_touint32_t, rather than casting at the comparison, removes the sign mismatch at the source and carried through to the four dependentfor (int slice_index …)loops in both parsers.num_slices_ - 1athevc_parser.cpp:475is safe because it only evaluates inside theslice_index < num_slices_loop, so the zero case cannot wrap. - Skipping an SEI message whose
payload_typeexceeds0xFFis a sharper answer than the truncation note I left. Delivering type 261 as type 5 would have had the consumers read it asSEI_TYPE_USER_DATA_UNREGISTERED, so dropping it with a log is the right call, and stepping over an already bounds-checked payload keeps the surrounding messages intact.
On the early return from the PARSER_INVALID_FORMAT branch: the new comment argues that neither reporting condition can fire with slices already accumulated. I checked that rather than take it on trust — pic_data_size_ is assigned exactly once per call, at avc_parser.cpp:107 and hevc_parser.cpp:572, before the NAL loop, so the short-packet test can only fire on the first pass while num_slices_ is still 0. The claim holds, and flagging the constraint for whoever adds a third condition is the useful part.
The one thing I raised that was not taken is the observation that the pic_data_size_ < 3 check is loop-invariant but re-evaluated per NAL unit. That was a nit and I do not think it is worth pursuing.
Resolving my threads accordingly. The standing gap remains test coverage: EbspToRbsp is a pure function over a byte buffer, and a handful of table-driven cases (00 00 03 01, a trailing 00 00 03, consecutive escapes, a malformed 00 00 03 FF, end < begin) would pin this behaviour cheaply against regression.
… parser (#12673) ## Motivation This PR addresses 5 review findings in `roc_video_parser`, the base shared by the AVC and HEVC parsers. Three are out-of-bounds reads reachable from a malformed or truncated packet. The other two both corrupt the size that every downstream parse function is handed, which makes the length checks inside those functions inert: an error sentinel that no caller checked, and an RBSP length that was reported larger than the buffer holding it. ## Technical Details **Invalid emulation prevention sequence — `EbspToRbsp()`** `EbspToRbsp()` returned `(size_t)-1` when `0x000003` was followed by a byte above `0x03`, and none of its nine callers checked for it. They assigned it into `rbsp_size_` and passed that on. `rbsp_size_` was an `int`, so the sentinel landed as −1, and every parse function it feeds — `ParseSps`, `ParsePps`, `ParseVps`, `ParseSliceHeader`, `ParseSeiMessage` — takes `size_t`, so it arrived as `SIZE_MAX`. Any length check inside those functions is then inert: the SEI bounds below are four `offset < size` tests that all pass unconditionally when `size` is `SIZE_MAX`. It now returns a `ParserResult` with the size handed back through an out parameter, so a failure cannot be read as a length. All nine call sites log and skip the NAL unit, which is what they already did for a failed parameter set or slice header parse. `rbsp_size_` becomes `size_t` to match what it is passed to. **Reported RBSP length — `EbspToRbsp()`** The same function reported `end_bytepos - begin_bytepos + reduce_count`. Every byte counted in `reduce_count` has just been removed from the buffer, so adding it gives a length larger than the EBSP ever held — `00 00 03 01` is 3 bytes of RBSP but was reported as 5. Callers pass that on as the size of `rbsp_buf_`, a fixed 1024 bytes that `ebsp_size` already fills in the worst case, so the parse functions were told the buffer was larger than it is. Same class of problem as the sentinel above. The subtraction is now the right way round. The `cabac_zero_word` path breaks with the trailing `0x03` still in the buffer and the spec discards it, so it is counted in `reduce_count` before the break; that keeps `reduce_count` meaning the number of discarded bytes and makes one subtraction correct at both exits. This predates the branch, and conformance does not exercise it: the parse functions stop at the end of the syntax they need rather than the end of the buffer, so an over-reported size changes nothing for a well formed stream. **SEI message parsing — `ParseSeiMessage()`** The function took the NAL unit size but used it only in the loop condition at the bottom, so a malformed SEI read past the end four ways: the two `ff_byte` scans ran unbounded; the terminating byte after each was read without checking the offset; `payload_size` came straight from the bitstream and was never compared against what was left, so the `memcpy` appending the payload read an attacker-chosen length; and both accumulators were `int` taking `+= 255` in an unbounded loop, overflowing a signed type before anything caught it. Every read is now bounded by `size`, `payload_size` must fit the bytes left after the headers, and the accumulation is `size_t` so the running total cannot wrap before it is checked. SEI is supplemental, so the parse stops at the first bad message and keeps the ones already read; the result is returned for the record and the callers carry on with the picture rather than failing it. That contract is written on the declaration. Separately, `sei_payload_buf_size_` was not updated when the payload buffer grew. Writes stayed in range because the buffer is never smaller than the recorded size, so this was repeated reallocation rather than an overflow, but the bookkeeping was wrong. **Start code scan — `GetNalUnit()`** The scan ran while `curr_byte_offset_ < pic_data_size_ - 2`. Both are `uint32_t`, so a one-byte packet made the bound `0xFFFFFFFF` and the loop read three bytes at a time far past the end. AVC and HEVC both guard `payload && payload_size`, so a zero length packet cannot arrive, but a one byte packet can; two bytes was already safe by accident. The size is now tested separately. **NAL unit offsets and sizes — signedness** `curr_start_code_offset_`, `next_start_code_offset_` and `nal_unit_size_` were `int` while `pic_data_size_` and `curr_byte_offset_` are `uint32_t`, so the arithmetic between them converted signedness. All three are now `uint32_t`. The `int` was load bearing, so it is replaced rather than dropped. `nal_unit_size_` was assigned from `pic_data_size_ - curr_start_code_offset_`, which already evaluates unsigned, so an offset past the end gave a huge value that became negative entering the `int` and was caught by the `nal_unit_size_ >= 4` / `>= 5` floors in the callers. Widening the member alone would have let that through the floor and into a `memcpy` of `RBSP_BUF_SIZE` bytes from past the end of the packet. `GetNalUnit()` now range checks the two offsets before deriving the size, replacing the accidental backstop with a deliberate one. `ebsp_size` and `sei_ebsp_size` were `int` for the same reason: with `nal_unit_size_` unsigned, a NAL unit larger than `INT_MAX` would have made them negative, and `sei_ebsp_size` reaches `new uint8_t[]` and `memcpy()` directly. Both are `uint32_t` now. ## Issue Tracking JIRA ID: ROCM-31875, AICV-310 ## Test Plan Run /validate. ## Test Result validate passed: ╔══════════════════════════════╗ ║ rocDecode Validation Summary ║ ╠══════════════════════════════╣ ║ CTest (16 tests) ✓ PASSED ║ ║ Conformance AVC ✓ PASSED ║ ║ Conformance AV1 ✓ PASSED ║ ║ Conformance HEVC ✓ PASSED ║ ║ Conformance VP9 ✓ PASSED ║ ╠══════════════════════════════╣ ║ Overall ✓ PASSED ║ ╚══════════════════════════════╝ Conformance breakdown ┌───────┬─────────┬────────┬────────┬────────────┐ │ Codec │ Streams │ Passed │ Failed │ Unfinished │ ├───────┼─────────┼────────┼────────┼────────────┤ │ AVC │ 127 │ 127 │ 0 │ 0 │ ├───────┼─────────┼────────┼────────┼────────────┤ │ AV1 │ 162 │ 162 │ 0 │ 0 │ ├───────┼─────────┼────────┼────────┼────────────┤ │ HEVC │ 135 │ 135 │ 0 │ 0 │ ├───────┼─────────┼────────┼────────┼────────────┤ │ VP9 │ 108 │ 108 │ 0 │ 0 │ ├───────┼─────────┼────────┼────────┼────────────┤ │ Total │ 532 │ 532 │ 0 │ 0 │ └───────┴─────────┴────────┴────────┴────────────┘ ## Submission Checklist - [ ] Look over the contributing guidelines at https://github.com/ROCm/rocm-systems/blob/develop/CONTRIBUTING.md. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Aryan Salmanpour <aryan.salmanpour@amd.com>
TheRock Submodule Bump ActivityNewest first
|



Motivation
This PR addresses 5 review findings in
roc_video_parser, the base shared by the AVC and HEVC parsers.Three are out-of-bounds reads reachable from a malformed or truncated packet. The other two
both corrupt the size that every downstream parse function is handed, which makes the length
checks inside those functions inert: an error sentinel that no caller checked, and an RBSP
length that was reported larger than the buffer holding it.
Technical Details
Invalid emulation prevention sequence —
EbspToRbsp()EbspToRbsp()returned(size_t)-1when0x000003was followed by a byte above0x03, andnone of its nine callers checked for it. They assigned it into
rbsp_size_and passed thaton.
rbsp_size_was anint, so the sentinel landed as −1, and every parse function itfeeds —
ParseSps,ParsePps,ParseVps,ParseSliceHeader,ParseSeiMessage— takessize_t, so it arrived asSIZE_MAX. Any length check inside those functions is then inert:the SEI bounds below are four
offset < sizetests that all pass unconditionally whensizeis
SIZE_MAX.It now returns a
ParserResultwith the size handed back through an out parameter, so afailure cannot be read as a length. All nine call sites log and skip the NAL unit, which is
what they already did for a failed parameter set or slice header parse.
rbsp_size_becomessize_tto match what it is passed to.Reported RBSP length —
EbspToRbsp()The same function reported
end_bytepos - begin_bytepos + reduce_count. Every byte countedin
reduce_counthas just been removed from the buffer, so adding it gives a length largerthan the EBSP ever held —
00 00 03 01is 3 bytes of RBSP but was reported as 5. Callerspass that on as the size of
rbsp_buf_, a fixed 1024 bytes thatebsp_sizealready fills inthe worst case, so the parse functions were told the buffer was larger than it is. Same class
of problem as the sentinel above.
The subtraction is now the right way round. The
cabac_zero_wordpath breaks with thetrailing
0x03still in the buffer and the spec discards it, so it is counted inreduce_countbefore the break; that keepsreduce_countmeaning the number of discardedbytes and makes one subtraction correct at both exits.
This predates the branch, and conformance does not exercise it: the parse functions stop at
the end of the syntax they need rather than the end of the buffer, so an over-reported size
changes nothing for a well formed stream.
SEI message parsing —
ParseSeiMessage()The function took the NAL unit size but used it only in the loop condition at the bottom, so
a malformed SEI read past the end four ways: the two
ff_bytescans ran unbounded; theterminating byte after each was read without checking the offset;
payload_sizecamestraight from the bitstream and was never compared against what was left, so the
memcpyappending the payload read an attacker-chosen length; and both accumulators were
inttaking+= 255in an unbounded loop, overflowing a signed type before anything caught it.Every read is now bounded by
size,payload_sizemust fit the bytes left after theheaders, and the accumulation is
size_tso the running total cannot wrap before it ischecked. SEI is supplemental, so the parse stops at the first bad message and keeps the ones
already read; the result is returned for the record and the callers carry on with the
picture rather than failing it. That contract is written on the declaration.
Separately,
sei_payload_buf_size_was not updated when the payload buffer grew. Writesstayed in range because the buffer is never smaller than the recorded size, so this was
repeated reallocation rather than an overflow, but the bookkeeping was wrong.
Start code scan —
GetNalUnit()The scan ran while
curr_byte_offset_ < pic_data_size_ - 2. Both areuint32_t, so aone-byte packet made the bound
0xFFFFFFFFand the loop read three bytes at a time far pastthe end. AVC and HEVC both guard
payload && payload_size, so a zero length packet cannotarrive, but a one byte packet can; two bytes was already safe by accident. The size is now
tested separately.
NAL unit offsets and sizes — signedness
curr_start_code_offset_,next_start_code_offset_andnal_unit_size_wereintwhilepic_data_size_andcurr_byte_offset_areuint32_t, so the arithmetic between themconverted signedness. All three are now
uint32_t.The
intwas load bearing, so it is replaced rather than dropped.nal_unit_size_wasassigned from
pic_data_size_ - curr_start_code_offset_, which already evaluates unsigned,so an offset past the end gave a huge value that became negative entering the
intand wascaught by the
nal_unit_size_ >= 4/>= 5floors in the callers. Widening the member alonewould have let that through the floor and into a
memcpyofRBSP_BUF_SIZEbytes from pastthe end of the packet.
GetNalUnit()now range checks the two offsets before deriving thesize, replacing the accidental backstop with a deliberate one.
ebsp_sizeandsei_ebsp_sizewereintfor the same reason: withnal_unit_size_unsigned, a NAL unit larger than
INT_MAXwould have made them negative, andsei_ebsp_sizereaches
new uint8_t[]andmemcpy()directly. Both areuint32_tnow.Issue Tracking
JIRA ID: ROCM-31875, AICV-310
Test Plan
Run /validate.
Test Result
validate passed:
╔══════════════════════════════╗
║ rocDecode Validation Summary ║
╠══════════════════════════════╣
║ CTest (16 tests) ✓ PASSED ║
║ Conformance AVC ✓ PASSED ║
║ Conformance AV1 ✓ PASSED ║
║ Conformance HEVC ✓ PASSED ║
║ Conformance VP9 ✓ PASSED ║
╠══════════════════════════════╣
║ Overall ✓ PASSED ║
╚══════════════════════════════╝
Conformance breakdown
┌───────┬─────────┬────────┬────────┬────────────┐
│ Codec │ Streams │ Passed │ Failed │ Unfinished │
├───────┼─────────┼────────┼────────┼────────────┤
│ AVC │ 127 │ 127 │ 0 │ 0 │
├───────┼─────────┼────────┼────────┼────────────┤
│ AV1 │ 162 │ 162 │ 0 │ 0 │
├───────┼─────────┼────────┼────────┼────────────┤
│ HEVC │ 135 │ 135 │ 0 │ 0 │
├───────┼─────────┼────────┼────────┼────────────┤
│ VP9 │ 108 │ 108 │ 0 │ 0 │
├───────┼─────────┼────────┼────────┼────────────┤
│ Total │ 532 │ 532 │ 0 │ 0 │
└───────┴─────────┴────────┴────────┴────────────┘
Submission Checklist