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:
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
+13
-6
@@ -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 {
|
||||||
sense[2] & 0x0F
|
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 {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-4
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user