Skip to content
  • Sponsor
  • Notifications You must be signed in to change notification settings
  • Fork 166
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add safe protocol wrapper for EFI_ATA_PASS_THRU_PROTOCOL #1595

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 28 additions & 26 deletions uefi-raw/src/protocol/ata.rs
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ bitflags::bitflags! {
}
}

#[derive(Debug)]
#[derive(Clone, Debug)]
#[repr(C)]
pub struct AtaPassThruMode {
pub attributes: AtaPassThruAttributes,
@@ -76,37 +76,37 @@ newtype_enum! {
#[repr(C)]
pub struct AtaStatusBlock {
pub reserved1: [u8; 2],
pub ata_status: u8,
pub ata_error: u8,
pub ata_sector_number: u8,
pub ata_cylinder_low: u8,
pub ata_cylinder_high: u8,
pub ata_device_head: u8,
pub ata_sector_number_exp: u8,
pub ata_cylinder_low_exp: u8,
pub ata_cylinder_high_exp: u8,
pub status: u8,
pub error: u8,
pub sector_number: u8,
pub cylinder_low: u8,
pub cylinder_high: u8,
pub device_head: u8,
pub sector_number_exp: u8,
pub cylinder_low_exp: u8,
pub cylinder_high_exp: u8,
pub reserved2: u8,
pub ata_sector_count: u8,
pub ata_sector_count_exp: u8,
pub sector_count: u8,
pub sector_count_exp: u8,
pub reserved3: [u8; 6],
}

#[derive(Debug)]
#[derive(Debug, Default)]
#[repr(C)]
pub struct AtaCommandBlock {
pub reserved1: [u8; 2],
pub ata_command: u8,
pub ata_features: u8,
pub ata_sector_number: u8,
pub ata_cylinder_low: u8,
pub ata_cylinder_high: u8,
pub ata_device_head: u8,
pub ata_sector_number_exp: u8,
pub ata_cylinder_low_exp: u8,
pub ata_cylinder_high_exp: u8,
pub ata_features_exp: u8,
pub ata_sector_count: u8,
pub ata_sector_count_exp: u8,
pub command: u8,
pub features: u8,
pub sector_number: u8,
pub cylinder_low: u8,
pub cylinder_high: u8,
pub device_head: u8,
pub sector_number_exp: u8,
pub cylinder_low_exp: u8,
pub cylinder_high_exp: u8,
pub features_exp: u8,
pub sector_count: u8,
pub sector_count_exp: u8,
pub reserved2: [u8; 6],
}

@@ -118,6 +118,8 @@ pub struct AtaPassThruCommandPacket {
pub timeout: u64,
pub in_data_buffer: *mut c_void,
pub out_data_buffer: *const c_void,
pub in_transfer_length: u32,
pub out_transfer_length: u32,
pub protocol: AtaPassThruCommandProtocol,
pub length: AtaPassThruLength,
}
@@ -131,7 +133,7 @@ pub struct AtaPassThruProtocol {
port: u16,
port_multiplier_port: u16,
packet: *mut AtaPassThruCommandPacket,
event: *mut Event,
event: Event,
) -> Status,
pub get_next_port: unsafe extern "efiapi" fn(this: *const Self, port: *mut u16) -> Status,
pub get_next_device: unsafe extern "efiapi" fn(
7 changes: 7 additions & 0 deletions uefi-test-runner/src/proto/ata/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

mod pass_thru;

pub fn test() {
pass_thru::test();
}
57 changes: 57 additions & 0 deletions uefi-test-runner/src/proto/ata/pass_thru.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

use uefi::boot;
use uefi::boot::{OpenProtocolAttributes, OpenProtocolParams};
use uefi::proto::ata::pass_thru::AtaPassThru;
use uefi::proto::ata::AtaRequestBuilder;

pub fn test() {
info!("Running ATA PassThru tests");

assert!(is_testdrive_present());
}

fn is_testdrive_present() -> bool {
let ata_ctrl_handles = boot::find_handles::<AtaPassThru>().unwrap();
assert_eq!(ata_ctrl_handles.len(), 1);

for handle in ata_ctrl_handles {
let params = OpenProtocolParams {
handle,
agent: boot::image_handle(),
controller: None,
};
let ata_pt = unsafe {
// don't open exclusive! That would break other tests
boot::open_protocol::<AtaPassThru>(params, OpenProtocolAttributes::GetProtocol).unwrap()
Copy link
Member

@phip1611 phip1611 Mar 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but why? They do not run concurrently but sequentially

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea. Maybe it unloads a driver that's required for later tests when I open it exclusively.
Using open_protocol_exclusive here results in:

[PANIC]: panicked at uefi-test-runner/src/proto/device_path.rs:29:85:
called `Result::unwrap()` on an `Err` value: Error { status: INVALID_PARAMETER, data: () }

};
for mut device in ata_pt.iter_devices() {
// ATA IDENTIFY command
let request = AtaRequestBuilder::read_udma(ata_pt.io_align(), 0xEC)
.unwrap()
.with_timeout(core::time::Duration::from_millis(500))
.with_read_buffer(255)
.unwrap()
.build();
if let Ok(result) = device.execute_command(request) {
let bfr = result.read_buffer().unwrap();
// what the...
let mut serial_bfr = [0u8; 20];
bfr[20..40]
.chunks_exact(2)
.zip(serial_bfr.chunks_exact_mut(2))
.for_each(|(src, dst)| {
dst[0] = src[1];
dst[1] = src[0];
});
let serial = core::str::from_utf8(&serial_bfr).unwrap().trim();
if serial == "AtaPassThru" {
info!("Found Testdisk at handle: {:?}", handle);
return true; // found our testdrive!
}
}
}
}

false
}
4 changes: 4 additions & 0 deletions uefi-test-runner/src/proto/media.rs
Original file line number Diff line number Diff line change
@@ -398,7 +398,11 @@ fn test_partition_info(disk_handle: Handle) {
fn find_test_disk() -> (Handle, ScopedProtocol<SimpleFileSystem>) {
let handles = boot::find_handles::<SimpleFileSystem>()
.expect("Failed to get handles for `SimpleFileSystem` protocol");

#[cfg(any(target_arch = "arm", target_arch = "aarch64"))]
assert_eq!(handles.len(), 2);
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
assert_eq!(handles.len(), 3);

for handle in handles {
let mut sfs = boot::open_protocol_exclusive::<SimpleFileSystem>(handle)
4 changes: 4 additions & 0 deletions uefi-test-runner/src/proto/mod.rs
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@ pub fn test() {
test_protocols_per_handle();
test_test_protocol();

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
ata::test();
debug::test();
device_path::test();
driver::test();
@@ -62,6 +64,8 @@ fn test_test_protocol() {
.unwrap());
}

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
mod ata;
mod console;
mod debug;
mod device_path;
1 change: 1 addition & 0 deletions uefi/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
- Added conversions between `proto::network::IpAddress` and `core::net` types.
- Added conversions between `proto::network::MacAddress` and the `[u8; 6]` type that's more commonly used to represent MAC addresses.
- Added `proto::media::disk_info::DiskInfo`.
- Added `proto::ata::pass_thru::AtaPassThru`.

## Changed
- **Breaking:** Removed `BootPolicyError` as `BootPolicy` construction is no
104 changes: 104 additions & 0 deletions uefi/src/helpers/aligned_buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

use alloc::alloc::{alloc, dealloc, Layout, LayoutError};
use core::error::Error;
use core::fmt;

/// Helper class to maintain the lifetime of a memory region allocated with a non-standard alignment.
/// Facilitates RAII to properly deallocate when lifetime of the object ends.
///
/// Note: This uses the global Rust allocator under the hood.
#[allow(clippy::len_without_is_empty)]
#[derive(Debug)]
pub struct AlignedBuffer {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with keeping this struct. However, the helpers module is supposed to provide helpers to interact with the outer environment.

Therefore, I see better fit for AlignedBuffer in <uefi>/mem/mod.rs.

Are you fine with keeping the AlignedBuffer struct, @nicholasbishop ?

ptr: *mut u8,
layout: Layout,
}

impl AlignedBuffer {
/// Allocate a new memory region with the requested len and alignment.
pub fn alloc(len: usize, alignment: usize) -> Result<Self, LayoutError> {
let layout = Layout::from_size_align(len, alignment)?;
let ptr = unsafe { alloc(layout) };
Ok(Self { ptr, layout })
}

/// Get a pointer to the aligned memory region managed by this instance.
#[must_use]
pub const fn ptr(&self) -> *const u8 {
self.ptr.cast_const()
}

/// Get a mutable pointer to the aligned memory region managed by this instance.
#[must_use]
pub fn ptr_mut(&mut self) -> *mut u8 {
self.ptr
}

/// Get the size of the aligned memory region managed by this instance.
#[must_use]
pub const fn len(&self) -> usize {
self.layout.size()
}

/// Fill the aligned memory region with data from the given buffer.
pub fn copy_from(&mut self, data: &[u8]) {
let len = data.len().min(self.len());
unsafe {
self.ptr.copy_from(data.as_ptr(), len);
}
}

/// Check the buffer's alignment against the `required_alignment`.
pub fn check_alignment(&self, required_alignment: usize) -> Result<(), AlignmentError> {
//TODO: use bfr.addr() when it's available
if (self.ptr as usize) % required_alignment != 0 {
return Err(AlignmentError); //TODO: use >is_aligned_to< when it's available
}
Ok(())
}
}

impl Drop for AlignedBuffer {
fn drop(&mut self) {
unsafe {
dealloc(self.ptr, self.layout);
}
}
}

/// The `AlignmentError` is returned if a user-provided buffer doesn't fulfill alignment requirements.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct AlignmentError;
impl Error for AlignmentError {}
impl fmt::Display for AlignmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid parameters to Layout::from_size_align")
}
}

#[cfg(test)]
mod tests {
use super::AlignedBuffer;

#[test]
fn test_invalid_arguments() {
// invalid alignments, valid len
for request_alignment in [0, 3, 5, 7, 9] {
for request_len in [1, 32, 64, 128, 1024] {
assert!(AlignedBuffer::alloc(request_len, request_alignment).is_err());
}
}
}

#[test]
fn test_allocation_alignment() {
for request_alignment in [1, 2, 4, 8, 16, 32, 64, 128] {
for request_len in [1 as usize, 32, 64, 128, 1024] {
let buffer = AlignedBuffer::alloc(request_len, request_alignment).unwrap();
assert_eq!(buffer.ptr() as usize % request_alignment, 0);
assert_eq!(buffer.len(), request_len);
}
}
}
}
5 changes: 5 additions & 0 deletions uefi/src/helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -24,6 +24,11 @@ use crate::Result;
#[doc(hidden)]
pub use println::_print;

#[cfg(feature = "alloc")]
mod aligned_buffer;
#[cfg(feature = "alloc")]
pub use aligned_buffer::{AlignedBuffer, AlignmentError};

#[cfg(feature = "global_allocator")]
mod global_allocator;
#[cfg(feature = "logger")]
322 changes: 322 additions & 0 deletions uefi/src/proto/ata/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

//! ATA Protocols.
use crate::helpers::{AlignedBuffer, AlignmentError};
use core::alloc::LayoutError;
use core::marker::PhantomData;
use core::ptr;
use core::time::Duration;
use uefi_raw::protocol::ata::{
AtaCommandBlock, AtaPassThruCommandPacket, AtaPassThruLength, AtaStatusBlock,
};

pub mod pass_thru;

/// Represents the protocol for ATA Pass Thru command handling.
///
/// This type defines the protocols supported for passing commands through the ATA controller.
pub use uefi_raw::protocol::ata::AtaPassThruCommandProtocol;

/// Represents an ATA request built for execution on an ATA controller.
#[derive(Debug)]
pub struct AtaRequest<'a> {
io_align: u32,
acb: AtaCommandBlock,
packet: AtaPassThruCommandPacket,
in_data_buffer: Option<AlignedBuffer>,
out_data_buffer: Option<AlignedBuffer>,
asb: AlignedBuffer,
_phantom: PhantomData<&'a u8>,
}

/// Builder for creating and configuring an [`AtaRequest`].
///
/// This builder simplifies the creation of an [`AtaRequest`] by providing chainable methods for
/// configuring fields like timeout, buffers, and ATA command details.
#[derive(Debug)]
pub struct AtaRequestBuilder<'a> {
req: AtaRequest<'a>,
}

impl<'a> AtaRequestBuilder<'a> {
/// Creates a new [`AtaRequestBuilder`] with the specified alignment, command, and protocol.
///
/// # Parameters
/// - `io_align`: The I/O buffer alignment required for the ATA controller.
/// - `command`: The ATA command byte specifying the operation to execute.
/// - `protocol`: The protocol type for the command (e.g., DMA, UDMA, etc.).
///
/// # Returns
/// `Result<Self, LayoutError>` indicating success or memory allocation failure.
///
/// # Errors
/// This method can fail due to alignment or memory allocation issues.
fn new(
io_align: u32,
command: u8,
protocol: AtaPassThruCommandProtocol,
) -> Result<Self, LayoutError> {
// status block has alignment requirements!
let mut asb = AlignedBuffer::alloc(size_of::<AtaStatusBlock>(), io_align as usize)?;
Ok(Self {
req: AtaRequest {
io_align,
acb: AtaCommandBlock {
command,
..Default::default()
},
packet: AtaPassThruCommandPacket {
asb: asb.ptr_mut().cast(),
acb: ptr::null(), // filled during execution
timeout: 0,
in_data_buffer: ptr::null_mut(),
out_data_buffer: ptr::null(),
in_transfer_length: 0,
out_transfer_length: 0,
protocol,
length: AtaPassThruLength::BYTES,
},
in_data_buffer: None,
out_data_buffer: None,
asb,
_phantom: PhantomData,
},
})
}

/// Creates a builder for a UDMA read operation.
///
/// # Parameters
/// - `io_align`: The I/O buffer alignment required for the ATA controller.
/// - `command`: The ATA command byte specifying the read operation.
///
/// # Returns
/// `Result<Self, LayoutError>` indicating success or memory allocation failure.
///
/// # Errors
/// This method can fail due to alignment or memory allocation issues.
pub fn read_udma(io_align: u32, command: u8) -> Result<Self, LayoutError> {
Self::new(io_align, command, AtaPassThruCommandProtocol::UDMA_DATA_IN)
}

/// Creates a builder for a UDMA write operation.
///
/// # Parameters
/// - `io_align`: The I/O buffer alignment required for the ATA controller.
/// - `command`: The ATA command byte specifying the write operation.
///
/// # Returns
/// `Result<Self, LayoutError>` indicating success or memory allocation failure.
///
/// # Errors
/// This method can fail due to alignment or memory allocation issues.
pub fn write_udma(io_align: u32, command: u8) -> Result<Self, LayoutError> {
Self::new(io_align, command, AtaPassThruCommandProtocol::UDMA_DATA_OUT)
}

// ########################################################################

/// Configure the given timeout for this request.
#[must_use]
pub const fn with_timeout(mut self, timeout: Duration) -> Self {
self.req.packet.timeout = (timeout.as_nanos() / 100) as u64;
self
}

/// Configure the `features` field.
#[must_use]
pub const fn with_features(mut self, features: u8) -> Self {
self.req.acb.features = features;
self
}

/// Configure the `sector_number` field.
#[must_use]
pub const fn with_sector_number(mut self, sector_number: u8) -> Self {
self.req.acb.sector_number = sector_number;
self
}

/// Configure the `cylinder` fields (low and high combined).
#[must_use]
pub const fn with_cylinder(mut self, low: u8, high: u8) -> Self {
self.req.acb.cylinder_low = low;
self.req.acb.cylinder_high = high;
self
}

/// Configure the `device_head` field.
#[must_use]
pub const fn with_device_head(mut self, device_head: u8) -> Self {
self.req.acb.device_head = device_head;
self
}

/// Configure the `sector_number_exp` field.
#[must_use]
pub const fn with_sector_number_exp(mut self, sector_number_exp: u8) -> Self {
self.req.acb.sector_number_exp = sector_number_exp;
self
}

/// Configure the `cylinder_exp` fields (low and high combined).
#[must_use]
pub const fn with_cylinder_exp(mut self, low_exp: u8, high_exp: u8) -> Self {
self.req.acb.cylinder_low_exp = low_exp;
self.req.acb.cylinder_high_exp = high_exp;
self
}

/// Configure the `features_exp` field.
#[must_use]
pub const fn with_features_exp(mut self, features_exp: u8) -> Self {
self.req.acb.features_exp = features_exp;
self
}

/// Configure the `sector_count` field.
#[must_use]
pub const fn with_sector_count(mut self, sector_count: u8) -> Self {
self.req.acb.sector_count = sector_count;
self
}

/// Configure the `sector_count_exp` field.
#[must_use]
pub const fn with_sector_count_exp(mut self, sector_count_exp: u8) -> Self {
self.req.acb.sector_count_exp = sector_count_exp;
self
}

// # READ 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 [`AtaRequest`].
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.len() as u32;
Ok(self)
}

/// Adds a newly allocated read buffer to the built ATA 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::alloc(len, self.req.io_align as usize)?;
self.req.packet.in_data_buffer = bfr.ptr_mut().cast();
self.req.packet.in_transfer_length = bfr.len() as u32;
self.req.in_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 [`AtaRequest`].
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.len() as u32;
Ok(self)
}

/// Adds a newly allocated write buffer to the built ATA 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::alloc(data.len(), self.req.io_align as usize)?;
bfr.copy_from(data);
self.req.packet.out_data_buffer = bfr.ptr_mut().cast();
self.req.packet.out_transfer_length = bfr.len() as u32;
self.req.out_data_buffer = Some(bfr);
Ok(self)
}

