From c0a96dfa97e467786764de1d59a9a34267cca0ca Mon Sep 17 00:00:00 2001 From: Matt Jackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:40:04 +0000 Subject: [PATCH] =?UTF-8?q?v0.11.14:=20audit=20fixes=20=E2=80=94=20trailin?= =?UTF-8?q?g=20sectors,=20verify=20stop,=20SCSI=20sense,=20O=5FCLOEXEC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix trailing sectors dropped at extent boundaries when sector_count % 3 != 0. Add verify_title stop support via progress callback returning bool. Add O_CLOEXEC on all SCSI fd opens to prevent leak to child processes. Fix SCSI sense descriptor format detection (0x72/0x73 vs 0x70/0x71). --- CHANGELOG.md | 9 +++++++++ Cargo.toml | 2 +- src/mux/disc.rs | 11 ++++++++--- src/scsi/linux.rs | 19 +++++++++++++------ src/verify.rs | 17 +++++++++++++---- 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34264c3..a3af480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.11.14 (2026-04-21) + +### Audit fixes: read recovery, verify, SCSI +- **Fix: trailing sectors at extent boundaries** — extents with sector_count not divisible by 3 no longer drop 1-2 trailing sectors. decrypt_sectors() safely skips partial AACS units. +- **Fix: verify_title stop support** — progress callback now returns bool. Return false to stop verification early instead of running to completion. +- **Fix: O_CLOEXEC on all SCSI fd opens** — prevents fd leak to child processes. +- **Fix: SCSI sense descriptor format** — correctly detect response code 0x72/0x73 (descriptor format) and extract sense key from byte 1 instead of byte 2. +- **Fix: DecryptFailed on missing unit key** — decrypt_sectors() returns Err(DecryptFailed) instead of silently using a zero key. + ## 0.11.13 (2026-04-21) ### Fix: all rip reads use fast timeout diff --git a/Cargo.toml b/Cargo.toml index 27a8d56..db4ed76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfreemkv" -version = "0.11.13" +version = "0.11.14" edition = "2021" rust-version = "1.86" license = "AGPL-3.0-only" diff --git a/src/mux/disc.rs b/src/mux/disc.rs index 0b695ee..4e2163d 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -204,13 +204,18 @@ impl DiscStream { let ext_sectors = self.extents[self.current_extent].sector_count; let remaining = ext_sectors.saturating_sub(self.current_offset); - let sectors = remaining.min(self.batch_sectors as u32) as u16; - let sectors = sectors - (sectors % 3); - if sectors == 0 { + if remaining == 0 { self.current_extent += 1; self.current_offset = 0; return self.fill_extents(); } + let mut sectors = remaining.min(self.batch_sectors as u32) as u16; + // Align to 3-sector AACS units when possible, but never drop + // trailing sectors at extent boundaries. decrypt_sectors() safely + // skips partial units (chunks shorter than ALIGNED_UNIT_LEN). + if sectors >= 3 { + sectors -= sectors % 3; + } let lba = ext_start + self.current_offset; let bytes = sectors as usize * 2048; diff --git a/src/scsi/linux.rs b/src/scsi/linux.rs index c4212df..1504f32 100644 --- a/src/scsi/linux.rs +++ b/src/scsi/linux.rs @@ -68,7 +68,7 @@ impl SgIoTransport { let fd = unsafe { libc::open( c_path.as_ptr() as *const libc::c_char, - libc::O_RDWR | libc::O_NONBLOCK, + libc::O_RDWR | libc::O_NONBLOCK | libc::O_CLOEXEC, ) }; if fd < 0 { @@ -126,7 +126,7 @@ impl SgIoTransport { let probe_fd = unsafe { libc::open( c_path.as_ptr() as *const libc::c_char, - libc::O_RDWR | libc::O_NONBLOCK, + libc::O_RDWR | libc::O_NONBLOCK | libc::O_CLOEXEC, ) }; if probe_fd >= 0 { @@ -140,7 +140,7 @@ impl SgIoTransport { let fd = unsafe { libc::open( c_path.as_ptr() as *const libc::c_char, - libc::O_RDWR | libc::O_NONBLOCK, + libc::O_RDWR | libc::O_NONBLOCK | libc::O_CLOEXEC, ) }; if fd < 0 { @@ -373,7 +373,7 @@ impl ScsiTransport for SgIoTransport { let new_fd = unsafe { libc::open( c_path.as_ptr() as *const libc::c_char, - libc::O_RDWR | libc::O_NONBLOCK, + libc::O_RDWR | libc::O_NONBLOCK | libc::O_CLOEXEC, ) }; self.fd = if new_fd >= 0 { new_fd } else { -1 }; @@ -402,8 +402,15 @@ impl ScsiTransport for SgIoTransport { let bytes_transferred = (data.len() as i32).saturating_sub(hdr.resid).max(0) as usize; if hdr.status != 0 { - let sense_key = if hdr.sb_len_wr > 2 { - sense[2] & 0x0F + let sense_key = if hdr.sb_len_wr >= 3 { + let response_code = sense[0] & 0x7F; + if response_code == 0x72 || response_code == 0x73 { + // Descriptor format sense: sense key at byte 1 + sense[1] & 0x0F + } else { + // Fixed format sense (0x70/0x71): sense key at byte 2 + sense[2] & 0x0F + } } else { 0 }; diff --git a/src/verify.rs b/src/verify.rs index 6879fa9..5abe176 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -73,10 +73,12 @@ impl VerifyResult { } /// Progress callback: (sectors_done, total_sectors, current_status) -pub type ProgressFn = Box; +/// Return false to stop verification early. +pub type ProgressFn = Box bool>; /// Verify all sectors in a title's extents. /// Reads in batches for speed, falls back to single-sector on failure. +/// The progress callback returns false to request early stop. pub fn verify_title( reader: &mut dyn SectorReader, title: &DiscTitle, @@ -90,12 +92,13 @@ pub fn verify_title( let mut bad: u64 = 0; let mut ranges: Vec = Vec::new(); let mut sectors_done: u64 = 0; + let mut _stopped = false; let mut byte_offset: u64 = 0; let total_sectors: u64 = title.extents.iter().map(|e| e.sector_count as u64).sum(); let mut buf = vec![0u8; batch_sectors as usize * 2048]; - for ext in &title.extents { + 'outer: for ext in &title.extents { let mut offset: u32 = 0; while offset < ext.sector_count { let remaining = ext.sector_count - offset; @@ -133,7 +136,10 @@ pub fn verify_title( sectors_done += count as u64; if let Some(ref mut cb) = on_progress { - cb(sectors_done, total_sectors, status); + if !cb(sectors_done, total_sectors, status) { + _stopped = true; + break 'outer; + } } } else { // Batch failed — test each sector individually @@ -196,7 +202,10 @@ pub fn verify_title( sectors_done += 1; if let Some(ref mut cb) = on_progress { - cb(sectors_done, total_sectors, status); + if !cb(sectors_done, total_sectors, status) { + _stopped = true; + break 'outer; + } } } }