Proposed design - 9elements/LinuxBootSMM GitHub Wiki

There are two possible ways of implementing SMM in the payload: either moving out the SMM setup code from firmware (coreboot) completely (reffered to later as SMM payload), or hooking up the payload-owned SMI handler to coreboot's SMI handler, and let it implement specific features (reffered to later as MM payload).

Considerations on each of these are given in:

Initial design approach - SMM payload

The proposed design architecture follows coreboot's "monolithic" approach to the SMM setup. By following this approach we assure the driver is kept minimalistic, and easy to review, without the need of jumping through multiple modules and understanding the connections and execution flow between them, which would rise-up entry threshold for potential reviewers.
The number of modules loaded by the Linux kernel is being kept to the minimum, with effectively 5 kernel modules being loaded to setup the SMM. We rely on the coreboot's downstream SMM payload interface [1], which was initially designed to improve security of coreboot's support for UEFI Authenticated Variables, when using coreboot+EDK2 (as payload) setup. Reusing the coreboot SMM payload interface allows the Linux SMM driver to own SMM, taking full advantage of benefits that come with the nature of Linux driver.
The flow during the "cold" boot (S0) is shown in Fig.3. As soon as ramstage finishes and the required data has been saved into the CBTABLE, coreboot moves control to the payload, and the Linux kernel loads the needed modules in the following order:

  1. coreboot table drivers - drivers responsible for parsing the needed informations stored in the coreboot table:
    • smram
    • smm_registers
    • spi_info
    • s3_comm
  2. smm_handler - consist of the functions that are to be called from the assembly trampoline, and effectively run in SMM.
  3. smm_loader - reads the previously parsed informations, sets up the parameters accordingly, loads assembly trampoline, relocation (C-)handlers, and the permanent handler. The existing solution for moving the SMM ownership to EDK2 payload distinguishes two boot paths [2] which require different approaches, these boot paths are: S0 boot - a basic boot path "boot with full configuration", i.e. usual power-up, and S3 Resume - "save to RAM resume", the system configuration is saved and system is placed in S3 "sleep" state, during the S3 resume phase the firmware loads the saved state. Issues introduced by these two boot paths mentioned in the documentation are:
  • S0 boot: Payloads shall not be silicon dependent, hence firmware (coreboot), must provide the payload with silicon specific register definitions. This issue was solved by providing these, as well as SPI layout, using the coreboot table (cbtable). The exact definitions are available given in the source file and omitted here [1].
  • S3 boot: On the S3 boot path, the payload execution is skipped, hence the minimal relocation has to be done by coreboot. The initial 4k of SMRAM containing software SMI number and SMBASE addresses for CPUs is preserved. coreboot then, performs SMM relocation and triggers payloads' software SMI.

The high-level overview of the flow for S0 and S3 are shown in Fig. 1 and Fig. 2 respectively.

%%{init: {"themeVariables": {"fontSize": "18px", "nodeSpacing": 60, "edgeLabelSpacing": 20, "actorFontSize": 18}}}%%
stateDiagram-v2
    state "bootblock -> verstage (optionally) -> romstage -> postcar" as prev
    [*] --> coreboot
    state coreboot {
        prev --> mp_init.c
        prev --> coreboot_table.c

        state mp_init.c {
            mp_init_with_smm() --> do_mp_init_with_smm()
            note right of do_mp_init_with_smm() : Skips SMM initialization - only the SoC specific parameters are being setup
        }
        state coreboot_table.c {
            lb_save_restore()
            note left of lb_save_restore() : SoC specific parameters are stored in CBTABLE.
        }
    }
        coreboot --> Payload(LinuxBoot)
    

    state Payload(LinuxBoot) {
        state "Linux SMM driver" as payload
        payload --> smram.c
        payload --> smm_registers.c
        payload --> spi_info.c
        payload --> s3_comm.c
        payload --> smm_handler.c
        payload --> smm_loader.c

        state smm_handler.c {
            start_handler() --> smi_lock()
            start_handler() --> smi_release_lock()
            start_handler() --> get_pci_address()
            start_handler() --> init_handler()
            note left of init_handler() : the "entry point" of the permanent handler, this code runs in SMM and is being called by the assembly trampoline.
            reloc_handler()
            note left of reloc_handler() : this code runs in SMM and is being indirectly triggered by the SMI from trigger_per_cpu(), and directly being called from the trampoline
        }

        state smm_loader.c {
            smm_loader_init() --> get_cb_data(): assumes the previous modules were correctly loaded
            note left of get_cb_data() : creates structures that are used only by this module (i.e. not exported), allowing to free the memory allocated by the "parser" modules, allowing them to be safely unloaded as soon as get_cb_data() is called.
            smm_loader_init() --> setup_permanent_handler()
            smm_loader_init() --> setup_reloc_handler()
            smm_loader_init() --> load_trampoline()
            smm_loader_init()  --> trigger_per_cpu()
        }
    }
