Payload MM: Journeys in Securely Supporting UEFI Secure Boot on coreboot

Or, "How I Decided to Reinvent the SMM Model, and the Bugs We Found Along the Way"

Introduction

Hi again! Last time, in KeyHijack: The Design Flaw in coreboot’s UEFI Secure Boot Support, we introduced the fundamental flaw in UEFI Secure Boot as it's implemented in coreboot + EDK2 downstreams. To summarise, coreboot's SMMSTORE driver implements variable storage, not secure variable storage. This time, we'll explore the possible solutions, beginning with the principle that closing the time-of-check, time-of-use (TOCTOU) vulnerability requires an isolated execution environment that will perform both tasks together, and that to prevent out-of-bound writes to the SPI flash by ring-0 code, we need the SPI controller to be in a write-protected state, so that neither code nor variables can be modified in unauthorised ways, but one where it can be made writeable if some conditions are met. On x86 platforms, the answer to this requirement is SMM.

First up, we could implement support for secure variable storage directly inside coreboot, thus closing the TOCTOU gap. EDK2's modularity is quite amenable to this, as a typical variable stack consists of:

  • Some storage backend module that implements the Firmware Volume Block protocol. (For context, in coreboot's current variable stack, this is provided by SmmStoreFvbRuntimeDxe. In UefiPayload's variable stack implemented for Slim Bootloader, this is provided by FvbSmm)
  • FaultTolerantWriteSmm: This module implements a layer of fault tolerance by maintaining some recovery data, similar to a database management system, allowing flash writes that were interrupted due to abrupt power loss to be recovered.
  • VariableSmm: This module implements variable support in SMM and provides the backend SMI handler used by the runtime service.
  • VariableSmmRuntimeDxe: This module implements the runtime service. It's the frontend to the variable stack for both UEFI and the OS.

We could enhance coreboot's SMMSTORE driver, possibly creating a new v3, by porting FaultTolerantWriteSmm and VariableSmm to coreboot. We would then implement our own VariableSmmRuntimeDxe in UefiPayload that calls this new, enhanced SMI handler.

This needs to be immediately disqualified though, since there is just so much code to port. The variable driver is 800-900K once compiled, and while much of it is OpensslLib, and in coreboot, we would use vboot - thereby avoiding 'rolling our own crypto,' which is discouraged - it's still both an impossible porting task and a bad idea. The module itself is over 7000 lines of code and 500 lines of headers, and a key library, AuthVariableLib, is close to 2000 lines itself. There are several more libraries compiled in, and other UEFI concepts and consumed services throughout the driver. Porting all of this would take a while, and if the resulting code had a bug, then the Secure Boot design flaw would have been replaced by an SMM privilege escalation vulnerability.

More than that: this is highly UEFI-specific code, and coreboot maintainers are unlikely to be able to maintain it and keep it secure. Until now, SMMSTORE has been an arbitrary read/write interface within a limited region of the SPI flash, so coreboot and EDK2 changes aren't coupled, but implementing such a solution would create strong coupling with UEFI, which conflicts with the philosophy that coreboot and its payload should be minimally coupled.

So, to the subtitle of this blog: how has this problem been solved elsewhere?

Slim Bootloader and UefiPayload

Slim Bootloader has a creative solution. They don't implement an SMM environment themselves, so they're free to implement one in UefiPayload. It's largely common EDK2 code, but there is some that's UefiPayload-specific. To keep this silicon-agnostic, they pass descriptions of the relevant registers and how to configure them, since enabling SMIs and a few other tasks are chipset-specific, and this solution reduces the maintenance burden. In fact, if the `define` variable names remain the same, then the bootloader's table generation code can be commonised, and the preprocessor will do all the work.

Enabling this solution means removing coreboot's SMM environment, but it's easy enough to try out, although (as usual) S3 sleep will need special handling. This is because on suspend, the CPU is powered down (mostly), but the memory remains powered. This means that the operating system and all of the user's applications remain open, but the firmware needs to rerun a special path to bring the CPU back to the same state and then resume directly to the kernel (rather than Windows Boot Manager, GRUB, etc).

So, Slim Bootloader actually does have SMM initialisation code, despite not containing an SMM environment, and on S3 resume, they run it to reinitialise the CPU with the UefiPayload environment. On a cold boot, the BlSupportSmm module identifies the location of each CPU's dedicated region of SMRAM (reserved for the earliest of SMI entry code, and the location where the CPU saves and restores the registers contents, which is required for SMM to be able to interrupt the OS without corrupting its running state). These are saved to a region of SMRAM that's reserved for sharing information with the bootloader. Upon resume, the bootloader loads these values back into the CPU, and SMM is reinitialised.

However, while sufficient, this approach is not ideal:

  • First, we don't want to give up the ability to install SMI handlers to handle board, EC, or other silicon-specific tasks or value-adds.
  • Furthermore, some coreboot platforms depend upon using SMM for some silicon-specific lockdown, and we'd leave them incomplete by abandoning coreboot's SMM for a completely different one. Slim Bootloader doesn't support these older Intel platforms, so there's no issue there, but coreboot does. One possible solution is to limit this new SMM feature to specific platforms, but this is suboptimal, especially when we're aiming to address a security flaw.
  • Finally: Ideologically, coreboot doesn't want to give up control of SMM.

