From 78a358168c540fe5154037954cac91664f0425ed Mon Sep 17 00:00:00 2001 From: Markus Ebner <info@ebner-markus.de> Date: Mon, 24 Mar 2025 13:56:43 +0100 Subject: [PATCH 1/2] uefi-raw: Remove unnecessary mutability on ExtScsiPassThruProtocol::reset_target_lun --- uefi-raw/src/protocol/scsi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uefi-raw/src/protocol/scsi.rs b/uefi-raw/src/protocol/scsi.rs index d724aebeb..db3bc6835 100644 --- a/uefi-raw/src/protocol/scsi.rs +++ b/uefi-raw/src/protocol/scsi.rs @@ -208,7 +208,7 @@ pub struct ExtScsiPassThruProtocol { ) -> Status, pub reset_channel: unsafe extern "efiapi" fn(this: *mut Self) -> Status, pub reset_target_lun: - unsafe extern "efiapi" fn(this: *mut Self, target: *const u8, lun: u64) -> Status, + unsafe extern "efiapi" fn(this: *const Self, target: *const u8, lun: u64) -> Status, pub get_next_target: unsafe extern "efiapi" fn(this: *const Self, target: *mut *mut u8) -> Status, } From 5b7173f8d3b57dd2be31f9fa0b4f2e641b25cb8d Mon Sep 17 00:00:00 2001 From: Markus Ebner <info@ebner-markus.de> Date: Mon, 24 Mar 2025 13:57:24 +0100 Subject: [PATCH 2/2] uefi: Add safe protocol bindings for EFI_EXT_SCSI_PASS_THRU_PROTOCOL --- uefi-raw/src/protocol/scsi.rs | 4 +- uefi-test-runner/src/proto/mod.rs | 2 + uefi-test-runner/src/proto/scsi/mod.rs | 7 + uefi-test-runner/src/proto/scsi/pass_thru.rs | 112 ++++++ uefi/CHANGELOG.md | 1 + uefi/src/proto/mod.rs | 2 + uefi/src/proto/scsi/mod.rs | 352 +++++++++++++++++++ uefi/src/proto/scsi/pass_thru.rs | 269 ++++++++++++++ 8 files changed, 747 insertions(+), 2 deletions(-) create mode 100644 uefi-test-runner/src/proto/scsi/mod.rs create mode 100644 uefi-test-runner/src/proto/scsi/pass_thru.rs create mode 100644 uefi/src/proto/scsi/mod.rs create mode 100644 uefi/src/proto/scsi/pass_thru.rs diff --git a/uefi-raw/src/protocol/scsi.rs b/uefi-raw/src/protocol/scsi.rs index db3bc6835..02ee07aa4 100644 --- a/uefi-raw/src/protocol/scsi.rs +++ b/uefi-raw/src/protocol/scsi.rs @@ -186,7 +186,7 @@ pub struct ExtScsiPassThruMode { pub struct ExtScsiPassThruProtocol { pub passthru_mode: *const ExtScsiPassThruMode, pub pass_thru: unsafe extern "efiapi" fn( - this: *const Self, + this: *mut Self, target: *const u8, lun: u64, packet: *mut ScsiIoScsiRequestPacket, @@ -208,7 +208,7 @@ pub struct ExtScsiPassThruProtocol { ) -> Status, pub reset_channel: unsafe extern "efiapi" fn(this: *mut Self) -> Status, pub reset_target_lun: - unsafe extern "efiapi" fn(this: *const Self, target: *const u8, lun: u64) -> Status, + unsafe extern "efiapi" fn(this: *mut Self, target: *const u8, lun: u64) -> Status, pub get_next_target: unsafe extern "efiapi" fn(this: *const Self, target: *mut *mut u8) -> Status, } diff --git a/uefi-test-runner/src/proto/mod.rs b/uefi-test-runner/src/proto/mod.rs index e39b525fd..2c33e22bd 100644 --- a/uefi-test-runner/src/proto/mod.rs +++ b/uefi-test-runner/src/proto/mod.rs @@ -25,6 +25,7 @@ pub fn test() { shell_params::test(); string::test(); misc::test(); + scsi::test(); #[cfg(any( target_arch = "x86", @@ -73,6 +74,7 @@ mod misc; mod network; mod pi; mod rng; +mod scsi; mod shell_params; #[cfg(any( target_arch = "x86", diff --git a/uefi-test-runner/src/proto/scsi/mod.rs b/uefi-test-runner/src/proto/scsi/mod.rs new file mode 100644 index 000000000..1c28fa962 --- /dev/null +++ b/uefi-test-runner/src/proto/scsi/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +mod pass_thru; + +pub fn test() { + pass_thru::test(); +} diff --git a/uefi-test-runner/src/proto/scsi/pass_thru.rs b/uefi-test-runner/src/proto/scsi/pass_thru.rs new file mode 100644 index 000000000..e0a16e836 --- /dev/null +++ b/uefi-test-runner/src/proto/scsi/pass_thru.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use uefi::proto::scsi::pass_thru::ExtScsiPassThru; +use uefi::proto::scsi::ScsiRequestBuilder; + +pub fn test() { + info!("Running extended SCSI Pass Thru tests"); + test_allocating_api(); + test_reusing_buffer_api(); +} + +fn test_allocating_api() { + let scsi_ctrl_handles = uefi::boot::find_handles::<ExtScsiPassThru>().unwrap(); + + // On I440FX and Q35 (both x86 machines), Qemu configures an IDE and a SATA controller + // by default respectively. We manually configure an additional SCSI controller. + // Thus, we should see two controllers with support for EXT_SCSI_PASS_THRU on this platform + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + assert_eq!(scsi_ctrl_handles.len(), 2); + #[cfg(any(target_arch = "arm", target_arch = "aarch64"))] + assert_eq!(scsi_ctrl_handles.len(), 1); + + let mut found_drive = false; + for handle in scsi_ctrl_handles { + let scsi_pt = uefi::boot::open_protocol_exclusive::<ExtScsiPassThru>(handle).unwrap(); + for mut device in scsi_pt.iter_devices() { + // see: https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf + // 3.6 INQUIRY command + let request = ScsiRequestBuilder::read(scsi_pt.io_align()) + .with_timeout(core::time::Duration::from_millis(500)) + .with_command_data(&[0x12, 0x00, 0x00, 0x00, 0xFF, 0x00]) + .unwrap() + .with_read_buffer(255) + .unwrap() + .build(); + let Ok(response) = device.execute_command(request) else { + continue; // no device + }; + let bfr = response.read_buffer().unwrap(); + // more no device checks + if bfr.len() < 32 { + continue; + } + if bfr[0] & 0b00011111 == 0x1F { + continue; + } + + // found device + let vendor_id = core::str::from_utf8(&bfr[8..16]).unwrap().trim(); + let product_id = core::str::from_utf8(&bfr[16..32]).unwrap().trim(); + if vendor_id == "uefi-rs" && product_id == "ExtScsiPassThru" { + info!( + "Found Testdisk at: {:?} | {}", + device.target(), + device.lun() + ); + found_drive = true; + } + } + } + + assert!(found_drive); +} + +fn test_reusing_buffer_api() { + let scsi_ctrl_handles = uefi::boot::find_handles::<ExtScsiPassThru>().unwrap(); + + let mut found_drive = false; + for handle in scsi_ctrl_handles { + let scsi_pt = uefi::boot::open_protocol_exclusive::<ExtScsiPassThru>(handle).unwrap(); + let mut cmd_bfr = scsi_pt.alloc_io_buffer(6).unwrap(); + cmd_bfr.copy_from_slice(&[0x12, 0x00, 0x00, 0x00, 0xFF, 0x00]); + let mut read_bfr = scsi_pt.alloc_io_buffer(255).unwrap(); + + for mut device in scsi_pt.iter_devices() { + // see: https://www.seagate.com/files/staticfiles/support/docs/manual/Interface%20manuals/100293068j.pdf + // 3.6 INQUIRY command + let request = ScsiRequestBuilder::read(scsi_pt.io_align()) + .with_timeout(core::time::Duration::from_millis(500)) + .use_command_buffer(&mut cmd_bfr) + .unwrap() + .use_read_buffer(&mut read_bfr) + .unwrap() + .build(); + let Ok(response) = device.execute_command(request) else { + continue; // no device + }; + let bfr = response.read_buffer().unwrap(); + // more no device checks + if bfr.len() < 32 { + continue; + } + if bfr[0] & 0b00011111 == 0x1F { + continue; + } + + // found device + let vendor_id = core::str::from_utf8(&bfr[8..16]).unwrap().trim(); + let product_id = core::str::from_utf8(&bfr[16..32]).unwrap().trim(); + if vendor_id == "uefi-rs" && product_id == "ExtScsiPassThru" { + info!( + "Found Testdisk at: {:?} | {}", + device.target(), + device.lun() + ); + found_drive = true; + } + } + } + + assert!(found_drive); +} diff --git a/uefi/CHANGELOG.md b/uefi/CHANGELOG.md index 943acc636..b7ed4ddf4 100644 --- a/uefi/CHANGELOG.md +++ b/uefi/CHANGELOG.md @@ -8,6 +8,7 @@ - Added `mem::AlignedBuffer`. - Added `proto::device_path::DevicePath::append_path()`. - Added `proto::device_path::DevicePath::append_node()`. +- Added `proto::scsi::pass_thru::ExtScsiPassThru`. ## Changed - **Breaking:** Removed `BootPolicyError` as `BootPolicy` construction is no diff --git a/uefi/src/proto/mod.rs b/uefi/src/proto/mod.rs index e77f856a0..b9ce30081 100644 --- a/uefi/src/proto/mod.rs +++ b/uefi/src/proto/mod.rs @@ -20,6 +20,8 @@ pub mod misc; pub mod network; pub mod pi; pub mod rng; +#[cfg(feature = "alloc")] +pub mod scsi; pub mod security; pub mod shell_params; pub mod shim; diff --git a/uefi/src/proto/scsi/mod.rs b/uefi/src/proto/scsi/mod.rs new file mode 100644 index 000000000..a96e56278 --- /dev/null +++ b/uefi/src/proto/scsi/mod.rs @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! SCSI Bus specific protocols. + +use crate::mem::{AlignedBuffer, AlignmentError}; +use core::alloc::LayoutError; +use core::marker::PhantomData; +use core::ptr; +use core::time::Duration; +use uefi_raw::protocol::scsi::{ + ScsiIoDataDirection, ScsiIoHostAdapterStatus, ScsiIoScsiRequestPacket, ScsiIoTargetStatus, +}; + +pub mod pass_thru; + +/// Represents the data direction for a SCSI request. +/// +/// Used to specify whether the request involves reading, writing, or bidirectional data transfer. +pub type ScsiRequestDirection = uefi_raw::protocol::scsi::ScsiIoDataDirection; + +/// Represents a SCSI request packet. +/// +/// This structure encapsulates the necessary data for sending a command to a SCSI device. +#[derive(Debug)] +pub struct ScsiRequest<'a> { + packet: ScsiIoScsiRequestPacket, + io_align: u32, + in_data_buffer: Option<AlignedBuffer>, + out_data_buffer: Option<AlignedBuffer>, + sense_data_buffer: Option<AlignedBuffer>, + cdb_buffer: Option<AlignedBuffer>, + _phantom: PhantomData<&'a u8>, +} + +/// A builder for constructing [`ScsiRequest`] instances. +/// +/// Provides a safe and ergonomic interface for configuring SCSI request packets, including timeout, +/// data buffers, and command descriptor blocks. +#[derive(Debug)] +pub struct ScsiRequestBuilder<'a> { + req: ScsiRequest<'a>, +} +impl ScsiRequestBuilder<'_> { + /// Creates a new instance with the specified data direction and alignment. + /// + /// # Parameters + /// - `direction`: Specifies the direction of data transfer (READ, WRITE, or BIDIRECTIONAL). + /// - `io_align`: Specifies the required alignment for data buffers. (SCSI Controller specific!) + #[must_use] + pub fn new(direction: ScsiRequestDirection, io_align: u32) -> Self { + Self { + req: ScsiRequest { + in_data_buffer: None, + out_data_buffer: None, + sense_data_buffer: None, + cdb_buffer: None, + packet: ScsiIoScsiRequestPacket { + timeout: 0, + in_data_buffer: ptr::null_mut(), + out_data_buffer: ptr::null_mut(), + sense_data: ptr::null_mut(), + cdb: ptr::null_mut(), + in_transfer_length: 0, + out_transfer_length: 0, + cdb_length: 0, + data_direction: direction, + host_adapter_status: ScsiIoHostAdapterStatus::default(), + target_status: ScsiIoTargetStatus::default(), + sense_data_length: 0, + }, + io_align, + _phantom: Default::default(), + }, + } + } + + /// Starts a new builder preconfigured for READ operations. + /// + /// Some examples of SCSI read commands are: + /// - INQUIRY + /// - READ + /// - MODE_SENSE + /// + /// # Parameters + /// - `io_align`: Specifies the required alignment for data buffers. + #[must_use] + pub fn read(io_align: u32) -> Self { + Self::new(ScsiIoDataDirection::READ, io_align) + } + + /// Starts a new builder preconfigured for WRITE operations. + /// + /// Some examples of SCSI write commands are: + /// - WRITE + /// - MODE_SELECT + /// + /// # Parameters + /// - `io_align`: Specifies the required alignment for data buffers. + #[must_use] + pub fn write(io_align: u32) -> Self { + Self::new(ScsiIoDataDirection::WRITE, io_align) + } + + /// Starts a new builder preconfigured for BIDIRECTIONAL operations. + /// + /// Some examples of SCSI bidirectional commands are: + /// - SEND DIAGNOSTIC + /// + /// # Parameters + /// - `io_align`: Specifies the required alignment for data buffers. + #[must_use] + pub fn bidirectional(io_align: u32) -> Self { + Self::new(ScsiIoDataDirection::BIDIRECTIONAL, io_align) + } +} + +impl<'a> ScsiRequestBuilder<'a> { + /// Sets a timeout for the SCSI request. + /// + /// # Parameters + /// - `timeout`: A [`Duration`] representing the maximum time allowed for the request. + /// The value is converted to 100-nanosecond units. + /// + /// # Description + /// By default (without calling this method, or by calling with [`Duration::ZERO`]), + /// SCSI requests have no timeout. + /// Setting a timeout here will cause SCSI commands to potentially fail with [`crate::Status::TIMEOUT`]. + #[must_use] + pub const fn with_timeout(mut self, timeout: Duration) -> Self { + self.req.packet.timeout = (timeout.as_nanos() / 100) as u64; + self + } + + // # IN BUFFER + // ######################################################################################## + + /// Uses a user-supplied buffer for reading data from the device. + /// + /// # Parameters + /// - `bfr`: A mutable reference to an [`AlignedBuffer`] that will be used to store data read from the device. + /// + /// # Returns + /// `Result<Self, AlignmentError>` indicating success or an alignment issue with the provided buffer. + /// + /// # Description + /// This method checks the alignment of the buffer against the protocol's requirements and assigns it to + /// the `in_data_buffer` of the underlying `ScsiRequest`. + pub fn use_read_buffer(mut self, bfr: &'a mut AlignedBuffer) -> Result<Self, AlignmentError> { + // check alignment of externally supplied buffer + bfr.check_alignment(self.req.io_align as usize)?; + self.req.in_data_buffer = None; + self.req.packet.in_data_buffer = bfr.ptr_mut().cast(); + self.req.packet.in_transfer_length = bfr.size() as u32; + Ok(self) + } + + /// Adds a newly allocated read buffer to the built SCSI request. + /// + /// # Parameters + /// - `len`: The size of the buffer (in bytes) to allocate for receiving data. + /// + /// # Returns + /// `Result<Self, LayoutError>` indicating success or a memory allocation error. + pub fn with_read_buffer(mut self, len: usize) -> Result<Self, LayoutError> { + let mut bfr = AlignedBuffer::from_size_align(len, self.req.io_align as usize)?; + self.req.packet.in_data_buffer = bfr.ptr_mut().cast(); + self.req.packet.in_transfer_length = bfr.size() as u32; + self.req.in_data_buffer = Some(bfr); + Ok(self) + } + + // # SENSE BUFFER + // ######################################################################################## + + /// Adds a newly allocated sense buffer to the built SCSI request. + /// + /// # Parameters + /// - `len`: The size of the buffer (in bytes) to allocate for receiving sense data. + /// + /// # Returns + /// `Result<Self, LayoutError>` indicating success or a memory allocation error. + pub fn with_sense_buffer(mut self, len: u8) -> Result<Self, LayoutError> { + let mut bfr = AlignedBuffer::from_size_align(len as usize, self.req.io_align as usize)?; + self.req.packet.sense_data = bfr.ptr_mut().cast(); + self.req.packet.sense_data_length = len; + self.req.sense_data_buffer = Some(bfr); + Ok(self) + } + + // # WRITE BUFFER + // ######################################################################################## + + /// Uses a user-supplied buffer for writing data to the device. + /// + /// # Parameters + /// - `bfr`: A mutable reference to an [`AlignedBuffer`] containing the data to be written to the device. + /// + /// # Returns + /// `Result<Self, AlignmentError>` indicating success or an alignment issue with the provided buffer. + /// + /// # Description + /// This method checks the alignment of the buffer against the protocol's requirements and assigns it to + /// the `out_data_buffer` of the underlying `ScsiRequest`. + pub fn use_write_buffer(mut self, bfr: &'a mut AlignedBuffer) -> Result<Self, AlignmentError> { + // check alignment of externally supplied buffer + bfr.check_alignment(self.req.io_align as usize)?; + self.req.out_data_buffer = None; + self.req.packet.out_data_buffer = bfr.ptr_mut().cast(); + self.req.packet.out_transfer_length = bfr.size() as u32; + Ok(self) + } + + /// Adds a newly allocated write buffer to the built SCSI request that is filled from the + /// given data buffer. (Done for memory alignment and lifetime purposes) + /// + /// # Parameters + /// - `data`: A slice of bytes representing the data to be written. + /// + /// # Returns + /// `Result<Self, LayoutError>` indicating success or a memory allocation error. + pub fn with_write_data(mut self, data: &[u8]) -> Result<Self, LayoutError> { + let mut bfr = AlignedBuffer::from_size_align(data.len(), self.req.io_align as usize)?; + bfr.copy_from_slice(data); + self.req.packet.out_data_buffer = bfr.ptr_mut().cast(); + self.req.packet.out_transfer_length = bfr.size() as u32; + self.req.out_data_buffer = Some(bfr); + Ok(self) + } + + // # COMMAND BUFFER + // ######################################################################################## + + /// Uses a user-supplied Command Data Block (CDB) buffer. + /// + /// # Parameters + /// - `data`: A mutable reference to an [`AlignedBuffer`] containing the CDB to be sent to the device. + /// + /// # Returns + /// `Result<Self, AlignmentError>` indicating success or an alignment issue with the provided buffer. + /// + /// # Notes + /// The maximum length of a CDB is 255 bytes. + pub fn use_command_buffer( + mut self, + data: &'a mut AlignedBuffer, + ) -> Result<Self, AlignmentError> { + assert!(data.size() <= 255); + // check alignment of externally supplied buffer + data.check_alignment(self.req.io_align as usize)?; + self.req.cdb_buffer = None; + self.req.packet.cdb = data.ptr_mut().cast(); + self.req.packet.cdb_length = data.size() as u8; + Ok(self) + } + + /// Adds a newly allocated Command Data Block (CDB) buffer to the built SCSI request that is filled from the + /// given data buffer. (Done for memory alignment and lifetime purposes) + /// + /// # Parameters + /// - `data`: A slice of bytes representing the command to be sent. + /// + /// # Returns + /// `Result<Self, LayoutError>` indicating success or a memory allocation error. + /// + /// # Notes + /// The maximum length of a CDB is 255 bytes. + pub fn with_command_data(mut self, data: &[u8]) -> Result<Self, LayoutError> { + assert!(data.len() <= 255); + let mut bfr = AlignedBuffer::from_size_align(data.len(), self.req.io_align as usize)?; + bfr.copy_from_slice(data); + self.req.packet.cdb = bfr.ptr_mut().cast(); + self.req.packet.cdb_length = bfr.size() as u8; + self.req.cdb_buffer = Some(bfr); + Ok(self) + } + + /// Build the final `ScsiRequest`. + /// + /// # Returns + /// A fully-configured [`ScsiRequest`] ready for execution. + #[must_use] + pub fn build(self) -> ScsiRequest<'a> { + self.req + } +} + +/// Represents the response of a SCSI request. +/// +/// This struct encapsulates the results of a SCSI operation, including data buffers +/// for read and sense data, as well as status codes returned by the host adapter and target device. +#[derive(Debug)] +#[repr(transparent)] +pub struct ScsiResponse<'a>(ScsiRequest<'a>); +impl<'a> ScsiResponse<'a> { + /// Retrieves the buffer containing data read from the device (if any). + /// + /// # Returns + /// `Option<&[u8]>`: A slice of the data read from the device, or `None` if no read buffer was assigned. + /// + /// # Safety + /// - If the buffer pointer is `NULL`, the method returns `None` and avoids dereferencing it. + #[must_use] + pub fn read_buffer(&self) -> Option<&'a [u8]> { + if self.0.packet.in_data_buffer.is_null() { + return None; + } + unsafe { + Some(core::slice::from_raw_parts( + self.0.packet.in_data_buffer.cast(), + self.0.packet.in_transfer_length as usize, + )) + } + } + + /// Retrieves the buffer containing sense data returned by the device (if any). + /// + /// # Returns + /// `Option<&[u8]>`: A slice of the sense data, or `None` if no sense data buffer was assigned. + /// + /// # Safety + /// - If the buffer pointer is `NULL`, the method returns `None` and avoids dereferencing it. + #[must_use] + pub fn sense_data(&self) -> Option<&'a [u8]> { + if self.0.packet.sense_data.is_null() { + return None; + } + unsafe { + Some(core::slice::from_raw_parts( + self.0.packet.sense_data.cast(), + self.0.packet.sense_data_length as usize, + )) + } + } + + /// Retrieves the status of the host adapter after executing the SCSI request. + /// + /// # Returns + /// [`ScsiIoHostAdapterStatus`]: The status code indicating the result of the operation from the host adapter. + #[must_use] + pub const fn host_adapter_status(&self) -> ScsiIoHostAdapterStatus { + self.0.packet.host_adapter_status + } + + /// Retrieves the status of the target device after executing the SCSI request. + /// + /// # Returns + /// [`ScsiIoTargetStatus`]: The status code returned by the target device. + #[must_use] + pub const fn target_status(&self) -> ScsiIoTargetStatus { + self.0.packet.target_status + } +} diff --git a/uefi/src/proto/scsi/pass_thru.rs b/uefi/src/proto/scsi/pass_thru.rs new file mode 100644 index 000000000..5d256d184 --- /dev/null +++ b/uefi/src/proto/scsi/pass_thru.rs @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Extended SCSI Pass Thru protocols. + +use super::{ScsiRequest, ScsiResponse}; +use crate::mem::{AlignedBuffer, PoolAllocation}; +use crate::proto::device_path::PoolDevicePathNode; +use crate::proto::unsafe_protocol; +use crate::StatusExt; +use core::alloc::LayoutError; +use core::ptr::{self, NonNull}; +use uefi_raw::protocol::device_path::DevicePathProtocol; +use uefi_raw::protocol::scsi::{ + ExtScsiPassThruMode, ExtScsiPassThruProtocol, SCSI_TARGET_MAX_BYTES, +}; +use uefi_raw::Status; + +/// Structure representing a SCSI target address. +pub type ScsiTarget = [u8; SCSI_TARGET_MAX_BYTES]; + +/// Structure representing a fully-qualified device address, consisting of SCSI target and LUN. +#[derive(Clone, Debug)] +pub struct ScsiTargetLun(ScsiTarget, u64); +impl Default for ScsiTargetLun { + fn default() -> Self { + Self([0xFF; SCSI_TARGET_MAX_BYTES], 0) + } +} + +/// Enables interaction with SCSI devices using the Extended SCSI Pass Thru protocol. +/// +/// This protocol allows communication with SCSI devices connected to the system, +/// providing methods to send commands, reset devices, and enumerate SCSI targets. +/// +/// This API offers a safe and convenient, yet still low-level interface to SCSI devices. +/// It is designed as a foundational layer, leaving higher-level abstractions responsible for implementing +/// richer storage semantics, device-specific commands, and advanced use cases. +/// +/// # UEFI Spec Description +/// Provides services that allow SCSI Pass Thru commands to be sent to SCSI devices attached to a SCSI channel. It also +/// allows packet-based commands (ATAPI cmds) to be sent to ATAPI devices attached to a ATA controller. +#[derive(Debug)] +#[repr(transparent)] +#[unsafe_protocol(ExtScsiPassThruProtocol::GUID)] +pub struct ExtScsiPassThru(ExtScsiPassThruProtocol); + +impl ExtScsiPassThru { + /// Retrieves the mode structure for the Extended SCSI Pass Thru protocol. + /// + /// # Returns + /// The [`ExtScsiPassThruMode`] structure containing configuration details of the protocol. + #[must_use] + pub fn mode(&self) -> ExtScsiPassThruMode { + let mut mode = unsafe { (*self.0.passthru_mode).clone() }; + mode.io_align = mode.io_align.max(1); // 0 and 1 is the same, says UEFI spec + mode + } + + /// Retrieves the I/O buffer alignment required by this SCSI channel. + /// + /// # Returns + /// - A `u32` value representing the required I/O alignment. + #[must_use] + pub fn io_align(&self) -> u32 { + self.mode().io_align + } + + /// Allocates an I/O buffer with the necessary alignment for this SCSI channel. + /// + /// You can alternatively do this yourself using the [`AlignedBuffer`] helper directly. + /// The Scsi api will validate that your buffers have the correct alignment and crash + /// if they don't. + /// + /// # Parameters + /// - `len`: The size (in bytes) of the buffer to allocate. + /// + /// # Returns + /// [`AlignedBuffer`] containing the allocated memory. + /// + /// # Errors + /// This method can fail due to alignment or memory allocation issues. + pub fn alloc_io_buffer(&self, len: usize) -> Result<AlignedBuffer, LayoutError> { + AlignedBuffer::from_size_align(len, self.io_align() as usize) + } + + /// Iterate over all potential SCSI devices on this channel. + /// + /// # Warning + /// Depending on the UEFI implementation, this does not only return all actually available devices. + /// Most implementations instead return a list of all possible fully-qualified device addresses. + /// You have to probe for availability yourself, using [`ScsiDevice::execute_command`]. + /// + /// # Returns + /// [`ScsiTargetLunIterator`] to iterate through connected SCSI devices. + #[must_use] + pub fn iter_devices(&self) -> ScsiTargetLunIterator<'_> { + ScsiTargetLunIterator { + proto: &self.0, + prev: ScsiTargetLun::default(), + } + } + + /// Resets the SCSI channel associated with the protocol. + /// + /// The EFI_EXT_SCSI_PASS_THRU_PROTOCOL.ResetChannel() function resets a SCSI channel. + /// This operation resets all the SCSI devices connected to the SCSI channel. + /// + /// # Returns + /// [`Result<()>`] indicating the success or failure of the operation. + /// + /// # Errors + /// - [`Status::UNSUPPORTED`] The SCSI channel does not support a channel reset operation. + /// - [`Status::DEVICE_ERROR`] A device error occurred while attempting to reset the SCSI channel. + /// - [`Status::TIMEOUT`] A timeout occurred while attempting to reset the SCSI channel. + pub fn reset_channel(&mut self) -> crate::Result<()> { + unsafe { (self.0.reset_channel)(&mut self.0).to_result() } + } +} + +/// Structure representing a potential ScsiDevice. +/// +/// In the UEFI Specification, this corresponds to a (SCSI target, LUN) tuple. +/// +/// # Warning +/// This does not actually have to correspond to an actual device! +/// You have to probe for availability before doing anything meaningful with it. +#[derive(Clone, Debug)] +pub struct ScsiDevice<'a> { + proto: &'a ExtScsiPassThruProtocol, + target_lun: ScsiTargetLun, +} +impl ScsiDevice<'_> { + fn proto_mut(&mut self) -> *mut ExtScsiPassThruProtocol { + ptr::from_ref(self.proto).cast_mut() + } + + /// Returns the SCSI target address of the potential device. + #[must_use] + pub const fn target(&self) -> &ScsiTarget { + &self.target_lun.0 + } + + /// Returns the logical unit number (LUN) of the potential device. + #[must_use] + pub const fn lun(&self) -> u64 { + self.target_lun.1 + } + + /// Get the final device path node for this device. + /// + /// For a full [`crate::proto::device_path::DevicePath`] pointing to this device, this needs to be appended to + /// the controller's device path. + pub fn path_node(&self) -> crate::Result<PoolDevicePathNode> { + unsafe { + let mut path_ptr: *const DevicePathProtocol = ptr::null(); + (self.proto.build_device_path)( + self.proto, + self.target().as_ptr(), + self.lun(), + &mut path_ptr, + ) + .to_result()?; + NonNull::new(path_ptr.cast_mut()) + .map(|p| PoolDevicePathNode(PoolAllocation::new(p.cast()))) + .ok_or(Status::OUT_OF_RESOURCES.into()) + } + } + + /// Resets the potential SCSI device represented by this instance. + /// + /// The `EFI_EXT_SCSI_PASS_THRU_PROTOCOL.ResetTargetLun()` function resets the SCSI logical unit + /// specified by `Target` and `Lun`. This allows for recovering a device that may be in an error state + /// or requires reinitialization. The function behavior is dependent on the SCSI channel's capability + /// to perform target resets. + /// + /// # Returns + /// [`Result<()>`] indicating the success or failure of the operation. + /// + /// # Errors + /// - [`Status::UNSUPPORTED`] The SCSI channel does not support a target reset operation. + /// - [`Status::INVALID_PARAMETER`] The `Target` or `Lun` values are invalid. + /// - [`Status::DEVICE_ERROR`] A device error occurred while attempting to reset the SCSI device + /// specified by `Target` and `Lun`. + /// - [`Status::TIMEOUT`] A timeout occurred while attempting to reset the SCSI device specified + /// by `Target` and `Lun`. + pub fn reset(&mut self) -> crate::Result<()> { + unsafe { + (self.proto.reset_target_lun)(self.proto_mut(), self.target_lun.0.as_ptr(), self.lun()) + .to_result() + } + } + + /// Sends a SCSI command to the potential target device and retrieves the response. + /// + /// This method sends a SCSI Request Packet to a SCSI device attached to the SCSI channel. + /// It supports both blocking and nonblocking I/O. Blocking I/O is mandatory, while + /// nonblocking I/O is optional and dependent on the driver's implementation. + /// + /// # Parameters + /// - `scsi_req`: The [`ScsiRequest`] containing the command and data to send to the device. + /// + /// # Returns + /// [`ScsiResponse`] containing the results of the operation, such as data and status. + /// + /// # Errors + /// - [`Status::BAD_BUFFER_SIZE`] The SCSI Request Packet was not executed because the data + /// buffer size exceeded the allowed transfer size for a single command. The number of bytes + /// that could be transferred is returned in `InTransferLength` or `OutTransferLength`. + /// - [`Status::NOT_READY`] The SCSI Request Packet could not be sent because too many packets + /// are already queued. The caller may retry later. + /// - [`Status::DEVICE_ERROR`] A device error occurred while attempting to send the SCSI Request Packet. + /// Additional status information is available in `HostAdapterStatus`, `TargetStatus`, `SenseDataLength`, + /// and `SenseData`. + /// - [`Status::INVALID_PARAMETER`] The `Target`, `Lun`, or the contents of `ScsiRequestPacket` are invalid. + /// The SCSI Request Packet was not sent, and no additional status information is available. + /// - [`Status::UNSUPPORTED`] The command described by the SCSI Request Packet is not supported by the + /// host adapter, including unsupported bi-directional SCSI commands. The SCSI Request Packet was not + /// sent, and no additional status information is available. + /// - [`Status::TIMEOUT`] A timeout occurred while executing the SCSI Request Packet. Additional status + /// information is available in `HostAdapterStatus`, `TargetStatus`, `SenseDataLength`, and `SenseData`. + pub fn execute_command<'req>( + &mut self, + mut scsi_req: ScsiRequest<'req>, + ) -> crate::Result<ScsiResponse<'req>> { + unsafe { + (self.proto.pass_thru)( + self.proto_mut(), + self.target_lun.0.as_ptr(), + self.target_lun.1, + &mut scsi_req.packet, + ptr::null_mut(), + ) + .to_result_with_val(|| ScsiResponse(scsi_req)) + } + } +} + +/// An iterator over SCSI devices available on the channel. +#[derive(Debug)] +pub struct ScsiTargetLunIterator<'a> { + proto: &'a ExtScsiPassThruProtocol, + prev: ScsiTargetLun, +} +impl<'a> Iterator for ScsiTargetLunIterator<'a> { + type Item = ScsiDevice<'a>; + + fn next(&mut self) -> Option<Self::Item> { + // get_next_target_lun() takes the target as a double ptr, meaning that the spec allows + // the implementation to return us a new buffer (most impls don't actually seem to do though) + let mut target: *mut u8 = self.prev.0.as_mut_ptr(); + let result = + unsafe { (self.proto.get_next_target_lun)(self.proto, &mut target, &mut self.prev.1) }; + if target != self.prev.0.as_mut_ptr() { + // impl has returned us a new pointer instead of writing in our buffer, copy back + unsafe { + target.copy_to(self.prev.0.as_mut_ptr(), SCSI_TARGET_MAX_BYTES); + } + } + let scsi_device = ScsiDevice { + proto: self.proto, + target_lun: self.prev.clone(), + }; + match result { + Status::SUCCESS => Some(scsi_device), + Status::NOT_FOUND => None, + _ => panic!("Must not happen according to spec!"), + } + } +}