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.
156 lines
6.9 KiB
Markdown
156 lines
6.9 KiB
Markdown
# 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.
|