Additionally, along the way, I found that UefiPayload's SMM support is a little too minimal, which results in a bug, of sorts.

First, some background about how SPI flash protection is implemented on Intel platforms:

  1. The 'write-protect disabled' bit in the "BIOS Control" register of the SPI controller controls whether SPI flash is writeable.
    • However, more protection is necessary. Since this bit sets the current configuration, it doesn't tell us (or the silicon) whether we've decided to leave the SPI flash entirely open, or whether it's only temporary. If it's only opened temporarily (by and for privileged code), then we need to be able to enforce that. So, it needs a lock bit.
  2. The 'lock enabled' bit in the "BIOS Control" register controls this 'lock.' When it's set, any attempts to write the WPD bit will generate an SMI.
    • For historical reasons, involving addressing a race condition as CPUs arrive in SMM, there is also the "enable InSMM.STS" bit that enforces an extra step before WPD can be set, defeating the race, but this doesn't change the general flow of the 'reapply protection' or 'enable write access' paths.
  3. The 'synchronous SMI status' bit in the "BIOS Control" register is a bit that firmware is supposed to set, indicating to the hardware that the event has been handled.
    • This is inaccurately described in the datasheet, which describes it as a plain status bit (and read-only), rather than the read-and-write-1-to-clear bit that it is.

While UefiPayload handles all of this correctly when *it* writes to the SPI flash, it does not install an SMI handler to acknowledge the event and reapply protection when ring-0 code attempts to do this. The integrity of the SPI flash is probably preserved though: since the SMI is never acknowledged, I suspect that the SPI controller will generate SMIs endlessly, and never return to the OS.

Ultimately, the initialisation and primary ownership of something this silicon-specific and with as many platform-specific responsibilities as SMM should remain with the bootloader.

So, it's time for something new, with some inspiration taken from elsewhere in the industry.

Payload MM

The first step is to reconsider the actual need: we want to grant the payload some control in SMM to implement a feature of its own, without taking it away from the bootloader. Unfortunately, that would seem to be a difficult task, gluing two different codebases together, with two very different ways of doing things. But once again, EDK2's modularity is incredibly helpful. The key elements of the typical SMM environment are:

  • PiSmmIpl: The ‘initial program loader’ of the SMM core. It also provides the SMM communication protocol that later will pass communication buffers from DXE or runtime code to registered SMI handlers (such as the variable stack).
  • PiSmmCore: A phase core similar to the PEI core or DXE core. It runs a dispatcher, maintains events and notifications, etc. Smaller than the DXE core, but like it, it's ring-0 code.
  • PiSmmCpuDxeSmm: The actual owner of the SMM environment on UEFI systems. It brings UEFI's SMM environment from modules loaded by DXE into a true SMM at the architectural and microarchitectural levels. At runtime, when an SMI is fired, control is first transferred here, then to PiSmmCore. To find the SMM core's runtime entrypoint, this driver produces the SMM configuration protocol, which (in the SMM design) PiSmmIpl is awaiting: once the protocol is installed, PiSmmIpl will call it and provide the entrypoint.

So, the solution is simple: we substitute PiSmmCpuDxeSmm for our own module, gluing our MM environment to coreboot, rather than the CPU's SMI entrypoint locations within SMRAM. It's unorthodox, but quite neat in how loosely coreboot and EDK2 are coupled if payload MM code handles the necessary environment changes in between. It's also reminiscent of Arm's approach with Trusted Firmware-A and EDK2 variable services, although they use a very different CPU driver in EDK2, and couple TF-A and EDK2 more strongly together against their FFA spec (Firmware Framework for A-profile).

There was a small issue though: at the time, the standalone MM core was lower quality than the SMM one, and there was no standalone MM IPL yet that would handle standalone MM-specific tasks. Due to the two issues together, payload MM began its existence based on the SMM design.

This forces a bigger issue: while standalone MM is decoupled from DXE by design, the SMM design requires function calls both in and out of SMRAM, because instead of the core passing its entrypoint address directly to the driver (and staying within the one environment), the IPL provides it instead using the data it shares with the core. Therefore, the initial plan was to leave SMRAM in an unlockable state, unlock it during the PiSmmIpl, and relock it once we're finished initialising. More recently, upstream interest in the standalone MM environment increased, a StandaloneMmIpl developed, and the core improved, which made it possible to move towards a purely software-driven initialisation, without hardware specific shenanigans like unlocking SMRAM.

Conclusion

In the final design, a bootloader (like coreboot) will offer an SMI handler to load one payload MM core module into a dedicated SMRAM region, then call into it. During this first entry, payload MM will obtain the MM core's runtime entrypoint for itself, and save its own entrypoint to a memory region that's shared with the bootloader. Then, on each SMI, the bootloader will call this payload MM function to enter standalone MM, and return to the bootloader once it's done.

And so, after enhancing both common EDK2 code and some of UefiPayload's drivers and libraries, we have a solution that's superior (for our purposes, at least) to Slim Bootloader's, successfully closes the TOCTOU gap, and allows further, future opportunities to enable helpful payload features inside the SMM environment, like securing the setup menu with a password, using the UserAuthFeaturePkg in edk2-platforms.