rust-osdev / uefi-rs

Rusty wrapper for the Unified Extensible Firmware Interface (UEFI). This crate makes it easy to develop Rust software that leverages safe, convenient, and performant abstractions for UEFI functionality.
https://rust-osdev.com/uefi-book
Mozilla Public License 2.0
1.33k stars 159 forks source link

Cannot open BlockIO protocol exclusively for logical partitions #1466

Open Mathis-Z opened 3 weeks ago

Mathis-Z commented 3 weeks ago

I am not sure if that is to be expected, an issue with uefi-rs or an issue with my setup but here is my problem:

I am writing a simple bootloader with uefi-rs (with the alloc feature) and I am testing it in QEMU with a .vdi (VirtualBox hard drive) mounted. I want to read ext4 partitions using an external crate and for this I need to access the partitions raw. I can list the handles supporting the BlockIO protocol but I can only open the protocol for handles that represent entire disks (rather than logical partitions). This is my minimal code example:

fn main() -> Status {
    uefi::helpers::init().unwrap();
    let _ = system::with_stdout(|stdout| stdout.clear());

    for disk_handle in find_handles::<DiskIo>().unwrap() {
        unsafe {
            match boot::open_protocol::<BlockIO>(
                OpenProtocolParams {
                    handle: disk_handle,
                    agent: image_handle(),
                    controller: None,
                },
                boot::OpenProtocolAttributes::Exclusive,
            ) {
                Ok(scoped_block_prot) => {
                    let block_io = scoped_block_prot.get().unwrap();
                    println!(
                        "{disk_handle:?} is logical partition: {}",
                        block_io.media().is_logical_partition()
                    )
                }
                Err(error) => {
                    println!("Could not open BlockIO protocol for {disk_handle:?} : {error}")
                }
            }
        }
    }

    Status::SUCCESS
}

This produces output like:

Handle(0xxxx) is logical partition: false
Handle(0xxxx) is logical partition: false
Handle(0xxxx) is logical partition: false
Could not open BlockIO protocol for Handle(0xxxx) : UEFI Error INVALID_PARAMETER: ()
Handle(0xxxx) is logical partition: false
Could not open BlockIO protocol for Handle(0xxxx) : UEFI Error INVALID_PARAMETER: ()
Could not open BlockIO protocol for Handle(0xxxx) : UEFI Error INVALID_PARAMETER: ()
Could not open BlockIO protocol for Handle(0xxxx) : UEFI Error INVALID_PARAMETER: ()

I know the handles I cannot open are for logical partitions because it works when I change boot::OpenProtocolAttributes::Exclusive to boot::OpenProtocolAttributes::GetProtocol. Then I get this output:

Handle(0xxxx) is logical partition: false
Handle(0xxxx) is logical partition: false
Handle(0xxxx) is logical partition: false
Handle(0xxxx) is logical partition: true
Handle(0xxxx) is logical partition: false
Handle(0xxxx) is logical partition: true
Handle(0xxxx) is logical partition: true
Handle(0xxxx) is logical partition: true

Another observation I made is that after running my code above I cannot call find_handles::<DiskIo>() again because it will just block forever as if the handles are still occupied or something. This does not happen when I open the protocols using boot::OpenProtocolAttributes::GetProtocol.

Finally, I can open the BlockIO protocol for my logical partitions using the following code:

for handle in find_handles::<PartitionInfo>().unwrap() {
        match open_protocol_exclusive::<BlockIO>(handle) {
            Ok(_) => println!("BlockIO opened for {handle:?}"),
            Err(error) => println!("BlockIO for {handle:?} not opened: {error}"),
        }
    }

Producing output like:

BlockIO opened for Handle(0xxxx)
BlockIO opened for Handle(0xxxx)
BlockIO opened for Handle(0xxxx)
BlockIO opened for Handle(0xxxx)

Combining this code with the code from above I can confirm that these are the very same handles that could not be opened previously.

Again, I am not sure if this is to be expected or potentially an issue but I hope I could give you enough info to tell.

Mathis-Z commented 2 weeks ago

I just found out that reversing the list of handles allows me to open all of them exclusively. This code:

fn main() -> Status {
    uefi::helpers::init().unwrap();
    let _ = system::with_stdout(|stdout| stdout.clear());

    let mut handles = boot::find_handles::<DiskIo>().unwrap();
    handles.reverse();

    for disk_handle in handles {
        unsafe {
            match boot::open_protocol::<BlockIO>(
                OpenProtocolParams {
                    handle: disk_handle,
                    agent: boot::image_handle(),
                    controller: None,
                },
                boot::OpenProtocolAttributes::Exclusive,
            ) {
                Ok(scoped_block_prot) => {
                    let block_io = scoped_block_prot.get().unwrap();
                    println!(
                        "{disk_handle:?} is logical partition: {}",
                        block_io.media().is_logical_partition()
                    )
                }
                Err(error) => {
                    println!("Could not open BlockIO protocol for {disk_handle:?} : {error}")
                }
            }
        }
    }

    boot::stall(100_000_000);

    Status::SUCCESS
}

Output:

Handle(0xxxx) is logical partition: true
Handle(0xxxx) is logical partition: true
Handle(0xxxx) is logical partition: true
Handle(0xxxx) is logical partition: false
Handle(0xxxx) is logical partition: true
Handle(0xxxx) is logical partition: false
Handle(0xxxx) is logical partition: false

This indicates to me that exclusively opening a protocol on the "root" handle of the disk (the one with logical partition: false) also locks the "children" handles but dropping the ScopedProtocol for the "root" handle does not release the locks on the children. This is why trying to exclusively open a protocol on a child handle fails afterwards. If we reverse the list of handles we open the children handles before opening the root handle and so we don't run into the problem. So this is probably a bug in uefi-rs.

nicholasbishop commented 2 weeks ago

The locking provided by OpenProtocolAttributes::Exclusive is not implemented by uefi-rs, but rather is part of the firmware. See EXCLUSIVE in https://uefi.org/specs/UEFI/2.10/07_Services_Boot_Services.html#efi-boot-services-openprotocol

So while it is possible there's a bug in uefi-rs, I think more likely you are seeing firmware behavior.

For disk operations, I would actually recommend not opening in exclusive mode. I've observed on real hardware that this can be quite slow, taking almost one second for the firmware to finish doing whatever cleanup is necessary to provide exclusive access.