v1.0.0-rc.3.1: silent-failure guards (mux empty/zero-frame, CSS crack-vs-unencrypted), Windows keydb path, AlignmentMask, English errors
This commit is contained in:
@@ -600,6 +600,82 @@ pub fn build_read10_fua(lba: u32, count: u16) -> [u8; 10] {
|
||||
]
|
||||
}
|
||||
|
||||
/// Round a pointer/address `p` up to the next boundary that satisfies an
|
||||
/// SPTI-style `AlignmentMask` (`STORAGE_ADAPTER_DESCRIPTOR::AlignmentMask`,
|
||||
/// ntddscsi.h / winioctl.h).
|
||||
///
|
||||
/// `mask` is a *mask*, not a power-of-two alignment value: `0` means "no
|
||||
/// alignment requirement" (any address is fine), `1` means 2-byte, `3`
|
||||
/// means DWORD (4-byte), `7` means 8-byte, etc. — always one less than the
|
||||
/// required alignment. An address is acceptable iff `(addr & mask) == 0`.
|
||||
///
|
||||
/// Returns the smallest `addr >= p` with `(addr & mask) == 0`. The
|
||||
/// standard branch-free idiom `(p + mask) & !mask` works for any valid
|
||||
/// (`2^n - 1`) mask, including `mask == 0` (where it is the identity).
|
||||
///
|
||||
/// Lives here, compiled on every platform, so the Windows SPTI bounce
|
||||
/// buffer in `windows.rs` can share it and so the arithmetic gets unit
|
||||
/// coverage on macOS/Linux CI even though the SPTI path only builds on
|
||||
/// Windows.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn align_up(p: usize, mask: usize) -> usize {
|
||||
(p + mask) & !mask
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod align_tests {
|
||||
use super::align_up;
|
||||
|
||||
#[test]
|
||||
fn mask_zero_is_identity() {
|
||||
// AlignmentMask 0 (USB optical bridges) — no alignment required.
|
||||
for p in [0usize, 1, 2, 3, 7, 8, 13, 4096, 0x7fff_ffff] {
|
||||
assert_eq!(align_up(p, 0), p);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn already_aligned_is_unchanged() {
|
||||
// DWORD (mask 3): multiples of 4 are already aligned.
|
||||
assert_eq!(align_up(0, 3), 0);
|
||||
assert_eq!(align_up(4, 3), 4);
|
||||
assert_eq!(align_up(8, 3), 8);
|
||||
// 8-byte (mask 7): multiples of 8.
|
||||
assert_eq!(align_up(0, 7), 0);
|
||||
assert_eq!(align_up(16, 7), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rounds_up_to_next_boundary() {
|
||||
// mask 1 (2-byte): odd → next even.
|
||||
assert_eq!(align_up(1, 1), 2);
|
||||
assert_eq!(align_up(3, 1), 4);
|
||||
// mask 3 (DWORD): 1,2,3 → 4; 5,6,7 → 8.
|
||||
assert_eq!(align_up(1, 3), 4);
|
||||
assert_eq!(align_up(2, 3), 4);
|
||||
assert_eq!(align_up(3, 3), 4);
|
||||
assert_eq!(align_up(5, 3), 8);
|
||||
// mask 7 (8-byte): 1..=7 → 8; 9 → 16.
|
||||
assert_eq!(align_up(1, 7), 8);
|
||||
assert_eq!(align_up(7, 7), 8);
|
||||
assert_eq!(align_up(9, 7), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_always_satisfies_mask() {
|
||||
for &mask in &[0usize, 1, 3, 7, 15, 31, 63] {
|
||||
for p in 0usize..256 {
|
||||
let a = align_up(p, mask);
|
||||
assert!(a >= p, "align_up({p},{mask})={a} went backwards");
|
||||
assert_eq!(a & mask, 0, "align_up({p},{mask})={a} not aligned");
|
||||
// Smallest such value: anything in (p-1-mask, a) would be < p
|
||||
// or unaligned; check a - p never exceeds mask.
|
||||
assert!(a - p <= mask, "align_up({p},{mask})={a} overshot");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod parse_sense_tests {
|
||||
//! Unit tests for [`parse_sense`]. Covers both SPC-4 sense data
|
||||
|
||||
+102
-18
@@ -140,6 +140,17 @@ pub struct SptiTransport {
|
||||
/// [`WINDOWS_MIN_TRANSFER_BYTES`]. A single READ larger than this fails
|
||||
/// `DeviceIoControl` outright, so [`crate::Drive::read`] chunks to it.
|
||||
max_transfer: usize,
|
||||
/// Adapter `AlignmentMask` (STORAGE_ADAPTER_DESCRIPTOR, ntddscsi.h /
|
||||
/// winioctl.h), queried alongside `max_transfer`. It is a *mask*: `0`
|
||||
/// (the common case on USB optical bridges) means the DataBuffer may
|
||||
/// sit at any address; `3` means DWORD-aligned, `7` 8-byte, etc. —
|
||||
/// always one less than the required alignment. SCSI/SAS HBAs report
|
||||
/// nonzero masks, and IOCTL_SCSI_PASS_THROUGH_DIRECT rejects a
|
||||
/// misaligned `DataBuffer` (DeviceIoControl fails → all reads return
|
||||
/// transport failure / status 0xFF). When set and the caller's buffer
|
||||
/// is misaligned, `execute()` bounces through an aligned scratch
|
||||
/// buffer (see there).
|
||||
alignment_mask: u32,
|
||||
}
|
||||
|
||||
// SptiTransport's only field is an isize HANDLE, so the compiler
|
||||
@@ -202,11 +213,12 @@ impl SptiTransport {
|
||||
});
|
||||
}
|
||||
|
||||
let max_transfer = query_max_transfer_bytes(handle);
|
||||
let (max_transfer, alignment_mask) = query_adapter_descriptor(handle);
|
||||
|
||||
Ok(SptiTransport {
|
||||
handle,
|
||||
max_transfer,
|
||||
alignment_mask,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -311,14 +323,24 @@ impl Drop for SptiTransport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Query the storage adapter's `MaximumTransferLength` (bytes) via
|
||||
/// IOCTL_STORAGE_QUERY_PROPERTY / StorageAdapterProperty. On any failure
|
||||
/// (IOCTL failed, short reply, or a nonsensical zero) returns the
|
||||
/// conservative [`WINDOWS_MIN_TRANSFER_BYTES`]; otherwise clamps the
|
||||
/// reported value up to that floor. Never returns 0.
|
||||
fn query_max_transfer_bytes(handle: isize) -> usize {
|
||||
/// Query the storage adapter descriptor via IOCTL_STORAGE_QUERY_PROPERTY /
|
||||
/// StorageAdapterProperty and return `(max_transfer_bytes, alignment_mask)`.
|
||||
///
|
||||
/// `max_transfer_bytes`: the adapter's `MaximumTransferLength`. On any
|
||||
/// failure (IOCTL failed, short reply, or a nonsensical zero) falls back to
|
||||
/// the conservative [`WINDOWS_MIN_TRANSFER_BYTES`]; otherwise clamped up to
|
||||
/// that floor. Never 0.
|
||||
///
|
||||
/// `alignment_mask`: the adapter's `AlignmentMask` (offset 16 in
|
||||
/// STORAGE_ADAPTER_DESCRIPTOR). `0` means no alignment requirement (the
|
||||
/// common case for USB optical bridges). A nonzero mask (SCSI/SAS HBAs)
|
||||
/// forces `execute()` to bounce the DataBuffer through an aligned scratch
|
||||
/// buffer. If the reply is too short to include `AlignmentMask`, returns
|
||||
/// `0` (no requirement) — the safe default, since any address satisfies a
|
||||
/// zero mask and the descriptor's leading fields are read first regardless.
|
||||
fn query_adapter_descriptor(handle: isize) -> (usize, u32) {
|
||||
if handle == INVALID_HANDLE_VALUE {
|
||||
return WINDOWS_MIN_TRANSFER_BYTES;
|
||||
return (WINDOWS_MIN_TRANSFER_BYTES, 0);
|
||||
}
|
||||
let query = StoragePropertyQuery {
|
||||
PropertyId: STORAGE_ADAPTER_PROPERTY,
|
||||
@@ -341,14 +363,25 @@ fn query_max_transfer_bytes(handle: isize) -> usize {
|
||||
};
|
||||
// MaximumTransferLength sits at offset 8; need at least that many bytes
|
||||
// written for the field to be valid.
|
||||
let valid = ok != 0
|
||||
let max_valid = ok != 0
|
||||
&& bytes_returned as usize
|
||||
>= std::mem::offset_of!(StorageAdapterDescriptor, MaximumTransferLength)
|
||||
+ std::mem::size_of::<u32>();
|
||||
if !valid || desc.MaximumTransferLength == 0 {
|
||||
return WINDOWS_MIN_TRANSFER_BYTES;
|
||||
}
|
||||
(desc.MaximumTransferLength as usize).max(WINDOWS_MIN_TRANSFER_BYTES)
|
||||
let max_transfer = if !max_valid || desc.MaximumTransferLength == 0 {
|
||||
WINDOWS_MIN_TRANSFER_BYTES
|
||||
} else {
|
||||
(desc.MaximumTransferLength as usize).max(WINDOWS_MIN_TRANSFER_BYTES)
|
||||
};
|
||||
|
||||
// AlignmentMask sits at offset 16; only trust it if the reply is long
|
||||
// enough. Otherwise assume 0 (no alignment requirement).
|
||||
let align_valid = ok != 0
|
||||
&& bytes_returned as usize
|
||||
>= std::mem::offset_of!(StorageAdapterDescriptor, AlignmentMask)
|
||||
+ std::mem::size_of::<u32>();
|
||||
let alignment_mask = if align_valid { desc.AlignmentMask } else { 0 };
|
||||
|
||||
(max_transfer, alignment_mask)
|
||||
}
|
||||
|
||||
impl ScsiTransport for SptiTransport {
|
||||
@@ -396,11 +429,49 @@ impl ScsiTransport for SptiTransport {
|
||||
// sub-second resolution; biasing toward "more time" is safer than
|
||||
// truncating (truncation broke 1500ms fast-reads on Drive::read).
|
||||
sptwb.spt.TimeOutValue = ((timeout_ms + 999) / 1000).max(1);
|
||||
sptwb.spt.DataBuffer = if data.is_empty() {
|
||||
|
||||
// AlignmentMask bounce buffer.
|
||||
//
|
||||
// IOCTL_SCSI_PASS_THROUGH_DIRECT requires `DataBuffer` to satisfy
|
||||
// the adapter's `AlignmentMask` (`(ptr & mask) == 0`). On USB
|
||||
// optical bridges the mask is 0, so the caller's buffer is always
|
||||
// acceptable and we point straight at it (zero-copy fast path).
|
||||
// On SCSI/SAS HBAs the mask can be 3/7/… ; if the caller's buffer
|
||||
// happens to be misaligned the IOCTL fails outright (status 0xFF /
|
||||
// all reads fail). In that case we transfer through an aligned
|
||||
// scratch buffer: over-allocate by `mask` extra bytes so an aligned
|
||||
// base is guaranteed to exist inside it, align the base with
|
||||
// [`crate::scsi::align_up`], and use that as `DataBuffer`. For a
|
||||
// FROM-device transfer the result is copied back into `data` after
|
||||
// the IOCTL; for a TO-device transfer `data` is copied in before.
|
||||
//
|
||||
// `bounce` is kept alive for the whole `execute()` body so the
|
||||
// aligned pointer we hand the driver stays valid across the IOCTL.
|
||||
let mask = self.alignment_mask as usize;
|
||||
let needs_bounce =
|
||||
!data.is_empty() && mask != 0 && (data.as_mut_ptr() as usize) & mask != 0;
|
||||
let mut bounce: Vec<u8> = Vec::new();
|
||||
let data_ptr: *mut u8 = if data.is_empty() {
|
||||
std::ptr::null_mut()
|
||||
} else if needs_bounce {
|
||||
// Over-allocate by `mask` so an aligned start exists within.
|
||||
bounce = vec![0u8; data.len() + mask];
|
||||
let base = bounce.as_mut_ptr() as usize;
|
||||
let aligned = crate::scsi::align_up(base, mask);
|
||||
let aligned_ptr = aligned as *mut u8;
|
||||
// For writes (ToDevice) prime the aligned region with the
|
||||
// caller's payload before the IOCTL. (FromDevice copies back
|
||||
// after.)
|
||||
if direction == DataDirection::ToDevice {
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(data.as_ptr(), aligned_ptr, data.len());
|
||||
}
|
||||
}
|
||||
aligned_ptr
|
||||
} else {
|
||||
data.as_mut_ptr()
|
||||
};
|
||||
sptwb.spt.DataBuffer = data_ptr;
|
||||
sptwb.spt.SenseInfoOffset = std::mem::offset_of!(SptwbDirect, sense) as u32;
|
||||
sptwb.spt.Cdb[..cdb_len].copy_from_slice(&cdb[..cdb_len]);
|
||||
|
||||
@@ -452,15 +523,28 @@ impl ScsiTransport for SptiTransport {
|
||||
});
|
||||
}
|
||||
|
||||
// Clamp to the caller's buffer length, matching Linux/macOS: a
|
||||
// driver that reports DataTransferLength > data.len() must never let
|
||||
// callers read past the buffer they handed in.
|
||||
let transferred = (sptwb.spt.DataTransferLength as usize).min(data.len());
|
||||
|
||||
// If we bounced a FROM-device read, copy the aligned scratch back
|
||||
// into the caller's buffer (only the bytes actually transferred).
|
||||
if needs_bounce && direction == DataDirection::FromDevice {
|
||||
let aligned_ptr = data_ptr; // points inside `bounce`
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(aligned_ptr, data.as_mut_ptr(), transferred);
|
||||
}
|
||||
}
|
||||
// `bounce` is dropped here, after the last use of `data_ptr`.
|
||||
drop(bounce);
|
||||
|
||||
let mut sense = [0u8; 32];
|
||||
sense.copy_from_slice(&sptwb.sense);
|
||||
|
||||
Ok(ScsiResult {
|
||||
status: sptwb.spt.ScsiStatus,
|
||||
// Clamp to the caller's buffer length, matching Linux/macOS: a
|
||||
// driver that reports DataTransferLength > data.len() must never
|
||||
// let callers read past the buffer they handed in.
|
||||
bytes_transferred: (sptwb.spt.DataTransferLength as usize).min(data.len()),
|
||||
bytes_transferred: transferred,
|
||||
sense,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user