Files
libfreemkv/docs/scsi-windows.md
T
Matthew Jackson 6e14fc0c0c libfreemkv: 1.7.0 groundwork — sector-recycle soundness + CI hardening
Security
- Remove the unsafe Vec::set_len over recycled sector buffers — UB reachable
  from safe code (GHSA-j8ww-f5fg-9pmh, low). Uses Vec::resize; pooling unchanged.

CI / tooling
- clippy on stable with the MSRV single-sourced from Cargo.toml; cargo-deny
  dependency-audit gate with first-party git sources allowed (#15); audience-based
  comment-guard (#17).

Docs
- README (keydb ownership, BD-J parser count), drive-access transport/reset
  notes, mux timeline module-doc links.
2026-09-02 12:40:55 -07:00

4.3 KiB

Windows SCSI transport (src/scsi/windows.rs)

IOCTL_STORAGE_RESET_DEVICE derivation

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 the module.

StorageAdapterDescriptor field layout

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 ….

STORAGE_BUS_TYPE is an int-sized enum (4 bytes), not a byte: with the preceding BOOLEANs filling 20..24, BusType sits at offset 24 and the USHORT version fields follow at 28/30, matching winioctl.h. A previous u8 declaration for BusType 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. The layout test below asserts every offset field-for-field against the SDK.

alignment_mask / bounce-buffer rationale

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.

normalize_device_path duplication

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.

query_adapter_descriptor contract

Queries the storage adapter descriptor via IOCTL_STORAGE_QUERY_PROPERTY / StorageAdapterProperty and returns (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. 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.

ScsiPassThroughDirect layout, cross-checked against SDK

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).

windows_ffi_constants_match_sdk test rationale

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.