/// Build the final [`AtaRequest`].
///
/// # Returns
/// A fully-configured [`AtaRequest`] ready for execution.
#[must_use]
pub fn build(self) -> AtaRequest<'a> {
self.req
}
}

/// Represents a response from an ATA request.
///
/// This structure provides access to the status block, read buffer, and other
/// details returned by the ATA controller after executing a request.
#[derive(Debug)]
pub struct AtaResponse<'a> {
req: AtaRequest<'a>,
}

impl<'a> AtaResponse<'a> {
/// Retrieves the status block from the response.
///
/// # Returns
/// A reference to the [`AtaStatusBlock`] containing details about the status of the executed operation.
#[must_use]
pub fn status(&self) -> &'a AtaStatusBlock {
unsafe {
self.req
.asb
.ptr()
.cast::<AtaStatusBlock>()
.as_ref()
.unwrap()
}
}

/// Retrieves the buffer containing data read from the device (if available).
///
/// # Returns
/// `Option<&[u8]>`: A slice of the data read from the device, or `None` if no read buffer was used.
#[must_use]
pub fn read_buffer(&self) -> Option<&'a [u8]> {
if self.req.packet.in_data_buffer.is_null() {
return None;
}
unsafe {
Some(core::slice::from_raw_parts(
self.req.packet.in_data_buffer.cast(),
self.req.packet.in_transfer_length as usize,
))
}
}
}
242 changes: 242 additions & 0 deletions uefi/src/proto/ata/pass_thru.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

