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.
6.9 KiB
src/scsi/mod.rs — relocated internal notes
Overflow detail for internal (non-public) comments in src/scsi/mod.rs,
moved here to keep the guard's internal-comment cap. Each section is
pointed to by a short // See docs/scsi-mod.md — ... comment at its
original site.
READ_TIMEOUT_MS calibration
10 s is calibrated from live empirical data on an LG BU40N + Initio 1618L bridge ripping a UHD with marginal sectors:
- Sustained sequential reads: 3–7 ms
- Cold-start seek + read: up to ~1500 ms
- Successful ECC recovery: 1.6–2.6 sec
- Confirmed unreadable sector: 3.6–8.8 sec (kernel timeout)
10 s catches every legitimate slow read with comfortable margin and short-circuits truly bad sectors at ~10 s rather than letting the kernel mid-layer escalate for 30 s+.
Pre-0.13.21 this was 1.5 s, which forced the kernel mid-layer to time out normal reads (cold-start often takes ~1.5 s) and run its full ABORT TASK / LUN RESET / BUS RESET escalation while userspace kept submitting fresh reads. The Initio bridge couldn't drain the resulting command queue and entered a wedge state that only physical replug recovered — proven by the v0.13.18 + v0.13.20 live tests.
Consumed only by the ripping read path (drive::read); gated so a
transport-only build doesn't warn on an unused const.
READ_RECOVERY_TIMEOUT_MS history
Matches sg_dd's 60 s ceiling: long enough that any sector the drive can
recover at all gets the time to do so, short enough that an unresponsive
bus is detected before the per-range watchdog fires.
In practice failed reads return in 1–4 s (the drive itself gives up on uncorrectable ECC before the timeout); the 60 s value is a safety ceiling, not a steady-state cost.
Historical note (2026-05-08): briefly lowered to 2 s with a 5x inline
retry loop in freemkv_engine::recovery::patch to mimic the kernel
sr_mod driver's auto-retry pattern. The synthetic logic worked but on
the live drive each "2 s" read paid ~1.5 s of kernel SCSI mid-layer
error escalation on top, so 5x retries took ~17 s per LBA and triggered
MAX_RANGE_SECS after 4 sectors — pushing recovery to 0/22 ranges (worse
than the 0/22 baseline of v0.17.3 single-shot at 60 s, since that at
least visited every range). Reverted; the kernel-auto-retry approach is
being pursued via a /dev/sr0 pread fallback instead.
checked_cdb_len rationale
Under SPC-4 the opcode's group code (bits 7-5 of byte 0) fixes the CDB length, so dropping the tail bytes does not produce a shorter form of the same command — it produces a DIFFERENT command with a different meaning for the bytes that remain. The drive will usually execute it and return GOOD status with data for a request the caller never made: a silently wrong result, on the transport layer everything else in the crate sits on.
Lives here, shared by all three platform backends, so the guard cannot drift per platform (it previously truncated on Linux and Windows while erroring on macOS — the "works on my platform, not theirs" class).
An EMPTY CDB is rejected here too. ScsiTransport is a public trait, so
an out-of-crate caller can pass one; every backend then either indexes
cdb[0] (a panic out of a public API) or hands the driver a zero-length
command descriptor, which under SPC-4 is not a command at all. That
guard used to exist ONLY in the Linux backend — macOS and Windows had
nothing — which is the same per-platform drift this helper exists to
prevent.
parse_sense format details
Handles both response-code formats SPC-4 mandates:
- Descriptor format (response code
0x72/0x73):- sense key =
sense[1] & 0x0F - asc =
sense[2] - ascq =
sense[3]
- sense key =
- Fixed format (response code
0x70/0x71and any unknown code per SPC-4 §4.5.3):- sense key =
sense[2] & 0x0F - asc =
sense[12] - ascq =
sense[13]
- sense key =
sb_len_wr is the number of bytes the transport actually wrote into
sense. When the buffer is too short for the relevant fields we return
[ScsiSense::NONE] for the missing pieces rather than reading
uninitialised memory. The minimum useful sense reply is 4 bytes
(descriptor, to reach ASCQ at offset 3) or 14 bytes (fixed, to reach
ASC/ASCQ at offsets 12/13).
Pure function — same parse on every platform backend (Linux SG_IO, macOS IOKit, Windows SPTI) so a regression here would silently mis-route SCSI errors on all three OSes simultaneously.
is_marginal detail
MEDIUM ERROR(3) — canonical bad-sector signalNOT READY(2) — on many drives (notably BU40N), this is the dominant response for unreadable sectors (ASC 04/3E, 04/01, etc.)ABORTED COMMAND(B) — transient; retry usually worksRECOVERED ERROR(1) /NO SENSE(0) — drive is healthy and either recovered the data or has no specific fault to report
max_transfer_bytes Windows rationale
The Windows backend overrides the 1 MiB default with the adapter's real
MaximumTransferLength (queried via IOCTL_STORAGE_QUERY_PROPERTY): a
16 MiB READ that exceeds the adapter limit makes DeviceIoControl fail
outright, which freemkv then mis-reads as a transport failure and falls
back to slow, log-spamming tiny reads. Chunking to this limit fixes
that. Linux/macOS keep the 1 MiB default (well within any real
max_sectors_kb).
drive_has_disc no-recovery rationale
A single TUR is issued; nothing else. No SCSI bus reset, no USB device reset, no retry is attempted in-library (the USB-reset escalation was removed in 0.13.4 after it was shown to deepen rather than clear the wedge). No SCSI primitive is exposed to outside crates — autorip / freemkv CLI / bdemu use this single function for the entire "is there a disc?" decision.
scsi module reset history
A top-level scsi::reset() wrapping platform reset in a
thread+recv_timeout used to live here; removed in 0.13.6 along with the
SG_SCSI_RESET / STOP+START UNIT escalation it existed to guard.
USB-layer recovery (scsi::usb_reset()) was rolled back in 0.13.4: on
the LG BU40N the USB reset itself succeeds but firmware below the bridge
stays locked until unplug-replug. See git tag v0.13.3.
The only hardware-touching APIs autorip + freemkv CLI use outside the
rip path: a one-shot list_drives() enumeration and a single-TUR
drive_has_disc(path). Drive::open/init()/Disc::scan stay heavy.
align_up mask semantics
mask is a mask, not a power-of-two alignment value: 0 means "no
alignment requirement" (any address is fine), 1 means 2-byte, 3 means
DWORD (4-byte), 7 means 8-byte, etc. — always one less than the
required alignment. An address is acceptable iff (addr & mask) == 0.
Returns the smallest addr >= p with (addr & mask) == 0. The standard
branch-free idiom (p + mask) & !mask works for any valid (2^n - 1)
mask, including mask == 0 (where it is the identity).
Lives here, compiled on every platform, so the Windows SPTI bounce
buffer in windows.rs can share it and so the arithmetic gets unit
coverage on macOS/Linux CI even though the SPTI path only builds on
Windows.