From: Laszlo Ersek Date: Wed, 26 Feb 2020 22:11:52 +0000 (+0100) Subject: OvmfPkg/CpuHotplugSmm: introduce First SMI Handler for hot-added CPUs X-Git-Tag: edk2-stable202005~372 X-Git-Url: https://git.proxmox.com/?p=mirror_edk2.git;a=commitdiff_plain;h=51a6fb41181529e4b50ea13377425bda6bb69ba6 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 Cc: Igor Mammedov Cc: Jiewen Yao Cc: Jordan Justen Cc: Michael Kinney Cc: Philippe Mathieu-Daudé Ref: https://bugzilla.tianocore.org/show_bug.cgi?id=1512 Signed-off-by: Laszlo Ersek Message-Id: <20200226221156.29589-13-lersek@redhat.com> Acked-by: Ard Biesheuvel Tested-by: Boris Ostrovsky --- diff --git a/OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf b/OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf index bf4162299c..04322b0d78 100644 --- a/OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf +++ b/OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf @@ -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] diff --git a/OvmfPkg/CpuHotplugSmm/FirstSmiHandler.nasm b/OvmfPkg/CpuHotplugSmm/FirstSmiHandler.nasm new file mode 100644 index 0000000000..5399b5fa43 --- /dev/null +++ b/OvmfPkg/CpuHotplugSmm/FirstSmiHandler.nasm @@ -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) diff --git a/OvmfPkg/CpuHotplugSmm/FirstSmiHandlerContext.h b/OvmfPkg/CpuHotplugSmm/FirstSmiHandlerContext.h new file mode 100644 index 0000000000..029de4cdea --- /dev/null +++ b/OvmfPkg/CpuHotplugSmm/FirstSmiHandlerContext.h @@ -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_ diff --git a/OvmfPkg/CpuHotplugSmm/Smbase.c b/OvmfPkg/CpuHotplugSmm/Smbase.c index ea21153d91..170571221d 100644 --- a/OvmfPkg/CpuHotplugSmm/Smbase.c +++ b/OvmfPkg/CpuHotplugSmm/Smbase.c @@ -7,13 +7,21 @@ **/ #include // BASE_1MB +#include // CpuPause() #include // CopyMem() #include // DEBUG() +#include // SendInitSipiSipi() +#include // InterlockedCompareExchange64() +#include // 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; +} diff --git a/OvmfPkg/CpuHotplugSmm/Smbase.h b/OvmfPkg/CpuHotplugSmm/Smbase.h index cb5aed98cd..e73730d199 100644 --- a/OvmfPkg/CpuHotplugSmm/Smbase.h +++ b/OvmfPkg/CpuHotplugSmm/Smbase.h @@ -12,6 +12,8 @@ #include // EFI_STATUS #include // 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_