OvmfPkg/CpuHotplugSmm: introduce First SMI Handler for hot-added CPUs

Implement the First SMI Handler for hot-added CPUs, in NASM.

Add the interfacing C-language function that the SMM Monarch calls. This
function launches and coordinates SMBASE relocation for a hot-added CPU.

Cc: Ard Biesheuvel <ard.biesheuvel@linaro.org>
Cc: Igor Mammedov <imammedo@redhat.com>
Cc: Jiewen Yao <jiewen.yao@intel.com>
Cc: Jordan Justen <jordan.l.justen@intel.com>
Cc: Michael Kinney <michael.d.kinney@intel.com>
Cc: Philippe Mathieu-Daudé <philmd@redhat.com>
Ref: https://bugzilla.tianocore.org/show_bug.cgi?id=1512
Signed-off-by: Laszlo Ersek <lersek@redhat.com>
Message-Id: <20200226221156.29589-13-lersek@redhat.com>
Acked-by: Ard Biesheuvel <ard.biesheuvel@linaro.org>
Tested-by: Boris Ostrovsky <boris.ostrovsky@oracle.com>
This commit is contained in:
Laszlo Ersek 2020-02-26 23:11:52 +01:00 committed by mergify[bot]
parent 63c89da242
commit 51a6fb4118
5 changed files with 376 additions and 0 deletions

View File

@ -24,6 +24,8 @@
[Sources]
ApicId.h
CpuHotplug.c
FirstSmiHandler.nasm
FirstSmiHandlerContext.h
PostSmmPen.nasm
QemuCpuhp.c
QemuCpuhp.h
@ -39,9 +41,11 @@
BaseLib
BaseMemoryLib
DebugLib
LocalApicLib
MmServicesTableLib
PcdLib
SafeIntLib
SynchronizationLib
UefiDriverEntryPoint
[Protocols]

View File

@ -0,0 +1,154 @@
;------------------------------------------------------------------------------
; @file
; Relocate the SMBASE on a hot-added CPU when it services its first SMI.
;
; Copyright (c) 2020, Red Hat, Inc.
;
; SPDX-License-Identifier: BSD-2-Clause-Patent
;
; The routine runs on the hot-added CPU in the following "big real mode",
; 16-bit environment; per "SMI HANDLER EXECUTION ENVIRONMENT" in the Intel SDM
; (table "Processor Register Initialization in SMM"):
;
; - CS selector: 0x3000 (most significant 16 bits of SMM_DEFAULT_SMBASE).
;
; - CS limit: 0xFFFF_FFFF.
;
; - CS base: SMM_DEFAULT_SMBASE (0x3_0000).
;
; - IP: SMM_HANDLER_OFFSET (0x8000).
;
; - ES, SS, DS, FS, GS selectors: 0.
;
; - ES, SS, DS, FS, GS limits: 0xFFFF_FFFF.
;
; - ES, SS, DS, FS, GS bases: 0.
;
; - Operand-size and address-size override prefixes can be used to access the
; address space beyond 1MB.
;------------------------------------------------------------------------------
SECTION .data
BITS 16
;
; Bring in SMM_DEFAULT_SMBASE from
; "MdePkg/Include/Register/Intel/SmramSaveStateMap.h".
;
SMM_DEFAULT_SMBASE: equ 0x3_0000
;
; Field offsets in FIRST_SMI_HANDLER_CONTEXT, which resides at
; SMM_DEFAULT_SMBASE.
;
ApicIdGate: equ 0 ; UINT64
NewSmbase: equ 8 ; UINT32
AboutToLeaveSmm: equ 12 ; UINT8
;
; SMRAM Save State Map field offsets, per the AMD (not Intel) layout that QEMU
; implements. Relative to SMM_DEFAULT_SMBASE.
;
SaveStateRevId: equ 0xFEFC ; UINT32
SaveStateSmbase: equ 0xFEF8 ; UINT32
SaveStateSmbase64: equ 0xFF00 ; UINT32
;
; CPUID constants, from "MdePkg/Include/Register/Intel/Cpuid.h".
;
CPUID_SIGNATURE: equ 0x00
CPUID_EXTENDED_TOPOLOGY: equ 0x0B
CPUID_VERSION_INFO: equ 0x01
GLOBAL ASM_PFX (mFirstSmiHandler) ; UINT8[]
GLOBAL ASM_PFX (mFirstSmiHandlerSize) ; UINT16
ASM_PFX (mFirstSmiHandler):
;
; Get our own APIC ID first, so we can contend for ApicIdGate.
;
; This basically reimplements GetInitialApicId() from
; "UefiCpuPkg/Library/BaseXApicLib/BaseXApicLib.c".
;
mov eax, CPUID_SIGNATURE
cpuid
cmp eax, CPUID_EXTENDED_TOPOLOGY
jb GetApicIdFromVersionInfo
mov eax, CPUID_EXTENDED_TOPOLOGY
mov ecx, 0
cpuid
test ebx, 0xFFFF
jz GetApicIdFromVersionInfo
;
; EDX has the APIC ID, save it to ESI.
;
mov esi, edx
jmp KnockOnGate
GetApicIdFromVersionInfo:
mov eax, CPUID_VERSION_INFO
cpuid
shr ebx, 24
;
; EBX has the APIC ID, save it to ESI.
;
mov esi, ebx
KnockOnGate:
;
; See if ApicIdGate shows our own APIC ID. If so, swap it to MAX_UINT64
; (close the gate), and advance. Otherwise, keep knocking.
;
; InterlockedCompareExchange64():
; - Value := &FIRST_SMI_HANDLER_CONTEXT.ApicIdGate
; - CompareValue (EDX:EAX) := APIC ID (from ESI)
; - ExchangeValue (ECX:EBX) := MAX_UINT64
;
mov edx, 0
mov eax, esi
mov ecx, 0xFFFF_FFFF
mov ebx, 0xFFFF_FFFF
lock cmpxchg8b [ds : dword (SMM_DEFAULT_SMBASE + ApicIdGate)]
jz ApicIdMatch
pause
jmp KnockOnGate
ApicIdMatch:
;
; Update the SMBASE field in the SMRAM Save State Map.
;
; First, calculate the address of the SMBASE field, based on the SMM Revision
; ID; store the result in EBX.
;
mov eax, dword [ds : dword (SMM_DEFAULT_SMBASE + SaveStateRevId)]
test eax, 0xFFFF
jz LegacySaveStateMap
mov ebx, SMM_DEFAULT_SMBASE + SaveStateSmbase64
jmp UpdateSmbase
LegacySaveStateMap:
mov ebx, SMM_DEFAULT_SMBASE + SaveStateSmbase
UpdateSmbase:
;
; Load the new SMBASE value into EAX.
;
mov eax, dword [ds : dword (SMM_DEFAULT_SMBASE + NewSmbase)]
;
; Save it to the SMBASE field whose address we calculated in EBX.
;
mov dword [ds : dword ebx], eax
;
; Set AboutToLeaveSmm.
;
mov byte [ds : dword (SMM_DEFAULT_SMBASE + AboutToLeaveSmm)], 1
;
; We're done; leave SMM and continue to the pen.
;
rsm
ASM_PFX (mFirstSmiHandlerSize):
dw $ - ASM_PFX (mFirstSmiHandler)

View File

@ -0,0 +1,47 @@
/** @file
Define the FIRST_SMI_HANDLER_CONTEXT structure, which is an exchange area
between the SMM Monarch and the hot-added CPU, for relocating the SMBASE of
the hot-added CPU.
Copyright (c) 2020, Red Hat, Inc.
SPDX-License-Identifier: BSD-2-Clause-Patent
**/
#ifndef FIRST_SMI_HANDLER_CONTEXT_H_
#define FIRST_SMI_HANDLER_CONTEXT_H_
//
// The following structure is used to communicate between the SMM Monarch
// (running the root MMI handler) and the hot-added CPU (handling its first
// SMI). It is placed at SMM_DEFAULT_SMBASE, which is in SMRAM under QEMU's
// "SMRAM at default SMBASE" feature.
//
#pragma pack (1)
typedef struct {
//
// When ApicIdGate is MAX_UINT64, then no hot-added CPU may proceed with
// SMBASE relocation.
//
// Otherwise, the hot-added CPU whose APIC ID equals ApicIdGate may proceed
// with SMBASE relocation.
//
// This field is intentionally wider than APIC_ID (UINT32) because we need a
// "gate locked" value that is different from all possible APIC_IDs.
//
UINT64 ApicIdGate;
//
// The new SMBASE value for the hot-added CPU to set in the SMRAM Save State
// Map, before leaving SMM with the RSM instruction.
//
UINT32 NewSmbase;
//
// The hot-added CPU sets this field to 1 right before executing the RSM
// instruction. This tells the SMM Monarch to proceed to polling the last
// byte of the normal RAM reserved page (Post-SMM Pen).
//
UINT8 AboutToLeaveSmm;
} FIRST_SMI_HANDLER_CONTEXT;
#pragma pack ()
#endif // FIRST_SMI_HANDLER_CONTEXT_H_