//! ATA Pass Thru Protocol.
use super::{AtaRequest, AtaResponse};
use crate::helpers::AlignedBuffer;
use crate::StatusExt;
use core::alloc::LayoutError;
use core::ptr;
use uefi_macros::unsafe_protocol;
use uefi_raw::protocol::ata::AtaPassThruProtocol;
use uefi_raw::Status;

/// TODO: docs
pub type AtaPassThruMode = uefi_raw::protocol::ata::AtaPassThruMode;

/// The ATA Pass Thru Protocol.
///
/// One protocol instance represents one ATA controller connected to the machine.
///
/// # UEFI Spec Description
/// Provides services that allow ATA commands to be sent to ATA Devices attached to an ATA controller. Packet-
/// based commands would be sent to ATAPI devices only through the Extended SCSI Pass Thru Protocol. While
/// the ATA_PASS_THRU interface would expose an interface to the underlying ATA devices on an ATA controller,
/// EXT_SCSI_PASS_THRU is responsible for exposing a packet-based command interface for the ATAPI devices on
/// the same ATA controller.
#[derive(Debug)]
#[repr(transparent)]
#[unsafe_protocol(AtaPassThruProtocol::GUID)]
pub struct AtaPassThru(AtaPassThruProtocol);

impl AtaPassThru {
/// Retrieves the mode structure for the Extended SCSI Pass Thru protocol.
///
/// # Returns
/// The [`AtaPassThruMode`] structure containing configuration details of the protocol.
#[must_use]
pub fn mode(&self) -> AtaPassThruMode {
unsafe { (*self.0.mode).clone() }
}

/// Retrieves the I/O buffer alignment required by this SCSI channel.
///
/// # Returns
/// - A `u32` value representing the required I/O alignment in bytes.
#[must_use]
pub fn io_align(&self) -> u32 {
self.mode().io_align
}

/// Allocates an I/O buffer with the necessary alignment for this ATA Controller.
///
/// You can alternatively do this yourself using the [`AlignedBuffer`] helper directly.
/// The `ata` api will validate that your buffers have the correct alignment and error
/// 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::alloc(len, self.io_align() as usize)
}

