v0.11.14: audit fixes — trailing sectors, verify stop, SCSI sense, O_CLOEXEC

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).
This commit is contained in:
Matt Jackson
2026-04-21 18:40:04 +00:00
parent ce8132d181
commit d8089ec491
5 changed files with 44 additions and 14 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog # 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) ## 0.11.13 (2026-04-21)
### Fix: all rip reads use fast timeout ### Fix: all rip reads use fast timeout
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "libfreemkv" name = "libfreemkv"
version = "0.11.13" version = "0.11.14"
edition = "2021" edition = "2021"
rust-version = "1.86" rust-version = "1.86"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
+8 -3
View File
@@ -204,13 +204,18 @@ impl DiscStream {
let ext_sectors = self.extents[self.current_extent].sector_count; let ext_sectors = self.extents[self.current_extent].sector_count;
let remaining = ext_sectors.saturating_sub(self.current_offset); let remaining = ext_sectors.saturating_sub(self.current_offset);
let sectors = remaining.min(self.batch_sectors as u32) as u16; if remaining == 0 {
let sectors = sectors - (sectors % 3);
if sectors == 0 {
self.current_extent += 1; self.current_extent += 1;
self.current_offset = 0; self.current_offset = 0;
return self.fill_extents(); 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 lba = ext_start + self.current_offset;
let bytes = sectors as usize * 2048; let bytes = sectors as usize * 2048;
+12 -5
View File
@@ -68,7 +68,7 @@ impl SgIoTransport {
let fd = unsafe { let fd = unsafe {
libc::open( libc::open(
c_path.as_ptr() as *const libc::c_char, 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 { if fd < 0 {
@@ -126,7 +126,7 @@ impl SgIoTransport {
let probe_fd = unsafe { let probe_fd = unsafe {
libc::open( libc::open(
c_path.as_ptr() as *const libc::c_char, 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 { if probe_fd >= 0 {
@@ -140,7 +140,7 @@ impl SgIoTransport {
let fd = unsafe { let fd = unsafe {
libc::open( libc::open(
c_path.as_ptr() as *const libc::c_char, 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 { if fd < 0 {
@@ -373,7 +373,7 @@ impl ScsiTransport for SgIoTransport {
let new_fd = unsafe { let new_fd = unsafe {
libc::open( libc::open(
c_path.as_ptr() as *const libc::c_char, 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 }; 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; let bytes_transferred = (data.len() as i32).saturating_sub(hdr.resid).max(0) as usize;
if hdr.status != 0 { if hdr.status != 0 {
let sense_key = if hdr.sb_len_wr > 2 { 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 sense[2] & 0x0F
}
} else { } else {
0 0
}; };
+13 -4
View File
@@ -73,10 +73,12 @@ impl VerifyResult {
} }
/// Progress callback: (sectors_done, total_sectors, current_status) /// Progress callback: (sectors_done, total_sectors, current_status)
pub type ProgressFn = Box<dyn FnMut(u64, u64, SectorStatus)>; /// Return false to stop verification early.
pub type ProgressFn = Box<dyn FnMut(u64, u64, SectorStatus) -> bool>;
/// Verify all sectors in a title's extents. /// Verify all sectors in a title's extents.
/// Reads in batches for speed, falls back to single-sector on failure. /// 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( pub fn verify_title(
reader: &mut dyn SectorReader, reader: &mut dyn SectorReader,
title: &DiscTitle, title: &DiscTitle,
@@ -90,12 +92,13 @@ pub fn verify_title(
let mut bad: u64 = 0; let mut bad: u64 = 0;
let mut ranges: Vec<SectorRange> = Vec::new(); let mut ranges: Vec<SectorRange> = Vec::new();
let mut sectors_done: u64 = 0; let mut sectors_done: u64 = 0;
let mut _stopped = false;
let mut byte_offset: u64 = 0; let mut byte_offset: u64 = 0;
let total_sectors: u64 = title.extents.iter().map(|e| e.sector_count as u64).sum(); 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]; 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; let mut offset: u32 = 0;
while offset < ext.sector_count { while offset < ext.sector_count {
let remaining = ext.sector_count - offset; let remaining = ext.sector_count - offset;
@@ -133,7 +136,10 @@ pub fn verify_title(
sectors_done += count as u64; sectors_done += count as u64;
if let Some(ref mut cb) = on_progress { 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 { } else {
// Batch failed — test each sector individually // Batch failed — test each sector individually
@@ -196,7 +202,10 @@ pub fn verify_title(
sectors_done += 1; sectors_done += 1;
if let Some(ref mut cb) = on_progress { 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;
}
} }
} }
} }