Files
libfreemkv/docs/hddvd.md
T
Matthew Jackson 6e14fc0c0c libfreemkv: 1.7.0 groundwork — sector-recycle soundness + CI hardening
Security
- Remove the unsafe Vec::set_len over recycled sector buffers — UB reachable
  from safe code (GHSA-j8ww-f5fg-9pmh, low). Uses Vec::resize; pooling unchanged.

CI / tooling
- clippy on stable with the MSRV single-sourced from Cargo.toml; cargo-deny
  dependency-audit gate with first-party git sources allowed (#15); audience-based
  comment-guard (#17).

Docs
- README (keydb ownership, BD-J parser count), drive-access transport/reset
  notes, mux timeline module-doc links.
2026-09-02 12:40:55 -07:00

28 KiB

HD-DVD title scanning (src/disc/hddvd.rs)

HD-DVD is a tree-level peer of DVD and Blu-ray (not a stream variant like FMTS): its content lives in HVDVD_TS/ as .evo clips — Enhanced VOB, an MPEG program stream — each with a small .map timemap sidecar, navigated by .xpl/.ifo playlists in HVDVD_TS/ and ADV_OBJ/. Because it is a different tree with a different playlist format, it gets its OWN scanner (this file), a peer to Disc::scan_bluray_titles — the two-format design rule: a genuinely different format is a new enumerator, not an extension bolted into the BD path.

Title composition (authoritative path)

Title composition is authoritative, from the Advanced-Content playlist. HD-DVD ships a real player playlist at ADV_OBJ/VPLST000.XPL (DVD-Forum HDDVDVideo/Playlist XML). The scanner parses it (with a real XML parser, roxmltree) into one DiscTitle per <Title>: its <PrimaryAudioVideoClip> clips in playback order (each an EVO, referenced via its .MAP sidecar), the titleDuration, the displayName, and the <ChapterList>. A layer-break split (FEATURE_1 + FEATURE_2, or feature/feature_Divide) is composed into ONE title with the two parts as clips, carrying each clip's title-time in/out points (45 kHz ticks) so a seamless join can be spliced onto one timeline. Container is ContentFormat::MpegPs, so the existing PS mux path handles it. Per-clip streams are enumerated by demuxing the clip head and building one Stream per distinct elementary stream (video + DD+ audio), codec sniffed from the ES bytes.

Fallback path

When no playlist is present (or it fails to parse), the scanner falls back to the older clip-name heuristic: parse the HVA*.VTI clip table, join the feature*-named clips into one title, and emit every other clip on its own.

Known gaps

Not parsed yet: subtitles (8-bit RLC on 0xBD sub 0x20..=0x3F) and per-track audio languages (the XPL carries <Audio description=...> but they are not yet wired onto the streams). Extents and size are real (the ripper images the clips).

VTI parsing

parse_vti_clip_order parses the ADVANCED-VTS VTI clip-name table. The table is a run of VTI_CLIP_ENTRY_STRIDE-spaced records, each carrying a NUL-terminated <name>.EVO. Rather than trust the (imprecise) header pointer, it collects every NUL-terminated *.EVO name and keeps the largest group sharing one residue modulo the stride — the clip table — in offset order. Returns empty for a non-VTI blob or one with no recognizable table.

Codec sniffing

sniff_video_codec reads MPEG / Annex-B start codes: 00 00 01 B3 → MPEG-2 (sequence_header), 00 00 01 0F → VC-1 (BD/HD-DVD sequence-header BDU), 00 00 01 [x7] H.264 SPS NAL (type 7, forbidden_zero_bit clear) → H.264. Returns None when no recognizable start code is present. The scan prefers the unambiguous MPEG-2 / VC-1 sequence headers; H.264 is inferred from an SPS NAL so a stray slice/picture code can't be mistaken for a different codec.

sniff_audio_codec recognizes only Dolby Digital Plus (E-AC-3) today, via its 0x0B77 syncword — what real retail HD-DVD titles carry on sub-ids 0xC0..=0xC7. Returns None for an unrecognized sample so the caller drops the stream rather than mislabeling it.

probe_evo_streams

Demuxes the head of an .evo clip (through the disc's SectorSource) and builds one Stream per distinct elementary stream found: the video track (mapped to the canonical DVD_VIDEO_PID for plain 0xE0-0xEF video, or the extended-stream-id PID for HD-DVD VC-1) and every DD+ audio sub-stream (mapped via dvd_audio_pid). Codec is sniffed from the demuxed ES bytes.

Mirrors the stream construction in Disc::scan_dvd_titles; resolution / language / channels use sane HD-DVD defaults (the muxer reads the true pixel dimensions from the H.264 SPS, and E-AC-3 channel counts are not decoded here). Returns an empty vec when the clip cannot be read or carries no recognizable stream (e.g. an AACS-encrypted clip probed as ciphertext).

Cancellation is checked before every chunk read and is the ONE condition that returns Err rather than an empty vec: a probe cut short by a Stop has not established that the clip carries no streams, and reporting it as if it had would enumerate a stream-less title as a scanned fact. A read that fails for any other reason keeps the existing best-effort behaviour.

collect_es

VC-1 video rides extended-stream-id 0xFD with stream_id_extension 0x55. HD audio (MLP/TrueHD) can also use 0xFD with other extensions — routing those to their own audio tracks is deferred (see the HD-DVD program-chain follow-up); until then only the VC-1 extension is treated as video, so an audio 0xFD sub-stream can never mis-stamp the video PID.

XPL amplification bounds

The XPL parser guards against a crafted ADV_OBJ/VPLST000.XPL amplifying a small file into an enormous amount of work or memory. Four separate axes are bounded, each independently, because they are separate attacks:

  • MAX_XPL_DEPTH (32) bounds element NESTING. A real VPLST000.XPL is about six levels deep at its deepest (Playlist/TitleSet/Title/PrimaryAudioVideoClip/Video, or .../ChapterList/Chapter); 32 leaves a 5x margin for authoring tools that wrap extra ApplicationSegment/ObjectMappingList layers, while staying trivially stack-safe.

  • MAX_XPL_TITLES (512) bounds how many <Title> elements one playlist may declare — BREADTH, separate from the depth guard: a flat playlist passes the depth check trivially and can still declare a title per 51 bytes, so a 64 MiB VPLST000.XPL (exactly udf::MAX_FILE_BYTES) reaches ~1.3 million of them. This matters here and not on the Blu-ray side because of where the count comes from: bluray.rs learns its playlists from the MPLS files actually present in the UDF directory, so the medium bounds it — an XPL DECLARES its titles, so nothing does. The cost is drive time, not memory: compose_xpl_titles runs probe_evo_streams once per title, and each probe reads real sectors, so an uncapped count turns a Disc::scan() the operator expects to take seconds into tens of terabytes of optical reads on a disc that is only pretending to be large. The bound that matters is cap x EVO_PROBE_SECTORS, since compose_xpl_titles memoizes on the title's RESOLVED extent list, and distinct titles built from different combinations of the same clips have different lists, so each one misses the memo — at 4096 that product was 64 GiB of optical reads, the same worst case, and the same contradiction with "seconds", as the sibling MAX_HDDVD_CLIPS path. 512 keeps it at 8 GiB and remains far above any real disc (retail HD-DVDs carry tens of titles).

  • MAX_HDDVD_CLIPS (512) caps .evo clips taken from the HVDVD_TS/ directory listing. MAX_XPL_TITLES and xpl_depth_within_limit bound the ADV_OBJ/*.XPL path, but a crafted disc simply omits /ADV_OBJ; control then reaches the clip-name fallback in Disc::scan_hddvd_titles, which emits one title per directory entry and probes each — the same unbounded- probe cost, reached without the playlist. The directory is the only bound the medium supplies, and udf::MAX_DIR_BYTES (1 MiB) still leaves room for ~24,000 FIDs. Memoizing the probe (EvoProbeCache) collapses entries that resolve to the SAME extents, closing the many-names-one-File-Entry case — but not this one: a File Entry is a single sector, so an attacker can give every FID its own, each declaring its own extent, for ~48 MiB of image; every probe then misses the memo and costs a full EVO_PROBE_SECTORS (16 MiB) read, hundreds of GiB on a scan the operator expects to take seconds. The cap's job is to bound cap x EVO_PROBE_SECTORS; it was 4096 (matching MAX_XPL_TITLES, which bounds a different, no-per-item-read-cost path) — 4096 x 16 MiB is 64 GiB, more than dual-layer HD-DVD media physically holds (reachable anyway, since distinct extent lists may overlap on the medium). 512 keeps the product at 8 GiB and still leaves a wide margin over any real disc (retail HD-DVDs carry TENS of .evo clips, ~10x the real-world maximum, the sizing convention MAX_XPL_CHAPTERS_PER_TITLE also uses against the 99-chapter authoring ceiling). Pinned by the_clip_cap_and_probe_budget_bound_a_scans_worst_case_read_volume, which asserts the PRODUCT rather than either constant, so raising one without re-examining the other fails.

  • MAX_XPL_CLIPS_PER_TITLE (256) bounds how many <PrimaryAudioVideoClip> elements a SINGLE <Title> may contribute — the fourth amplification axis, the one the other three guards leave open. A 64 MiB XPL holds ~1.7 million <PrimaryAudioVideoClip src="A.EVO"/> elements at ~38 bytes each, and the collector is descendants(), not children(), so a clip nested inside N ancestor <Title> elements is collected by EVERY one of them — the depth cap still permits 32 such ancestors (measured: a bare <Title> root nests 32 deep and still parses, because the guard counts the self-closing clip element as non-nesting). The product is ~1.7M x 32 ≈ 56 million heap-allocated XplClips, each with its own String, and then as many Clips again in compose_xpl_titles. Unlike the title cap, the cost here is MEMORY rather than drive time: compose_xpl_titles de-duplicates by .evo name before probing (seen_evos), so repeats cost no extra sector reads, but the Vecs are built and held regardless. 256 is far above any real disc — a feature is one clip, or two across a layer break; even seamless-branching/multi-angle is tens — and keeps the aggregate bounded at MAX_XPL_TITLES x 256 clips rather than the tens of millions above.

  • MAX_XPL_CHAPTERS_PER_TITLE (1024) bounds <Chapter> elements per <Title> — the same descendants() amplification, on the second unbounded collect() in the same loop, bounded separately so capping clips doesn't simply move the attack next door. A chapter is a bare f64 rather than a String-carrying struct so it's cheaper, but a <Chapter titleTimeBegin="…"/> element is also smaller, so the element count an XPL can reach is comparable. 1024 is far above any real disc: the DVD/HD-DVD authoring convention tops out at 99 chapters per title, a 10x margin.

scan_hddvd_titles

Scans HD-DVD titles from the HVDVD_TS/ .evo clips. The main feature is authored as one or more .evo clips (a layer-break split — FEATURE_1/FEATURE_2 or feature/feature_Divide). The HVA*.VTI navigation file names every clip in authored order; this parses it to concatenate the feature clips into ONE title (so the largest-title pick gets the whole movie, not just part 1), emitting every other clip as its own title. Falls back to one title per clip when the VTI is absent or unparseable, so a disc with no readable navigation still enumerates. chapters/duration are left empty pending deeper VTI parsing.

Cancellation: halt is polled once per clip and again before every probe chunk, and a read that fails with Error::Halted — how a live drive reports a Stop, since Drive::checked_exec fails every command once its flag is set — is propagated rather than swallowed. Halted is the only error this returns; every other read failure keeps its best-effort behaviour. It has to be an error and not a short title list: a cancelled enumeration that returned Ok would be indistinguishable from a disc that genuinely holds fewer titles, and the caller would cache and act on it.

xpl_depth_within_limit

Rejects a disc-supplied XML blob whose element nesting exceeds MAX_XPL_DEPTH, BEFORE it reaches the parser. roxmltree is recursive-descent and its depth-10 limit applies only to ENTITY expansion, so element nesting is unbounded: a few hundred KB of well-formed XML (far under the read cap) overflows the thread stack. That is a process ABORT, not an Err and not an unwind, so neither the Document::parse fallback nor catch_unwind can contain it; the only fix is to not hand the document over at all. A byte-size cap is deliberately NOT added: file size is already bounded by the UDF read path, and a size cap does not close this class (a 180 KB document already aborts) — depth is the bound that matters.

This is a conservative scan, not a validator: it tracks <name …> / </name> while skipping comments, CDATA, processing instructions and declarations, and treats <… /> as non-nesting. A miscount can only cost a pathological document its fast path, and the failure mode is the existing one — fall back to the clip-name heuristic.

Test rationale

Detailed rationale for tests in src/disc/hddvd.rs's #[cfg(test)] module, kept here so the in-code doc can stay within the internal-comment cap.

scan_hddvd_feature_composition_saturates_absurd_disc_declared_sizes — The SECOND size accumulator. compose_xpl_titles sums an XPL's clip sizes; this one sums the composed FEATURE title's parts, and it reads the same disc-declared u64 Information Length from the same clip_extents map. A split feature whose two parts each declare a size near u64::MAX overflows it exactly as the XPL path did. Nothing reconciles the declared size against the extents that back it — here each part declares u64::MAX while occupying ten sectors — because the allocation descriptor's length field is only 32 bits wide, so the disc can always claim more than it holds. Mutation: restore size_bytes += *size in the feature-composition path and this goes red (debug: attempt to add with overflow).

scan_hddvd_does_not_compose_a_feature_over_an_unrecorded_part — A split feature (FEATURE_1 + FEATURE_2) where ONE part carries an unrecorded extent must not be composed into a FEATURE title at all. Composing around the unusable part is the dangerous outcome: the title would play through as if it were the whole movie while silently missing that part's runtime, and its size still counts it. The other clips remain available as their own titles — refusing the composition is not the same as refusing the disc.

scan_hddvd_logs_an_unrecorded_feature_part_with_its_own_code — The refusal above must also be ACCOUNTED, with the unrecorded extent's own code. This warn site carried a hardcoded code = 6017 literal — in the very file whose sibling arm was changed to stop doing exactly that — and nothing tested it, so neither the literal nor its absence broke anything. Refusing a feature and saying nothing leaves the operator with a disc that scanned "fine" and is quietly missing its main title. Mutations: deleting the tracing::warn! (no event to find); restoring the literal 6017 still passes here BY VALUE, which is the point — what is pinned is E_UDF_UNRECORDED_EXTENT's value reaching the log, so a renumbering that the literal could not follow goes red; logging a neighbouring code (e.g. E_UDF_NO_USABLE_EXTENT) goes red immediately.

scan_hddvd_does_not_compose_a_feature_over_a_part_with_no_usable_extent — The Ok-but-empty twin of the test above, and the one that actually bites on an ordinary disc: a feature part whose file_extents call SUCCEEDS and yields nothing usable — here a zero-byte FEATURE_2.EVO, no crafted ICB required — must refuse the composition too, and must say so in the log. Before this, the empty-Ok clip entered neither clip_extents nor unusable, so the any(|n| unusable.contains(..)) guard missed it and the filter(|n| clip_extents.contains_key(..)) beside it quietly DELETED the part: the scan composed a FEATURE title out of part one alone, still advertising itself as the feature, at rc=0, with no diagnostic anywhere. Half a movie presented as a whole one — strictly worse than the Err case the guard was built for, and reachable without a hostile disc. Mutations this catches, all of which the pre-fix code exhibited: dropping the extents.is_empty() branch entirely (a FEATURE title appears with one extent); inserting into clip_extents anyway (same); marking the clip unusable but NOT logging (the log assertion fails — absence of a log is itself the defect); logging the neighbouring E_UDF_UNRECORDED_EXTENT instead of E_UDF_NO_USABLE_EXTENT (the code assertion fails, because a zero-length file is not an authoring hole and triaging the two together sends whoever reads it at the wrong population).

parse_xpl_titles_caps_a_playlist_declaring_absurdly_many_titles — A disc-supplied playlist nested far deeper than any real one must be REFUSED BEFORE the XML parser sees it. roxmltree is recursive-descent and its depth-10 limit covers only ENTITY expansion, so unbounded element nesting blows the thread stack — an ABORT, which no catch_unwind and no Err fallback can contain. ~50k levels is a few hundred KB of well-formed XML, far under the read cap. The correct outcome is the same as any other unusable playlist: an empty Vec, so the caller falls back to the clip-name heuristic. Separately, this test also exercises BREADTH, not depth: a flat playlist passes the nesting guard trivially and can still declare a title per ~51 bytes, so a 64 MiB XPL (exactly udf::MAX_FILE_BYTES) reaches ~1.3 million of them. The cost is drive time, not memory: compose_xpl_titles runs probe_evo_streams once per title and each probe reads real sectors, so an uncapped count turns a scan the operator expects to take seconds into tens of terabytes of optical reads. bluray.rs is not exposed to this because its playlist count comes from the MPLS files present in the UDF directory; an XPL DECLARES its titles, so nothing bounds them. Mutation: delete the .take(MAX_XPL_TITLES) and this goes red at the declared count.

nested_title_xpl — Builds a playlist that nests nesting <Title> elements inside one another and puts clips self-closing <PrimaryAudioVideoClip> elements at the innermost level, plus chapters <Chapter> elements. parse_xpl_titles collects a title's clips with descendants(), not children(), so EVERY one of the nesting ancestors collects the SAME innermost clip list — the amplification this exercises.

parse_xpl_titles_caps_clips_per_title_against_descendant_amplification — CLIPS PER TITLE is the axis the title cap, the depth cap and the directory cap all leave open. MAX_XPL_TITLES bounds how many titles a playlist may declare and MAX_XPL_DEPTH bounds how deeply it may nest, but NEITHER bounds how many <PrimaryAudioVideoClip> elements a single title collects. Worse, the collector is descendants(), so one clip nested inside N ancestor <Title>s is collected N times over — the clip count MULTIPLIES by the nesting the depth guard still permits. A 64 MiB XPL (udf::MAX_FILE_BYTES) holds ~1.7M clip elements at ~38 bytes each; with the 32 <Title> ancestors the depth cap still permits, that is ~56 million heap-allocated XplClips, and then as many Clips again in compose_xpl_titles. This test uses small numbers that exercise the same multiplication without a 64 MiB fixture: pre-fix it collected 756 clips per title, 18,900 across the 25 nested titles, from a document declaring only 756 clip elements — an exact 25x, the nesting count. Mutation: delete the clips.len() >= MAX_XPL_CLIPS_PER_TITLE break and this goes red at the unamplified per-title count.

parse_xpl_titles_caps_chapters_per_title — CHAPTERS PER TITLE is the same descendants() amplification on the second unbounded collect() in the same loop. Bounded separately so closing the clip axis does not simply move the attack next door. Mutation: delete the .take(MAX_XPL_CHAPTERS_PER_TITLE) and this goes red at the declared count.

parse_xpl_titles_keeps_every_clip_of_a_realistic_title — The control: a realistic multi-clip title still resolves EVERY one of its clips and chapters. SYNTH_XPL's main movie is a layer-break split — two clips on one timeline — plus two chapters; losing either to the cap would cost a genuine disc half its feature. Mutation: set MAX_XPL_CLIPS_PER_TITLE to 1 and this goes red.

parse_xpl_titles_accepts_real_world_nesting_depth — The depth guard must not reject real discs. A genuine VPLST000.XPL is a handful of levels deep; this is the same document the parsing test uses, plus self-closing tags, a comment and a processing instruction that the pre-parse scanner has to account for without inflating its depth count.

scan_hddvd_composes_titles_from_xpl_playlist — The authoritative path: when a VPLST000.XPL is present, titles come from the playlist — the layer-break split (FEATURE_1 + FEATURE_2) is composed into ONE title with the real duration, name, chapters, and per-clip title-time offsets — not the clip-name heuristic.

compose_xpl_titles_saturates_absurd_disc_declared_clip_sizes — A title's size_bytes is a running sum of DISC-DECLARED clip sizes: the u64 UDF File Entry Information Length, which nothing cross-checks against the extent list that file actually occupies. Two clips can therefore each declare a size near u64::MAX. Summed with a plain +=, that overflows: a panic in a debug build — a crash on untrusted disc content, which this library must never do — and a wrap to a small garbage total in release, which misreports the title's size while looking entirely normal. Mutation: restore size_bytes += *size in compose_xpl_titles and this goes red (debug: attempt to add with overflow).

compose_xpl_titles_dedups_repeated_clip_references_by_evo — A crafted Advanced-Content playlist can name the SAME .evo in <PrimaryAudioVideoClip> many times over (the XML has no fixed element count, unlike MPLS's binary PlayItem count). Each repeat must NOT push another copy of that clip's extent list onto the title — mirroring bluray.rs's first_ref = seen_clips.insert(...) gate, which pushes a clip's extents only the first time its id is seen. Here the analogous key is the .evo filename (this file has no clip_id; .evo is what clip_extents is keyed by). Without the gate, extents grows by the referenced clip's full extent list on EVERY repetition — unbounded by the number of on-disc files, bounded only by playlist size.

scan_hddvd_titles_drops_a_split_feature_whose_part_cannot_be_read — A clip whose extents cannot be resolved must drop the SPLIT FEATURE. Catches reverting the Err(e) => { warn; unusable.insert(..) } arm in Disc::scan_hddvd_titles back to the bare Err(_) => {} it replaced. RED BEFORE GREEN, and TWO earlier attempts at this test did NOT go red. The first asserted "no title names the broken clip" — that passes either way, because a clip that resolves to no extents is never inserted into clip_extents and so yields no per-clip title regardless. The second fixed that but shipped no .vti: order is built solely from parse_vti_clip_order of the navigation file, so with no .vti it is EMPTY, feature is empty, and no composed title is ever built — the assertion held vacuously with the fix reverted. Hence the synthetic HVA00001.VTI fixture: it is what makes the composer run at all. The defect lives one level up from the per-clip titles, in the composed feature: unusable is what tells the composer that a part is MISSING rather than merely absent, and the old bare Err(_) => {} populated it for nothing but an unrecorded extent. So a scratched sector under FEATURE_2's ICB (Error::DiscRead) left FEATURE_1 composing a title named "FEATURE" by itself — half a movie offered as the whole one.

scan_hddvd_titles_excludes_a_clip_with_zero_sectors — A clip whose file has a zero-byte size (a degenerate/empty allocation: its ICB's allocation descriptor has data_len == 0, the UDF AD-list terminator, so file_extents yields no extent at all) must not produce a title. NOTE: this exercises the upstream data_len == 0 terminator path in crate::udf::UdfFs::file_extents, not the sectors > 0 && lba > 0 guard in scan_hddvd_titles itself — with this fixture (file_extents never returns a (lba, 0) tuple, and PART_START unconditionally makes every resolved lba positive) that guard is unreachable in a divergent way; kept here as a regression check on the zero-byte-file behavior in its own right.

scan_hddvd_titles_probes_once_for_entries_resolving_to_identical_extents — THE THIRD AMPLIFICATION AXIS on HD-DVD title composition, past both existing guards. xpl_depth_within_limit bounds XML NESTING; MAX_XPL_TITLES + the .evo de-dup bound what an XPL can DECLARE. Both live on the playlist path — and an attacker simply omits /ADV_OBJ, so read_adv_obj_xpl returns None and neither guard is ever consulted. Control then reaches the clip-name FALLBACK, which emits one title per .evo DIRECTORY ENTRY and probes each. Nothing de-duplicates a FID's ICB LBA, so every name in a 1 MiB directory (udf::MAX_DIR_BYTES, ~24,000 FIDs) can point at ONE File Entry. Each resolves to the SAME extents and each costs a full EVO_PROBE_SECTORS (16 MiB) probe: ~375 GiB of optical reads from a directory that describes a single file. Memoizing the probe on the resolved extent list collapses that to ONE probe, because all of those names resolve to one extent list. Mutation: drop the memo lookup in EvoProbeCache::streams (always probe) and this goes red at ENTRIES probes.

the_clip_cap_and_probe_budget_bound_a_scans_worst_case_read_volume — The clip cap and the per-probe read budget are ONE bound, not two, and only their product is meaningful: MAX_HDDVD_CLIPS probes each cost up to EVO_PROBE_SECTORS, so the pair fixes the worst-case drive time of a Disc::scan(). EvoProbeCache does not help against a crafted disc here: it keys on the RESOLVED extent list, and distinct-but-overlapping extent lists all reading one physical region miss the memo every time while every read succeeds. So the product is genuinely reachable, and it is the number that has to stay within what a scan can absorb. 8 GiB is the ceiling asserted here. It is far above any genuine disc — retail HD-DVDs carry tens of .evo clips, i.e. well under 1 GiB of probing — and far below the 64 GiB the pair used to permit, which exceeded the capacity of the dual-layer media this format tops out at and took tens of minutes of optical reads. This is a relationship, not a magic number: raising EITHER constant without re-examining the other trips it.

scan_hddvd_titles_still_probes_each_distinct_clip — CONTROL: memoization must not silently collapse DISTINCT clips. A legitimate disc with several different .evo files still probes each one, and each title keeps the streams of ITS OWN clip. Mutation: make EvoProbeCache's key a constant (ignore the extents) and this goes red — the junk clip inherits the first clip's H.264 streams.

scan_hddvd_titles_caps_a_directory_declaring_absurdly_many_clips — Memoization alone is NOT the whole fix. Collapsing identical extent lists bounds the one-File-Entry attack, but distinct extent lists are only as bounded as the File Entries an attacker cares to lay down — and a File Entry is ONE sector. A 1 MiB directory's ~24,000 FIDs can each point at their own 1-sector File Entry (a ~48 MiB image) declaring its own huge extent, so every probe misses the memo and the amplification is back. MAX_HDDVD_CLIPS is the bound that closes that. Mutation: drop the clips.len() < MAX_HDDVD_CLIPS gate in the directory scan and this goes red at ENTRIES.

EvoProbeCache

EvoProbeCache memoizes probe_evo_streams, keyed on the RESOLVED extent list. Probing is the expensive half of title composition: each pass reads up to EVO_PROBE_SECTORS (16 MiB) off the medium. Both composition paths can reach the same physical clip many times over — an XPL naming one .evo from many <Title>s, or a directory whose FIDs all point at one File Entry — and a probe is a pure function of the extents it reads, so the second and later passes over an identical extent list are re-reads of bytes already seen. The key is the extent list itself, NOT the clip name or title: two names for one File Entry share a key (one probe), while two genuinely different clips have different start_lbas and so keep their own probes and streams. Cache size is bounded by the caller's title/clip cap (MAX_XPL_TITLES / MAX_HDDVD_CLIPS).