From c9bf92cd6f522ae957e585c33fb5b0bb7e653715 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 23 Jun 2026 03:51:51 -0700 Subject: [PATCH] Fix oversized read batch on non-sysfs (Windows) optical drives detect_max_batch_sectors() is a Linux-sysfs probe with no platform gate. It derived the device name with rsplit('/'), which never splits a Windows \.\CdRom0 / \.\D: path, so the whole path became the device name, no /sys node matched, is_optical fell to false, and the function returned the 8192-sector block default (16 MiB/request) instead of the 60-sector optical default. That value then took the Some(b) arm in Disc::copy and bypassed the 510-sector optical clamp that lives only in the sysfs branch, leaving every Windows rip/verify running ~16x over the optical cap (coarser bad-sector recovery, 16 MiB UDF reads). Gate the sysfs probe behind a new sysfs_batch_probe_supported() helper (Linux-only, requires a '/'-delimited path) and return the optical default for any path the probe can't handle. Add regression tests for the \.\ device-path forms. --- src/disc/mod.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/disc/mod.rs b/src/disc/mod.rs index d2c8848..1fdda82 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -3286,8 +3286,29 @@ mod severity_tests { } } +/// Whether the Linux-sysfs transfer-size probe applies to this device path. +/// +/// The probe reads `/sys/block//...` / `/sys/class/scsi_generic//...`, +/// which only exist on Linux and only for `/`-delimited node paths. A Windows +/// `\\.\CdRom0` / `\\.\D:` path has no forward slash and no sysfs node, so the +/// probe cannot run and the caller must fall back to the optical default. +fn sysfs_batch_probe_supported(device_path: &str) -> bool { + cfg!(target_os = "linux") && device_path.contains('/') +} + /// Detect the maximum transfer size in sectors for a device. pub fn detect_max_batch_sectors(device_path: &str) -> u16 { + // The sysfs probe below is Linux-only. Non-sysfs platforms (Windows in + // particular) use `\\.\`-form device paths (e.g. `\\.\CdRom0`, `\\.\D:`) + // that have no forward slash, so the Linux name-parsing below would treat + // the whole path as the device name, find no `/sys` node, and fall through + // to the block default (8192 sectors = 16 MiB) — far over the optical cap. + // Every device we open on a non-sysfs platform here is an optical drive, + // so return the optical default directly. + if !sysfs_batch_probe_supported(device_path) { + return DEFAULT_BATCH_SECTORS_OPTICAL; + } + let dev_name = device_path.rsplit('/').next().unwrap_or(""); if dev_name.is_empty() { return DEFAULT_BATCH_SECTORS_OPTICAL; @@ -3354,6 +3375,33 @@ pub fn detect_max_batch_sectors(device_path: &str) -> u16 { mod tests { use super::*; + /// A Windows-form optical device path (`\\.\CdRom0`, `\\.\D:`) must never + /// fall through to the block default (8192 sectors = 16 MiB, well over the + /// optical 510-sector cap). It has no forward slash, so the Linux-sysfs + /// name parse cannot apply; the detector must return the optical default. + #[test] + fn windows_device_path_uses_optical_default() { + for path in ["\\\\.\\CdRom0", "\\\\.\\CdRom15", "\\\\.\\D:", "\\\\.\\E:"] { + let batch = detect_max_batch_sectors(path); + assert_eq!( + batch, DEFAULT_BATCH_SECTORS_OPTICAL, + "windows path {path:?} must map to the optical default, got {batch}" + ); + assert!( + batch <= MAX_BATCH_SECTORS, + "windows path {path:?} batch {batch} exceeds optical cap {MAX_BATCH_SECTORS}" + ); + } + } + + /// The sysfs probe only applies on Linux and only to `/`-delimited node + /// paths. A backslash-form path is never sysfs-probeable on any platform. + #[test] + fn windows_path_not_sysfs_probeable() { + assert!(!sysfs_batch_probe_supported("\\\\.\\CdRom0")); + assert!(!sysfs_batch_probe_supported("\\\\.\\D:")); + } + /// AACS unit-alignment of the DECRYPTING multipass sweep. AACS aligned units /// are 3 sectors (6144 bytes); `decrypt_sectors` anchors units at buffer /// offset 0, so the sweep MUST (a) round its per-batch sector count UP to a