View File

@ -7,13 +7,21 @@
**/
#include <Base.h> // BASE_1MB
#include <Library/BaseLib.h> // CpuPause()
#include <Library/BaseMemoryLib.h> // CopyMem()
#include <Library/DebugLib.h> // DEBUG()
#include <Library/LocalApicLib.h> // SendInitSipiSipi()
#include <Library/SynchronizationLib.h> // InterlockedCompareExchange64()
#include <Register/Intel/SmramSaveStateMap.h> // SMM_DEFAULT_SMBASE
#include "FirstSmiHandlerContext.h" // FIRST_SMI_HANDLER_CONTEXT
#include "Smbase.h"
extern CONST UINT8 mPostSmmPen[];
extern CONST UINT16 mPostSmmPenSize;
extern CONST UINT8 mFirstSmiHandler[];
extern CONST UINT16 mFirstSmiHandlerSize;
/**
Allocate a non-SMRAM reserved memory page for the Post-SMM Pen for hot-added
@ -108,3 +116,152 @@ SmbaseReleasePostSmmPen (
{
BootServices->FreePages (PenAddress, 1);
}
/**
Place the handler routine for the first SMIs of hot-added CPUs at
(SMM_DEFAULT_SMBASE + SMM_HANDLER_OFFSET).
Note that this effects an "SMRAM to SMRAM" copy.
Additionally, shut the APIC ID gate in FIRST_SMI_HANDLER_CONTEXT.
This function may only be called from the entry point function of the driver,
and only after PcdQ35SmramAtDefaultSmbase has been determined to be TRUE.
**/
VOID
SmbaseInstallFirstSmiHandler (
VOID
)
{
FIRST_SMI_HANDLER_CONTEXT *Context;
CopyMem ((VOID *)(UINTN)(SMM_DEFAULT_SMBASE + SMM_HANDLER_OFFSET),
mFirstSmiHandler, mFirstSmiHandlerSize);
Context = (VOID *)(UINTN)SMM_DEFAULT_SMBASE;
Context->ApicIdGate = MAX_UINT64;
}
/**
Relocate the SMBASE on a hot-added CPU. Then pen the hot-added CPU in the
normal RAM reserved memory page, set up earlier with
SmbaseAllocatePostSmmPen() and SmbaseReinstallPostSmmPen().
The SMM Monarch is supposed to call this function from the root MMI handler.
The SMM Monarch is responsible for calling SmbaseInstallFirstSmiHandler(),
SmbaseAllocatePostSmmPen(), and SmbaseReinstallPostSmmPen() before calling
this function.
If the OS maliciously boots the hot-added CPU ahead of letting the ACPI CPU
hotplug event handler broadcast the CPU hotplug MMI, then the hot-added CPU
returns to the OS rather than to the pen, upon RSM. In that case, this
function will hang forever (unless the OS happens to signal back through the
last byte of the pen page).
@param[in] ApicId The APIC ID of the hot-added CPU whose SMBASE should
be relocated.
@param[in] Smbase The new SMBASE address. The root MMI handler is
responsible for passing in a free ("unoccupied")
SMBASE address that was pre-configured by
PiSmmCpuDxeSmm in CPU_HOT_PLUG_DATA.
@param[in] PenAddress The address of the Post-SMM Pen for hot-added CPUs, as
returned by SmbaseAllocatePostSmmPen(), and installed
by SmbaseReinstallPostSmmPen().
@retval EFI_SUCCESS The SMBASE of the hot-added CPU with APIC ID
ApicId has been relocated to Smbase. The
hot-added CPU has reported back about leaving
SMM.
@retval EFI_PROTOCOL_ERROR Synchronization bug encountered around
FIRST_SMI_HANDLER_CONTEXT.ApicIdGate.
@retval EFI_INVALID_PARAMETER Smbase does not fit in 32 bits. No relocation
has been attempted.
**/
EFI_STATUS
SmbaseRelocate (
IN APIC_ID ApicId,
IN UINTN Smbase,
IN UINT32 PenAddress
)
{
EFI_STATUS Status;
volatile UINT8 *SmmVacated;
volatile FIRST_SMI_HANDLER_CONTEXT *Context;
UINT64 ExchangeResult;
if (Smbase > MAX_UINT32) {
Status = EFI_INVALID_PARAMETER;
DEBUG ((DEBUG_ERROR, "%a: ApicId=" FMT_APIC_ID " Smbase=0x%Lx: %r\n",
__FUNCTION__, ApicId, (UINT64)Smbase, Status));
return Status;
}
SmmVacated = (UINT8 *)(UINTN)PenAddress + (EFI_PAGE_SIZE - 1);
Context = (VOID *)(UINTN)SMM_DEFAULT_SMBASE;
//
// Clear AboutToLeaveSmm, so we notice when the hot-added CPU is just about
// to reach RSM, and we can proceed to polling the last byte of the reserved
// page (which could be attacked by the OS).
//
Context->AboutToLeaveSmm = 0;
//
// Clear the last byte of the reserved page, so we notice when the hot-added
// CPU checks back in from the pen.
//
*SmmVacated = 0;
//
// Boot the hot-added CPU.
//
// If the OS is benign, and so the hot-added CPU is still in RESET state,
// then the broadcast SMI is still pending for it; it will now launch
// directly into SMM.
//
// If the OS is malicious, the hot-added CPU has been booted already, and so
// it is already spinning on the APIC ID gate. In that case, the
// INIT-SIPI-SIPI below will be ignored.
//
SendInitSipiSipi (ApicId, PenAddress);
//
// Expose the desired new SMBASE value to the hot-added CPU.
//
Context->NewSmbase = (UINT32)Smbase;
//
// Un-gate SMBASE relocation for the hot-added CPU whose APIC ID is ApicId.
//
ExchangeResult = InterlockedCompareExchange64 (&Context->ApicIdGate,
MAX_UINT64, ApicId);
if (ExchangeResult != MAX_UINT64) {
Status = EFI_PROTOCOL_ERROR;
DEBUG ((DEBUG_ERROR, "%a: ApicId=" FMT_APIC_ID " ApicIdGate=0x%Lx: %r\n",
__FUNCTION__, ApicId, ExchangeResult, Status));
return Status;
}
//
// Wait until the hot-added CPU is just about to execute RSM.
//
while (Context->AboutToLeaveSmm == 0) {
CpuPause ();
}
//
// Now wait until the hot-added CPU reports back from the pen (or the OS
// attacks the last byte of the reserved page).
//
while (*SmmVacated == 0) {
CpuPause ();
}
Status = EFI_SUCCESS;
return Status;
}

View File

@ -12,6 +12,8 @@
#include <Uefi/UefiBaseType.h> // EFI_STATUS
#include <Uefi/UefiSpec.h> // EFI_BOOT_SERVICES
#include "ApicId.h" // APIC_ID
EFI_STATUS
SmbaseAllocatePostSmmPen (
OUT UINT32 *PenAddress,
@ -29,4 +31,16 @@ SmbaseReleasePostSmmPen (
IN CONST EFI_BOOT_SERVICES *BootServices
);
VOID
SmbaseInstallFirstSmiHandler (
VOID
);
EFI_STATUS
SmbaseRelocate (
IN APIC_ID ApicId,
IN UINTN Smbase,
IN UINT32 PenAddress
);
#endif // SMBASE_H_