/// Iterate over all potential ATA 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 [`AtaDevice::execute_command`].
///
/// # Returns
/// [`AtaDeviceIterator`] to iterate through connected ATA devices.
#[must_use]
pub const fn iter_devices(&self) -> AtaDeviceIterator<'_> {
AtaDeviceIterator {
proto: &self.0,
end_of_port: true,
prev_port: 0xFFFF,
prev_pmp: 0xFFFF,
}
}
}

/// Represents an ATA device on a controller.
///
/// # Warning
/// This is only a potentially valid device address. Verify it by probing for an actually
/// available / connected device using [`AtaDevice::execute_command`] before doing anything meaningful.
#[derive(Debug)]
pub struct AtaDevice<'a> {
proto: &'a AtaPassThruProtocol,
port: u16,
pmp: u16,
}

impl AtaDevice<'_> {
fn proto_mut(&mut self) -> *mut AtaPassThruProtocol {
ptr::from_ref(self.proto).cast_mut()
}

/// Returns the port number of the device.
///
/// # Details
/// - For SATA: This is the port number on the motherboard or controller.
/// - For IDE: This is `0` for the primary bus and `1` for the secondary bus.
#[must_use]
pub const fn port(&self) -> u16 {
self.port
}

/// Returns the port multiplier port (PMP) number for the device.
///
/// # Details
/// - For SATA: `0xFFFF` indicates a direct connection to the port, while other values
/// indicate the port number on a port-multiplier device.
/// - For IDE: `0` represents the master device, and `1` represents the slave device.
#[must_use]
pub const fn port_multiplier_port(&self) -> u16 {
self.pmp
}

