uefi-test-runner: add basic test for blockio protocols

- improve the ergonomics of the Blocks I/O 2 protocol
- figure out how to use it at all and put that into a very basic test
This commit is contained in:
Philipp Schuster
2025-12-06 21:32:27 +01:00
parent 920e50bf7b
commit 6a72d1e967
3 changed files with 254 additions and 8 deletions
+208
View File
@@ -0,0 +1,208 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Very basic tests for the BlockIo and BlockIo2 protocols.
//!
//! We look for some well-known data on a few well-known disks
//! of our test environment.
use alloc::string::String;
use core::ffi::c_void;
use core::hint;
use core::ptr::NonNull;
use core::sync::atomic::{AtomicBool, Ordering};
use uefi::boot::{OpenProtocolAttributes, OpenProtocolParams, ScopedProtocol};
use uefi::proto::Protocol;
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::media::block::{BlockIO, BlockIO2, BlockIO2Token};
use uefi::{CString16, Event, Handle, boot};
use uefi_raw::Status;
use uefi_raw::protocol::device_path::DeviceSubType;
use uefi_raw::table::boot::{EventType, Tpl};
fn verify_block_device(dvp: &DevicePath, first_block: &[u8]) {
// We only look for storage technologies that we are interested in.
let storage_device_types = [
DeviceSubType::MESSAGING_SCSI,
DeviceSubType::MESSAGING_NVME_NAMESPACE,
DeviceSubType::MESSAGING_SATA,
];
let maybe_storage_node = dvp
.node_iter()
.find(|x| storage_device_types.contains(&x.sub_type()));
if maybe_storage_node.is_none() {
// This happens on CI for the AArch64 target for a handle with
// device path `PciRoot(0x0)/Pci(0x9,0x0)` only.
return;
}
let storage_node = maybe_storage_node.unwrap();
let storage_node_string = storage_node
.to_string(DisplayOnly(true), AllowShortcuts(true))
.unwrap();
debug!("Storage technology: {storage_node_string}");
//debug!("First 512 bytes: {first_block:?}");
match storage_node.sub_type() {
DeviceSubType::MESSAGING_SCSI => { /* empty disks so far, nothing to check for */ }
DeviceSubType::MESSAGING_NVME_NAMESPACE => {
/* empty disks so far, nothing to check for */
}
DeviceSubType::MESSAGING_SATA => {
// We check that the right SATA disk indeed contains a correct
// FAT16 volume.
let expected = "MbrTestDisk";
let contains_volume_label = first_block
.windows(expected.len())
.any(|w| w == expected.as_bytes());
let oem_name = {
let bytes = &first_block[3..10];
String::from_utf8(bytes.to_vec())
};
let is_valid_fat = first_block[0] != 0 && oem_name.is_ok();
if is_valid_fat && storage_node.data() == [0x2, 0, 0xff, 0xff, 0x0, 0x0] {
if !contains_volume_label {
panic!(
"Sata disk {storage_node_string} does not contain {expected} in its first 512 bytes"
)
} else {
debug!(
"Found volume label {expected} with OEM name: {}",
oem_name.unwrap()
);
}
}
}
_ => unreachable!(),
}
}
fn open_proto_and_dvp<P: Protocol>(
handle: Handle,
) -> (ScopedProtocol<P>, ScopedProtocol<DevicePath>, CString16) {
let proto = unsafe {
boot::open_protocol::<P>(
OpenProtocolParams {
handle,
agent: boot::image_handle(),
controller: None,
},
OpenProtocolAttributes::GetProtocol,
)
.unwrap()
};
let dvp = unsafe {
boot::open_protocol::<DevicePath>(
OpenProtocolParams {
handle,
agent: boot::image_handle(),
controller: None,
},
OpenProtocolAttributes::GetProtocol,
)
.unwrap()
};
let dvp_string = dvp
.to_string(DisplayOnly(true), AllowShortcuts(true))
.unwrap();
(proto, dvp, dvp_string)
}
fn test_blockio_protocol() {
info!("Testing BLOCKIO protocol");
for handle in boot::find_handles::<BlockIO>().unwrap() {
let (proto, dvp, dvp_string) = open_proto_and_dvp::<BlockIO>(handle);
debug!("Found handle supporting protocol: {dvp_string}");
debug!("media: {:?}", proto.media());
let mut first_block = vec![0; 512];
proto
.read_blocks(proto.media().media_id(), 0, &mut first_block)
.unwrap();
verify_block_device(&dvp, first_block.as_slice());
}
}
fn test_blockio2_protocol() {
info!("Testing BLOCKIO 2 protocol");
for handle in boot::find_handles::<BlockIO2>().unwrap() {
let (proto, dvp, dvp_string) = open_proto_and_dvp::<BlockIO2>(handle);
debug!("Found handle supporting protocol: {dvp_string}");
debug!("media: {:?}", proto.media());
// sync test
{
let mut first_block = vec![0; 512];
unsafe {
proto
.read_blocks_ex(
proto.media().media_id(),
0,
None, /* sync */
first_block.len(),
first_block.as_mut_ptr(),
)
.unwrap();
}
verify_block_device(&dvp, first_block.as_slice());
}
// async test
{
static ASYNC_READ_LOCK: AtomicBool = AtomicBool::new(false);
let mut first_block = vec![0; 512];
extern "efiapi" fn callback(_event: Event, _context: Option<NonNull<c_void>>) {
log::info!("event fired: block I/O 2 read_blocks_ex done");
ASYNC_READ_LOCK.store(true, Ordering::SeqCst);
}
let event = unsafe {
boot::create_event(
EventType::NOTIFY_SIGNAL,
Tpl::CALLBACK,
Some(callback),
None,
)
.expect("should create event")
};
let mut token = BlockIO2Token::new(event, Status::NOT_READY);
let token_ptr = NonNull::new(&raw mut token).unwrap();
unsafe {
proto
.read_blocks_ex(
proto.media().media_id(),
0,
Some(token_ptr), /* sync */
first_block.len(),
first_block.as_mut_ptr(),
)
.unwrap();
}
// This works for some disks but the implementations behave
// differently.
// assert_ne!(token.transaction_status(), Status::SUCCESS);
// Wait util callback notified us the read is done
while !ASYNC_READ_LOCK.load(Ordering::SeqCst) {
hint::spin_loop();
}
ASYNC_READ_LOCK.store(false, Ordering::SeqCst);
// No boot::check_event() here, doesn't work, invalid parameter.
// Instead, one must use the notify function to perform further
// action.
assert_eq!(token.transaction_status(), Status::SUCCESS);
verify_block_device(&dvp, first_block.as_slice());
}
}
}
pub fn test() {
test_blockio_protocol();
test_blockio2_protocol();
}
+8 -2
View File
@@ -9,10 +9,11 @@ pub fn test() {
console::test();
find_protocol();
test_find_protocol_handles();
test_protocols_per_handle();
test_test_protocol();
block::test();
debug::test();
device_path::test();
driver::test();
@@ -46,7 +47,11 @@ pub fn test() {
tcg::test();
}
fn find_protocol() {
/// Tests that the [`boot::find_handles`] wrapper can find handles implementing
/// a certain protocol (here: [`Output`] protocol).
///
/// [`Output`]: proto::console::text::Output
fn test_find_protocol_handles() {
let handles = boot::find_handles::<proto::console::text::Output>()
.expect("Failed to retrieve list of handles");
@@ -76,6 +81,7 @@ fn test_test_protocol() {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
mod ata;
mod block;
mod console;
mod debug;
mod device_path;
+38 -6
View File
@@ -2,11 +2,11 @@
//! Block I/O protocols [`BlockIO`] and [`BlockIO2`].
use core::ptr::NonNull;
use crate::proto::unsafe_protocol;
use crate::util::opt_nonnull_to_ptr;
use crate::{Event, Result, Status, StatusExt};
use core::ptr::NonNull;
use core::sync::atomic::{AtomicUsize, Ordering};
pub use uefi_raw::protocol::block::{BlockIo2Protocol, BlockIoProtocol, Lba};
@@ -196,9 +196,39 @@ impl BlockIOMedia {
pub struct BlockIO2Token {
/// Event to be signalled when an asynchronous block I/O operation
/// completes.
pub event: Option<Event>,
pub event: Event,
/// Transaction status code.
pub transaction_status: Status,
// UEFI can change this at any time, so we need atomic access.
pub transaction_status: AtomicUsize,
}
impl BlockIO2Token {
/// Creates a new token.
#[must_use]
pub const fn new(event: Event, status: Status) -> Self {
Self {
event,
transaction_status: AtomicUsize::new(status.0),
}
}
/// Returns the transaction current status.
pub fn transaction_status(&self) -> Status {
Status(self.transaction_status.load(Ordering::SeqCst))
}
/// Clone this token.
///
/// # Safety
/// The caller must ensure that any clones of a closed `Event` are never
/// used again.
#[must_use]
pub unsafe fn unsafe_clone(&self) -> Self {
Self {
event: unsafe { self.event.unsafe_clone() },
transaction_status: AtomicUsize::new(self.transaction_status.load(Ordering::SeqCst)),
}
}
}
/// Block I/O 2 [`Protocol`].
@@ -236,7 +266,8 @@ impl BlockIO2 {
/// # Arguments
/// * `media_id` - The media ID that the read request is for.
/// * `lba` - The starting logical block address to read from on the device.
/// * `token` - Transaction token for asynchronous read.
/// * `token` - Transaction token for asynchronous read or `None` for
/// synchronous operation.
/// * `len` - Buffer size.
/// * `buffer` - The target buffer of the read operation
///
@@ -269,7 +300,8 @@ impl BlockIO2 {
/// # Arguments
/// * `media_id` - The media ID that the write request is for.
/// * `lba` - The starting logical block address to be written.
/// * `token` - Transaction token for asynchronous write.
/// * `token` - Transaction token for asynchronous write or `None` for
/// synchronous operation
/// * `len` - Buffer size.
/// * `buffer` - Buffer to be written from.
///