FFI Authoring
Patina exposes C FFIs for interoperability with C based drivers. This documentation gives guidance on how to write these FFIs with patina specific guidance. For general guidance, view the Rust FFI docs.
Patina Specific Considerations
EFIAPI
All FFI functions must be marked with extern "efiapi" in order to use the EFIAPI ABI. Failure to include this
will cause undefined behavior.
Note:: The one exception to this, temporarily, is for variadic functions. Rust only contains support for these through the unstable c_variadics feature. This feature only supports variadics for
extern "C". The UEFI spec has some variadic functions that are required. As such, these functions useextern "C"for now. This issue tracks adding support forextern "efiapi".
va_list
AARCH64 defines the AAPCS64 ABI as the single ABI for AARCH64. However, MSVC (and clang's aarch64-unknown-windows-msvc target to align to it) has broken this ABI in some places, resulting in an MS ABI for AARCH64 as well.
The aarch64-unknown-uefi target used by Patina uses the MS ABI because that is what LLVM supports for building PE/COFF images, which are required for UEFI. Within Patina and Rust build code, this is immaterial, as long as the target is the same. However, it affects interoperability with C based code built by gcc and clang's aarch64-linux-gnu target.
In most cases, the MS ABI aligns with the AAPCS64 ABI. However, an exception is the va_list type. In order to keep using the aarch64-unknown-uefi target and maintain compatibility with C based code, Patina does not allow using the va_list type in FFIs. In practice, this has been seen very little.
Note: This does not affect using variadic functions that take
...as a parameter. Those are supported across C FFIs. It is only the va_list type itself that cannot be passed by value or by reference.
Pointer Alignment
Raw pointers received across an extern "efiapi" (or extern "C") boundary must be treated as potentially
unaligned. C callers are not required to honor Rust's alignment invariants for *mut T / *const T, and the UEFI
specification often passes out-pointers whose alignment cannot be statically guaranteed (e.g. fields embedded in packed
C structures and addresses computed from byte offsets).
A misaligned read or write through a normal Rust dereference is undefined behavior even on architectures that
tolerate it in hardware. The compiler is also free to assume the pointer is aligned and reorder or coalesce accesses
based on that assumption. So, when working with caller-supplied pointers across an FFI boundary, you must use the
unaligned access intrinsics read_unaligned() and write_unaligned() or a safe abstraction like zerocopy.
Rules
- Prefer
zerocopyfor structured reads. When pulling a typed value out of a caller-supplied byte buffer,zerocopy::FromBytesprovides a safe, alignment-agnostic alternative to manualread_unaligned()of a casted pointer. - Read FFI pointers with
read_unaligned(). Do not use methods that assume alignment, such as*ptr,ptr::read,&*ptr,(*ptr).field, or(*(ptr as *const T)).clone()on a caller-supplied pointer. - Write FFI pointers with
write_unaligned(). Do not use*ptr = valueorptr::write. - Be consistent within a function. Mixing aligned and unaligned access on the same pointer is a code smell and risks UB if the pointer is in fact misaligned.
- Null-check before every unaligned access.
read_unaligned()andwrite_unaligned()only relax the alignment requirement. The pointer must still be non-null and valid for the access. A null check should be the first operation performed on an untrusted caller-supplied pointer before any other dereference or access, including unaligned ones.
Zerocopy example
The following example shows an extern "efiapi" function that receives an untrusted, potentially unaligned byte
buffer from a C caller and parses a fixed-layout header out of it using zerocopy.
Note that there are no raw-pointer casts, no read_unaligned() calls, and no unsafe blocks needed for the parse
itself: FromBytes::ref_from_prefix validates the length and returns a &PacketHeader borrowed directly from the
caller's buffer, regardless of its alignment.
extern crate zerocopy; extern crate zerocopy_derive; use std::slice; use zerocopy_derive::{FromBytes, Immutable, KnownLayout, Unaligned}; // This is a stand-in for `r_efi::efi::Status` so the example is self-contained // and runnable on the Rust Playground (which does not have `r_efi`). #[repr(transparent)] #[derive(Copy, Clone, PartialEq, Eq, Debug)] struct Status(usize); impl Status { const SUCCESS: Self = Self(0); const INVALID_PARAMETER: Self = Self(0x8000_0000_0000_0002); } // `#[repr(C, packed)]` plus `Unaligned` means this type has no alignment requirement, // so `zerocopy` will reinterpret an arbitrary byte slice as `&PacketHeader`. #[repr(C, packed)] #[derive(Copy, Clone, Debug, FromBytes, Immutable, KnownLayout, Unaligned)] struct PacketHeader { version: u16, flags: u16, payload_len: u32, } /// Parses the header of a caller-supplied buffer and writes the payload length back through /// `out_payload_len`. Returns `INVALID_PARAMETER` if any pointer is null or the buffer is /// too small to contain a `PacketHeader`. /// /// # Safety /// /// Although this function is not marked `unsafe` (it is invoked across the EFIAPI C boundary /// with the UEFI ABI), the caller must still uphold the following invariants: /// /// - If `buffer` is non-null, it points to at least `buffer_len` consecutive, initialized, /// readable bytes. /// - If `out_payload_len` is non-null, it points to a writable `u32`. /// - Both regions remain valid and are not mutated by another thread for the duration of /// the call. /// /// Violating any of these is undefined behavior. Null pointers and a `buffer_len` smaller /// than `size_of::<PacketHeader>()` are handled safely and return `INVALID_PARAMETER`. pub extern "efiapi" fn parse_packet_header( buffer: *const u8, buffer_len: usize, out_payload_len: *mut u32, ) -> Status { if buffer.is_null() || out_payload_len.is_null() { return Status::INVALID_PARAMETER; } // SAFETY: Per the function's safety contract, when `buffer` is non-null it points to // `buffer_len` readable, initialized bytes in a single allocation. `from_raw_parts::<u8>()` // only requires `align_of::<u8>() == 1`, so any non-null `buffer` is sufficiently aligned. // The resulting `[u8]` is only handed to `zerocopy::FromBytes::ref_from_prefix()`, which // (thanks to `PacketHeader: Unaligned`) imposes no further alignment requirement. let bytes = unsafe { slice::from_raw_parts(buffer, buffer_len) }; let Ok((header, _rest)) = <PacketHeader as zerocopy::FromBytes>::ref_from_prefix(bytes) else { return Status::INVALID_PARAMETER; }; // Copy the packed field into a local to avoid taking a reference to an unaligned field. let payload_len = header.payload_len; // SAFETY: Per the function's safety contract, when `out_payload_len` is non-null it // points to a writable `u32`. `write_unaligned` removes the alignment requirement, so // any alignment the caller supplied is sound. unsafe { out_payload_len.write_unaligned(payload_len) }; Status::SUCCESS } // You would not normally include a `main()` function in an FFI module, this is just done // for demonstration purposes so the code can run on the Rust Playground. fn main() { // Simulate a buffer handed to us by a C caller: a `PacketHeader` followed by // four extra trailing bytes. The buffer is intentionally larger than the header // to show that `ref_from_prefix` only consumes what it needs. let buffer: [u8; 12] = [ 0x01, 0x00, // version = 1 0x02, 0x00, // flags = 2 0x34, 0x12, 0x00, 0x00, // payload_len = 0x1234 0xAA, 0xBB, 0xCC, 0xDD, // trailing data (ignored by the header parse) ]; let mut payload_len: u32 = 0; let status = parse_packet_header(buffer.as_ptr(), buffer.len(), &mut payload_len); assert_eq!(status, Status::SUCCESS); println!("payload_len = 0x{:x}", payload_len); }
Running the example will print:
payload_len = 0x1234
Key points:
- Null checks come first, before any dereference or slice construction.
- The parse itself is safe Rust. The only
unsafeblocks are the unavoidable FFI primitives:- Constructing the slice from the caller's
(ptr, len)pair - Writing through the out-pointer.
- Constructing the slice from the caller's
UnalignedonPacketHeaderletszerocopyborrow directly from any byte alignment. Without it,ref_from_prefix()would only succeed on a properly aligned buffer.ref_from_prefix()performs the length check, so a manualbuffer_len >= size_of::<PacketHeader>()comparison is not required.
When direct dereference is acceptable
Direct dereference (*ptr, &*ptr, &mut *ptr) is sound when the pointer's provenance guarantees alignment. In
Patina, this is typically the case for:
- Pointers obtained from
Box::leak,Vec, or other Rust allocators (where alignment is guaranteed by construction). - The
thispointer of an internally-produced protocol struct that Patina itself installed viaBox::leak. The layout of#[repr(C)]protocol structs is well-defined and Patina-allocated instances are aligned. - Pointers derived from
&T/&mut Treferences obtained earlier in the same function.
When in doubt, prefer the unaligned variants as they have very little to no measurable cost on properly aligned pointers on the targets Patina supports.
Anti-pattern examples
// WRONG: direct write through a caller-supplied out-pointer.
unsafe { *handle = installed_handle };
// WRONG: aligned read of a caller-supplied integer.
let n = unsafe { *num_bytes };
// WRONG: aligned struct read out of an arbitrary byte buffer.
let header = unsafe { (*(buffer as *const Header)).clone() };
// WRONG: inconsistent as the same pointer is read aligned and is written unaligned.
let n = unsafe { *num_bytes };
// ...
unsafe { num_bytes.write_unaligned(actual) };
Replace each with the corresponding read_unaligned() / write_unaligned() form (and, for the struct case, a
core::ptr::read_unaligned()::<Header>(buffer.cast()) or a zerocopy::FromBytes parse).