Skip to content

fix(probe): evidence in hand outranks the budget that bounded fetching it - #586

Merged
superuser404notfound merged 1 commit into
mainfrom
fix/probe-controls-round-2
Sep 21, 2026
Merged

superuser404notfound merged 1 commit into
mainfrom
fix/probe-controls-round-2

Conversation

@superuser404notfound

Copy link
Copy Markdown
Owner

Follow-up to #583, whose validator and whole-probe controls are right in the core. Four places where a bound meant to limit cost was also deciding what the probe may report.

1. A validated message was withdrawn by damage next to it

HDR10PlusMetadataScan collected a confirmation in found and then threw it away whenever anything later in the same packet failed the structural walk. Verified against the scan directly, before this change:

lone valid HDR10+ SEI                            -> true
that SEI, then a NAL with forbidden_zero_bit set -> false
that SEI, then an SEI missing rbsp_trailing_bits -> false

The byte scan this replaced answered yes to all three. A vendor SEI, a trailing byte or a second unreadable NAL next to the metadata is ordinary shape on real media, and this reaches the playback path through HLSSegmentProducer, not only the probe. The NAL walk, the OBU walk and the SEI message walk now stop at the damage and report what they validated. Nothing can invent a positive there: only validT35 ever sets one.

2. A pass retracted a detection it had already made

The HDR10+ and Atmos detail passes turned a confirmation into .timeCap (and decodedProfile: nil) when their soft wall-clock budget expired mid-pass. A caller cannot tell a withheld confirmation apart from a source that carries none, so an overrun on a slow origin silently became "no Atmos" on the existing API, with no opt-in. An overrun now retires the pass without taking its answer back.

The whole-probe deadline from #583 is untouched. That one throws, so the caller knows.

3. limits also chose how deeply to analyse

Passing limits switched the open to .stillExtraction, so probe(url:limits:) read 2 MiB / 2 s where probe(url:) reads 50 MiB / 60 s, and could report fewer streams for it. The caller's own limits should be the only thing that binds: the profile is built from .playback with probesize clamped to maxInputBytes, and the deadline plus the FFmpeg interrupt callback bound the analysis instead.

The recordless Dolby Vision audit (#567) stays out of reach, and docs/api.md now states it in terms of what a host observes rather than the mechanism: that audit opens the source a second time by URL, traffic this probe's budget and cancellation do not police, so an untagged 10-bit HEVC source comes back without a DV record from a controlled probe while an uncontrolled one synthesizes it.

4. sourceBusy waited zero

A controlled HTTP probe took the origin slot with tryAcquire and failed the instant any other request held it, which is the ordinary shape when a host probes several items off one server. It waits until its own deadline instead, since a slot wait is the one wait the deadline watchdog cannot interrupt.

Test plan

  • swift test, real exit code 0: 3298 swift-testing tests in 441 suites and 642 XCTest cases, checked in both halves rather than through a pipe.
  • Scripts/check-doc-links.py clean.
  • Tests that encoded the old contracts were rewritten rather than deleted: malformedNALs and av1Malformed keep every case where nothing was validated (those still fail closed), and the cases where a validated message precedes the damage moved into a new test that pins the new contract. The "found at the deadline is not published" integration test became "found as the deadline passes is still published".
  • New: remainingTime on ProbeControl, the bound the slot wait now uses.

One finding is worth keeping in view: the "missing rbsp_trailing_bits" case is only observable under length-prefixed framing. In Annex B the zero bytes ahead of a start code are stripped, and the payload's own zero padding goes with them, so that case truncates the message instead of damaging what follows it and correctly stays negative.

🤖 Generated with Claude Code

https://claude.ai/code/session_015PM3xUJB6ZQyqnmGK1fp6F

…g it

Follow-up to PR #583, whose validator and whole-probe controls are right in the
core. Four places where a bound meant to limit COST was also deciding what the
probe may report.

The HDR10+ validator discarded a message it had already parsed in full whenever
anything later in the same packet failed the structural walk. Verified against
the scan directly: a valid ST 2094-40 SEI followed by a NAL with its forbidden
bit set, or by an SEI missing rbsp_trailing_bits, came back negative, where the
byte scan this replaced said yes. That is ordinary shape on real media, and it
reaches the playback path through HLSSegmentProducer, not just the probe. The
walks now stop at the damage and report what they validated. Nothing can invent
a positive there: only validT35 ever sets one.

The HDR10+ and Atmos detail passes retracted a detection they had already made
when their soft wall-clock budget expired mid-pass. The caller cannot tell a
withheld confirmation apart from a source that carries none, so an overrun on a
slow origin silently became "no Atmos", on the existing API with no opt-in. An
overrun now retires the pass without taking its answer back. The whole-probe
deadline from #583 is untouched: that one throws, so the caller knows.

Passing `limits` switched the open to the still extractor's profile, so
probe(url:limits:) read 2 MiB / 2 s where probe(url:) reads 50 MiB / 60 s and
could report fewer streams for it. The caller's own limits are the only thing
that should bind: the profile is built from .playback with probesize clamped to
maxInputBytes, and the deadline plus the FFmpeg interrupt callback bound the
analysis. The recordless Dolby Vision audit stays out of reach, and docs/api.md
now says so in terms of what a host observes: that audit opens the source a
second time by URL, outside this probe's budget and cancellation.

A controlled HTTP probe took the origin slot with tryAcquire and failed with
sourceBusy the instant any other request held it, which is the ordinary shape
when a host probes several items off one server. It waits for the slot until its
own deadline instead, since a slot wait is the one wait the watchdog cannot
interrupt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015PM3xUJB6ZQyqnmGK1fp6F
@superuser404notfound
superuser404notfound merged commit 2a5f756 into main Sep 21, 2026
8 of 14 checks passed
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.

1 participant