The safety half of the rust-safety programme's §8.4: `std::env::set_var`/`remove_var` are
`unsafe fn` in edition 2024, converting the class of bug the programme found the hard way
(the 972af299 environ data race lived in a file with ZERO occurrences of the word
`unsafe`) from invisible to counted and compiler-enforced.
Manifests: [workspace.package] edition 2021→2024, rust-version 1.82→1.85 (the pinned
toolchain is 1.96.0, so no toolchain bump — only the declared floor rises); the 13 crates
pinning `edition = "2021"` literally now inherit it (Trap 1: the root bump alone reaches
only `edition.workspace = true` crates and would have left pf-encode/pf-capture/pf-inject
et al. on 2021 while reading as complete); pf-driver-proto's stale rust-version 1.82 pin
now inherits; pf-vkhdr-layer (a separate workspace, inherits nothing) bumped to 2024. The
four vendored crates (fec-rs, cros-codecs, usbip-sim, the patched ndk) stay on 2021
deliberately — upstream code stays pristine. The excluded usbip-poc standalone PoC is
untouched.
Mechanical, done textually across ALL cfg branches so no platform's half is left behind
(Trap 3 — 44% of the host's unsafe is Windows-only and a one-platform `cargo fix` misses
it): 148 `#[no_mangle]` → `#[unsafe(no_mangle)]` (83 in abi.rs); 12 bare extern blocks →
`unsafe extern`; `gen` is a reserved keyword, so pf-vdisplay's generation stamps
(registry.rs, windows/manager.rs) and the WinUI shell's animation counters rename
gen → generation (internal identifiers only, no serde/wire surface); two
match-ergonomics patterns take the compiler's suggested reference form.
env mutation: every `set_var`/`remove_var` site (20 files) now sits in an `unsafe` block
whose SAFETY comment states the real serialization argument (pf-vdisplay's ENV_LOCK,
CONFIG_DIR_TEST_LOCK, ART_ROOTS_LOCK, vkdecode's gpu_lock, the `--test-threads=1`
contracts of the hardware spikes, or single-threaded startup). Two genuine hazards
surfaced en route — exactly the WP3b-class finds this migration exists to make visible —
and are fixed here:
- windows/service.rs spawned the network-profile warner thread BEFORE `load_host_env()`,
so a child-spawning thread (child spawn snapshots the env block) was live while
`set_var` ran in a loop; the load now precedes the spawn.
- pf-console-ui's `fake_home()` re-set HOME outside its OnceLock on EVERY call, so two
parallel tests could race the write; the set now happens exactly once inside
`get_or_init`.
cbindgen (Trap 2): 0.29.4 parses `#[unsafe(no_mangle)]` — verified empirically; the
header regenerates byte-identical. The ci.yml drift check could never catch "failed to
regenerate" (build.rs demotes a cbindgen failure to a warning and writes nothing, leaving
the checked-in header untouched and the diff clean), so the step now first asserts the
"punktfunk-core: wrote" line and the absence of "cbindgen failed" (sh -e safe: no `!`
pipeline, no tee-masked exit).
rustfmt: style_edition pinned to 2021 at the root — edition 2024 would otherwise flip the
style edition and reformat ~370 untouched files inside this same commit, burying the
migration diff. The drivers workspace pins its already-current 2024 style. Adopting the
2024 style tree-wide is its own future one-line-plus-reformat commit.
Census: the primary metric moves UP BY DESIGN — 2435 → 2453 operations, unsafe blocks
1534 → 1577, and env_set_var is now a counted category (45 ops). The newly counted env
sites are a truer number, not a regression; baseline snapshot saved as punktfunk-planning
design/rust-safety-census-baseline-2026-08-12-edition-2024.txt. Gate C's env ratchet is
now compiler-enforced (the hygiene-script header says so); the two shrunk file counts
(nvenc_cuda 49→2 via the test helpers, shell/tests 2→1) are lowered in the same commit
per the gate's own rule.
Drop order (the semantic change most likely to bite this codebase): the migration lint
`-W tail-expr-drop-order` reports zero findings on the macOS-visible halves of
pf-encode / pf-zerocopy / pf-capture / pf-frame; the Linux and Windows halves run the
same lint on the gate boxes. The four #[ignore]d alloc/drop-cycle tests on the hardware
boxes remain owed, as before this change.
448 lines
19 KiB
Rust
448 lines
19 KiB
Rust
//! Fault injection proves detection — M4's exit criterion, on the CPU.
|
|
//!
|
|
//! The native-decode program exists because a field corruption was
|
|
//! ARCHITECTURALLY undetectable: FFmpeg's Vulkan decoder creates no status queries,
|
|
//! never sets `AV_FRAME_FLAG_CORRUPT`, and reports trouble only as log lines. The
|
|
//! native lane now has the signals. This harness fires them on purpose, so
|
|
//! "detection works" is a test result rather than a belief.
|
|
//!
|
|
//! It drives the REAL detection path minus the GPU: [`pf_vkdecode::AuFault`] — the
|
|
//! very injector the client's `PUNKTFUNK_AU_FAULT` knob arms — damages a real
|
|
//! 250-AU host-shaped stream, the damaged AUs go through the planner exactly as
|
|
//! `VkH264Decoder::decode` / `VkH265Decoder::decode` feed it, and the verdict is
|
|
//! read with [`pf_vkdecode::is_integrity_warning`] /
|
|
//! [`pf_vkdecode::is_integrity_warning_h265`] — the same predicates the client
|
|
//! conceals on. Nothing is mocked; what is absent is only the Vulkan submission
|
|
//! below the planner, which cannot change a plan's warnings.
|
|
//!
|
|
//! **Both codecs**, because they detect a lost AU through genuinely different
|
|
//! predicates and a harness covering one proves nothing about the other. H.264 has
|
|
//! a `frame_num` gap — an explicit, cheap counter break that fires whether or not
|
|
//! the missing picture is ever referenced. HEVC has no analogue at all (POC is
|
|
//! coded per picture and simply jumps), so its entire drop detection rests on the
|
|
//! RPS resolving a reference the DPB does not hold: `MissingReference` or nothing.
|
|
//! HEVC ships in this milestone, so it is tested here on the same terms.
|
|
//!
|
|
//! What it therefore proves, and — just as deliberately — what it proves is
|
|
//! IMPOSSIBLE here:
|
|
//!
|
|
//! * **Proved**: a dropped AU is DETECTED as *integrity* damage within a bounded
|
|
//! number of AUs, on both codecs — the class that makes the client release the
|
|
//! picture unshown and ask for a re-anchor. That is M4's exit criterion for the
|
|
//! parser-visible half.
|
|
//! * **Proved**: the clean stream produces NO integrity warning anywhere, so the
|
|
//! detector cannot be passing by firing on everything (the failure mode that
|
|
//! would cost a keyframe round trip per second on healthy links).
|
|
//! * **Proved impossible**: truncation and payload flips are INVISIBLE to the
|
|
//! parser. Annex-B carries no NALU length, so a cut slice is just a shorter
|
|
//! slice, and a flipped payload byte is syntactically perfect. Both decode to a
|
|
//! wrong picture with nothing in the bitstream to object to. The only detector
|
|
//! left is the driver's per-op `RESULT_STATUS` verdict, which needs real hardware
|
|
//! — the GPU smoke/parity tests' ground. These assertions are the negative space
|
|
//! that justifies the status ring existing at all, and the reason a session on a
|
|
//! driver without `queryResultStatusSupport` must be reported as unmeasured
|
|
//! rather than clean.
|
|
//! * **Proved impossible, and correctly so**: a dropped SUB-LAYER NON-REFERENCE
|
|
//! picture is invisible to the planner on either codec, because nothing ever
|
|
//! references it. That is not a hole in detection — no later picture is damaged
|
|
//! — it is a missing OUTPUT frame, which is the wire's frame-index gap detector's
|
|
//! job (`punktfunk_core::reanchor::index_gap`), not the bitstream's. The HEVC
|
|
//! vector below contains both kinds and the test asserts both verdicts, so the
|
|
//! distinction cannot quietly become "HEVC misses drops".
|
|
|
|
use pf_bitstream::h264::H264Planner;
|
|
use pf_bitstream::h265::H265Planner;
|
|
use pf_vkdecode::{
|
|
is_integrity_warning, is_integrity_warning_h265, AuFault, FaultAction, FaultMode,
|
|
};
|
|
use std::io::Cursor;
|
|
|
|
/// The same vendored vectors the WP-A conversion tests and the GPU tests use: 250
|
|
/// AUs of real encoder output each, an IDR then P-frames — the punktfunk
|
|
/// envelope's shape, in both codecs.
|
|
const TEST_25FPS_H264: &[u8] = include_bytes!(
|
|
"../../pf-bitstream/vendor/cros-codecs/src/codec/h264/test_data/test-25fps.h264"
|
|
);
|
|
const TEST_25FPS_H265: &[u8] = include_bytes!(
|
|
"../../pf-bitstream/vendor/cros-codecs/src/codec/h265/test_data/test-25fps.h265"
|
|
);
|
|
|
|
/// Test-only H.264 AU splitter, mirroring pf-bitstream's (`#[cfg(test)]`-private
|
|
/// there) and the GPU tests'.
|
|
fn split_h264(stream: &[u8]) -> Vec<&[u8]> {
|
|
use cros_codecs::codec::h264::parser::{Nalu, NaluType};
|
|
let mut aus = Vec::new();
|
|
let mut cursor = Cursor::new(stream);
|
|
let mut au_start = 0usize;
|
|
let mut au_has_slice = false;
|
|
|
|
while let Ok(nalu) = Nalu::next(&mut cursor) {
|
|
let nalu_offset = cursor.position() as usize;
|
|
let start = nalu_offset - nalu.offset;
|
|
let is_slice = matches!(nalu.header.type_, NaluType::Slice | NaluType::SliceIdr);
|
|
let first_mb_zero = is_slice && stream.get(nalu_offset + 1).is_some_and(|b| b & 0x80 != 0);
|
|
|
|
if au_has_slice && (!is_slice || first_mb_zero) {
|
|
aus.push(&stream[au_start..start]);
|
|
au_start = start;
|
|
au_has_slice = false;
|
|
}
|
|
au_has_slice |= is_slice;
|
|
}
|
|
aus.push(&stream[au_start..]);
|
|
aus
|
|
}
|
|
|
|
/// The H.265 twin, mirroring `pic_h265`'s test splitter: a new AU starts at a
|
|
/// non-VCL NALU following slices, or at a slice segment whose
|
|
/// `first_slice_segment_in_pic_flag` is set (the first bit of the byte after the
|
|
/// 2-byte NAL header) when the current AU already has slices.
|
|
fn split_h265(stream: &[u8]) -> Vec<&[u8]> {
|
|
use cros_codecs::codec::h265::parser::Nalu;
|
|
let mut aus = Vec::new();
|
|
let mut cursor = Cursor::new(stream);
|
|
let mut au_start = 0usize;
|
|
let mut au_has_slice = false;
|
|
|
|
while let Ok(nalu) = Nalu::next(&mut cursor) {
|
|
let header_start = cursor.position() as usize;
|
|
let start = header_start - nalu.offset;
|
|
let is_slice = (nalu.header.type_ as u32) < 32;
|
|
let first_slice_flag =
|
|
is_slice && stream.get(header_start + 2).is_some_and(|b| b & 0x80 != 0);
|
|
|
|
if au_has_slice && (!is_slice || first_slice_flag) {
|
|
aus.push(&stream[au_start..start]);
|
|
au_start = start;
|
|
au_has_slice = false;
|
|
}
|
|
au_has_slice |= is_slice;
|
|
}
|
|
aus.push(&stream[au_start..]);
|
|
aus
|
|
}
|
|
|
|
/// What the client would do with each AU, condensed to the one bit that matters:
|
|
/// did THIS AU make the client conceal and ask for recovery?
|
|
///
|
|
/// Mirrors `video_vk_native::NativeVulkanDecoder::decode`'s CPU half exactly — a
|
|
/// plan that fails outright is trouble, and a plan whose warnings include an
|
|
/// integrity warning is damage. (The third source, a driver `Failed` verdict on a
|
|
/// prior frame, has no CPU analogue and is the GPU tests' business.)
|
|
fn damaged_h264(planner: &mut H264Planner, au: &[u8]) -> bool {
|
|
match planner.plan_au(au) {
|
|
Ok(plan) => plan.warnings.iter().any(is_integrity_warning),
|
|
// A refusal is the loudest possible detection: the client turns it into an
|
|
// Err, releases nothing, and asks. Counting it as detected is what the
|
|
// client does.
|
|
Err(_) => true,
|
|
}
|
|
}
|
|
|
|
/// The H.265 twin — with the one arm that is NOT damage.
|
|
///
|
|
/// `RaslSkipped` is the spec's own answer (8.1.3 NOTE) for a leading picture whose
|
|
/// references precede an open-GOP join: `VkH265Decoder::decode` turns it into
|
|
/// `Ok(None)` and clears the warning ledger, so the client neither drops a frame
|
|
/// nor asks for anything. Counting it here would let this harness "detect" a fault
|
|
/// through a path production treats as a non-event.
|
|
fn damaged_h265(planner: &mut H265Planner, au: &[u8]) -> bool {
|
|
use pf_bitstream::h265::PlanError;
|
|
match planner.plan_au(au) {
|
|
Ok(plan) => plan.warnings.iter().any(is_integrity_warning_h265),
|
|
Err(PlanError::RaslSkipped { .. }) => false,
|
|
Err(_) => true,
|
|
}
|
|
}
|
|
|
|
/// Replay a vector with `fault` armed, returning for each AU index whether the
|
|
/// client would have flagged damage — plus how many AUs were actually faulted, so
|
|
/// a test can prove the injector fired at all rather than passing vacuously.
|
|
///
|
|
/// Generic over the codec through the split + verdict pair, so both legs replay
|
|
/// through ONE driver: the injector, the AU cadence and the "a dropped AU has no
|
|
/// verdict of its own" rule are codec-independent, and forking them per codec is
|
|
/// how the two legs would quietly stop testing the same thing.
|
|
fn replay<'a>(
|
|
stream: &'a [u8],
|
|
split: fn(&'a [u8]) -> Vec<&'a [u8]>,
|
|
mut verdict: impl FnMut(&[u8]) -> bool,
|
|
fault: Option<AuFault>,
|
|
) -> (Vec<bool>, usize) {
|
|
let aus = split(stream);
|
|
let mut fault = fault;
|
|
let mut flags = Vec::with_capacity(aus.len());
|
|
let mut faulted = 0usize;
|
|
for au in aus {
|
|
let action = match &mut fault {
|
|
Some(f) => f.apply(au),
|
|
None => FaultAction::Pass,
|
|
};
|
|
match action {
|
|
FaultAction::Pass => flags.push(verdict(au)),
|
|
FaultAction::Drop => {
|
|
faulted += 1;
|
|
// The AU never reaches the decoder — exactly what the client does
|
|
// for a dropped AU, and exactly what the network does for a lost
|
|
// one. No verdict for this index.
|
|
flags.push(false);
|
|
}
|
|
FaultAction::Corrupt(bytes) => {
|
|
faulted += 1;
|
|
flags.push(verdict(&bytes));
|
|
}
|
|
}
|
|
}
|
|
(flags, faulted)
|
|
}
|
|
|
|
fn replay_h264(fault: Option<AuFault>) -> (Vec<bool>, usize) {
|
|
let mut planner = H264Planner::new();
|
|
replay(
|
|
TEST_25FPS_H264,
|
|
split_h264,
|
|
move |au| damaged_h264(&mut planner, au),
|
|
fault,
|
|
)
|
|
}
|
|
|
|
fn replay_h265(fault: Option<AuFault>) -> (Vec<bool>, usize) {
|
|
let mut planner = H265Planner::new();
|
|
replay(
|
|
TEST_25FPS_H265,
|
|
split_h265,
|
|
move |au| damaged_h265(&mut planner, au),
|
|
fault,
|
|
)
|
|
}
|
|
|
|
/// How many AUs after the fault detection is allowed to take on H.264. ONE: a
|
|
/// dropped reference is visible to the planner on the very next AU that references
|
|
/// it. The bound is stated as a number rather than "eventually" because
|
|
/// "eventually" is what the 500 ms freeze backstop already provides — the whole
|
|
/// point of local detection is that it is immediate.
|
|
const DETECT_WITHIN: usize = 1;
|
|
|
|
/// The fault period the tests replay at: five faults over the 250-AU vector, far
|
|
/// enough apart that each one's detection is unambiguously about ITS fault.
|
|
const PERIOD: u32 = 50;
|
|
|
|
/// The 0-based indices [`AuFault`] faults at `PERIOD`, over `total` AUs. The
|
|
/// injector counts the AUs it is OFFERED, 1-based, so the first fault lands on
|
|
/// index `PERIOD - 1` — which is also why any period above 1 leaves a session's
|
|
/// opening parameter sets and IDR untouched.
|
|
fn fault_indices(total: usize) -> Vec<usize> {
|
|
(PERIOD as usize - 1..total)
|
|
.step_by(PERIOD as usize)
|
|
.collect()
|
|
}
|
|
|
|
/// Every faulted AU is followed within [`DETECT_WITHIN`] AUs by a damage verdict.
|
|
/// Returns how many faults actually had a successor window to check, so the caller
|
|
/// can refuse a vacuous pass.
|
|
fn assert_detected_after_each_fault(flags: &[bool], what: &str) -> usize {
|
|
let mut checked = 0usize;
|
|
for dropped in fault_indices(flags.len()) {
|
|
// The last AU of the vector has no successor to detect on — the stream
|
|
// simply ends there. Skipping it is honest; asserting on it would be a
|
|
// statement about the fixture's length, not about detection.
|
|
let Some(window) = flags.get(dropped + 1..(dropped + 1 + DETECT_WITHIN).min(flags.len()))
|
|
else {
|
|
continue;
|
|
};
|
|
if window.is_empty() {
|
|
continue;
|
|
}
|
|
checked += 1;
|
|
assert!(
|
|
window.iter().any(|&d| d),
|
|
"{what}: the AU(s) after dropped AU {dropped} must read as damaged — \
|
|
the reference it needs was never decoded"
|
|
);
|
|
}
|
|
checked
|
|
}
|
|
|
|
/// The indices that read as damaged — the message a failing assertion needs.
|
|
fn flagged(flags: &[bool]) -> Vec<usize> {
|
|
flags
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|&(_, &d)| d)
|
|
.map(|(i, _)| i)
|
|
.collect()
|
|
}
|
|
|
|
/// The control: a healthy stream must produce NO damage verdict anywhere, on
|
|
/// either codec. Without this the fault tests below prove nothing — a detector
|
|
/// that fires on every AU would pass them and cost a keyframe round trip per
|
|
/// second in the field.
|
|
#[test]
|
|
fn a_clean_stream_never_reads_as_damaged() {
|
|
for (codec, (flags, faulted)) in [("h264", replay_h264(None)), ("h265", replay_h265(None))] {
|
|
assert_eq!(faulted, 0, "{codec}: no fault armed");
|
|
assert_eq!(flags.len(), 250, "{codec}: the whole vector replayed");
|
|
assert!(
|
|
flagged(&flags).is_empty(),
|
|
"{codec}: the clean vector must plan without a single integrity \
|
|
warning — flagged AUs: {:?}",
|
|
flagged(&flags)
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A DROPPED access unit — the everyday network-loss shape — is detected on the
|
|
/// next AU, because that AU references a picture the DPB never received. This is
|
|
/// the exit criterion's first half: deliberately corrupted input, detection within
|
|
/// a bounded number of frames.
|
|
#[test]
|
|
fn a_dropped_access_unit_is_detected_on_the_very_next_one() {
|
|
let (flags, faulted) = replay_h264(Some(AuFault::new(FaultMode::Drop, PERIOD)));
|
|
assert!(faulted >= 4, "the injector fired ({faulted} AUs dropped)");
|
|
let checked = assert_detected_after_each_fault(&flags, "h264");
|
|
assert!(
|
|
checked >= 4,
|
|
"{checked} drops actually had a successor to check"
|
|
);
|
|
}
|
|
|
|
/// The H.265 leg of the same criterion, and the reason it is a separate test
|
|
/// rather than a loop over both codecs: HEVC detects a dropped AU through a
|
|
/// DIFFERENT predicate, and it has a class of AU whose loss is legitimately
|
|
/// invisible.
|
|
///
|
|
/// There is no `frame_num` gap to notice — POC is coded per picture and a jump in
|
|
/// it is legal — so everything rests on the RPS asking for a picture the DPB does
|
|
/// not hold (`MissingReference`). If that ever stopped firing, HEVC would lose
|
|
/// drop detection entirely while the H.264 leg above stayed green.
|
|
///
|
|
/// And the RPS can only speak for pictures something REFERENCES. A sub-layer
|
|
/// non-reference picture (`TRAIL_N` and friends — 3 of the 5 faults this vector
|
|
/// takes) is referenced by nothing, so its loss damages no later picture and the
|
|
/// planner is right to stay silent: it is a missing output frame, caught by the
|
|
/// wire's frame-index gap, not by the bitstream. Asserting BOTH verdicts is what
|
|
/// stops that correct silence from being mistaken for a detection hole — or a
|
|
/// detection hole from hiding behind it.
|
|
#[test]
|
|
fn a_dropped_hevc_reference_picture_is_detected_through_the_rps() {
|
|
// Which AUs carry a picture something can reference? Read off a CLEAN replay,
|
|
// so the classification is the stream's own and not this test's guess.
|
|
let mut planner = H265Planner::new();
|
|
let referenced: Vec<bool> = split_h265(TEST_25FPS_H265)
|
|
.into_iter()
|
|
.map(|au| {
|
|
planner
|
|
.plan_au(au)
|
|
.map(|plan| !plan.picture.nalu_type.is_slnr())
|
|
.unwrap_or(false)
|
|
})
|
|
.collect();
|
|
|
|
let (flags, faulted) = replay_h265(Some(AuFault::new(FaultMode::Drop, PERIOD)));
|
|
assert!(faulted >= 4, "the injector fired ({faulted} AUs dropped)");
|
|
|
|
let (mut checked_refs, mut checked_slnr) = (0usize, 0usize);
|
|
for dropped in fault_indices(flags.len()) {
|
|
let Some(window) = flags.get(dropped + 1..(dropped + 1 + DETECT_WITHIN).min(flags.len()))
|
|
else {
|
|
continue;
|
|
};
|
|
if window.is_empty() {
|
|
continue;
|
|
}
|
|
if referenced[dropped] {
|
|
checked_refs += 1;
|
|
assert!(
|
|
window.iter().any(|&d| d),
|
|
"h265: the AU after dropped REFERENCE picture {dropped} must read \
|
|
as damaged — its RPS names a picture the DPB never received"
|
|
);
|
|
} else {
|
|
checked_slnr += 1;
|
|
assert!(
|
|
!window.iter().any(|&d| d),
|
|
"h265: dropping sub-layer non-reference picture {dropped} damages \
|
|
nothing — if the planner starts complaining here it is reporting \
|
|
damage that did not happen, and every such report costs a frame \
|
|
and a keyframe round trip"
|
|
);
|
|
}
|
|
}
|
|
assert!(
|
|
checked_refs >= 2 && checked_slnr >= 2,
|
|
"the vector must exercise BOTH classes ({checked_refs} reference drops, \
|
|
{checked_slnr} non-reference drops) or this test proves only half of what \
|
|
it claims"
|
|
);
|
|
}
|
|
|
|
/// A TRUNCATED access unit is NOT parser-visible, and the assertion is that it
|
|
/// stays that way — on both codecs.
|
|
///
|
|
/// Annex-B has no NALU length field: a slice cut at a byte boundary is
|
|
/// indistinguishable from a shorter slice. Its header parses, the picture plans,
|
|
/// it enters the DPB, and every later AU resolves its reference against an entry
|
|
/// that exists — so nothing in the syntax is ever wrong. (pf-bitstream's
|
|
/// `TruncatedAu` warning is a narrower thing entirely: a NALU whose HEADER is
|
|
/// malformed with real data still behind it.) The hardware, meanwhile, is handed a
|
|
/// slice whose bitstream ends mid-picture, which is a decode error it can report —
|
|
/// so this mode is how a lab run fires the driver's `RESULT_STATUS` detector
|
|
/// deterministically, and it is useless without one.
|
|
#[test]
|
|
fn a_truncated_access_unit_is_invisible_to_the_parser_and_needs_the_driver_verdict() {
|
|
for (codec, (flags, faulted)) in [
|
|
(
|
|
"h264",
|
|
replay_h264(Some(AuFault::new(FaultMode::Truncate, PERIOD))),
|
|
),
|
|
(
|
|
"h265",
|
|
replay_h265(Some(AuFault::new(FaultMode::Truncate, PERIOD))),
|
|
),
|
|
] {
|
|
assert!(
|
|
faulted >= 4,
|
|
"{codec}: the injector fired ({faulted} AUs truncated)"
|
|
);
|
|
assert!(
|
|
flagged(&flags).is_empty(),
|
|
"{codec}: a mid-slice cut carries no syntax error — if this starts \
|
|
firing, the cut is landing on a NALU header and the mode has stopped \
|
|
exercising the driver-only path it exists for (flagged AUs: {:?})",
|
|
flagged(&flags)
|
|
);
|
|
}
|
|
}
|
|
|
|
/// The mode that shows what the DRIVER's status query is for: a byte flipped deep
|
|
/// in a slice payload leaves a bitstream that parses perfectly, resolves every
|
|
/// reference, and decodes to a wrong picture. The planner is silent — as it should
|
|
/// be, since nothing about the syntax is wrong — and on the FFmpeg rungs that
|
|
/// silence is the end of the story (`nb_queries = 0`, no `AV_FRAME_FLAG_CORRUPT`).
|
|
/// This is the Xbox Ally X class exactly, on both codecs.
|
|
#[test]
|
|
fn a_payload_bit_flip_is_invisible_to_the_parser_which_is_why_the_status_query_exists() {
|
|
for (codec, (flags, faulted)) in [
|
|
(
|
|
"h264",
|
|
replay_h264(Some(AuFault::new(FaultMode::Flip, PERIOD))),
|
|
),
|
|
(
|
|
"h265",
|
|
replay_h265(Some(AuFault::new(FaultMode::Flip, PERIOD))),
|
|
),
|
|
] {
|
|
assert!(
|
|
faulted >= 4,
|
|
"{codec}: the injector fired ({faulted} AUs flipped)"
|
|
);
|
|
assert!(
|
|
flagged(&flags).is_empty(),
|
|
"{codec}: a payload flip must not be parser-visible — if this ever \
|
|
starts firing, the flip is landing in syntax rather than payload and \
|
|
the mode has stopped testing what it claims (flagged AUs: {:?})",
|
|
flagged(&flags)
|
|
);
|
|
}
|
|
}
|