Writing UEFI Protocol in Rust
Hello everyone. As a part of Porting Rust std for UEFI, I had to write a hacky implementation of pipes (using UEFI Variables) for piping output from launched programs. However, this implementation had some problems and failed to run tests using the feature
panic_abort_tests. Since I had some time this week, I decided to write a Protocol to fix the pipes. Along the way, I also found many ways to shoot myself on foot. So I decided to write this post documenting all the gotchas I discovered while writing a new protocol in Rust. This post will only contain 1 of the two protocols I decided to create, internally named
Note: This Protocol is not public API and thus should not be relied upon/used outside of Rust std, at least for the foreseeable future.
UEFI_COMMAND_PROTOCOL has a straightforward use: to share the Handles of pipes (stdout, stderr, stdin) with the image to be started. It might contain more stuff in the future, but for right now, that is all it does. It is installed on all UEFI executables launched using the Rust
std::process::Command API and needs Pipes (In case they don’t need any kinds of Pipes, this Protocol will not be installed).
Here is the Rust Structure to define the Protocol:
As you can see, there is not much to it.
Here is the protocol GUID:
pub const PROTOCOL_GUID: Guid = from_fields;
When constructing and installing a Protocol from Rust, some things must be kept in mind. Here are some of the lessons I learned the hard way:
By default, all Rust variables are movable in memory. Once a UEFI Protocol is registered, it has to be valid until it is uninstalled. Installing a Protocol whose interface is on the stack can lead to all sorts of UB. In my particular case, I decided to use Heap allocated Protocol in
std::box::Box. From what I understand, there are ways to move the contents of
Box in memory. However, for my simple needs, it was sufficient since I was not performing any operations on Box once the Protocol was installed.
In practice, the code looks something like this:
let mut protocol = Boxnew;
// Do something with Command
For internal usage, I have wrappers that handle uninstalling the protocol on Drop to play nice with Rust memory management. It might be possible to have a better solution using rust
std::pin::Pin wrapper, but I haven’t used it personally.
Most modern UEFI drivers use the
EFI_BOOT_SERVICES.UninstallMultipleProtocolInterfaces() for installing and uninstalling protocols on a handle. However,
rustc currently doesn’t support variable arguments for UEFI. For more information about this, you can look at this issue.
You need to make sure to strictly define the lifetimes of protocols with their lifetimes as expected by UEFI. I wrote some structures in Rust std to make the internal working easier. However, having a difference in lifetimes can lead to everything ranging from CPU exceptions to an infinite loop.
Also take care of any data the protocol points to.
There will be cases when you want the strictly define the drop order of members of Protocol wrapper structures. I found this great answer for this. However, the rule of thumb is that Rust, by default, drops members in the same order they were declared. So make sure to keep this in mind.
Storing dynamic data in UEFI Protocol is generally not a great idea. However, Rust-specific types (Vec, Box, etc) can lead to all kinds of UB since all Rust types are movable by default. I worked around this by storing pointers to a struct that wraps over the Rust-specific type. A simple example is the following:
Since there are not many examples of Rust usage in UEFI drivers, there are not many standard practices established yet. With this post, I hope to help anyone trying to use Rust for UEFI driver development. Finally, with the new
UEFI_PIPE_PROTOCOL, Rust std port can now use complete
should_panic, which was previously broken). This is useful since even if you go the
no_std route, you can still use std to test your
no_std code. Feel free to check out Rust std for UEFI.
Consider supporting me if you like my work.