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.
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 realVPLST000.XPLis 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 extraApplicationSegment/ObjectMappingListlayers, 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 MiBVPLST000.XPL(exactlyudf::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.rslearns 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_titlesrunsprobe_evo_streamsonce per title, and each probe reads real sectors, so an uncapped count turns aDisc::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 iscap x EVO_PROBE_SECTORS, sincecompose_xpl_titlesmemoizes 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 siblingMAX_HDDVD_CLIPSpath. 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.evoclips taken from theHVDVD_TS/directory listing.MAX_XPL_TITLESandxpl_depth_within_limitbound theADV_OBJ/*.XPLpath, but a crafted disc simply omits/ADV_OBJ; control then reaches the clip-name fallback inDisc::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, andudf::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 fullEVO_PROBE_SECTORS(16 MiB) read, hundreds of GiB on a scan the operator expects to take seconds. The cap's job is to boundcap x EVO_PROBE_SECTORS; it was 4096 (matchingMAX_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.evoclips, ~10x the real-world maximum, the sizing conventionMAX_XPL_CHAPTERS_PER_TITLEalso uses against the 99-chapter authoring ceiling). Pinned bythe_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 isdescendants(), notchildren(), 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-allocatedXplClips, each with its ownString, and then as manyClips again incompose_xpl_titles. Unlike the title cap, the cost here is MEMORY rather than drive time:compose_xpl_titlesde-duplicates by.evoname before probing (seen_evos), so repeats cost no extra sector reads, but theVecs 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 atMAX_XPL_TITLESx 256 clips rather than the tens of millions above. -
MAX_XPL_CHAPTERS_PER_TITLE(1024) bounds<Chapter>elements per<Title>— the samedescendants()amplification, on the second unboundedcollect()in the same loop, bounded separately so capping clips doesn't simply move the attack next door. A chapter is a baref64rather than aString-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).