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.
This commit is contained in:
Matthew Jackson
2026-06-23 03:51:51 -07:00
parent 4a76deadeb
commit c9bf92cd6f
+48
View File
@@ -3286,8 +3286,29 @@ mod severity_tests {
} }
} }
/// Whether the Linux-sysfs transfer-size probe applies to this device path.
///
/// The probe reads `/sys/block/<name>/...` / `/sys/class/scsi_generic/<name>/...`,
/// 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. /// Detect the maximum transfer size in sectors for a device.
pub fn detect_max_batch_sectors(device_path: &str) -> u16 { 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(""); let dev_name = device_path.rsplit('/').next().unwrap_or("");
if dev_name.is_empty() { if dev_name.is_empty() {
return DEFAULT_BATCH_SECTORS_OPTICAL; return DEFAULT_BATCH_SECTORS_OPTICAL;
@@ -3354,6 +3375,33 @@ pub fn detect_max_batch_sectors(device_path: &str) -> u16 {
mod tests { mod tests {
use super::*; 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 /// AACS unit-alignment of the DECRYPTING multipass sweep. AACS aligned units
/// are 3 sectors (6144 bytes); `decrypt_sectors` anchors units at buffer /// 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 /// offset 0, so the sweep MUST (a) round its per-batch sector count UP to a