Skip to content

dovi: return errors instead of panicking on malformed RPUs - #426

Closed
pkajaba wants to merge 8 commits into
quietvoid:mainfrom
pkajaba:harden-rpu-parsing-v2
Closed

pkajaba wants to merge 8 commits into
quietvoid:mainfrom
pkajaba:harden-rpu-parsing-v2

Conversation

@pkajaba

@pkajaba pkajaba commented Oct 16, 2026 •

Copy link
Copy Markdown

A malformed RPU currently panics or over-allocates in several places. In a player that uses the C API (built with panic = abort), that takes down the whole process instead of failing one frame. This PR turns each case into an Err.

I ran into these while adding Dolby Vision Profile 7 FEL support to AetherEngine (superuser404notfound/AetherEngine#702), which passes RPUs from arbitrary files to the C API.

What went wrong and how it is handled now:

  • CRC32. DoviRpu::parse compared the CRC only after the payload had been parsed, so corrupt data went through the whole parser first. The CRC is now compared before read_rpu_data. Same error message, same set of accepted RPUs (the stored CRC is read from the same last five bytes the parser used); only the order of errors changes.
  • Linear interpolation mapping. unimplemented! on parse and write is now bail!.
  • L8/L9/L10 blocks. An undefined ext_block_length reached unreachable! in required_bits. A small ensure_valid_length helper (same pattern as level253.rs) now runs in parse and validate. Allowed lengths: L8 10/12/13/19/25, L9 1/17, L10 5/21.
  • num_pivots_minus2. num_pivots_minus2 is now limited to 7 (at most 9 pivots, the range in the spec and in ffmpeg's AV_DOVI_MAX_PIECES). Previously larger values were accepted when the data was present, and very large values overflowed or over-allocated.
  • Writing undefined block lengths. Writing an RPU with an L8/L9/L10 block of undefined length now returns an error instead of panicking (reachable via dovi_generate_from_json).
  • DM ext block count. num_ext_blocks is checked against the bits left (available_bits / 9, the smallest possible block) before the Vec is allocated.
  • AV1 EMDF. emdf_payload_size must fit in the remaining data before allocating, and parse_variable_bits uses checked arithmetic.

Behaviour change for callers: inputs that used to panic or abort now return Err. Valid RPUs are parsed as before. Streams with undefined L8/L9/L10 lengths always panicked before and now return an error. Streams with more than 9 pivots were previously accepted and are now rejected, since the spec allows at most 9.

Tests: new dolby_vision/src/rpu/tests.rs builds malformed RPUs bit by bit (7 tests), plus 3 tests in av1/emdf.rs. Each new negative test fails on current main, 7 of 8 by a panic.

Testing: cargo fmt --check; cargo clippy -- -D warnings in dolby_vision with --all-targets, --all-targets --features capi and --all-features --all-targets --tests; cargo test at the root (37 + 73, includes the real sample RPUs in assets/tests) and cargo test --all-features --all-targets in dolby_vision (all pass). Root clippy in the --all-features --all-targets --tests form fails only on two pre-existing unused imports in src/tests/rpu.rs, which this patch does not touch.

AI assistance: this patch, its tests and this description were written with the help of Claude (Anthropic's AI assistant). Each commit was built and tested as listed above before it was pushed.

@quietvoid

Copy link
Copy Markdown
Owner

The commit needs to be split, it does too much and the message is not clear at all.

DoviRpu::parse read and parsed the whole payload first and only then
compared the CRC32. Corrupt data therefore reached the parser, where
the panics and over-allocations fixed in the following commits could be
triggered by a stream that would have been rejected anyway.

Read the stored CRC32 from the end of the data and compare it before
read_rpu_data. The error message is unchanged.

Add a test module that crafts RPUs bit by bit, used by the tests of
this series.
A polynomial curve with poly_order_minus1 == 0 and linear_interp_flag
set hit unimplemented!() in both parsing and writing. A crafted RPU
could therefore panic the process, which aborts when built as the C
library with panic=abort.

Replace both with bail!(). The message is the same as before.
num_pivots_minus2 was read as an unbounded ue(v) and used as the size
of the pivots vector. A large value overflowed the addition or tried to
allocate that many entries, which panics or aborts the process.

The spec allows at most 8 pieces per component, that is 9 pivots, so
reject values above 7 before allocating. FFmpeg enforces the same limit
(AV_DOVI_MAX_PIECES).
A level 8, 9 or 10 block with a length other than the defined ones
(8: 10, 12, 13, 19 or 25 bytes, 9: 1 or 17, 10: 5 or 21) was read
without complaint, and required_bits() then hit unreachable!() while
parsing, in validate_and_read_remaining. No RPU that parsed before is
rejected by this check.

Check the length at the start of parse() and validate(), the same way
level253 already does.
WithExtMetadataBlocks::write computed the padding with required_bits()
before the block was written and validated. For a level 8, 9 or 10
block with an undefined length, which can be set through the C API or
the JSON generation path, that hit unreachable!().

Compute the padding after the block has been written, so the validation
error is returned first.
num_ext_blocks was read as an unbounded ue(v) and passed to
with_blocks_allocation(), which reserves that many blocks up front. A
large count overflowed the allocation size or exhausted memory, which
panics or aborts the process before any block is read.

Every block takes at least 9 bits (ext_block_length and ext_block_level),
so reject counts above available_bits / 9. A valid stream is never
rejected by this.
convert_av1_rpu_payload_to_regular allocated emdf_payload_size + 1
bytes, with the size read from the stream, before checking that the
data holds that many bytes. A small T.35 payload declaring a size of
several megabytes allocated that much and then failed on the read,
and a larger size could abort the process.

Return an error if the size exceeds the remaining data.
parse_variable_bits accumulated the value in a u32 with plain +, << and
+. Enough groups with the read_more flag set overflowed it: a panic in
debug builds, and a silently wrong size in release builds.

Use checked_add and checked_mul and return an error on overflow.
@pkajaba
pkajaba force-pushed the harden-rpu-parsing-v2 branch from 97ee971 to 2442e3a Compare October 16, 2026 19:06
@pkajaba

pkajaba commented Oct 16, 2026

Copy link
Copy Markdown
Author

Thanks for the review, and sorry for the oversized commit. I split it into eight small commits, one per check, each with its own test. Each builds and passes cargo test, cargo fmt --check and clippy on the dolby_vision crate on its own. The final code is the same as before; only the CHANGELOG lines are gone, since those are yours to write.

  1. dovi/rpu: check the CRC32 before parsing the payload - corrupt data no longer reaches the parser. Adds the test module that crafts RPUs.
  2. dovi/rpu: return an error for linear interpolation mapping curves - unimplemented!() becomes bail!() in parse and write.
  3. dovi/rpu: limit num_pivots_minus2 to 7 - rejects the value before the pivots vector is allocated.
  4. dovi/ext: validate level 8, 9 and 10 block lengths when parsing - undefined lengths are rejected in parse() and validate(), like level253.
  5. dovi/ext: return an error when writing an undefined block length - the padding is computed after the block is validated, so required_bits() is not reached for it.
  6. dovi/ext: bound num_ext_blocks by the remaining data - rejects counts above available_bits / 9 before allocating.
  7. dovi/av1: check emdf_payload_size against the remaining data - rejects the size before allocating the buffer.
  8. dovi/av1: use checked arithmetic in the EMDF variable_bits parser - overflow returns an error.

Why this matters: the C API is built with panic=abort, so a panic or failed allocation while parsing a malformed RPU aborts the host process.

@quietvoid

Copy link
Copy Markdown
Owner

Most of this seems AI assisted so it might take a while to go through and verify everything.

@pkajaba

pkajaba commented Oct 16, 2026

Copy link
Copy Markdown
Author

Most of this seems AI assisted so it might take a while to go through and verify everything.

yes, they were, I will put a disclaimer about it into a PR description.

Is there anything I can do to help with verification?

@quietvoid

quietvoid commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Not really, it just feels mostly like fuzzing and I'm not really sure yet if all of it is necessary.
In the end the issues are mostly from crafted inputs.

@quietvoid

Copy link
Copy Markdown
Owner

2442e3a and 7d7ec66 are fixed by #428.

@quietvoid

quietvoid commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Thanks for reporting, all the issues are fixed in the main branch.
Most commits from this PR were not fixing things correctly.

@quietvoid quietvoid closed this Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants