Conversation
`on_error` clears `f.frame_hdr` itself, so it must tolerate being reached when the header is already gone — and it can be. In the `c.fc.len() == 1` branch of `rav1d_submit_frame`, `rav1d_decode_frame` runs inline and tears the frame data down on failure; `rav1d_submit_frame` then calls `on_error` on that same data. The second call unwraps a `None`. Because every entry point into this crate is `extern "C"`, that panic cannot unwind across the ABI boundary: it aborts the process rather than surfacing as an error the caller can handle. A single corrupted byte in an otherwise valid AV1 bitstream is enough to take the host application down. Only single-frame-context decoding is affected, which is why it survives in threaded use: with `n_fc > 1` errors route through the task thread's `retval` instead. `n_fc = min(max_frame_delay, n_threads)`, so any caller pinning `max_frame_delay = 1` for single-shot decode selects the broken path. Checking the option instead of unwrapping it preserves behaviour exactly wherever the header exists, and turns the abort back into the error the caller already handles.
|
Independent corroboration for this, from a downstream that hit it without knowing #1497 existed. kaleb (Tekcore-Technologie/kaleb), a pure-Rust media stack, wraps rav1d as its AV1 decoder. Writing an AV1 encoder against it, a malformed stream took the whole test process down with SIGABRT. We arrived at exactly the change in this PR — The trigger is broader than the dropped-temporal-unit reproducer in #1497. A single byte flipped inside the tile data of an otherwise valid stream is enough — the frame header parses, and the failure happens inside #1497's
The configurations that survive are the ones where Effect of this PR, over 600 mutated streams (single- and multi-bit flips plus truncations, across four seeds including a real libaom encode), each decoded in a child process so aborts could be counted:
Every abort became a clean error and nothing else moved — the same 161 streams still decode to a frame, so the change does not make the decoder reject anything it previously accepted. Worth adding that this is not a robustness nit for a wrapping library: Happy to share the mutation harness if it would be useful. |
|
Thanks for the report. I can't speak to your numbers, but the One thing worth flagging, since it may change what kaleb has to carry:
A one-shot caller does have one, if it drains rather than reading Worth noting too that If the harness is easy to post publicly, please do — a standalone reproducer |
Summary
on_errorinrav1d_submit_frameunwrapsf.frame_hdr, buton_errorisalso what clears
f.frame_hdr— so reaching it twice on the same frame datapanics. It can be reached twice, and because every entry point into this crate
is
extern "C", the panic cannot unwind across the ABI boundary: the processaborts instead of returning an error the caller can handle.
A single corrupted byte in an otherwise valid AV1 bitstream is enough to take
the host application down. We hit this in a media viewer, on a real
.mp4with one byte flipped inside its
mdat.Root cause
rav1d_submit_frame's single-frame-context branch:rav1d_decode_framehas already cleaned up by the time this runs, soon_errorgets frame data whose header is gone. Instrumentingon_errorwith#[track_caller]confirms it, reaching it from that call site withframe_hdr=false seq_hdr=false.Only
n_fc == 1is affected, which is presumably why it has survived: withmore than one frame context, decode errors route through the task thread's
retvaland this branch is never taken. Sincen_fc = min(max_frame_delay, n_threads), any caller pinningmax_frame_delay = 1— a natural choice for single-shot, deterministicdecoding — selects the broken path.
The change
Check the option rather than unwrapping it. Behaviour is identical wherever the
header exists (i.e. every path that works today); the only case that changes is
the one that currently aborts.
on_errorclearsf.frame_hdra few lineslater regardless.
Verification
Built a patched
rav1d1.1.0 and re-ran the case that aborted: 160 systematicmutations of a real AV1 MP4 (truncations, single-byte flips, and
0xFFFFFFFFwritten over header fields), decoded with
max_frame_delay = 1. Before: abortat
decode.rs:4997. After: every mutation returns cleanly, either a picture oran error, and no other assertion in our suite moved.
The same one-line change applies to
main, which is what this PR targets.Note
We also worked around it on our side by raising
max_frame_delayto 2, so thisPR is not urgent for us — but the abort is reachable by anyone decoding
single-threaded, and an unwinding panic would at least be catchable where an
abort is not.