/// Resets the ATA device.
///
/// This method attempts to reset the specified ATA device, restoring it to its default state.
///
/// # Errors
/// - [`Status::UNSUPPORTED`] The ATA controller does not support a device reset operation.
/// - [`Status::INVALID_PARAMETER`] The `Port` or `PortMultiplierPort` values are invalid.
/// - [`Status::DEVICE_ERROR`] A device error occurred while attempting to reset the specified ATA device.
/// - [`Status::TIMEOUT`] A timeout occurred while attempting to reset the specified ATA device.
pub fn reset(&mut self) -> crate::Result<()> {
unsafe { (self.proto.reset_device)(self.proto_mut(), self.port, self.pmp).to_result() }
}

/// Executes a command on the device.
///
/// # Parameters
/// - `req`: The request structure containing details about the command to execute.
///
/// # Returns
/// [`AtaResponse`] containing the results of the operation, such as data and status.
///
/// # Errors
/// - [`Status::BAD_BUFFER_SIZE`] The ATA command was not executed because the buffer size exceeded the allowed transfer size.
/// The number of bytes that could be transferred is returned in `InTransferLength` or `OutTransferLength`.
/// - [`Status::NOT_READY`] The ATA command could not be sent because too many commands are already queued. Retry the operation later.
/// - [`Status::DEVICE_ERROR`] A device error occurred while attempting to send the ATA command. Refer to `Asb` for additional status details.
/// - [`Status::INVALID_PARAMETER`] The `Port`, `PortMultiplierPort`, or the contents of `Acb` are invalid.
/// The command was not sent, and no additional status information is available.
/// - [`Status::UNSUPPORTED`] The host adapter does not support the command described by the ATA command.
/// The command was not sent, and no additional status information is available.
/// - [`Status::TIMEOUT`] A timeout occurred while waiting for the ATA command to execute. Refer to `Asb` for additional status details.
pub fn execute_command<'req>(
&mut self,
mut req: AtaRequest<'req>,
) -> crate::Result<AtaResponse<'req>> {
req.packet.acb = &req.acb;
unsafe {
(self.proto.pass_thru)(
self.proto_mut(),
self.port,
self.pmp,
&mut req.packet,
ptr::null_mut(),
)
.to_result_with_val(|| AtaResponse { req })
}
}
}

