Real World Case Study: UEFI Memory Safety Issues Preventable by Rust
Overview
This document provides analysis of real UEFI firmware vulnerabilities found in the EDK II codebase and demonstrates how Rust's memory safety features would have prevented each one. The analysis is based on actual CVEs that affected production systems and required security patches.
⚠️ Note: These case studies are based on publicly disclosed CVEs and are intended for education only.
- The examples are simplified for clarity and may not represent the full complexity of the original vulnerabilities.
- The goal of the examples is to show how Rust's safety features can prevent memory safety problems in real-world firmware code. The suggestions are not intended to be complete or production-ready Rust implementations.
Summary Table
The are actual CVEs found in UEFI firmware that could have been prevented with the memory safety features in Rust.
CVE ID | CVSS Score | Vulnerability Type | Potential Rust Prevention Mechanism |
---|---|---|---|
CVE-2023-45230 | 8.3 (HIGH) | Buffer Overflow in DHCPv6 | Automatic slice bounds checking |
CVE-2022-36765 | 7.0 (HIGH) | Integer Overflow in CreateHob() | Checked arithmetic operations |
CVE-2023-45229 | 6.5 (MEDIUM) | Out-of-Bounds Read in DHCPv6 | Slice bounds verification |
CVE-2014-8271 | 6.8 (MEDIUM) | Buffer Overflow in Variable Processing | Dynamic Vec sizing eliminates fixed buffers |
CVE-2023-45233 | 7.5 (HIGH) | Infinite Loop in IPv6 Parsing | Iterator patterns with explicit termination |
CVE-2021-38575 | 8.1 (HIGH) | Remote Buffer Overflow in iSCSI | Slice-based network parsing with bounds checking |
CVE-2019-14563 | 7.8 (HIGH) | Integer Truncation | Explicit type conversions with error handling |
CVE-2024-1298 | 6.0 (MEDIUM) | Division by Zero from Integer Overflow | Checked arithmetic prevents overflow-induced division by zero |
CVE-2014-4859 | Not specified | Integer Overflow in Capsule Update | Safe arithmetic with explicit overflow checking |
Vulnerability Classes Eliminated by Rust
These CVEs would be prevented by Rust's compile-time checks or runtime safety guarantees by preventing these common vulnerability classes:
- Buffer Overflows: Automatic bounds checking eliminates this entire vulnerability class
- Use-After-Free: Ownership system prevents dangling pointers at compile time
- Integer Overflow: Checked arithmetic operations prevent overflow-induced vulnerabilities
- Out-of-Bounds Access: Slice bounds verification ensures memory safety
- Infinite Loops: Iterator patterns with explicit termination conditions
- Type Confusion: Strong type system prevents conversion errors
Detailed CVE Analysis
CVE-2023-45230: Buffer Overflow in DHCPv6 Client
- CVE Details: CVE-2023-45230
- CVSS Score: 8.3 (HIGH)
- Vulnerability Type: CWE-119 (Improper Restriction of Operations within Memory Buffer Bounds)
- Vulnerabilities in EDK2 NetworkPkg IP stack implementation
- Fixed in: f31453e8d6 (Unit Tests)
Issue Description: "EDK2's Network Package is susceptible to a buffer overflow vulnerability via a long server ID option in DHCPv6 client when constructing outgoing DHCP packets."
C Problem:
// From NetworkPkg/Dhcp6Dxe/Dhcp6Utility.c (prior to the fix)
UINT8 *
Dhcp6AppendOption (
IN OUT UINT8 *Buf,
IN UINT16 OptType,
IN UINT16 OptLen,
IN UINT8 *Data
)
{
// Vulnerable: No bounds checking
WriteUnaligned16 ((UINT16 *)Buf, OptType);
Buf += 2;
WriteUnaligned16 ((UINT16 *)Buf, OptLen);
Buf += 2;
CopyMem (Buf, Data, NTOHS (OptLen)); // Buffer overflow is possible if the packet is too small
Buf += NTOHS (OptLen);
return Buf;
}
// Usage in Dhcp6SendRequestMsg, Dhcp6SendRenewRebindMsg, etc:
Cursor = Dhcp6AppendOption (
Cursor,
HTONS (Dhcp6OptServerId),
ServerId->Length,
ServerId->Duid // Large ServerId->Length causes overflow
);
How Rust Prevents This:
#![allow(unused)] fn main() { use zerocopy::{FromBytes, Immutable, KnownLayout, Unaligned}; // Type-safe DHCP6 option codes #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u16)] pub enum Dhcp6OptionCode { ClientId = 1, ServerId = 2, IaNa = 3, IaTa = 4, IaAddr = 5, OptionRequest = 6, Preference = 7, ElapsedTime = 8, // ... other options } // Safe packet builder that tracks remaining space #[derive(Debug)] pub struct Dhcp6PacketBuilder { buffer: Vec<u8>, max_size: usize, } impl Dhcp6PacketBuilder { pub fn new(max_size: usize) -> Self { Self { buffer: Vec::with_capacity(max_size), max_size, } } // Safe option appending with automatic bounds checking pub fn append_option( &mut self, option_type: Dhcp6OptionCode, data: &[u8], ) -> Result<(), Dhcp6Error> { let option_header_size = 4; // 2 bytes type + 2 bytes length let total_size = option_header_size + data.len(); // Rust prevents buffer overflow through bounds checking if self.buffer.len() + total_size > self.max_size { return Err(Dhcp6Error::InsufficientSpace); } // Safe serialization with automatic length tracking self.buffer.extend_from_slice(&(option_type as u16).to_be_bytes()); self.buffer.extend_from_slice(&(data.len() as u16).to_be_bytes()); self.buffer.extend_from_slice(data); Ok(()) } pub fn append_server_id(&mut self, server_id: &ServerId) -> Result<(), Dhcp6Error> { self.append_option(Dhcp6OptionCode::ServerId, server_id.as_bytes()) } pub fn finish(self) -> Vec<u8> { self.buffer } } // Type-safe Server ID that prevents overflow #[derive(Debug, Clone)] pub struct ServerId { duid: Vec<u8>, } impl ServerId { pub fn new(data: &[u8]) -> Result<Self, Dhcp6Error> { // Validate server ID length (DHCP6 spec limits) if data.len() > 130 { // RFC 8415 section 11.1 return Err(Dhcp6Error::InvalidServerIdLength); } Ok(Self { duid: data.to_vec() }) } pub fn as_bytes(&self) -> &[u8] { &self.duid } } #[derive(Debug, thiserror::Error)] pub enum Dhcp6Error { #[error("Insufficient space in packet buffer")] InsufficientSpace, #[error("Invalid server ID length")] InvalidServerIdLength, } // Usage example - safe by construction: fn build_dhcp6_request(server_id: &ServerId) -> Result<Vec<u8>, Dhcp6Error> { let mut builder = Dhcp6PacketBuilder::new(1500); // Standard MTU // The bounds checking is automatic - no manual buffer management needed builder.append_server_id(server_id)?; Ok(builder.finish()) } IaNa = 3, IaTa = 4, IaAddr = 5, OptionRequest = 6, Preference = 7, ElapsedTime = 8, // ... other options } // Similar to `EFI_DHCP6_PACKET_OPTION` in the C code. // // Note this is deriving some zerocopy traits onto this type that provide these benefits: // - `FromBytes` - Allows for safe deserialization from bytes in the memory area without copying // - `KnownLayout` - Allows the layout characteristics of the type to be evaluated to guarantee the struct layout // matches the defined C structure exactly // - `Immutable` - Asserts the struct is free from interior mutability (changes after creation) // - `Unaligned` - Allows parsing from unaligned memory (which might be the case for network packets) #[derive(Debug, FromBytes, KnownLayout, Immutable, Unaligned)] #[repr(C)] pub struct Dhcp6OptionHeader { pub op_code: u16, pub op_len: u16, } // Type-safe wrapper for DHCP6 options #[derive(Debug)] pub struct Dhcp6Option<'a> { }
How This Helps (Eliminates Buffer Overflow):
- Automatic Bounds Checking:
append_option
checks available space before writing - Type-Safe Buffer Management:
Vec<u8>
grows dynamically and prevents overflows - Structured Error Handling:
Result<T, E>
forces explicit error handling - Safe by Construction: The API prevents creation of oversized packets
- Compile-Time Prevention: Buffer overflow becomes a compile error, not a runtime vulnerability
How is This Different from Just Adding Bounds Checks in C?
The fundamental difference between Rust's memory safety and defensive C programming lies in where and how safety is enforced. While both approaches can prevent vulnerabilities, Rust's approach provides stronger guarantees through language-level enforcement rather than solely relying on developer discipline.
Language-Level Safety vs. Defensive Programming
In C, safety depends entirely on developer discipline and tooling:
- Bounds checks are optional and easily forgotten
- Memory management is manual and error-prone
- Safety violations compile successfully but fail at runtime
- No clear (and enforced) separation between safe and potentially dangerous operations
- Tools like static analyzers are external, optional, and of varying quality
// C: All of these compile successfully, but some are dangerous
UINT8 *cursor = packet->options;
cursor = Dhcp6AppendOption(cursor, type, len, data); // No bounds checking
cursor = Dhcp6AppendOption(cursor, type, huge_len, data); // Potential overflow - compiles fine
// No way to tell which operations are safe just by looking
int *ptr = malloc(sizeof(int));
*ptr = 42; // Safe right now
free(ptr); // ptr becomes dangling
*ptr = 43; // Use-after-free, compiles fine
In Rust: Safety is enforced by the compiler at the language level:
- Memory safety violations are compile-time errors, not runtime bugs
- There is clear (and enforced) separation between safe and unsafe code using the
unsafe
keyword - Safe abstractions are guaranteed safe by the compiler, not by developer promises
- Unsafe code has strict requirements and caller obligations that are compiler-enforced
It is important to understand that unsafe code does not mean the code is not safe. It is a way to tell the compiler that the programmer is taking responsibility for upholding certain safety guarantees that the compiler cannot automatically verify. There are tools like Miri that can help verify unsafe code correctness, but the key point is that the compiler enforces a clear boundary between safe and unsafe code.
Rust's Safe/Unsafe Code Separation
The separation between safe and unsafe code is enforced by the compiler:
Safe Code (Most Rust code)
#![allow(unused)] fn main() { // Safe code - the compiler guarantees memory safety let mut buffer = Vec::new(); // Dynamic allocation buffer.extend_from_slice(user_input); // Automatic bounds checking let value = buffer[0]; // Bounds checked - panics if out of bounds let safe_value = buffer.get(0); // Returns Option<T> - no panic possible // Ownership prevents use-after-free at compile time let data = vec![1, 2, 3]; let reference = &data[0]; drop(data); // COMPILE ERROR: cannot drop while borrowed println!("{}", reference); // This line would never be reached }
The compiler guarantees that safe code cannot:
- Access memory out of bounds
- Use memory after it's freed
- Have data races in multi-threaded code
- Dereference null or dangling pointers
Unsafe Code (requires explicit opt-in)
#![allow(unused)] fn main() { // Unsafe code must be explicitly marked and justified unsafe { // Raw pointer operations that bypass Rust's safety checks let raw_ptr = buffer.as_ptr(); let value = *raw_ptr.add(index); // Could be out of bounds } // Unsafe functions must declare their safety requirements /// # Safety /// /// The caller must ensure: /// - `ptr` is valid for reads of `size` bytes /// - `ptr` is properly aligned for type T /// - The memory referenced by `ptr` is not mutated during this function call /// - The memory referenced by `ptr` contains a valid value of type T unsafe fn read_unaligned<T>(ptr: *const u8, size: usize) -> T { // Implementation that bypasses compiler safety checks std::ptr::read_unaligned(ptr as *const T) } }
Compiler-Enforced Safety Requirements
Unlike C where safety comments are just documentation, Rust's unsafe
keyword creates compiler-enforced
obligations. This is required. The developer cannot perform operations (such as dereferencing a raw pointer) that
are considered "unsafe" without marking the code as such.
1. Unsafe Code Must Be Explicitly Marked
#![allow(unused)] fn main() { // This will NOT compile - raw pointer dereference requires unsafe fn broken_function(ptr: *const u8) -> u8 { *ptr // COMPILE ERROR: dereference of raw pointer is unsafe } // Must be written as: fn safe_wrapper(ptr: *const u8) -> Option<u8> { if ptr.is_null() { return None; } unsafe { // Safety: We checked for null above Some(*ptr) } } }
2. Unsafe Functions Require Safety Documentation
The Rust compiler and tools in the ecosystem enforce that unsafe functions document their safety requirements:
#![allow(unused)] fn main() { /// # Safety /// /// This function is unsafe because it dereferences a raw pointer without /// verifying its validity. The caller must ensure: /// /// 1. `data_ptr` points to valid memory containing at least `len` bytes /// 2. The memory remains valid for the duration of this function call /// 3. The memory is properly aligned for the data type being read /// 4. The memory contains valid UTF-8 data if being interpreted as a string unsafe fn parse_network_packet(data_ptr: *const u8, len: usize) -> Result<Packet, ParseError> { // Implementation that works with raw bytes from network let slice = unsafe { // Safety: Caller guarantees ptr and len are valid std::slice::from_raw_parts(data_ptr, len) }; // Rest of function uses safe code operating on the slice Packet::parse(slice) } }
An unsafe function (like parse_network_packet
) cannot be called from safe code without an unsafe
block, forcing
the caller to acknowledge the safety requirements.
3. Safe Abstractions Hide Unsafe Implementation Details
#![allow(unused)] fn main() { // Public safe interface - users cannot misuse this impl NetworkBuffer { /// Safe interface for reading network packets /// /// This function handles all bounds checking and validation internally. /// Users cannot cause memory safety violations through this interface. pub fn read_packet(&self, offset: usize) -> Result<Packet, NetworkError> { // Bounds checking in safe code if offset >= self.len() { return Err(NetworkError::OffsetOutOfBounds); } // All unsafe operations are contained within this implementation unsafe { // Safety: We verified bounds above and self.data is always valid let ptr = self.data.as_ptr().add(offset); let remaining = self.len() - offset; parse_network_packet(ptr, remaining) } } } // Users can only call the safe interface: let packet = buffer.read_packet(offset)?; }
Advantages
-
Audit Surface: In a large codebase, you only need to audit the small amount of
unsafe
code, not every function that handles pointers or arrays. -
Compiler Enforcement: Safety isn't dependent on developers catching mistakes in code reviews - the compiler prevents most memory safety bugs from being written in the first place.
-
Safe by Default: New code is safe unless explicitly marked
unsafe
, reversing the C model where code is unsafe by default. -
Clear Contracts: Unsafe code must document its safety requirements, and safe wrappers must uphold these contracts. This creates a clear chain of responsibility.
-
Incremental Adoption: You can write safe Rust code that calls into existing C libraries through well-defined unsafe boundaries, gradually improving safety over time. This is important for UEFI firmware given the large amount of pre-existing C code that needs to continue being used during a transition to Rust.
Summary: The C vulnerability in this CVE existed because it used a fixed-size buffer (UINT8 ServerId[256]
) and
performed unchecked copying. Rust eliminates this entire vulnerability class by preventing unsafe operations -
you cannot overflow a Vec<u8>
because it automatically grows, and you cannot access invalid slice indices because
bounds are checked automatically. The zerocopy
approach also ensures that the binary layout exactly matches the C
structures while providing memory safety.
This is why it is important to write a minimum amount of unsafe Rust code that is checked with tools like Miri and then build safe abstractions on top of that unsafe code. The safe abstractions are what prevent entire classes of vulnerabilities from ever occurring in the first place and the Rust compiler ensures that safe code is always safe.
CVE-2023-45229: Out-of-Bounds Read in DHCPv6
- CVE Details: CVE-2023-45229
- CVSS Score: 6.5 (MEDIUM)
- Vulnerability Type: CWE-125 (Out-of-bounds Read)
- Vulnerabilities in EDK2 NetworkPkg IP stack implementation
Issue Description: "EDK2's Network Package is susceptible to an out-of-bounds read vulnerability when processing IA_NA or IA_TA options in DHCPv6 Advertise messages. This vulnerability can be exploited by an attacker to gain unauthorized access and potentially lead to a loss of confidentiality."
C Problem:
There was not sufficient bounds checks when parsing IA (Identity Association) options. Before the fix, the code did not properly validate the option length against the packet boundaries, leading to potential out-of-bounds reads when processing malformed DHCPv6 packets.
// From NetworkPkg/Dhcp6Dxe/Dhcp6Io.c (before fixes)
EFI_STATUS
Dhcp6UpdateIaInfo (
IN OUT DHCP6_INSTANCE *Instance,
IN EFI_DHCP6_PACKET *Packet
)
{
// ... existing code ...
// Vulnerable: Option length not properly validated against packet boundaries
Option = Dhcp6SeekIaOption (
Packet->Dhcp6.Option,
OptionLen, // OptionLen could extend beyond actual packet data
&Instance->Config->IaDescriptor
);
// Vulnerable: No bounds checking when reading IA option fields
if (Instance->Config->IaDescriptor.Type == Dhcp6OptIana) {
// Direct memory access without bounds validation
T1 = NTOHL (ReadUnaligned32 ((UINT32 *)(DHCP6_OFFSET_OF_IA_NA_T1 (Option))));
T2 = NTOHL (ReadUnaligned32 ((UINT32 *)(DHCP6_OFFSET_OF_IA_NA_T2 (Option))));
}
// Seeks inner options without proper bounds checking
Status = Dhcp6SeekInnerOption ( // Old unsafe function
Instance->Config->IaDescriptor.Type,
Option,
OptionLen, // Could extend beyond actual option data
&IaInnerOpt,
&IaInnerLen
);
}
Example Rust Design (Prevention by Design):
Rust can leverage its strong memory safety capabilities like zero-copy parsing, strong typing, and ownership to make a safe design available to developers:
#![allow(unused)] fn main() { use zerocopy::{FromBytes, Immutable, KnownLayout, Unaligned}; /// DHCPv6 IA option types - prevents option type confusion #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u16)] pub enum IaOptionType { IaNa = 3, // Identity Association for Non-temporary Addresses IaTa = 4, // Identity Association for Temporary Addresses IaAddr = 5, // IA Address option IaPd = 25, // Identity Association for Prefix Delegation } /// Zero-copy DHCPv6 IA option header matching C structure layout #[derive(Debug, FromBytes, KnownLayout, Immutable, Unaligned)] #[repr(C)] pub struct IaOptionHeader { pub option_code: u16, // Network byte order pub option_length: u16, // Network byte order pub iaid: u32, // Network byte order pub t1: u32, // Network byte order pub t2: u32, // Network byte order // Followed by variable-length sub-options } /// Type-safe wrapper that owns its slice and guarantees bounds safety #[derive(Debug)] pub struct IaOption<'a> { header: &'a IaOptionHeader, sub_options: &'a [u8], option_type: IaOptionType, } /// Iterator for IA sub-options with guaranteed memory safety pub struct IaSubOptionIterator<'a> { data: &'a [u8], offset: usize, } impl<'a> IaOption<'a> { /// Safe zero-copy parsing with compile-time layout verification pub fn parse(option_data: &'a [u8]) -> Result<Self, Dhcp6ParseError> { // Ensure minimum size for complete option (4-byte option header + 12-byte IA data = 16 bytes) let header = IaOptionHeader::read_from_prefix(option_data) .ok_or(Dhcp6ParseError::InsufficientData { needed: size_of::<IaOptionHeader>(), available: option_data.len(), })?; // Convert from network byte order and validate let option_code = u16::from_be(header.option_code); let option_length = u16::from_be(header.option_length) as usize; // Type-safe option code validation let option_type = match option_code { 3 => IaOptionType::IaNa, 4 => IaOptionType::IaTa, 25 => IaOptionType::IaPd, _ => return Err(Dhcp6ParseError::InvalidOptionType(option_code)), }; // Bounds verification - option_length includes only the payload, not the 4-byte option header if option_data.len() < 4 + option_length { return Err(Dhcp6ParseError::TruncatedOption { declared_length: option_length, available: option_data.len().saturating_sub(4), }); } // Safe slice extraction for sub-options (starts after 16-byte total header) let sub_options_start = size_of::<IaOptionHeader>(); let sub_options_end = 4 + option_length; // 4-byte option header + declared payload length let sub_options = &option_data[sub_options_start..sub_options_end]; Ok(IaOption { header, sub_options, option_type, }) } /// Safe accessor methods with automatic byte order conversion pub fn iaid(&self) -> u32 { u32::from_be(self.header.iaid) } pub fn t1(&self) -> u32 { u32::from_be(self.header.t1) } pub fn t2(&self) -> u32 { u32::from_be(self.header.t2) } pub fn option_type(&self) -> IaOptionType { self.option_type } /// Iterator over sub-options with guaranteed bounds safety pub fn sub_options(&self) -> IaSubOptionIterator<'a> { IaSubOptionIterator { data: self.sub_options, offset: 0, } } } impl<'a> Iterator for IaSubOptionIterator<'a> { type Item = Result<SubOption<'a>, Dhcp6ParseError>; fn next(&mut self) -> Option<Self::Item> { // Check if we have reached the end of data if self.offset >= self.data.len() { return None; } // Ensure we have enough bytes for sub-option header (4 bytes minimum) if self.offset + 4 > self.data.len() { return Some(Err(Dhcp6ParseError::TruncatedSubOption { offset: self.offset, remaining: self.data.len() - self.offset, })); } // Safe extraction of sub-option header let option_code = u16::from_be_bytes([ self.data[self.offset], self.data[self.offset + 1], ]); let option_length = u16::from_be_bytes([ self.data[self.offset + 2], self.data[self.offset + 3], ]) as usize; // Bounds check for sub-option data let data_start = self.offset + 4; let data_end = match data_start.checked_add(option_length) { Some(end) if end <= self.data.len() => end, _ => return Some(Err(Dhcp6ParseError::SubOptionTooLong { declared_length: option_length, available: self.data.len() - data_start, })), }; // Safe slice extraction let option_data = &self.data[data_start..data_end]; // Advance iterator position with overflow protection self.offset = data_end; Some(Ok(SubOption { code: option_code, data: option_data, })) } } /// Type-safe sub-option representation #[derive(Debug)] pub struct SubOption<'a> { pub code: u16, pub data: &'a [u8], } /// More specific error types to facilitate better error handling #[derive(Debug, Clone, PartialEq, Eq)] pub enum Dhcp6ParseError { InsufficientData { needed: usize, available: usize }, InvalidOptionType(u16), TruncatedOption { declared_length: usize, available: usize }, TruncatedSubOption { offset: usize, remaining: usize }, SubOptionTooLong { declared_length: usize, available: usize }, } // Usage example - out-of-bounds reads are prevented by design: pub fn process_ia_option(packet_data: &[u8]) -> Result<(), Dhcp6ParseError> { let ia_option = IaOption::parse(packet_data)?; println!("IA ID: {}, T1: {}, T2: {}", ia_option.iaid(), ia_option.t1(), ia_option.t2()); // Safe iteration over sub-options - bounds checking is automatic for sub_option_result in ia_option.sub_options() { let sub_option = sub_option_result?; println!("Sub-option code: {}, length: {}", sub_option.code, sub_option.data.len()); } Ok(()) } }
How This Helps (Eliminates Out-of-Bounds Reads):
- Binary Layout Safety: The traits from the
zerocopy
crate ensure binary layouts match the C structures - Compile-Time Layout Verification: The
FromBytes
trait guarantees safe deserialization from byte arrays - Ownership-Based Bounds: The iterator owns its slice and cannot access memory beyond the slice bounds
- Checked Arithmetic: All size calculations use checked operations preventing integer overflow
- Type-Level Validation: Option types are validated at parse time, preventing developers from confusing types
- Explicit Error Handling: All parsing failures are captured as typed errors rather than memory corruption
Summary: The C vulnerability existed because it performed unchecked pointer arithmetic
(IaInnerOpt += 4 + InnerOptLen
) and direct memory access without bounds verification. Rust eliminates this by
preventing unsafe operations. You cannot access invalid slice indices, arithmetic overflow is detected, and the type
system ensures only valid option types are processed.
CVE-2014-8271: Buffer Overflow in Variable Name Processing
- CVE Details: CVE-2014-8271
- CVSS Score: 6.8 (MEDIUM)
- Vulnerability Type: CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')
- Fixed in: 6ebffb67c8
Issue Description: "Buffer overflow in the Reclaim function allows physically proximate attackers to gain privileges via a long variable name."
C Problem:
The primary issue was unbounded iteration through the variable store without proper bounds checking, which could lead to infinite loops, out-of-bounds memory access, and secondary buffer overflows.
// From MdeModulePkg/Universal/Variable/RuntimeDxe/Variable.c
EFI_STATUS
Reclaim (
IN EFI_PHYSICAL_ADDRESS VariableBase,
OUT UINTN *LastVariableOffset
)
{
VARIABLE_HEADER *Variable;
CHAR16 VariableName[256]; // Fixed-size buffer vulnerability exists
UINTN VariableNameSize;
Variable = GetStartPointer(VariableBase);
// Vulnerable: No bounds checking - loop can run forever or access invalid memory
while (IsValidVariableHeader(Variable)) {
// If Variable store is corrupted, this loop may:
// 1. Never terminate (infinite loop)
// 2. Access memory beyond the variable store (out-of-bounds read)
// 3. Process corrupted variable names (buffer overflow in CopyMem)
VariableNameSize = NameSizeOfVariable(Variable);
CopyMem(VariableName, GetVariableNamePtr(Variable), VariableNameSize);
Variable = GetNextVariablePtr(Variable); // May point to invalid memory
}
return EFI_SUCCESS;
}
The C Fix Made:
BOOLEAN
IsValidVariableHeader (
IN VARIABLE_HEADER *Variable,
IN VARIABLE_HEADER *VariableStoreEnd // NEW: End boundary
)
{
if ((Variable == NULL) || (Variable >= VariableStoreEnd) || (Variable->StartId != VARIABLE_DATA)) {
// Variable is NULL or has reached the end of variable store, or the StartId is not correct.
return FALSE;
}
// ... rest of validation
}
// And updated all the while loops:
while (IsValidVariableHeader(Variable, GetEndPointer(VariableStoreHeader))) {
// Loop now terminates safely when reaching the end of the variable store
Variable = GetNextVariablePtr(Variable);
}
How Rust Prevents This (Prevention by Design):
Rust eliminates this vulnerability through safe iteration patterns, dynamic memory management, and automatic bounds checking:
#![allow(unused)] fn main() { use zerocopy::{FromBytes, KnownLayout, Unaligned}; /// Zero-copy compatible UEFI variable header that matches the C structure layout #[derive(Debug, FromBytes, KnownLayout, Unaligned)] #[repr(C)] pub struct VariableHeader { pub start_id: u16, // Variable start marker (0x55AA) pub state: u8, // Variable state flags pub reserved: u8, // Reserved for alignment pub attributes: u32, // Variable attributes bitfield pub name_size: u32, // Size of variable name in bytes pub data_size: u32, // Size of variable data in bytes pub vendor_guid: [u8; 16], // Vendor GUID // Followed by: variable name (UTF-16), variable data } /// Type-safe variable name that dynamically grows as needed #[derive(Debug, Clone)] pub struct VariableName { name: String, } impl VariableName { pub fn from_utf16_bytes(bytes: &[u8]) -> Result<Self, VariableError> { // Safe UTF-16 validation and conversion let utf16_data: Vec<u16> = bytes .chunks_exact(2) .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) .collect(); let name = String::from_utf16(&utf16_data) .map_err(|_| VariableError::InvalidNameEncoding)?; // Reasonable limits prevent DoS, but no arbitrary buffer size if name.len() > 1024 { return Err(VariableError::NameTooLong { len: name.len() }); } Ok(Self { name }) } pub fn as_str(&self) -> &str { &self.name } } /// Safe iterator with automatic bounds checking and termination pub struct VariableStoreIterator<'a> { data: &'a [u8], offset: usize, } impl<'a> VariableStoreIterator<'a> { pub fn new(store_data: &'a [u8]) -> Self { Self { data: store_data, offset: 0 } } } impl<'a> Iterator for VariableStoreIterator<'a> { type Item = Result<VariableName, VariableError>; fn next(&mut self) -> Option<Self::Item> { // Automatic termination when reaching end of data if self.offset + size_of::<VariableHeader>() > self.data.len() { return None; // Safe termination - no infinite loop possible } // Safe zero-copy header parsing let header_bytes = &self.data[self.offset..self.offset + size_of::<VariableHeader>()]; let header = match VariableHeader::read_from_bytes(header_bytes) { Some(h) => h, None => return Some(Err(VariableError::CorruptedHeader)), }; // Validate header before proceeding if header.start_id != 0x55AA { return Some(Err(VariableError::InvalidStartMarker)); } let name_size = header.name_size as usize; let data_size = header.data_size as usize; // Checked arithmetic prevents integer overflow let total_size = match size_of::<VariableHeader>() .checked_add(name_size) .and_then(|s| s.checked_add(data_size)) { Some(size) => size, None => return Some(Err(VariableError::SizeOverflow)), }; // Bounds check prevents out-of-bounds access if self.offset + total_size > self.data.len() { return Some(Err(VariableError::TruncatedVariable)); } // Safe slice extraction for variable name let name_start = self.offset + size_of::<VariableHeader>(); let name_bytes = &self.data[name_start..name_start + name_size]; let variable_name = match VariableName::from_utf16_bytes(name_bytes) { Ok(name) => name, Err(e) => return Some(Err(e)), }; // Safe advancement to next variable self.offset += total_size; self.offset = (self.offset + 7) & !7; // 8-byte alignment Some(Ok(variable_name)) } } #[derive(Debug, Clone)] pub enum VariableError { CorruptedHeader, InvalidStartMarker, InvalidNameEncoding, NameTooLong { len: usize }, SizeOverflow, TruncatedVariable, } // Usage - infinite loops and buffer overflows are prevented: pub fn reclaim_variables(store_data: &[u8]) -> Result<Vec<VariableName>, VariableError> { let mut variables = Vec::new(); // Iterator automatically terminates safely at end of data for variable_result in VariableStoreIterator::new(store_data) { let variable_name = variable_result?; variables.push(variable_name); } Ok(variables) } }
How This Helps (Eliminates Buffer Overflow):
- Safe Iteration: Iterator pattern with automatic termination prevents infinite loops
- Dynamic Memory Management:
String
andVec<u8>
grow as needed, eliminating fixed-size buffers and complicated logic to grow them - Automatic Bounds Checking: All slice access is bounds-checked by the compiler
- Checked Arithmetic: Integer overflow is detected and handled as an error, not silent corruption
- Zero-Copy Parsing:
zerocopy
traits ensure safe binary layout parsing without manual pointer arithmetic - Type-Safe Validation: Variable headers and names are validated before use, preventing corruption-based attacks
Summary: The C vulnerability existed because it used unbounded iteration through variable stores without
checking if the iteration had reached the end of valid memory. The primary attack vector was corrupted variable
headers that could cause infinite loops or out-of-bounds memory access during variable store traversal. Rust prevents
this class of vulnerability by preventing invalid accesses in safe code - you cannot access invalid slice
indices, and iterators automatically handle bounds checking. The zerocopy
approach also ensures that binary layout
parsing matches C structures while providing memory safety.
CVE-2022-36765: Integer Overflow in CreateHob()
- CVE Details: CVE-2022-36765
- CVSS Score: 7.0 (HIGH)
- Vulnerability Type: CWE-680 (Integer Overflow to Buffer Overflow)
- Integer Overflow in CreateHob() could lead to HOB OOB R/W
The Vulnerability: "EDK2's CreateHob()
function was susceptible to integer overflow when calculating HOB
alignment, allowing attackers to trigger buffer overflows."
Attack Scenario: An attacker provides HobLength = 0xFFFA
:
HobLength + 0x7 = 0x10001
(65537) - overflows UINT16 to0x0001
(0x0001) & (~0x7) = 0x0000
- aligned length becomes 0- Function allocates 0 bytes but caller expects 65530 bytes
- Subsequent HOB access overflows the HOB buffer
C Problem:
EFI_STATUS
PeiCreateHob (
IN CONST EFI_PEI_SERVICES **PeiServices,
IN UINT16 Type,
IN UINT16 Length,
IN OUT VOID **Hob
)
{
// Vulnerable: No overflow checking
HobLength = (UINT16) ((Length + 0x7) & (~0x7));
// ... buffer overflow when accessing memory beyond allocated size
}
How Rust Prevents This (Quick Defensive Translation):
If a similar function signature were retained in a relatively straightforward port of the C code, the code could be more defensively written as:
#![allow(unused)] fn main() { impl HobAllocator { pub fn create_hob(&mut self, hob_type: u16, length: u16) -> Result<*mut HobHeader, HobError> { // Checked arithmetic prevents overflow let aligned_length = length .checked_add(7) .ok_or(HobError::LengthOverflow)? & !7; // Bounds checking ensures allocation safety let total_size = self.free_memory_bottom .checked_add(aligned_length as u64) .ok_or(HobError::LengthOverflow)?; if total_size > self.free_memory_top { return Err(HobError::OutOfMemory); } // Safe allocation with verified bounds Ok(/* ... */) } } }
Idiomatic Rust Design (Prevention by Design):
However, the goal of writing firmware in Rust is to not write it like C code and litter the implementation with bounds checks and defensive programming bloat. The goal is to write code that is correct by construction (safe to use) and those checks are not needed. A more idiomatic Rust design eliminates the vulnerability entirely through type safety and ownership.
Some sample types in this example can help accomplish this:
HobLength
: A type-safe wrapper that guarantees no overflow can occur when creating HOB lengthsHobBuilder<T>
: A way to build HOBs that ensures only valid lengths can be usedHobRef<T>
: A type-safe reference that owns its memory region, preventing use-after-free
#![allow(unused)] fn main() { /// A type-safe HOB length that cannot overflow #[derive(Debug, Clone, Copy)] pub struct HobLength { // Note: The maximum size of HOB data is 64k value: u16, aligned: u16, } impl HobLength { /// Creates a HOB length with safety guaranteed at compile time pub const fn new(length: u16) -> Option<Self> { // Compile-time overflow detection match length.checked_add(7) { Some(sum) => Some(Self { value: length, aligned: sum & !7, }), None => None, } } pub const fn aligned_value(self) -> u16 { self.aligned } } /// Type-safe HOB builder that owns its memory pub struct HobBuilder<T> { hob_type: u16, length: HobLength, _phantom: PhantomData<T>, } impl<T> HobBuilder<T> { /// Creates a HOB with guaranteed valid length pub fn new(hob_type: u16, length: HobLength) -> Self { Self { hob_type, length, _phantom: PhantomData, } } /// Allocates and initializes HOB with type safety pub fn build(self, allocator: &mut HobAllocator) -> Result<HobRef<T>, HobError> { // Length is guaranteed valid by type system let aligned_length = self.length.aligned_value(); // Use safe allocation that returns owned memory let memory = allocator.allocate_aligned(aligned_length as usize)?; // Initialize the HOB header safely let hob_ref = HobRef::new(memory, self.hob_type)?; Ok(hob_ref) } } /// Type-safe HOB reference that owns its memory region pub struct HobRef<T> { data: NonNull<u8>, size: usize, _phantom: PhantomData<T>, } impl<T> HobRef<T> { /// Safe HOB creation with automatic cleanup fn new(memory: AlignedMemory, hob_type: u16) -> Result<Self, HobError> { let size = memory.size(); let data = memory.into_raw(); // Limit unsafe code for initialization so others can create HOBs in safe code unsafe { let header = data.cast::<HobHeader>(); header.as_ptr().write(HobHeader { hob_type, length: size as u16, }); } Ok(Self { data, size, _phantom: PhantomData, }) } /// Provides safe access to HOB data in a byte slice pub fn data(&self) -> &[u8] { unsafe { slice::from_raw_parts(self.data.as_ptr(), self.size) } } } // Usage example - overflow is prevented by design: let length = HobLength::new(0xFFFA).ok_or(HobError::LengthTooLarge)?; let builder = HobBuilder::<CustomHob>::new(HOB_TYPE_CUSTOM, length); let hob = builder.build(&mut allocator)?; }
How This Helps:
- Compile-Time Overflow Prevention:
HobLength::new()
useschecked_add()
, preventing overflows - Type-Level Guarantees: The type system ensures only valid lengths can be used to create HOBs
- Ownership-Based Safety:
HobRef<T>
owns its memory region, preventing use-after-free
What is PhantomData
and Why is it Needed Here?
If you haven't worked in Rust, the use of PhantomData<T>
in the HobBuilder<T>
and HobRef<T>
structs may be confusing. It is explained within the context of this example in
a bit more detail here to give more insight into Rust type safety.
-
Type Association Without Storage: These structs don't actually store a
T
value - they store raw bytes. But we want the type system to track what type of HOB this represents (e.g.,HobRef<CustomHob>
vsHobRef<MemoryHob>
).T
is a generic type parameter representing the specific HOB type (likeCustomHob
orMemoryHob
). -
Generic Parameter Usage: Without
PhantomData<T>
, the compiler would error because the generic typeT
appears in the struct declaration but isn't actually used in any fields. Rust requires all generic parameters to be "used" somehow. -
Drop Check Safety:
PhantomData<T>
tells the compiler that this struct "owns" data of typeT
for the purposes of drop checking, even though it's stored as raw bytes. This ensures proper cleanup order ifT
has a customDrop
trait implementation. -
Auto Trait Behavior: The presence of
PhantomData<T>
makes the struct inherit auto traits (likeSend
/Sync
) based on whetherT
implements them. -
Variance:
PhantomData<T>
is invariant overT
, which prevents dangerous type coercions that could violate memory safety when dealing with raw pointers.
Example of the Type Safety This Provides:
#![allow(unused)] fn main() { // These are distinct types that cannot be confused: let custom_hob: HobRef<CustomHob> = create_custom_hob()?; let memory_hob: HobRef<MemoryHob> = create_memory_hob()?; // Compile error - cannot assign different HOB types: // let bad: HobRef<CustomHob> = memory_hob; // Type mismatch // Safe typed access: let custom_data: &CustomHob = custom_hob.as_typed()?; // Type-safe }
In summary, without PhantomData<T>
, we'd lose impportant type safety and end up with untyped HobRef
structs that
could be confused with each other, defeating the purpose of the safe abstraction.
Additional CVEs Preventable by Rust's Safety Guarantees
These are additional instances of classes of vulnerabilities that Rust's safety guarantees can help prevent:
CVE-2023-45233: Infinite Loop in IPv6 Parsing
- CVE Details: CVE-2023-45233
- CVSS Score: 7.5 (HIGH)
- Vulnerability Type: CWE-835 (Loop with Unreachable Exit Condition)
CVE-2021-38575: Remote Buffer Overflow in iSCSI
- CVE Details: CVE-2021-38575
- CVSS Score: 8.1 (HIGH)
- Vulnerability Type: CWE-119 (Improper Restriction of Operations within Memory Buffer Bounds)
CVE-2019-14563: Integer Truncation
- CVE Details: CVE-2019-14563
- CVSS Score: 7.8 (HIGH)
- Vulnerability Type: CWE-681 (Incorrect Conversion between Numeric Types)
CVE-2024-1298: Division by Zero from Integer Overflow
- CVE Details: CVE-2024-1298
- CVSS Score: 6.0 (MEDIUM)
- Vulnerability Type: CWE-369 (Divide By Zero)
CVE-2014-4859: Integer Overflow in Capsule Update
- CVE Details: CVE-2014-4859
- CVSS Score: Not specified
- Vulnerability Type: Integer Overflow in DXE Phase