docs(client): write down why the CPU rung is not process-isolated

#97's frame-context floor closes the one rav1d abort we hit and can prove. It
does not make the rung panic-proof and nothing at that call site can, because
rav1d's public surface is dav1d's C ABI: any reachable panic crosses
`extern "C"` as `panic_cannot_unwind` and becomes `abort()`, past every
`catch_unwind`, rung demotion and typed refusal we have.

Counted across rav1d 1.1.0's 60 source files: 285 `unwrap()`, 214 `assert!`,
19 `unreachable!`, 11 `expect()`, 10 `panic!`. 539 sites that end the client if
a stream can reach them. #97 fixed one of them.

Process isolation is the only defence that actually works, and this records the
decision NOT to build it, with the reasoning, so it is not re-argued from
scratch each time someone reads that number:

  * the defect is upstream's and is one line (memorysafety/rav1d#1497, filed
    2026-08-07 with the fix and a reproducer; still open, no PR, as of today);
  * 539 is an unbounded number, not a risk estimate — none of those sites is
    known reachable from a punktfunk stream, and the honest next step is to
    fuzz the rung and find out, which is cheap, rather than buy insurance,
    which is not;
  * the cost lands on the video path across Linux, Windows and Android (the
    Apple clients decode through VideoToolbox and never reach this code), each
    needing its own shared-memory frame transport, child lifecycle and
    backpressure, and it adds a scheduling boundary to the slowest rung on the
    ladder while zero-copy is a hard requirement;
  * an abort here costs a session that was already degraded — this rung exists
    because the GPU rungs failed first.

The trigger to revisit is named as an event rather than a feeling: a SECOND
distinct abort in the field, or a fuzzer finding a reachable panic. Either
makes it a class of bugs instead of one, and a class is what would justify the
architecture.

Documentation only — no behaviour change.
This commit is contained in:
2026-08-07 19:25:22 +02:00
parent 669176982d
commit 6b4be28d24
@@ -45,6 +45,46 @@
//! no equivalent here. rav1d gets the machine's cores, and **at least two frame contexts**;
//! [`Av1Software::new`] carries the whole argument, because "at least two" is not a
//! performance choice but the difference between an error and `abort()`.
//!
//! # Why this rung is NOT process-isolated
//!
//! The frame-context floor closes the one abort we hit and can prove. It does not make the
//! rung panic-proof, and nothing at this call site can: rav1d exposes dav1d's C ABI, every
//! internal `rav1d_*` entry point is `pub(crate)`, so any reachable panic crosses
//! `extern "C"` as `panic_cannot_unwind` → `abort()`. No `catch_unwind`, no rung demotion
//! and no [`NoSoftwareRung`] refusal can contain it. Counted in rav1d 1.1.0's 60 source
//! files: 285 `unwrap()`, 214 `assert!`, 19 `unreachable!`, 11 `expect()`, 10 `panic!` —
//! 539 sites that are an `abort()` if a stream can reach them. #97 fixed ONE.
//!
//! Isolating the decoder in its own process is the only defence that actually works, and
//! it is deliberately NOT taken. The decision, so it is not re-litigated from scratch:
//!
//! * **The defect is a dependency's, and it is one line.** memorysafety/rav1d#1497 was
//! filed 2026-08-07 with the fix (`is_some_and` for the `unwrap`) and a reproducer.
//! Paying a permanent architectural tax to route around a bug that costs upstream one
//! line is the wrong trade while that line is still plausibly coming.
//! * **The residual risk is real but unquantified.** 539 panic sites is a scary number
//! and a meaningless one: not one of them is known to be reachable from a punktfunk
//! stream. The honest next step is to MEASURE reachability — fuzz this rung with
//! truncated, reordered and bit-flipped AUs and see whether any input aborts — not to
//! buy insurance against a number nobody has bounded. That is cheap; this is not.
//! * **The cost lands on the video path, and on three platforms.** pf-client-core builds
//! into the Linux, Windows and Android clients (the Apple clients decode through
//! VideoToolbox and never reach here). Each needs its own shared-memory transport for
//! `CpuPlanarFrame`s, its own child lifecycle, crash detection and restart, and its own
//! backpressure — and it adds a scheduling boundary to the rung that is ALREADY the
//! slowest one on the ladder. Zero-copy is a hard requirement here; an IPC hop that
//! copies frames would be rejected on its own terms.
//! * **What an abort actually costs is bounded.** This rung is reached because the GPU
//! rungs already failed, so the session is degraded before rav1d sees a byte. Losing
//! the process loses a session the user was going to have a bad time in regardless.
//! That is bad, and it is not the same as losing a working session.
//!
//! **Revisit when the calculus changes, which is a specific event, not a feeling:** a
//! SECOND distinct abort observed in the field, or a fuzzer finding a reachable panic.
//! Either turns this from one upstream bug into a class of them, and a class is what
//! justifies isolation. Until then the floor plus the upstream fix is the proportionate
//! answer, and the fuzzing is the work that would tell us we were wrong.
use crate::video::{CpuPlanarFrame, RungLoss};
use crate::video_color::ColorDesc;