/// An iterator over the drives connected to an ATA controller.
///
/// The iterator yields [`AtaDevice`] instances, each representing one *potential*
/// drive connected to the ATA controller. You have to probe whether the drive
/// is actually available and connected!
#[derive(Debug)]
pub struct AtaDeviceIterator<'a> {
proto: &'a AtaPassThruProtocol,
// when there are no more devices on this port -> get next port
end_of_port: bool,
prev_port: u16,
prev_pmp: u16,
}

impl<'a> Iterator for AtaDeviceIterator<'a> {
type Item = AtaDevice<'a>;

fn next(&mut self) -> Option<Self::Item> {
loop {
if self.end_of_port {
let result = unsafe { (self.proto.get_next_port)(self.proto, &mut self.prev_port) };
match result {
Status::SUCCESS => self.end_of_port = false,
Status::NOT_FOUND => return None, // no more ports / devices. End of list
_ => panic!("Must not happen according to spec!"),
}
}
// get next device on port
// The UEFI spec states, that:
// If there is no port multiplier detected on the given port, the initial query of get_next_device()
// is allowed to return either of:
// - EFI_SUCCESS & PMP = 0xFFFF
// - EFI_NOT_FOUND
// But even when there is no detected port multiplier, there might be a device directly connected
// to the port! Port without attached port-multiplier uses a pmp of 0xFFFF.
let was_first = self.prev_pmp == 0xFFFF;
let result = unsafe {
(self.proto.get_next_device)(self.proto, self.prev_port, &mut self.prev_pmp)
};
match result {
Status::SUCCESS => {
if self.prev_pmp == 0xFFFF {
self.end_of_port = true;
}
return Some(AtaDevice {
proto: self.proto,
port: self.prev_port,
pmp: self.prev_pmp,
});
}
Status::NOT_FOUND => {
self.end_of_port = true;
self.prev_pmp = 0xFFFF;
if was_first {
// no port multiplier on port, return valid device anyway.
return Some(AtaDevice {
proto: self.proto,
port: self.prev_port,
pmp: 0xFFFF,
});
}
}
_ => panic!("Must not happen according to spec!"),
}
}
}
}
2 changes: 2 additions & 0 deletions uefi/src/proto/mod.rs
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@
//!
//! [`boot`]: crate::boot#accessing-protocols
#[cfg(feature = "alloc")]
pub mod ata;
pub mod console;
pub mod debug;
pub mod device_path;
12 changes: 12 additions & 0 deletions xtask/src/qemu.rs
Original file line number Diff line number Diff line change
@@ -489,6 +489,18 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> {
cmd.arg("-device"); // attach disk to SCSI controller
cmd.arg("scsi-hd,drive=scsidisk0,vendor=uefi-rs,product=ExtScsiPassThru");

if arch == UefiArch::IA32 || arch == UefiArch::X86_64 {
// Fourth (ATA) disk
let ata_test_disk = tmp_dir.join("test_disk3.empty.img");
create_mbr_test_disk(&ata_test_disk)?;
cmd.arg("-drive");
let mut drive_arg = OsString::from("if=none,format=raw,id=satadisk0,file=");
drive_arg.push(ata_test_disk.clone());
cmd.arg(drive_arg);
cmd.arg("-device");
cmd.arg("ide-hd,drive=satadisk0,bus=ide.2,serial=AtaPassThru,model=AtaPassThru");
}

let qemu_monitor_pipe = Pipe::new(tmp_dir, "qemu-monitor")?;
let serial_pipe = Pipe::new(tmp_dir, "serial")?;