Read and Write UEFI Variables Using Runtime Services

This article provides an overview of how software interacts with Unified Extensible Firmware Interface (UEFI) NVRAM variables through UEFI Runtime Services. It explains the core firmware interfaces—GetVariable and SetVariable—and demonstrates how both native UEFI applications and operating system-level applications query, create, update, and delete these variables across Linux and Windows environments.

The Core UEFI Runtime Services APIs

UEFI exposes its variable interface through the EFI_RUNTIME_SERVICES table, which remains resident in memory even after the operating system boots via the ExitBootServices() transition. Two primary function pointers handle variable operations:

  • GetVariable: Retrieves the data payload, size, and attributes of a specified variable name and vendor GUID.
  • SetVariable: Creates a new variable, updates an existing variable's payload, or deletes a variable by passing a data size of zero.

Both functions require an identifying name (a null-terminated UTF-16 string) and a Vendor GUID (EFI_GUID), which prevents namespace collisions between hardware vendors and OS bootloaders.

Key Variable Attributes

When writing variables, applications must define bitmask attributes that dictate lifecycle and accessibility:

  • EFI_VARIABLE_NON_VOLATILE (0x00000001): Stores the variable in persistent flash memory across power cycles.
  • EFI_VARIABLE_BOOTSERVICE_ACCESS (0x00000002): Makes the variable accessible during the pre-boot phase.
  • EFI_VARIABLE_RUNTIME_ACCESS (0x00000004): Allows the variable to be read or written by the OS kernel and runtime applications. Without this flag, the variable disappears from view after ExitBootServices().
  • EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS (0x00000020): Requires a cryptographic signature and timestamp to modify, typically used for Secure Boot keys like PK, KEK, and db.

Native UEFI Applications (C / EDK II)

Within native UEFI executables (such as bootloaders or diagnostics run from the UEFI Shell), the runtime services table is accessed globally via the gRT pointer provided by the EDK II environment.

Reading a Variable

EFI_STATUS Status;
UINTN DataSize = 0;
VOID *Buffer = NULL;
EFI_GUID VendorGuid = SOME_VENDOR_GUID;

// Step 1: Query the required buffer size
Status = gRT->GetVariable(
    L"MyVariable",
    &VendorGuid,
    NULL,
    &DataSize,
    NULL
);

if (Status == EFI_BUFFER_TOO_SMALL) {
    Buffer = AllocatePool(DataSize);
    Status = gRT->GetVariable(
        L"MyVariable",
        &VendorGuid,
        NULL,
        &DataSize,
        Buffer
    );
}

Writing a Variable

UINT32 Value = 42;
UINT32 Attributes = EFI_VARIABLE_NON_VOLATILE | 
                    EFI_VARIABLE_BOOTSERVICE_ACCESS | 
                    EFI_VARIABLE_RUNTIME_ACCESS;

Status = gRT->SetVariable(
    L"MyVariable",
    &VendorGuid,
    Attributes,
    sizeof(Value),
    &Value
);

OS-Level Application Access

User-space applications cannot directly invoke firmware memory addresses due to CPU privilege separation (Ring 3 vs. Ring 0) and virtual memory paging. Instead, operating system kernels provide wrappers around the firmware calls.

Linux: efivarfs and libefivar

On modern Linux systems, the kernel maps UEFI Runtime Services through the efivarfs pseudo-filesystem, typically mounted at /sys/firmware/efi/efivars/.

Variables are represented as files formatted as [VariableName]-[VendorGUID]. The first 4 bytes of each file represent a 32-bit bitmask of the variable attributes, followed immediately by the binary data payload.

  • Reading: An application opens the file, reads the first 4 bytes to determine attributes, and reads the remaining bytes to extract the payload.
  • Writing: An application writes the 4-byte attribute header followed by the binary payload to the file. Deletion is handled using unlink() or writing a 0-byte payload.
  • Immutable Protection: By default, the Linux kernel sets the chattr +i (immutable) attribute on critical variables to prevent accidental deletion or corruption. Applications must strip this attribute using ioctl(fd, FS_IOC_SETFLAGS, ...) before writing updates.

User-space C applications typically use libefivar to abstract these low-level file and ioctl operations.

Windows: Win32 Firmware APIs

Windows exposes direct system calls that invoke GetVariable and SetVariable through the Windows kernel's Hardware Abstraction Layer (HAL).

User-space applications utilize two primary functions in kernel32.dll:

  • GetFirmwareEnvironmentVariableExW()
  • SetFirmwareEnvironmentVariableExW()

Implementation Requirements

  1. Privileges: The calling process must hold the SE_SYSTEM_ENVIRONMENT_NAME privilege. This requires administrator elevation and explicit privilege token adjustment via AdjustTokenPrivileges().
  2. GUID Format: The Vendor GUID must be passed as a string enclosed in curly braces (e.g., "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}").

Windows C++ Example

#include <windows.h>
#include <iostream>

void WriteVariable() {
    // Elevate privileges to SE_SYSTEM_ENVIRONMENT_NAME before calling
    const wchar_t* varName = L"MyVariable";
    const wchar_t* varGuid = L"{12345678-1234-1234-1234-123456789abc}";
    DWORD data = 1;
    DWORD attributes = VARIABLE_ATTRIBUTE_NON_VOLATILE |
                       VARIABLE_ATTRIBUTE_BOOTSERVICE_ACCESS |
                       VARIABLE_ATTRIBUTE_RUNTIME_ACCESS;

    BOOL success = SetFirmwareEnvironmentVariableExW(
        varName,
        varGuid,
        &data,
        sizeof(data),
        attributes
    );

    if (!success) {
        std::cerr << "Failed to write: " << GetLastError() << std::endl;
    }
}

Operational Safeguards and Limitations

  • Flash Wear: UEFI variables reside in NOR flash memory. NOR flash supports a limited number of erase cycles (typically 10,000 to 100,000). Applications should minimize write frequency to prevent hardware wear.
  • Garbage Collection: Firmware flash storage is small (often 64 KB to a few megabytes shared across all system variables). Appending or overwriting variables without firmware garbage collection can lead to EFI_OUT_OF_RESOURCES errors.
  • Runtime Paging Changes: On x86_64 systems, when an OS transitions to virtual memory, it invokes SetVirtualAddressMap(). At this point, the firmware re-maps its pointers, ensuring that calls made via the OS kernel's HAL or drivers remain valid. Applications must always route operations through OS-provided interfaces rather than cached physical pointers.