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.
7.9 KiB
src/drive/mod.rs — internal notes
Long-form rationale relocated from src/drive/mod.rs comments that exceeded
the comment-guard's internal-comment cap. Each section is pointed to by a
short // See docs/drive-mod.md — <topic> comment at its original site.
Error-recovery mode page (PER bit)
MODE_PAGE_ERROR_RECOVERY (0x01) is the SBC/MMC Read-Write Error Recovery
mode page. We flip the PER bit to make the drive REPORT a recovered read
(via CHECK CONDITION + sense key RECOVERED ERROR) instead of silently
returning best-effort data as GOOD status. On marginal/dirty media that
silent-GOOD data can be mis-corrected — a rip that "passed clean" but decoded
with errors. With PER on, freemkv sees the marginal read and re-reads it in
Pass N (a loud miss, never a silent commit). See
build_error_recovery_select_payload.
sleep_until_halted — why it exists and why it was dormant
This was #[cfg(test)] for two releases, kept alive only by its unit tests,
while the two production paths that actually sleep — the wait_ready poll
backoff (60 x 500 ms) and spin_cycle's spin-down/settle pauses
(SPIN_DOWN_IDLE_SECS + SPIN_UP_SETTLE_SECS) — used a plain
std::thread::sleep and so were deaf to the operator's Stop for ~30 s and
~15 s respectively. Every other drive path returns Halted at the next
checked_exec boundary; a Stop pressed during spin-up simply did nothing
visible until the poll ran out. The primitive existed; the sleeping code just
did not call it. It is production code again.
Drive::read — single-shot contract and the removed inline retries
recovery=true uses crate::scsi::READ_RECOVERY_TIMEOUT_MS (60 s, matches
sg_dd) for the freemkv_engine::recovery::patch pass; recovery=false uses
crate::scsi::READ_TIMEOUT_MS (10 s) for freemkv_engine::recovery::copy's
fast skip-forward sweep. Both budgets are generous enough that the drive can
finish ECC recovery on a marginal sector — pre-0.13.21 this was 1.5 s on the
fast path which forced the kernel mid-layer to time out and escalate while we
waited anyway. On any failure this returns Err(DiscRead) immediately;
orchestration (freemkv_engine::recovery::patch multi-pass, DiscStream
adaptive batch halving) handles retry policy.
Inline retry phases (5x gentle + reset+reopen + 5x more) were removed in
0.13.6: on some USB-SATA bridges the inline reset wedged drive firmware
without ever recovering a sector. The remaining recovery layers
(freemkv_engine::recovery::patch multi-pass, DiscStream batch halving) do
not touch the wedge-prone reset path.
find_drive — media-preference selection policy
On a multi-drive system (common on Windows, where an empty/not-ready drive
can enumerate first) returning the first drive blindly can pick a drive with
no disc, dooming the operation. So find_drive opens each candidate in
enumeration order, queries Drive::drive_status (GET EVENT STATUS, which
works regardless of firmware state), and returns the first drive reporting
DriveStatus::DiscPresent. If no drive reports a disc — or drive_status()
is unavailable/returns Unknown everywhere (single-drive or quirky bridges)
— it falls back to the first drive that opened, preserving historical
behavior so those setups don't regress. For just listing drives without
opening (e.g. UI sidebar), use scsi::list_drives() instead — it returns
DriveInfo (path + identity) without the cost of running every drive's
profile + identity probe.
open_block_device_for_sg — why no O_DIRECT
Resolves a /dev/sg* path to the corresponding /dev/sr* block device by
walking sysfs, then opens it for read. No O_DIRECT:
posix_fadvise(POSIX_FADV_DONTNEED) flushes the cache before each pread,
which avoids buffer-alignment requirements while still forcing fresh device
reads. Returns None on any error (sysfs not present, no matching block
device, open failed); callers treat that as "no fallback available" and
propagate the original SCSI READ error.
resolve_device — staged, not yet wired
The cross-platform dispatch is kept ready for the caller that will consume
it, so the per-platform implementations below it (and their tests) stay
live. allow(dead_code) marks that deliberately — this is not an accidental
orphan.
Test rationale: recovery/halt/chunking coverage
The halt_tests and command_tests modules exercise mutation-sensitive
edges (halt-aware sleep timing, chunked-read offset arithmetic, drive-status
byte decoding, spin-cycle/wait-ready halt responsiveness). The following
notes preserve the "why this exact scenario" reasoning that the inline test
doc comments were trimmed from:
-
disc_is_dvd_matches_only_dvd_profile_family: must match the DVD profile family (0x0010..=0x001F) and ONLY that family. A false positive on a BD/UHD profile (0x0040+) would skip the drive unlock UHD reads require; a false negative on a DVD would re-introduce the CSS read failure. Widening the range to..=0x0040makes the BD-ROM assert fire; a failed/short GET CONFIGURATION must default to NOT-DVD so the unlock still runs. -
media_event_reply/ media-status decoding: MMC-6 §6.7 — byte 5 of the GET EVENT STATUS reply is a Media Status only when the Event Header says a media event descriptor follows (NEA clear AND Notification Class == 4). Decoding byte 5 regardless of those preconditions reads a reserved/zero byte as Media Status 0 and reports NoDisc on a drive that has a disc loaded — the classic "works on my drive, not theirs" firmware split. The drive is untrusted input, so an event-less reply must fall back to the TEST UNIT READY status instead. -
Chunked-read tests (
read_chunks_write_into_correctly_offset_buffer_regions,an_undersized_buffer_errors_on_the_chunked_path_just_like_the_single_one,chunk_lba_near_u32_max_errors_not_overflows): the multi-chunk path slices the caller's buffer bycount * 2048and advances the per-chunk LBA withlba + done, both unchecked. An undersized buffer PANICKED ("range end index out of range") out of the publicread/read_fuainstead of returningErr(DiscRead)like the single-chunk path; an LBA range crossingu32::MAXoverflowed (debug panic, or release wrap to a low LBA silently read). These tests pin both to error-not-panic/overflow, and one writes a distinct marker per chunk to confirm the byte-offset arithmetic (done * 2048, notdone + 2048) is correct — no other mock touches the buffer contents, so a*->+//mutation would otherwise go unasserted. -
wait_ready/spin_cyclehalt tests:wait_readywas the one drive path that calledself.scsi.as_mut().execute(..)instead ofchecked_exec, and its 60 x 500 ms loop never readself.halt— a cancel during spin-up was ignored for ~30 s.spin_cyclehad the same gap across two bareexecutecalls and two plainstd::thread::sleeppauses (SPIN_DOWN_IDLE_SECS+SPIN_UP_SETTLE_SECS), and it runs from the recovery path — precisely when an operator is most likely to press Stop. The tests pin: a Stop set before the call returnsHaltedimmediately with no command issued; a Stop set mid-poll (on a deterministic Nth call, no wall-clock race) exits at the nextchecked_execboundary; a Stop that lands during the spin-down pause wakes the halt-aware sleep within ~100 ms instead of blocking for the full 5 s. -
read_logs_a_good_status_short_transfer: a READ(10) that completes with GOOD status but a residual underrun is correctly refused, but the refusal used to log nothing — so a drive that residual-underruns on GOOD status produced the same silent journal as a scratched disc, and the operator was sent after the wrong remedy (replace the drive vs. clean the disc). The test pins that the refusal now warns with lba/transferred/ expected/code.
open_block_device_for_sg doc contract (kept short in code)
See the section above for the O_DIRECT rationale; the pointer comment near
the function keeps only the API contract.