Files
libfreemkv/docs/scsi-mod.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

6.9 KiB
Raw Blame History

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]
  • Fixed format (response code 0x70 / 0x71 and any unknown code per SPC-4 §4.5.3):
    • sense key = sense[2] & 0x0F
    • asc = sense[12]
    • ascq = sense[13]

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 signal
  • NOT 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 works
  • RECOVERED 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.