Loading
Figure 1: S0 boot path SMM initialization flow
stateDiagram-v2
    [*] --> coreboot
    coreboot --> Payload(LinuxBoot)
    state coreboot {
        [*] --> mp_init.c
        [*] --> coreboot_table.c
        [*] --> smm_core_support.c

        state mp_init.c {
            mp_init_with_smm() --> do_mp_init_with_smm()
            note left of do_mp_init_with_smm() : Performs SMBASE relocation, does NOT install the permanent handler.
        }
        state coreboot_table.c {
            lb_save_restore()
            note left of lb_save_restore() : Required data (registers, SPI, etc.) is stored in CBTABLE.
        }
        state smm_core_support.c {
            linux_s3_restore()
            note left of linux_s3_restore() : Triggers Linux SMI handler and passes the needed info to lock down SMM. Please note that this function differs from perform_s3_restore() used by EDK2.
        }
    }

    state Payload(LinuxBoot) {
        state "Linux SMM Driver" as payload
        payload --> smm_handler.c
        state smm_handler.c {
            start_handler() --> smi_lock()
            start_handler() --> smi_release_lock()
            start_handler() --> get_pci_address()
            start_handler() --> init_handler()
            note left of init_handler() : the "entry point" of the permanent handler, this code runs n SMM and is being called by SMI sent from the linux_s3_restore(). On S3 suspend, this code resides in SMRAM and waits to be called.
            reloc_handler()
        }
    }
Loading
Figure 2: S3 Resume boot path SMM initialization flow
When on S3 track the idea is again similar to the EDK2 implementation: coreboot performs SMBASE relocation, triggers Linux SMM drivers' SMI handler, which then finishes up SMM lockdown.

Where SMM payload design fails

There are two main flaws in this approach when implemented with Linux:

  • Relocation code is SoC specific, meaning there would be a need to implement every SoC specific relocation handler as a Linux driver. This contradicts with the general principle of payloads being SoC independent.
  • On some modern platforms, relocation should be possible to be completed parallelly on all CPUs (see Section on SMBASE relocation). This can not be done without modifying the internals of local APIC implementation in the Linux kernel. Alternative would be to support SMM payload on single core systems only. As a consequence, we switch the design to the MM payload approach. The details on this approach are given in below.

Current design approach - MM payload

In MM payload approach, it is assumed that coreboot takes care of SMBASE relocation and installing the permantent handler. coreboot's permanent handler effectively acts as a "bridge" between coreboot-owned and payload-owned handlers. The general idea of MM payload was designed for EDK2 [3], and the detailed overview is omitted here. Instead, in this document we put focus on how the MM payload concept can be implemented with Linux as a payload.

Some of the ideas are shared with SMM payload approach, and namely:

  • The parsers are conceptually the same, the SoC specific SMM registers are not needed in Linux anymore, hence the smm_registers parser is omitted. On the other hand, we have new parser mm_info which contains additional informations that are needed such as the architecture for which coreboot was compiled (32 or 64bit).
  • On the S3 resume, the idea is the same as before - coreboot saves the entry point for the Linux-owned handler.

The responsibilites of the "loader" driver are bit more different:

  • it has place the handler in lower 4GB of the memory (this is because we can not assume that coreboot will be in long mode while trying to copy the handler to SMRAM)
  • perform relocations (still outside of SMRAM)
  • calculate offsets of the entrypoints
  • fill out the header with offsets, signature and handler's size
  • fire out an SMI with address where loader placed the handler

At this point, remaining work is done by the MM payload interface, and namely:

  • it checks whether the header is indeed placed at given address
  • if that's the case the handler is being copied to the dedicated SMRAM region
  • entry point is calculated based on coreboot bitness and offsets provided by the loader driver.

Fig. 1 shows the execution flow of the loader driver.

sequenceDiagram
    box Linux kernel (as payload)
        participant mm_loader
    end
    box coreboot ramstage (in SMM context)
        participant smihandler
        participant payload_mm_interface/smi
        
    end
    
    mm_loader ->>mm_loader: entry_point = place_handler(void)
    mm_loader->>mm_loader: register_entry_point(cb_data, entry_point)
    mm_loader ->> smihandler: trigger_smi(register, entry_point)
    smihandler->>smihandler: southbridge_smi_payload(save_state_ops)
    smihandler->>payload_mm_interface/smi: payload_mm_exec_interface(register, entry_point)
    payload_mm_interface/smi->>payload_mm_interface/smi: payload_mm_load(entry_point)
    payload_mm_interface/smi->>payload_mm_interface/smi: payload_mm_get_entrypoint(region_base, region_size)
    payload_mm_interface/smi-->>smihandler: status
    smihandler-->>mm_loader: rsm
Loading
Figure 1: Execution flow for MM payload initialization with Linux
After MM payload is initialized, all upcoming SMIs will eventually land in payload-owned SMI handler. The execution flow is given in Fig. 2.
sequenceDiagram
    box coreboot ramstage (in SMM context)
        participant smihandler
        participant payload_mm_payload/smi
    end
    box Linux kernel (in SMM context)
        participant mm_handler
    end

    smihandler->>smihandler: southbridge_smi(save_state_ops)
    smihandler->>payload_mm_payload/smi: payload_mm_call_entrypoint(context)
    payload_mm_payload/smi->>mm_handler: mm_entrypoint(context)
    note right of mm_handler: context shall contain arguments for the handler based on the purpose of the issued SMI
    mm_handler-->>smihandler: *restores registers to the pre-Linux state*
    smihandler->>smihandler: rsm
Loading
Figure 2: Normal SMI handling flow for MM payload with Linux

The demo (and how to run it) of the above described concept is further described in a dedicated README. More in depth discussion on implementation of the PoC of MM payload in Linux kernel is given in dedicated wiki page.

References

[1] 70378: drivers/smm_payload_interface: Add initial support for SMM payload
[2] 10. Boot Paths - UEFI PI Specification
[3] link to the blog post about MM payload (add when publshed)

⚠️ **GitHub.com Fallback** ⚠️