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 IDCVSS ScoreVulnerability TypePotential Rust Prevention Mechanism
CVE-2023-452308.3 (HIGH)Buffer Overflow in DHCPv6Automatic slice bounds checking
CVE-2022-367657.0 (HIGH)Integer Overflow in CreateHob()Checked arithmetic operations
CVE-2023-452296.5 (MEDIUM)Out-of-Bounds Read in DHCPv6Slice bounds verification
CVE-2014-82716.8 (MEDIUM)Buffer Overflow in Variable ProcessingDynamic Vec sizing eliminates fixed buffers
CVE-2023-452337.5 (HIGH)Infinite Loop in IPv6 ParsingIterator patterns with explicit termination
CVE-2021-385758.1 (HIGH)Remote Buffer Overflow in iSCSISlice-based network parsing with bounds checking
CVE-2019-145637.8 (HIGH)Integer TruncationExplicit type conversions with error handling
CVE-2024-12986.0 (MEDIUM)Division by Zero from Integer OverflowChecked arithmetic prevents overflow-induced division by zero
CVE-2014-4859Not specifiedInteger Overflow in Capsule UpdateSafe 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:

  1. Buffer Overflows: Automatic bounds checking eliminates this entire vulnerability class
  2. Use-After-Free: Ownership system prevents dangling pointers at compile time
  3. Integer Overflow: Checked arithmetic operations prevent overflow-induced vulnerabilities
  4. Out-of-Bounds Access: Slice bounds verification ensures memory safety
  5. Infinite Loops: Iterator patterns with explicit termination conditions
  6. Type Confusion: Strong type system prevents conversion errors

Detailed CVE Analysis

CVE-2023-45230: Buffer Overflow in DHCPv6 Client

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):

  1. Automatic Bounds Checking: append_option checks available space before writing
  2. Type-Safe Buffer Management: Vec<u8> grows dynamically and prevents overflows
  3. Structured Error Handling: Result<T, E> forces explicit error handling
  4. Safe by Construction: The API prevents creation of oversized packets
  5. 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

  1. 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.

  2. 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.

  3. Safe by Default: New code is safe unless explicitly marked unsafe, reversing the C model where code is unsafe by default.

  4. Clear Contracts: Unsafe code must document its safety requirements, and safe wrappers must uphold these contracts. This creates a clear chain of responsibility.

  5. 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

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):

  1. Binary Layout Safety: The traits from the zerocopy crate ensure binary layouts match the C structures
  2. Compile-Time Layout Verification: The FromBytes trait guarantees safe deserialization from byte arrays
  3. Ownership-Based Bounds: The iterator owns its slice and cannot access memory beyond the slice bounds
  4. Checked Arithmetic: All size calculations use checked operations preventing integer overflow
  5. Type-Level Validation: Option types are validated at parse time, preventing developers from confusing types
  6. 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

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):

  1. Safe Iteration: Iterator pattern with automatic termination prevents infinite loops
  2. Dynamic Memory Management: String and Vec<u8> grow as needed, eliminating fixed-size buffers and complicated logic to grow them
  3. Automatic Bounds Checking: All slice access is bounds-checked by the compiler
  4. Checked Arithmetic: Integer overflow is detected and handled as an error, not silent corruption
  5. Zero-Copy Parsing: zerocopy traits ensure safe binary layout parsing without manual pointer arithmetic
  6. 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()

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:

  1. HobLength + 0x7 = 0x10001 (65537) - overflows UINT16 to 0x0001
  2. (0x0001) & (~0x7) = 0x0000 - aligned length becomes 0
  3. Function allocates 0 bytes but caller expects 65530 bytes
  4. 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 lengths
  • HobBuilder<T>: A way to build HOBs that ensures only valid lengths can be used
  • HobRef<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:

  1. Compile-Time Overflow Prevention: HobLength::new() uses checked_add(), preventing overflows
  2. Type-Level Guarantees: The type system ensures only valid lengths can be used to create HOBs
  3. 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.

  1. 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> vs HobRef<MemoryHob>).

    T is a generic type parameter representing the specific HOB type (like CustomHob or MemoryHob).

  2. Generic Parameter Usage: Without PhantomData<T>, the compiler would error because the generic type T appears in the struct declaration but isn't actually used in any fields. Rust requires all generic parameters to be "used" somehow.

  3. Drop Check Safety: PhantomData<T> tells the compiler that this struct "owns" data of type T for the purposes of drop checking, even though it's stored as raw bytes. This ensures proper cleanup order if T has a custom Drop trait implementation.

  4. Auto Trait Behavior: The presence of PhantomData<T> makes the struct inherit auto traits (like Send/Sync) based on whether T implements them.

  5. Variance: PhantomData<T> is invariant over T, 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