Skip to content
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

uefi: Add safe protocol wrapper for EFI_EXT_SCSI_PASS_THRU_PROTOCOL #1589

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

seijikun
Copy link
Contributor

@seijikun seijikun commented Mar 24, 2025

Added a safe wrapper for the extended SCSI Pass Thru protocol.
I did my best to design the API in a way that avoids accidentally using it incorrectly, yet allowing it to operate efficiently.
The ScsiRequestBuilder API is designed in a way that should easily make it possible to use it for both EFI_EXT_SCSI_PASS_THRU_PROTOCOL and a possible future safe protocol wrapper of EFI_SCSI_IO_PROTOCOL.

Exemplary usage to probe all devices potentially connected to every SCSI channel in the system:

Easy variant (io/cmd buffer allocations per request):

// query handles with support for the protocol (one handle per SCSI controller)
let scsi_ctrl_handles = uefi::boot::find_handles::<ExtScsiPassThru>().unwrap();
// Iterate over scsi controllers with passthru support:
for handle in scsi_ctrl_handles {
    // open protocol for controller
    let scsi_pt = uefi::boot::open_protocol_exclusive::<ExtScsiPassThru>(handle).unwrap();
    // iterate (potential!) devices on the controller
    for device in scsi_pt.iter_devices() {
        // this is not an actual (guaranteed-to-exist) device, but a !!potential!! device.
        // We have to probe it to find out if there is something connected to this chan/target/lun

        // construct SCSI INQUIRY (0x12) request
        let request = ScsiRequestBuilder::read(scsi_pt.io_align())
            .with_timeout(Duration::from_millis(500))
            .with_command_data(&[0x12, 0x00, 0x00, 0x00, 0xFF, 0x00]).unwrap()
            .with_read_buffer(255).unwrap()
            .build();
        // send device through controller to potential device
        if let Ok(response) = device.execute_command(request) {
            println!(
                "SUCCESS HostAdapterStatus: {:?}, TargetStatus: {:?}\r",
                response.host_adapter_status(),
                response.target_status()
            );
            let inquiry_response = response.read_buffer().unwrap();
            println!("ResponseBfr: {:?}\r", inquiry_response);
        } else {
            println!("ERROR - probably not a device\r");
        }
    }
}

Buffer-reuse API variant:

