mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
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.
220 lines
11 KiB
Markdown
220 lines
11 KiB
Markdown
# `src/mux/ps.rs` — extended notes
|
|
|
|
Relocated prose from doc/inline comments in `ps.rs`, trimmed there to stay
|
|
within the comment-guard's prose caps. Each `//`/`///` pointer in the source
|
|
names the section here it corresponds to.
|
|
|
|
## Stream-ID constant rationale
|
|
|
|
- `EXTENDED_STREAM_ID` (`0xFD`): the H.222.0 escape whereby the real stream id
|
|
is the `stream_id_extension` carried in the PES extension. HD-DVD `.evo`
|
|
puts its VC-1 video (and HD audio) here (a real HD-DVD title: VC-1 on
|
|
`0xFD` ext `0x55`); a transport stream never uses it. The elementary-stream
|
|
bytes follow the PES header exactly like any other PES — only the routing
|
|
key differs.
|
|
- `MAX_PS_BUFFER`: a length-0 (unbounded) video PES is delimited by the next
|
|
PS-layer boundary; if a corrupt stream declares an unbounded PES and never
|
|
follows it with a boundary, `feed()` would otherwise accumulate the entire
|
|
input. Past this cap we force the in-progress unbounded PES to flush at
|
|
the buffer end so untrusted input cannot drive unbounded allocation. A
|
|
real DVD pack/PES is at most a few KB; this leaves generous slack while
|
|
still bounding worst-case memory.
|
|
|
|
## `dvd_audio_pid` rationale
|
|
|
|
The PID is `0xBD00 | sub_stream_id`, which is unique per sub-stream id (AC-3
|
|
/ DTS `0x80..=0x8F`, LPCM `0xA0..=0xA7`). Unlike the old per-codec relative
|
|
arithmetic, distinct sub-ids therefore always yield distinct PIDs — so a
|
|
mixed-codec title (e.g. AC-3 + DTS, whose sub-ids are 0x80 and 0x88) can
|
|
never collide on one PID. This is the single source of truth shared with
|
|
`Disc::scan_dvd_titles` (`src/disc/dvd.rs`), which sets each
|
|
`AudioStream.pid` from the same function so demuxer output routes through
|
|
the title's `pid_to_track`.
|
|
|
|
HD-DVD (`.evo` Enhanced VOB) carries Dolby Digital Plus (E-AC-3) on
|
|
`private_stream_1` sub-stream ids `0xC0..=0xC7` — a range DVD never uses
|
|
(DVD audio is `0x80..=0x8F` / `0xA0..=0xA7`), so admitting it here is purely
|
|
additive and cannot change any DVD mapping. The PID is `0xBD00 | sub` just
|
|
like the DVD audio ranges, so a mixed HD-DVD title (four DD+ tracks
|
|
`0xC0..0xC3`) routes each track to its own distinct PID.
|
|
|
|
## `dvd_pid` rationale
|
|
|
|
Routes by the REAL on-wire `(stream_id, sub_stream_id)` via the shared
|
|
[`dvd_audio_pid`] / [`dvd_subtitle_pid`] tables the scanner also uses —
|
|
never per-codec relative arithmetic, which collided on mixed-codec audio
|
|
(AC-3 0x80 and DTS 0x88 both mapping to 0xBD00).
|
|
|
|
`None` is returned for stream/sub-stream combinations the DVD title scanner
|
|
does not assign a PID to (e.g. MPEG audio 0xC0-0xDF, private stream 2,
|
|
unrecognized sub-stream ranges). The caller is expected to WARN-and-drop in
|
|
that case rather than silently mis-routing the packet.
|
|
|
|
## `PsDemuxer::pending_scan`
|
|
|
|
Boundary-scan cursor for an unbounded (length-0) PES still waiting for its
|
|
terminating PS-layer unit: `(buffer offset of the PES start code, buffer
|
|
offset up to which the search has already proved there is no boundary)`.
|
|
Both are buffer-relative and are rebased when the buffer drains.
|
|
|
|
Without it, every `feed` re-searches the WHOLE accumulated payload from the
|
|
PES header: the buffer only stops growing at `MAX_PS_BUFFER`, so a stream
|
|
that declares an unbounded PES and then never emits a PS-layer start code (a
|
|
corrupt or crafted VOB) makes the demuxer scan up to 4 MiB per call,
|
|
quadratic in the bytes fed. Cleared whenever the PES is emitted, so it can
|
|
never outlive the packet it describes.
|
|
|
|
## `find_ps_boundary`
|
|
|
|
Find the next PS-layer unit boundary at or after `from`: a start code whose
|
|
ID byte is a pack (0xBA), system header (0xBB), program-end (0xB9), or a
|
|
payload-carrying PES stream ID (0xBD..=0xEF).
|
|
|
|
A length-0 (unbounded) video PES must be delimited by the next PS-layer unit
|
|
— NOT by the next raw `00 00 01`. The MPEG-2 video elementary stream inside
|
|
the PES is itself full of `00 00 01 xx` start codes (picture 0x00, slices
|
|
0x01..=0xAF, GOP 0xB8, sequence 0xB3); a plain start-code scan would cut the
|
|
PES inside its own payload and re-scan the discarded video bytes as bogus PS
|
|
units. Restricting the search to PS-layer IDs (>= 0xB9, excluding the video
|
|
ES codes below it) frames the unbounded PES at the right boundary.
|
|
|
|
Returns `(boundary, searched_to)`. `searched_to` is the offset up to which
|
|
every byte has been PROVED not to begin a PS-layer boundary start code, so a
|
|
later call over the same buffer (grown at the tail) may resume there instead
|
|
of re-scanning the payload from the PES header. When the scan runs off the
|
|
end, the last two bytes are NOT proved: a `00 00 01` prefix can straddle the
|
|
next feed's boundary by up to two bytes.
|
|
|
|
## `parse_pts` — 5-byte PTS/DTS layout (ISO/IEC 13818-1 Table 2-17)
|
|
|
|
```text
|
|
byte0: [prefix:4][pts 32..30:3][marker:1]
|
|
byte1: [pts 29..22:8]
|
|
byte2: [pts 21..15:7][marker:1]
|
|
byte3: [pts 14..7:8]
|
|
byte4: [pts 6..0:7][marker:1]
|
|
```
|
|
|
|
## Test: `only_private_stream_2_is_navigation_and_never_a_routable_stream`
|
|
|
|
`is_nav()` exists to separate the ONE unmappable stream a DVD is expected to
|
|
contain — private_stream_2 (0xBF), the PCI/DSI navigation packs (ISO/IEC
|
|
13818-1 Table 2-22) — from every other packet whose `dvd_pid()` comes back
|
|
`None`, which is an unexpected, possibly-lost real stream. The mux loops use
|
|
the distinction to choose between a silent tally and a per-packet WARN, so
|
|
collapsing it to a constant either buries a genuine stream loss in the nav
|
|
tally, or floods the log with one warning per navigation pack on every DVD
|
|
ever ripped.
|
|
|
|
The invariant that ties the two together: `is_nav()` may only ever be true
|
|
where `dvd_pid()` is `None` — a packet that routes to a real track must
|
|
never be silently classified as navigation.
|
|
|
|
## Test: `max_ps_buffer_has_the_documented_value`
|
|
|
|
`MAX_PS_BUFFER` is read by its own tests only through the same symbol, so a
|
|
mutated arithmetic expression in its definition changes what the symbol
|
|
itself evaluates to and every self-referential assertion still passes. Pin
|
|
the compiled value against a literal computed independently.
|
|
|
|
## Test: `pack_header_exact_fit_is_consumed_not_awaited`
|
|
|
|
Pack-header framing (`sc + 14 > len`, then `sc + pack_len > len`) must accept
|
|
an EXACT fit — the whole pack (mandatory 14 bytes, or with stuffing) present
|
|
and not one byte more — rather than waiting for data that will never come.
|
|
Both checks are preceded by an unrelated start code so `sc != 0`: at
|
|
`sc == 0` a `sc + pack_len` vs. `sc * pack_len` mutant collapses to the same
|
|
value (`0`) and the bound stays unreachable from any input.
|
|
|
|
## Test: `system_header_length_high_byte_is_not_dropped`
|
|
|
|
`header_len` is a 16-bit big-endian field (`buffer[sc+4] << 8 |
|
|
buffer[sc+5]`). A `<<` -> `>>` mutation collapses the high byte to zero, so
|
|
any `header_length > 255` is misread as just its low byte — here 300
|
|
(`0x012C`) misread as 44. A start code embedded 50 bytes in (well inside the
|
|
true 306-byte unit but exactly where the mis-parsed 50-byte unit would end)
|
|
must stay buried in the skipped body under correct parsing, and surface as a
|
|
bogus extra PES under the mutant.
|
|
|
|
## Test: `find_ps_boundary_handles_a_bare_start_code_at_the_buffer_head`
|
|
|
|
`find_ps_boundary`'s bounds check (`sc + 3 >= data.len()`) must stay an
|
|
ADDITION: a `+` -> `-` mutation at `sc == 0` underflows the `usize`
|
|
subtraction and panics on a plain 3-byte start code with nothing after it —
|
|
exactly the tail a real feed can end on.
|
|
|
|
## Test: `an_unterminated_pes_is_not_rescanned_from_its_header_every_feed`
|
|
|
|
An unbounded (length-0) PES is terminated by the next PS-LAYER unit, and
|
|
until one arrives the payload accumulates in the buffer. The search for that
|
|
unit must not restart at the PES header on every feed: the buffer only stops
|
|
growing at `MAX_PS_BUFFER` (4 MiB), and a feed is one read batch (at most
|
|
510 sectors ≈ 1 MiB, 60 sectors ≈ 120 KiB on an optical drive), so
|
|
re-scanning from byte 0 costs work quadratic in the bytes fed — up to 4 MiB
|
|
of scanning per call, for as long as a corrupt or crafted VOB withholds the
|
|
boundary. A conformant DVD ends every pack within 2048 bytes and never
|
|
reaches this state.
|
|
|
|
Measured directly, because a work bound has no packet-level shadow:
|
|
`boundary_bytes_scanned` counts the bytes `find_ps_boundary` examines. 256
|
|
chunks x 4 KiB of boundary-free payload is 1 MiB of input; re-scanning from
|
|
the header on every call examines 4 KiB * 256*257/2 = ~128 MiB.
|
|
|
|
Mutation: drop the `Some((pes_at, searched_to)) if pes_at == sc` arm so
|
|
`from` is always `sc + 4`.
|
|
|
|
## Test: `a_resume_cursor_survives_the_drain_of_units_ahead_of_the_unbounded_pes`
|
|
|
|
The resume cursor is a BUFFER offset, so it must be rebased when the buffer
|
|
drains — and this is the only test in which a drain actually happens while a
|
|
cursor is live.
|
|
|
|
Mutations this catches, both halves of
|
|
`self.pending_scan.map(|(pes_at, searched_to)| (pes_at - pos, searched_to - pos))`
|
|
in `extract_packets`:
|
|
* `searched_to - pos` -> `searched_to`: the next call resumes `pos` bytes
|
|
PAST where the previous scan actually stopped, so that window is never
|
|
examined. Here the terminating pack header lands inside it and is missed
|
|
outright — the unbounded PES runs on past its real end, swallowing the
|
|
following unit. That is a CORRECTNESS failure, not a slow path, and the
|
|
assertion on the emitted packet catches it.
|
|
* `pes_at - pos` -> `pes_at`: the stale offset no longer equals the PES's
|
|
post-drain `sc`, the resume arm stops matching and the search restarts
|
|
at the PES header. Caught by `boundary_bytes_scanned`, which is why the
|
|
fixture puts 64 KiB of payload in the SAME chunk that opens the PES:
|
|
that is exactly the span a restart re-examines, so the mutant roughly
|
|
doubles the bytes scanned.
|
|
|
|
`an_unterminated_pes_is_not_rescanned_from_its_header_every_feed` cannot
|
|
reach either: it opens the unbounded PES as the very FIRST bytes of the very
|
|
first feed, so nothing ever drains ahead of it, `pos` stays 0 and the
|
|
subtraction is a no-op. The comment above it asserts neither component can
|
|
underflow; nothing exercised the arithmetic at all.
|
|
|
|
So this fixture puts COMPLETE PS units — a pack header and a length-bounded
|
|
PES — ahead of the unbounded video PES *in the same chunk*. The loop
|
|
consumes them, breaks on the unbounded PES, and drains `pos` bytes with
|
|
`pending_scan` live: exactly the real DVD shape, where a video PES opens
|
|
partway through a read batch.
|
|
|
|
## Test: `parse_stream_id_extension_walks_every_optional_field_to_the_right_offset`
|
|
|
|
`parse_stream_id_extension` walks every optional PES-header field (PTS/DTS,
|
|
ESCR, ES_rate, DSM_trick_mode, additional_copy_info, PES_CRC) and every
|
|
optional PES_extension sub-field (PES_private_data, pack_header_field,
|
|
program_packet_sequence_counter, P-STD_buffer) before reaching
|
|
`stream_id_extension`. Every one of those skips is a `pos +=`; a single
|
|
mutated increment (`-=`/`*=`) misaligns every read after it. This test arms
|
|
EVERY optional field at once with a known byte count, so any single wrong
|
|
skip anywhere in the chain lands on the wrong byte and the assertion fails —
|
|
one test proving the whole walk, rather than one per field.
|
|
|
|
## Test: `length_bounded_pes_exact_fit_is_emitted_not_awaited`
|
|
|
|
A length-bounded PES (`pes_packet_len != 0`) must be emitted the moment its
|
|
declared length is EXACTLY satisfied by the buffer (`sc + 6 > len`, then
|
|
`e = sc + 6 + pes_packet_len; e > len`), not held back waiting for a byte
|
|
that will never arrive. Feed nothing after the packet and don't flush — if
|
|
the boundary checks were `>=` instead of `>`, an exact fit would incorrectly
|
|
be treated as "not enough data yet" and the packet would never be produced.
|