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!"),
+        }
+    }
+}