[...]
    // open protocol for controller
    let scsi_pt = uefi::boot::open_protocol_exclusive::<ExtScsiPassThru>(handle).unwrap();
    // allocate buffers and reuse amongst drives on this SCSI controller
    // It's important this is not shared across SCSI controllers !! Alignment differs
    let mut cmd_bfr = scsi_pt.alloc_io_buffer(6).unwrap();
    cmd_bfr.copy_from(&[0x12, 0x00, 0x00, 0x00, 0xFF, 0x00]);
    let mut read_bfr = scsi_pt.alloc_io_buffer(255).unwrap();
    // iterate (potential!) devices on the controller
    for device in scsi_pt.iter_devices() {
        // this is not an actual devices, but a !!potential!! device.
        // We have to probe it to find out if there is something connected to this chan/target/lun

        // construct SCSI INQUIRY (0x12) request
        let request = ScsiRequestBuilder::read(scsi_pt.io_align())
            .with_timeout(Duration::from_millis(500))
            .use_command_buffer(&mut cmd_bfr).unwrap()
            .use_read_buffer(&mut read_bfr).unwrap()
            .build();
[...]

Checklist

  • Sensible git history (for example, squash "typo" or "fix" commits). See the Rewriting History guide for help.
  • Update the changelog (if necessary)

@seijikun seijikun force-pushed the mr-extscsipt branch 7 times, most recently from 6aff09b to 3787821 Compare March 24, 2025 13:33
@seijikun
Copy link
Contributor Author

seijikun commented Mar 24, 2025

image

@seijikun seijikun force-pushed the mr-extscsipt branch 3 times, most recently from 2406c3a to 359311e Compare March 24, 2025 14:44
Copy link
Member

@phip1611 phip1611 left a comment

Choose a reason for hiding this comment

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

Thanks for your contribution! I left a few remarks. Further, can you please add an integration test for the new functionality?

@seijikun seijikun force-pushed the mr-extscsipt branch 4 times, most recently from 9518d32 to ff4607d Compare March 24, 2025 18:28
@seijikun
Copy link
Contributor Author

seijikun commented Mar 24, 2025

@phip1611 @nicholasbishop
I hope I addressed all opened review comments now.
CI on x86_64 is green now, including the AlignedBuffer unit-test and the integration-test.
For the integration test, I changed the second (FAT32 formatted) disk to SCSI.

Since that change, the qemu process of the aarch64 integration test runner is hard-crashing in the Disk I/O 2 test..
Do you have an idea of what this might be?

EDIT: Was able to fix it by leaving the second disk to the way it was before and instead adding a third disk, then located on a SCSI Controller.

@seijikun seijikun force-pushed the mr-extscsipt branch 2 times, most recently from 5b1f56e to bca67e0 Compare March 24, 2025 19:10
@seijikun seijikun requested a review from phip1611 March 24, 2025 19:16
Copy link
Member

@phip1611 phip1611 left a comment

Choose a reason for hiding this comment

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

This is a good PR with a comprehensive improvement! Thanks! LGTM

As this PR is a little bigger than others, I'd like to ask @nicholasbishop to give a second approval

@nicholasbishop
Copy link
Member

I'm looking through this in more detail now :) Mind moving the first commit ("uefi-raw: Add documentation to ScsiIoScsiRequestPacket") to a new PR? Can merge that separately from the rest of the changes.

@seijikun
Copy link
Contributor Author

Done: #1593

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

For SCSI, does the alignment ever exceed 4096? I'm wondering if it would make more sense for callers to allocate memory with boot::allocate_pages, which are always 4K aligned. We might not need AlignedBuffer in that case. (It might still be a helpful interface either way, I just want to see if a simpler solution can work.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Main idea behind the AlignedBuffer struct was, that I don't have to think if dealloc-ing a buffer I allocated myself.
I disliked having the user copy over the io_align myself - but I'd be too scared to make assumptions as to what can be expected as possible values.

Copy link
Contributor Author

@seijikun seijikun Mar 26, 2025

Choose a reason for hiding this comment

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

Instead of letting a user construct the request builder freely, we could instead add something like:

pub enum ScsiRequestDirection { READ, WRITE, BIDIRECTIONAL }

And then add the following method to ExtScsiPassThru for starting new requests:

pub fn start_request(&self, direction: ScsiRequestDirection) -> ScsiRequestBuilder {}

That would then avoid the user ever coming into direct contact with io_align.

So something like this:

let scsi_pt = uefi::boot::open_protocol_exclusive::<ExtScsiPassThru>(handle).unwrap();
let request = ScsiRequestBuilder::read(scsi_pt.io_align())
    .with_timeout(Duration::from_millis(500))
    .build();

becomes:

let scsi_pt = uefi::boot::open_protocol_exclusive::<ExtScsiPassThru>(handle).unwrap();
let request = scsi_pt.start_request(ScsiRequestDirection::READ)
    .with_timeout(Duration::from_millis(500))
    .build();

Then we are more free w.r.t. changing the buffer logic. I would prefer to keep the AlignedBuffer struct for that, though.

@seijikun
Copy link
Contributor Author

seijikun commented Mar 26, 2025

@nicholasbishop I think I might have a nice solution to the mutable / immutable problem. Please have a look at 5a44ef4

Usage looks like this:

let scsi_ctrl_handles = uefi::boot::find_handles::<ExtScsiPassThru>().unwrap();
for handle in scsi_ctrl_handles {
    //     v  does not require mutable
    let scsi_pt = uefi::boot::open_protocol_exclusive::<ExtScsiPassThru>(handle).unwrap();
    //  ScsiDevices returned from the device iterator are owned structs (no &ref, no &mut ref)
    //   v requires mutable            v does not require mutable
    for mut device in scsi_pt.iter_devices() {
        println!("- Disk: {:?} | {}\r", device.target(), device.lun());

        let request = ScsiRequestBuilder::read(scsi_pt.io_align())
            .with_timeout(Duration::from_millis(500))
            .use_command_data(&[0x06, 0x00, 0x00, 0x00, 0x00]).unwrap()
            .with_read_buffer(255).unwrap()
            .build();
        //             v requires device to be mutable
        let result = device.execute_command(request).unwrap();
    }
}

Internally, ScsiDevice only has a *const pointer (otherwise the Clone derive doesn't work).
And on actions that require a *mut pointer, I cast it.

What do you think about this?

@nicholasbishop
Copy link
Member

I'm wondering if some of the lifetime complexity could be reduced by moving the methods of ScsiDevice to ExtScsiPassThru. So instead of ScsiDevice being a "smart" object that you can call execute_command (and reset) on, you would call execute_command on the protocol, and pass the ScsiTargetLun as an argument.

Would that work? If so, are there downsides I'm not thinking of?

@seijikun
Copy link
Contributor Author

Yes, that would eliminate the lifetime problems. If you prefer that, I can refactor it.

@nicholasbishop
Copy link
Member

Yes, as long as it doesn't cause other issues I think that would be a good refactor. Fewer lifetimes, and avoiding internal pointers and Phantom markers feels worth it to me, to make it less likely that the code is accidentally unsound.

@seijikun
Copy link
Contributor Author

seijikun commented Mar 27, 2025

grr
I just did the refactoring, and that architecture produces the same lifetime problems.
I can not send commands within an iterator, because the device iterator borrows the protocol.

let mut scsi_pt = uefi::boot::open_protocol_exclusive::<ExtScsiPassThru>(handle).unwrap();
                // v borrows scsi_pt immutably
for device in scsi_pt.iter_devices() {
    let request = ...;
    let result = scsi_pt.execute_command(&device, request);
                // ^ attempts to borrow scsi_pt mutably
}

@nicholasbishop
Copy link
Member

Ah right. What about collecting the iterator into a Vec first?

@seijikun
Copy link
Contributor Author

seijikun commented Mar 27, 2025

Yes, that works (just tested it). Although it's an additional allocation. On the other hand, we require alloc for this anyway.
Your call.

let mut scsi_pt = uefi::boot::open_protocol_exclusive::<ExtScsiPassThru>(handle).unwrap();
let devices: Vec<_> = scsi_pt.iter_devices().collect();
for device in devices {
    let request = ...;
    let result = scsi_pt.execute_command(&device, request);
    // ...
    // profit
}

One minor caveat: ScsiDevices are then nolonger bound to a certain protocol. So you could accidentally use an ScsiDevice instance on any protocol instance.

let device = scsi_pt.iter_devices().next().unwrap();

and then run:

other_scsi_pt.execute_command(&device, request);

@seijikun
Copy link
Contributor Author

seijikun commented Mar 27, 2025

@nicholasbishop
My personal favorite stays the smart object design I suggested. It has several advantages:

  • By far the best usability (it is intuitive - you don't have to search what to do with the objects returned from the iterator)
  • It avoids an unnecessary allocation (no iter_devices().collect())
  • It actually makes sense, in that:
    • it does not allow you to borrow the protocol instance mutably while you have ScsiDevice instances alive. So you can e.g. not reset the entire channel with ScsiDevice's borrowing the protocol immutably
    • it's not possible to accidentally use a ScsiDevice instance with another ExtScsiPassThru protocol instance

I just pushed a version without internal pointer and without PhantomData usage - thus alleviating some of your concerns with it.

@nicholasbishop
Copy link
Member

FYI, I'll return to looking at this soon -- I've been a way for a few days so lots to catch up on at work :)

@nicholasbishop
Copy link
Member

Mind pulling AlignedBuf into a separate PR? I think I'm sold on the usefulness of that addition to the API, and the need for aligned buffers comes up in lots of places, so I'd like to review that independently.

@phip1611 phip1611 mentioned this pull request Apr 6, 2025
10 tasks
@seijikun seijikun force-pushed the mr-extscsipt branch 4 times, most recently from 5cb00bb to 670362b Compare April 8, 2025 12:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants