-
Notifications
You must be signed in to change notification settings - Fork 157
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
Let's discuss UEFI's pointer conventions #40
Comments
Req. 1: is it even possible to allocate a structure in memory mapped space? I'm pretty sure even regular unsafe Rust code would fail to work in such cases. Req. 2 & Req. 3: we can get around this by simply never exposing raw pointers in safe functions. All functions which take non-null pointers use references, and the ones which do allow null pointers take in an Req. 4: the way I understand this line, it doesn't talk about the state of the "parameters pointed by a pointer", but rather the pointer parameters. In other words, if UEFI takes a reference to a |
As a matter of fact, we do already expose a memory-mapped device: the GOP frame buffer. And indeed, come to think of it, our current testing code probably uses it incorrectly, as volatile writes should be used to avoid optimization problems. Instead of a slice, we should probably expose a slice wrapper which enforces volatile write semantics in order to avoid that kind of issue in the future. I don't think the spec dictates that firmware data structures cannot be in memory-mapped memory (which could make sense for things like the ACPI tables). That could be another source of hidden memory-mapped IO, but likely a harmless one: since the memory mapping must be transparent to the application, they can't do "dangerous" MMIO stuff like changing data structures under our feet. Therefore, only a very anal UEFI implementation could reject passing that as a parameter to a function (should we ever need to). Not sure if UEFI exposes any memory-mapped I/O other than the GOP frame buffer and possibly some firmware data structures. I do intend to ultimately check the 2400 pages of spec in order to make uefi-rs achieve full API coverage, but that will take me a while, and right now I do not feel confidence to make a blanket statement about what all UEFI interfaces do and do not. 😄
As I said, I think we can mostly ignore these requirements since they match normal expectations of unsafe Rust code. Requirements 1 and 4 are the only ones that really get me concerned.
I'm not sure if I fully understand the nuance. I suspect that we might be saying the same thing with different words. Can you provide an example of a situation where our two understandings of this requirement differ? My understanding is that if a UEFI function that takes an So for example, if we send in an |
It's debatable what the correct usage is, since the spec doesn't mention if this memory is write-combined or something.
Well, my point is that we should not use pointers in the safe API. For example, look at the
At no point do we take a mutable reference to a |
It's true that the proper usage pattern on the hardware side is unclear. However, the current usage is provably incorrect at the language level. The Rust compiler is within its right to optimize out any non-volatile write to memory, especially if it does not get subsequently read by the program, since it works under the assumption that the program is the only entity that will ever read from memory. This is what I would like to address by providing a higher-level abstraction on top of the GOP framebuffer which enforces volatile writes. I agree with you that the |
These requirements cannot be cleanly encoded in the type system without something akin to the efiapi macro discussed in #41, which is itself unfeasible within Rust's current macro system. I'll try to document this kind of considerations in the contribution guide instead. |
@HadrienG2 We should try to make this lib as safe-ish as possible, but not any safer :) Considering the huge amount of ways low-level developers can shoot themselves in the foot, it's doubtful we can truly provide memory safety in all cases without limiting the usage of the UEFI API. If people find |
I'm trying to reach the level of safety that is normally guaranteed by Rust: APIs should be safe to use as long as...
This means that some UEFI APIs cannot be exposed in a safe form. But that's okay, we can just expose them as unsafe APIs with well-documented contracts :) |
The "Calling conventions" section of the the "Overview" chapter of the UEFI spec starts with an interesting read about what can be expected of UEFI when passing pointers to the API, which may or may not influence our FFI interface (the "extern" functions that we use internally), unsafe APIs (those that ingest pointers), and what we consider to be safe APIs.
This is from page 20 of UEFI spec 2.7A, with numbering added to ease discussion:
Requirement 2 (correct alignment) is in line with Unsafe Rust's normal expectations: unaligned pointers are very special in Rust, and may only be used when one has special permission to do so (here, the answer is "never in the public API"). So we don't need to do anything about it.
Requirement 3 (no NULLs unless given permission) is also common in Unsafe Rust. If we wanted to encode the non-nullness requirement in the type system, we could use NonNulls (which are repr(transparent) and therefore ABI-compatible with C pointers), but a lot of unsafe Rust APIs don't bother and we probably don't need to either.
Requirement 1 (only physical memory, no MMIO and such) is where things become more interesting. The concern is very specific to low-level code, and this is not a normal expectation of an unsafe Rust API. It is also prohibitively expensive to check in software (you basically need to take a memory map and walk through it). Therefore, it could be a contract which we want to expose in the FFI, or in unsafe APIs that consume pointers like memmove/memset. Since every UEFI entry point is concerned by this contract, a way to move it into the API would be to take inspiration from NonNull and build our own wrapper type (e.g. "EfiPtr") which encodes this requirement.
Finally, requirement 4 (EFI may freely garble pointer parameters on error) is I think the most interesting from the perspective of Rust's safety guarantees. A pessimistic interpretation of this sentence would mean that every EFI function is unsafe with an unknown contract, since an out-of-bounds pointer access can corrupt everything, and so the only thing to do on error would be to abort immediately. Not something very pleasant to build upon. But if we disregard the spec's advice and make the IMO reasonable assumption that all invalid pointer accesses during erronerous execution will be in bounds, then it becomes something that we can build safe APIs upon. The only thing we need to take care of is that any function which takes as input a mutable pointer parameter to a type which has safety invariants must be kept unsafe with a "may corrupt input on error" contract.
I'll try to see if I can encode these requirements in the unsafe APIs and check the safe APIs against them somehow.
The text was updated successfully, but these errors were encountered: