Audited every #[repr(C)] struct and IOCTL/flag constant in scsi/windows.rs against the authoritative Windows SDK headers (ntddscsi.h, winioctl.h, devioctl.h, winnt.h, fileapi.h). All correct except the already-reverted ScsiPassThroughDirect packing. Add the missing regression guards: - StoragePropertyQuery layout (STORAGE_PROPERTY_QUERY: 0/4/8, size 12). - IOCTL/flag constants, with IOCTLs asserted against an independent CTL_CODE re-derivation (not a tautological literal) so a mistyped code is caught. Validated compiling via cargo xwin check --target x86_64-pc-windows-msvc.
745 lines
31 KiB
Rust
745 lines
31 KiB
Rust
//! Windows SCSI transport via SPTI (SCSI Pass-Through Interface).
|
|
//!
|
|
//! Sends SCSI commands through DeviceIoControl with IOCTL_SCSI_PASS_THROUGH_DIRECT.
|
|
//! Accepts device paths like `D:`, `E:`, `\\.\CdRom0`, or `\\.\D:`.
|
|
//!
|
|
//! Requires administrator privileges for raw SCSI access.
|
|
|
|
use super::{DataDirection, ScsiResult, ScsiTransport};
|
|
use crate::error::{Error, Result};
|
|
use std::path::Path;
|
|
|
|
// ── Windows constants ──────────────────────────────────────────────────────
|
|
|
|
const IOCTL_SCSI_PASS_THROUGH_DIRECT: u32 = 0x4D014;
|
|
/// IOCTL_STORAGE_QUERY_PROPERTY — CTL_CODE(IOCTL_STORAGE_BASE(0x2D),
|
|
/// 0x500, METHOD_BUFFERED(0), FILE_ANY_ACCESS(0)) = 0x002D1400.
|
|
const IOCTL_STORAGE_QUERY_PROPERTY: u32 = 0x002D1400;
|
|
/// IOCTL_STORAGE_RESET_DEVICE (ntddstor.h) —
|
|
/// CTL_CODE(IOCTL_STORAGE_BASE=0x2D, 0x0401, METHOD_BUFFERED=0,
|
|
/// FILE_READ_ACCESS=1) = (0x2D<<16) | (1<<14) | (0x0401<<2) | 0
|
|
/// = 0x002D0000 | 0x4000 | 0x1004 = 0x002D5004.
|
|
/// Two earlier values were wrong: 0x002D1004 (function 0x401 but access
|
|
/// bits cleared) and 0x002DD000 (function 0x400 + R|W access — the
|
|
/// OBSOLETE RESET_BUS code class drivers reject). Both made
|
|
/// `DeviceIoControl` fail ERROR_INVALID_FUNCTION, silently skipping the
|
|
/// reset. See the value-regression test at the bottom of this module.
|
|
const IOCTL_STORAGE_RESET_DEVICE: u32 = 0x002D_5004;
|
|
/// STORAGE_PROPERTY_ID::StorageAdapterProperty.
|
|
const STORAGE_ADAPTER_PROPERTY: u32 = 1;
|
|
/// STORAGE_QUERY_TYPE::PropertyStandardQuery.
|
|
const PROPERTY_STANDARD_QUERY: u32 = 0;
|
|
|
|
/// Conservative fallback when the adapter MaximumTransferLength query
|
|
/// fails — 64 KiB is universally safe for SPTD on any Windows storage
|
|
/// stack. Also the floor we clamp a reported value up to.
|
|
const WINDOWS_MIN_TRANSFER_BYTES: usize = 64 * 1024;
|
|
|
|
const SCSI_IOCTL_DATA_OUT: u8 = 0;
|
|
const SCSI_IOCTL_DATA_IN: u8 = 1;
|
|
const SCSI_IOCTL_DATA_UNSPECIFIED: u8 = 2;
|
|
|
|
const GENERIC_READ: u32 = 0x80000000;
|
|
const GENERIC_WRITE: u32 = 0x40000000;
|
|
const FILE_SHARE_READ: u32 = 0x00000001;
|
|
const FILE_SHARE_WRITE: u32 = 0x00000002;
|
|
const OPEN_EXISTING: u32 = 3;
|
|
const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
|
|
const INVALID_HANDLE_VALUE: isize = -1;
|
|
|
|
const K_MAX_CDB_SIZE: usize = 16;
|
|
const K_SENSE_SIZE: usize = 32;
|
|
|
|
// ── SCSI_PASS_THROUGH_DIRECT structure ─────────────────────────────────────
|
|
|
|
#[repr(C)]
|
|
#[allow(non_snake_case)]
|
|
struct ScsiPassThroughDirect {
|
|
Length: u16,
|
|
ScsiStatus: u8,
|
|
PathId: u8,
|
|
TargetId: u8,
|
|
Lun: u8,
|
|
CdbLength: u8,
|
|
SenseInfoLength: u8,
|
|
DataIn: u8,
|
|
_padding1: [u8; 3],
|
|
DataTransferLength: u32,
|
|
TimeOutValue: u32,
|
|
DataBuffer: *mut u8,
|
|
SenseInfoOffset: u32,
|
|
Cdb: [u8; K_MAX_CDB_SIZE],
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct SptwbDirect {
|
|
spt: ScsiPassThroughDirect,
|
|
sense: [u8; K_SENSE_SIZE],
|
|
}
|
|
|
|
// ── STORAGE_QUERY_PROPERTY structures (winioctl.h) ─────────────────────────
|
|
|
|
/// Input to IOCTL_STORAGE_QUERY_PROPERTY. Mirrors `STORAGE_PROPERTY_QUERY`:
|
|
/// `{ PropertyId: u32, QueryType: u32, AdditionalParameters: [u8; 1] }`.
|
|
#[repr(C)]
|
|
#[allow(non_snake_case)]
|
|
struct StoragePropertyQuery {
|
|
PropertyId: u32,
|
|
QueryType: u32,
|
|
AdditionalParameters: [u8; 1],
|
|
}
|
|
|
|
/// Subset of `STORAGE_ADAPTER_DESCRIPTOR` (winioctl.h) up to and including
|
|
/// `MaximumTransferLength`. The real struct has more trailing fields, but
|
|
/// the driver fills the whole thing and we only read this prefix; reading a
|
|
/// truncated descriptor is the documented usage. Field layout (all the
|
|
/// leading fields are present so the offset of `MaximumTransferLength` is
|
|
/// correct):
|
|
/// Version, Size, MaximumTransferLength, MaximumPhysicalPages,
|
|
/// AlignmentMask: u32 …
|
|
#[repr(C)]
|
|
#[allow(non_snake_case)]
|
|
struct StorageAdapterDescriptor {
|
|
Version: u32,
|
|
Size: u32,
|
|
MaximumTransferLength: u32,
|
|
MaximumPhysicalPages: u32,
|
|
AlignmentMask: u32,
|
|
AdapterUsesPio: u8,
|
|
AdapterScansDown: u8,
|
|
CommandQueueing: u8,
|
|
AcceleratedTransfer: u8,
|
|
// STORAGE_BUS_TYPE is an `int`-sized enum (4 bytes), not a byte. With the
|
|
// four preceding `BOOLEAN`s filling offsets 20..24, `BusType` sits at
|
|
// offset 24 and the two `USHORT` version fields follow at 28 and 30 —
|
|
// matching winioctl.h. (Declaring this `u8` total-sized to 32 by luck but
|
|
// pushed BusMajor/BusMinorVersion to offsets 26/28, so any reader of those
|
|
// fields got garbage.)
|
|
BusType: u32,
|
|
BusMajorVersion: u16,
|
|
BusMinorVersion: u16,
|
|
}
|
|
|
|
// ── Windows FFI ────────────────────────────────────────────────────────────
|
|
|
|
unsafe extern "system" {
|
|
fn CreateFileW(
|
|
lpFileName: *const u16,
|
|
dwDesiredAccess: u32,
|
|
dwShareMode: u32,
|
|
lpSecurityAttributes: *const std::ffi::c_void,
|
|
dwCreationDisposition: u32,
|
|
dwFlagsAndAttributes: u32,
|
|
hTemplateFile: *const std::ffi::c_void,
|
|
) -> isize;
|
|
|
|
fn CloseHandle(hObject: isize) -> i32;
|
|
|
|
fn GetLastError() -> u32;
|
|
|
|
fn DeviceIoControl(
|
|
hDevice: isize,
|
|
dwIoControlCode: u32,
|
|
lpInBuffer: *mut std::ffi::c_void,
|
|
nInBufferSize: u32,
|
|
lpOutBuffer: *mut std::ffi::c_void,
|
|
nOutBufferSize: u32,
|
|
lpBytesReturned: *mut u32,
|
|
lpOverlapped: *mut std::ffi::c_void,
|
|
) -> i32;
|
|
}
|
|
|
|
// ── Transport implementation ───────────────────────────────────────────────
|
|
|
|
pub struct SptiTransport {
|
|
handle: isize,
|
|
/// Adapter MaximumTransferLength in bytes, queried once at open via
|
|
/// IOCTL_STORAGE_QUERY_PROPERTY and clamped to at least
|
|
/// [`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
|
|
// auto-derives BOTH Send and Sync. Exclusive use of the raw handle is
|
|
// enforced by `&mut self` on `execute()`, not by any absence of Sync.
|
|
|
|
/// Normalize a device path to Windows \\.\X: format.
|
|
///
|
|
/// NOTE: A near-identical `normalize_path` exists in `drive::windows`.
|
|
/// Both are kept because they live in separate `cfg(windows)` modules that
|
|
/// cannot easily share a helper without introducing cross-module coupling.
|
|
fn normalize_device_path(path: &str) -> String {
|
|
if path.starts_with("\\\\.\\") {
|
|
return path.to_string();
|
|
}
|
|
let trimmed = path.trim_end_matches('\\');
|
|
if trimmed.len() == 2 && trimmed.as_bytes()[1] == b':' {
|
|
return format!("\\\\.\\{}", trimmed);
|
|
}
|
|
if path.to_lowercase().starts_with("cdrom") {
|
|
return format!("\\\\.\\{}", path);
|
|
}
|
|
format!("\\\\.\\{}", path)
|
|
}
|
|
|
|
impl SptiTransport {
|
|
pub fn open(device: &Path) -> Result<Self> {
|
|
let dev_str = device.to_str().ok_or_else(|| Error::DeviceNotFound {
|
|
path: device.display().to_string(),
|
|
})?;
|
|
|
|
// Normalize device path to \\.\X: format
|
|
let win_path = normalize_device_path(dev_str);
|
|
let wide: Vec<u16> = win_path.encode_utf16().chain(std::iter::once(0)).collect();
|
|
|
|
let handle = unsafe {
|
|
CreateFileW(
|
|
wide.as_ptr(),
|
|
GENERIC_READ | GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
std::ptr::null(),
|
|
OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
std::ptr::null(),
|
|
)
|
|
};
|
|
|
|
if handle == INVALID_HANDLE_VALUE {
|
|
// Map last-os-error → Error variant; don't embed English hints
|
|
// in the path field (the CLI handles localization).
|
|
let err = std::io::Error::last_os_error();
|
|
return Err(if err.kind() == std::io::ErrorKind::PermissionDenied {
|
|
Error::DevicePermission {
|
|
path: dev_str.to_string(),
|
|
}
|
|
} else {
|
|
Error::DeviceNotFound {
|
|
path: dev_str.to_string(),
|
|
}
|
|
});
|
|
}
|
|
|
|
let (max_transfer, alignment_mask) = query_adapter_descriptor(handle);
|
|
|
|
Ok(SptiTransport {
|
|
handle,
|
|
max_transfer,
|
|
alignment_mask,
|
|
})
|
|
}
|
|
|
|
/// Reset the drive to a known good state.
|
|
/// Opens the device, sends IOCTL_STORAGE_RESET_DEVICE to reset
|
|
/// the USB/SCSI bus, then closes. Same concept as SG_SCSI_RESET on Linux.
|
|
pub fn reset(device: &Path) -> Result<()> {
|
|
let dev_str = device.to_str().ok_or_else(|| Error::DeviceNotFound {
|
|
path: device.display().to_string(),
|
|
})?;
|
|
let win_path = normalize_device_path(dev_str);
|
|
let wide: Vec<u16> = win_path.encode_utf16().chain(std::iter::once(0)).collect();
|
|
|
|
// Open
|
|
let handle = unsafe {
|
|
CreateFileW(
|
|
wide.as_ptr(),
|
|
GENERIC_READ | GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
std::ptr::null(),
|
|
OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
std::ptr::null(),
|
|
)
|
|
};
|
|
if handle == INVALID_HANDLE_VALUE {
|
|
return Ok(()); // can't open — skip reset, not fatal
|
|
}
|
|
|
|
// Send device reset. The result must be checked: a wrong/unsupported
|
|
// IOCTL code fails with ERROR_INVALID_FUNCTION (0x1) and no-ops
|
|
// silently — exactly the regression class the doc block above records
|
|
// for the two earlier (incorrect) code values. Surface failures so a
|
|
// non-functional reset is observable rather than masked by the
|
|
// unconditional settle sleep below.
|
|
let mut returned: u32 = 0;
|
|
let ok = unsafe {
|
|
DeviceIoControl(
|
|
handle,
|
|
IOCTL_STORAGE_RESET_DEVICE,
|
|
std::ptr::null_mut(),
|
|
0,
|
|
std::ptr::null_mut(),
|
|
0,
|
|
&mut returned,
|
|
std::ptr::null_mut(),
|
|
)
|
|
};
|
|
let reset_ok = ok != 0;
|
|
if reset_ok {
|
|
tracing::debug!("IOCTL_STORAGE_RESET_DEVICE succeeded");
|
|
} else {
|
|
let err = unsafe { GetLastError() };
|
|
// Not fatal — the caller treats reset as best-effort — but a
|
|
// failing reset (especially ERROR_INVALID_FUNCTION = 1) means the
|
|
// device was NOT reset, so there is nothing to settle and we must
|
|
// not pay the settle-sleep penalty below.
|
|
tracing::warn!(
|
|
last_error = err,
|
|
ioctl = format_args!("{IOCTL_STORAGE_RESET_DEVICE:#010x}"),
|
|
"IOCTL_STORAGE_RESET_DEVICE failed; drive not reset"
|
|
);
|
|
}
|
|
|
|
// Close the handle, then — only if the reset actually happened — wait
|
|
// for the drive to settle. A failed IOCTL reset performed no reset, so
|
|
// sleeping would burn 2 s for nothing.
|
|
unsafe { CloseHandle(handle) };
|
|
if reset_ok {
|
|
std::thread::sleep(std::time::Duration::from_secs(2));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Enumerate optical drives on Windows via `find_drives()` (CdRom0..15
|
|
/// scan) and re-shape into `DriveInfo`. Existing implementation already
|
|
/// returns `(path, DriveId)`; mapped here to the public struct.
|
|
pub(super) fn list_drives() -> Vec<super::DriveInfo> {
|
|
crate::drive::windows::find_drives()
|
|
.into_iter()
|
|
.map(|(path, id)| super::DriveInfo {
|
|
path,
|
|
vendor: id.vendor_id.trim().to_string(),
|
|
model: id.product_id.trim().to_string(),
|
|
firmware: id.product_revision.trim().to_string(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// TEST UNIT READY probe on Windows. No in-library recovery — see the
|
|
/// Linux `drive_has_disc` doc block for the rationale.
|
|
pub(super) fn drive_has_disc(path: &Path) -> Result<bool> {
|
|
let mut transport = SptiTransport::open(path)?;
|
|
let cdb = [crate::scsi::SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0];
|
|
let mut buf = [0u8; 0];
|
|
match transport.execute(
|
|
&cdb,
|
|
crate::scsi::DataDirection::None,
|
|
&mut buf,
|
|
crate::scsi::TUR_TIMEOUT_MS,
|
|
) {
|
|
Ok(_) => Ok(true),
|
|
Err(ref e) if e.scsi_sense().is_some_and(|s| s.is_not_ready()) => Ok(false),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
impl Drop for SptiTransport {
|
|
fn drop(&mut self) {
|
|
if self.handle != INVALID_HANDLE_VALUE {
|
|
unsafe {
|
|
CloseHandle(self.handle);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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, 0);
|
|
}
|
|
let query = StoragePropertyQuery {
|
|
PropertyId: STORAGE_ADAPTER_PROPERTY,
|
|
QueryType: PROPERTY_STANDARD_QUERY,
|
|
AdditionalParameters: [0u8; 1],
|
|
};
|
|
let mut desc: StorageAdapterDescriptor = unsafe { std::mem::zeroed() };
|
|
let mut bytes_returned: u32 = 0;
|
|
let ok = unsafe {
|
|
DeviceIoControl(
|
|
handle,
|
|
IOCTL_STORAGE_QUERY_PROPERTY,
|
|
&query as *const _ as *mut std::ffi::c_void,
|
|
std::mem::size_of::<StoragePropertyQuery>() as u32,
|
|
&mut desc as *mut _ as *mut std::ffi::c_void,
|
|
std::mem::size_of::<StorageAdapterDescriptor>() as u32,
|
|
&mut bytes_returned,
|
|
std::ptr::null_mut(),
|
|
)
|
|
};
|
|
// MaximumTransferLength sits at offset 8; need at least that many bytes
|
|
// written for the field to be valid.
|
|
let max_valid = ok != 0
|
|
&& bytes_returned as usize
|
|
>= std::mem::offset_of!(StorageAdapterDescriptor, MaximumTransferLength)
|
|
+ std::mem::size_of::<u32>();
|
|
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 {
|
|
fn max_transfer_bytes(&self) -> usize {
|
|
self.max_transfer
|
|
}
|
|
|
|
fn execute(
|
|
&mut self,
|
|
cdb: &[u8],
|
|
direction: DataDirection,
|
|
data: &mut [u8],
|
|
timeout_ms: u32,
|
|
) -> Result<ScsiResult> {
|
|
// Zero the data buffer for reads to prevent returning uninitialized data
|
|
// if the driver doesn't fully update DataTransferLength.
|
|
if direction == DataDirection::FromDevice {
|
|
data.fill(0);
|
|
}
|
|
|
|
let mut sptwb: SptwbDirect = unsafe { std::mem::zeroed() };
|
|
|
|
let cdb_len = cdb.len().min(K_MAX_CDB_SIZE);
|
|
sptwb.spt.Length = std::mem::size_of::<ScsiPassThroughDirect>() as u16;
|
|
sptwb.spt.CdbLength = cdb_len as u8;
|
|
sptwb.spt.SenseInfoLength = K_SENSE_SIZE as u8;
|
|
sptwb.spt.DataIn = match direction {
|
|
DataDirection::None => SCSI_IOCTL_DATA_UNSPECIFIED,
|
|
DataDirection::FromDevice => SCSI_IOCTL_DATA_IN,
|
|
DataDirection::ToDevice => SCSI_IOCTL_DATA_OUT,
|
|
};
|
|
// Match the macOS/Linux guard: a >=4 GiB buffer would wrap when cast to
|
|
// u32 below, producing a short transfer reported as success with the
|
|
// wrong byte count.
|
|
if data.len() > u32::MAX as usize {
|
|
return Err(Error::ScsiError {
|
|
opcode: cdb.first().copied().unwrap_or(0),
|
|
status: super::SCSI_STATUS_TRANSPORT_FAILURE,
|
|
sense: None,
|
|
});
|
|
}
|
|
sptwb.spt.DataTransferLength = data.len() as u32;
|
|
// Round up to the next whole second so a 1500ms request gets at
|
|
// least 2s, not 1s. SPTI's TimeOutValue is u32 seconds with no
|
|
// 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);
|
|
|
|
// 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]);
|
|
|
|
let buf_size = std::mem::size_of::<SptwbDirect>() as u32;
|
|
let mut bytes_returned: u32 = 0;
|
|
|
|
let ok = unsafe {
|
|
DeviceIoControl(
|
|
self.handle,
|
|
IOCTL_SCSI_PASS_THROUGH_DIRECT,
|
|
&mut sptwb as *mut _ as *mut std::ffi::c_void,
|
|
buf_size,
|
|
&mut sptwb as *mut _ as *mut std::ffi::c_void,
|
|
buf_size,
|
|
&mut bytes_returned,
|
|
std::ptr::null_mut(),
|
|
)
|
|
};
|
|
|
|
if ok == 0 {
|
|
// Driver-level failure (timeout, handle gone, etc.). Bubble
|
|
// up; in-library handle recovery was removed in 0.13.20 along
|
|
// with Linux's async fd-recovery and macOS's `try_recover` —
|
|
// the kernel mid-layer already did its escalation by the time
|
|
// DeviceIoControl returned, and re-issuing reset/reopen here
|
|
// is at best redundant and at worst deepens the wedge. Caller
|
|
// surfaces the failure to UX.
|
|
return Err(Error::ScsiError {
|
|
opcode: cdb.first().copied().unwrap_or(0),
|
|
status: super::SCSI_STATUS_TRANSPORT_FAILURE,
|
|
sense: None,
|
|
});
|
|
}
|
|
|
|
if sptwb.spt.ScsiStatus != 0 {
|
|
// SPTI doesn't surface a "bytes written into sense buffer"
|
|
// count separate from SenseInfoLength (input). Pass the full
|
|
// K_SENSE_SIZE; parse_sense keys off byte 0's response code
|
|
// to handle descriptor (0x72/0x73) vs fixed (0x70/0x71).
|
|
//
|
|
// 0.13.23: carry the full SPC-4 sense triple in
|
|
// `Error::ScsiError::sense` so callers can route on
|
|
// `ScsiSense::is_medium_error()` etc.
|
|
let parsed = super::parse_sense(&sptwb.sense, K_SENSE_SIZE as u8);
|
|
return Err(Error::ScsiError {
|
|
opcode: cdb.first().copied().unwrap_or(0),
|
|
status: sptwb.spt.ScsiStatus,
|
|
sense: Some(parsed),
|
|
});
|
|
}
|
|
|
|
// 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,
|
|
bytes_transferred: transferred,
|
|
sense,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Regression guard for `SptiTransport::reset()`. Two earlier IOCTL
|
|
/// values (0x002D1004 and 0x002DD000) silently failed with
|
|
/// ERROR_INVALID_FUNCTION while appearing to work — the reset no-oped
|
|
/// but the unconditional settle sleep made it look successful. This
|
|
/// recomputes IOCTL_STORAGE_RESET_DEVICE from the CTL_CODE formula
|
|
/// independently of the hardcoded constant so a wrong value can't slip
|
|
/// back in unnoticed.
|
|
#[test]
|
|
fn ioctl_storage_reset_device_value_is_correct() {
|
|
// CTL_CODE(DeviceType, Function, Method, Access) =
|
|
// (DeviceType << 16) | (Access << 14) | (Function << 2) | Method
|
|
const fn ctl_code(device_type: u32, function: u32, method: u32, access: u32) -> u32 {
|
|
(device_type << 16) | (access << 14) | (function << 2) | method
|
|
}
|
|
const IOCTL_STORAGE_BASE: u32 = 0x2D;
|
|
const METHOD_BUFFERED: u32 = 0;
|
|
const FILE_READ_ACCESS: u32 = 1;
|
|
let expected = ctl_code(
|
|
IOCTL_STORAGE_BASE,
|
|
0x0401,
|
|
METHOD_BUFFERED,
|
|
FILE_READ_ACCESS,
|
|
);
|
|
|
|
assert_eq!(
|
|
IOCTL_STORAGE_RESET_DEVICE, expected,
|
|
"IOCTL_STORAGE_RESET_DEVICE must equal CTL_CODE(0x2D, 0x0401, \
|
|
METHOD_BUFFERED, FILE_READ_ACCESS); a wrong value fails \
|
|
ERROR_INVALID_FUNCTION and silently no-ops the reset"
|
|
);
|
|
assert_eq!(IOCTL_STORAGE_RESET_DEVICE, 0x002D_5004);
|
|
// The two historically wrong values must never reappear.
|
|
assert_ne!(IOCTL_STORAGE_RESET_DEVICE, 0x002D_1004);
|
|
assert_ne!(IOCTL_STORAGE_RESET_DEVICE, 0x002D_D000);
|
|
}
|
|
|
|
/// Regression guard for the `StorageAdapterDescriptor` layout. It must
|
|
/// match `STORAGE_ADAPTER_DESCRIPTOR` (winioctl.h) field-for-field so a
|
|
/// driver-filled buffer is interpreted at the correct offsets. `BusType`
|
|
/// is `STORAGE_BUS_TYPE`, an `int`-sized (4-byte) enum, NOT a byte; a
|
|
/// previous `u8` declaration kept the total size at 32 by coincidence but
|
|
/// shifted `BusMajorVersion`/`BusMinorVersion` to offsets 26/28 (vs the
|
|
/// SDK's 28/30), so any reader of those fields got wrong values.
|
|
#[test]
|
|
fn storage_adapter_descriptor_matches_sdk_layout() {
|
|
use std::mem::{offset_of, size_of};
|
|
assert_eq!(offset_of!(StorageAdapterDescriptor, Version), 0);
|
|
assert_eq!(offset_of!(StorageAdapterDescriptor, Size), 4);
|
|
assert_eq!(
|
|
offset_of!(StorageAdapterDescriptor, MaximumTransferLength),
|
|
8
|
|
);
|
|
assert_eq!(
|
|
offset_of!(StorageAdapterDescriptor, MaximumPhysicalPages),
|
|
12
|
|
);
|
|
assert_eq!(offset_of!(StorageAdapterDescriptor, AlignmentMask), 16);
|
|
assert_eq!(offset_of!(StorageAdapterDescriptor, AdapterUsesPio), 20);
|
|
assert_eq!(offset_of!(StorageAdapterDescriptor, AdapterScansDown), 21);
|
|
assert_eq!(offset_of!(StorageAdapterDescriptor, CommandQueueing), 22);
|
|
assert_eq!(
|
|
offset_of!(StorageAdapterDescriptor, AcceleratedTransfer),
|
|
23
|
|
);
|
|
// The fields that were misplaced by the old `u8` BusType.
|
|
assert_eq!(offset_of!(StorageAdapterDescriptor, BusType), 24);
|
|
assert_eq!(offset_of!(StorageAdapterDescriptor, BusMajorVersion), 28);
|
|
assert_eq!(offset_of!(StorageAdapterDescriptor, BusMinorVersion), 30);
|
|
assert_eq!(size_of::<StorageAdapterDescriptor>(), 32);
|
|
}
|
|
|
|
/// Regression guard for the `ScsiPassThroughDirect` layout, cross-checked
|
|
/// against the authoritative `SCSI_PASS_THROUGH_DIRECT` in the Windows SDK
|
|
/// `ntddscsi.h`. That struct has **no `#pragma pack`** — it uses natural
|
|
/// alignment — so on 64-bit Windows (LLP64, 8-byte `PVOID`) the compiler
|
|
/// pads `DataBuffer` to offset 24 and the struct is 56 bytes. That is the
|
|
/// layout `DeviceIoControl` expects, and bare `#[repr(C)]` reproduces it.
|
|
///
|
|
/// Do NOT add `packed(4)`: that yields offset 20 / 48 bytes, which is the
|
|
/// SDK's SEPARATE 32-bit thunk struct `SCSI_PASS_THROUGH_DIRECT32`
|
|
/// (`VOID* POINTER_32 DataBuffer`). Using that 32-bit layout on a 64-bit
|
|
/// host malforms every SPTI ioctl, so INQUIRY fails and drive enumeration
|
|
/// returns zero drives (the rc.4 Windows "no drives detected" regression).
|
|
#[test]
|
|
#[cfg(target_pointer_width = "64")]
|
|
fn scsi_pass_through_direct_matches_sdk_layout_win64() {
|
|
use std::mem::{offset_of, size_of};
|
|
assert_eq!(offset_of!(ScsiPassThroughDirect, Length), 0);
|
|
assert_eq!(offset_of!(ScsiPassThroughDirect, DataTransferLength), 12);
|
|
assert_eq!(offset_of!(ScsiPassThroughDirect, TimeOutValue), 16);
|
|
assert_eq!(offset_of!(ScsiPassThroughDirect, DataBuffer), 24);
|
|
assert_eq!(offset_of!(ScsiPassThroughDirect, SenseInfoOffset), 32);
|
|
assert_eq!(offset_of!(ScsiPassThroughDirect, Cdb), 36);
|
|
assert_eq!(size_of::<ScsiPassThroughDirect>(), 56);
|
|
// `sense` immediately follows the 56-byte spt; SenseInfoOffset points
|
|
// here via offset_of! in execute(), so this must stay consistent.
|
|
assert_eq!(offset_of!(SptwbDirect, spt), 0);
|
|
assert_eq!(offset_of!(SptwbDirect, sense), 56);
|
|
assert_eq!(size_of::<SptwbDirect>(), 56 + K_SENSE_SIZE);
|
|
}
|
|
|
|
/// Cross-checked against `STORAGE_PROPERTY_QUERY` (winioctl.h):
|
|
/// `STORAGE_PROPERTY_ID PropertyId; STORAGE_QUERY_TYPE QueryType;
|
|
/// BYTE AdditionalParameters[1];`. Both enums are `int`-sized (4 bytes),
|
|
/// so PropertyId@0, QueryType@4, AdditionalParameters@8, size 12.
|
|
#[test]
|
|
fn storage_property_query_matches_sdk_layout() {
|
|
use std::mem::{offset_of, size_of};
|
|
assert_eq!(offset_of!(StoragePropertyQuery, PropertyId), 0);
|
|
assert_eq!(offset_of!(StoragePropertyQuery, QueryType), 4);
|
|
assert_eq!(offset_of!(StoragePropertyQuery, AdditionalParameters), 8);
|
|
assert_eq!(size_of::<StoragePropertyQuery>(), 12);
|
|
}
|
|
|
|
/// Cross-checked against the Windows SDK headers. Each IOCTL is asserted
|
|
/// against an INDEPENDENT re-derivation of the `CTL_CODE` macro
|
|
/// (devioctl.h: `(DeviceType<<16) | (Access<<14) | (Function<<2) | Method`),
|
|
/// not just its literal — so a mistyped constant fails the derivation, not
|
|
/// a tautology. Flag/enum values cite their defining header.
|
|
#[test]
|
|
fn windows_ffi_constants_match_sdk() {
|
|
// CTL_CODE re-derivation. FILE_DEVICE_CONTROLLER=0x4,
|
|
// FILE_DEVICE_MASS_STORAGE=0x2D, METHOD_BUFFERED=0,
|
|
// FILE_ANY_ACCESS=0, FILE_READ_ACCESS=1, FILE_WRITE_ACCESS=2.
|
|
const fn ctl_code(dev: u32, func: u32, method: u32, access: u32) -> u32 {
|
|
(dev << 16) | (access << 14) | (func << 2) | method
|
|
}
|
|
// ntddscsi.h: CTL_CODE(IOCTL_SCSI_BASE, 0x0405, METHOD_BUFFERED, READ|WRITE)
|
|
assert_eq!(
|
|
IOCTL_SCSI_PASS_THROUGH_DIRECT,
|
|
ctl_code(0x4, 0x405, 0, 1 | 2)
|
|
);
|
|
// winioctl.h: CTL_CODE(IOCTL_STORAGE_BASE, 0x0500, METHOD_BUFFERED, ANY)
|
|
assert_eq!(IOCTL_STORAGE_QUERY_PROPERTY, ctl_code(0x2D, 0x500, 0, 0));
|
|
// winioctl.h: CTL_CODE(IOCTL_STORAGE_BASE, 0x0401, METHOD_BUFFERED, READ)
|
|
assert_eq!(IOCTL_STORAGE_RESET_DEVICE, ctl_code(0x2D, 0x401, 0, 1));
|
|
|
|
// winioctl.h STORAGE_PROPERTY_ID / STORAGE_QUERY_TYPE enums.
|
|
assert_eq!(STORAGE_ADAPTER_PROPERTY, 1); // StorageAdapterProperty (Device=0, Adapter=1)
|
|
assert_eq!(PROPERTY_STANDARD_QUERY, 0); // PropertyStandardQuery
|
|
|
|
// ntddscsi.h SCSI_IOCTL_DATA_*.
|
|
assert_eq!(SCSI_IOCTL_DATA_OUT, 0);
|
|
assert_eq!(SCSI_IOCTL_DATA_IN, 1);
|
|
assert_eq!(SCSI_IOCTL_DATA_UNSPECIFIED, 2);
|
|
|
|
// winnt.h GENERIC_* / FILE_SHARE_* / FILE_ATTRIBUTE_*; fileapi.h OPEN_EXISTING.
|
|
assert_eq!(GENERIC_READ, 0x8000_0000);
|
|
assert_eq!(GENERIC_WRITE, 0x4000_0000);
|
|
assert_eq!(FILE_SHARE_READ, 0x1);
|
|
assert_eq!(FILE_SHARE_WRITE, 0x2);
|
|
assert_eq!(OPEN_EXISTING, 3);
|
|
assert_eq!(FILE_ATTRIBUTE_NORMAL, 0x80);
|
|
assert_eq!(INVALID_HANDLE_VALUE, -1);
|
|
}
|
|
}
|