-
-
Notifications
You must be signed in to change notification settings - Fork 165
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
base: main
Are you sure you want to change the base?
Conversation
6aff09b
to
3787821
Compare
2406c3a
to
359311e
Compare
There was a problem hiding this 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?
9518d32
to
ff4607d
Compare
@phip1611 @nicholasbishop Since that change, the qemu process of the aarch64 integration test runner is hard-crashing in the 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. |
5b1f56e
to
bca67e0
Compare
There was a problem hiding this 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
4e3e14f
to
f37d520
Compare
uefi-raw/src/protocol/scsi.rs
Outdated
@@ -150,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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one also feels "logicially mut" to me, can we make this mut and ditto for the corresponding uefi
wrapper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nicholasbishop @phip1611
I can make the raw API mut, but...:
The entity in the safe uefi
wrapper that corresponds to this funcitonality is ScsiDevice<'a>
in function ScsiDevice::reset()
.
And the only way to get an instance of ScsiDevice<'a>
is through the device iterator: ExtScsiPassThru::iter_devices()
.
How would you suggest to make this mut? Have a second ExtScsiPassThru::iter_devices_mut()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think ScsiDevice::execute_command
should also be &mut
. So maybe there should only be iter_devices_mut
, and drop iter_devices
(since iter_devices
indicates you will need to probe with execute_command
anyway). Would that work, or do you see problems in API usage if we require exclusive access?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, and a general note: even if we decide to make the uefi
interface &
instead of &mut
, we can still use *mut
in the raw interface. Making a mut pointer from a non-mut reference is allowed, and doesn't affect soundness in and of itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just rewrote the API to use mutable and I ran into some ugly problems:
When you call iter_devices_mut()
to produce ScsiDevice
s, you mutably borrow the protocol (ExtScsiPassThru
) itself for the lifetime of the generated ScsiDevice
. Thus, while a device exists, you can't interact with the protocol itself. That produces all kinds of usability issues:
for mut device in scsi_pt.iter_devices_mut() {
let request = ScsiRequestBuilder::read(scsi_pt.io_align())
.with_timeout(Duration::from_millis(500))
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for experimenting and analyzing this. I don't have the capacity right not unfortunately to provide good guidance. @nicholasbishop can you help, please?
d6623ef
to
c5b9ca3
Compare
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. |
Done: #1593 |
/// Note: This uses the global Rust allocator under the hood. | ||
#[allow(clippy::len_without_is_empty)] | ||
#[derive(Debug)] | ||
pub struct AlignedBuffer { |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
AlignedBuffer is a helper class that manages the livetime of a memory region, allocated using a certain alignment. Like Box, it handles deallocation when the object isn't used anymore.
@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, What do you think about this? |
I'm wondering if some of the lifetime complexity could be reduced by moving the methods of Would that work? If so, are there downsides I'm not thinking of? |
Yes, that would eliminate the lifetime problems. If you prefer that, I can refactor it. |
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. |
grr 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
} |
Ah right. What about collecting the iterator into a |
Yes, that works (just tested it). Although it's an additional allocation. On the other hand, we require 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: let device = scsi_pt.iter_devices().next().unwrap(); and then run: other_scsi_pt.execute_command(&device, request); |
@nicholasbishop
I just pushed a version without internal pointer and without |
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 bothEFI_EXT_SCSI_PASS_THRU_PROTOCOL
and a possible future safe protocol wrapper ofEFI_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):
Buffer-reuse API variant:
Checklist