From af1a266670d040d2f4083ff309d732d648afba2a Mon Sep 17 00:00:00 2001 From: Angelos Mouzakitis Date: Tue, 10 Oct 2023 14:33:42 +0000 Subject: Add submodule dependency files Change-Id: Iaf8d18082d3991dec7c0ebbea540f092188eb4ec --- roms/edk2/OvmfPkg/CpuHotplugSmm/ApicId.h | 23 ++ roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplug.c | 445 +++++++++++++++++++++ roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf | 64 +++ .../OvmfPkg/CpuHotplugSmm/FirstSmiHandler.nasm | 154 +++++++ .../OvmfPkg/CpuHotplugSmm/FirstSmiHandlerContext.h | 47 +++ roms/edk2/OvmfPkg/CpuHotplugSmm/PostSmmPen.nasm | 151 +++++++ roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.c | 301 ++++++++++++++ roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.h | 61 +++ roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.c | 290 ++++++++++++++ roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.h | 46 +++ 10 files changed, 1582 insertions(+) create mode 100644 roms/edk2/OvmfPkg/CpuHotplugSmm/ApicId.h create mode 100644 roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplug.c create mode 100644 roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf create mode 100644 roms/edk2/OvmfPkg/CpuHotplugSmm/FirstSmiHandler.nasm create mode 100644 roms/edk2/OvmfPkg/CpuHotplugSmm/FirstSmiHandlerContext.h create mode 100644 roms/edk2/OvmfPkg/CpuHotplugSmm/PostSmmPen.nasm create mode 100644 roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.c create mode 100644 roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.h create mode 100644 roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.c create mode 100644 roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.h (limited to 'roms/edk2/OvmfPkg/CpuHotplugSmm') diff --git a/roms/edk2/OvmfPkg/CpuHotplugSmm/ApicId.h b/roms/edk2/OvmfPkg/CpuHotplugSmm/ApicId.h new file mode 100644 index 000000000..3c365148e --- /dev/null +++ b/roms/edk2/OvmfPkg/CpuHotplugSmm/ApicId.h @@ -0,0 +1,23 @@ +/** @file + Type and macro definitions for representing and printing APIC IDs, compatibly + with the LocalApicLib and PrintLib classes, respectively. + + Copyright (c) 2020, Red Hat, Inc. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#ifndef APIC_ID_H_ +#define APIC_ID_H_ + +// +// The type that LocalApicLib represents an APIC ID with. +// +typedef UINT32 APIC_ID; + +// +// The PrintLib conversion specification for formatting an APIC_ID. +// +#define FMT_APIC_ID "0x%08x" + +#endif // APIC_ID_H_ diff --git a/roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplug.c b/roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplug.c new file mode 100644 index 000000000..cfe698ed2 --- /dev/null +++ b/roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplug.c @@ -0,0 +1,445 @@ +/** @file + Root SMI handler for VCPU hotplug SMIs. + + Copyright (c) 2020, Red Hat, Inc. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include // CPU_HOT_PLUG_DATA +#include // ICH9_APM_CNT +#include // QEMU_CPUHP_CMD_GET_PENDING +#include // CpuDeadLoop() +#include // ASSERT() +#include // gMmst +#include // PcdGetBool() +#include // SafeUintnSub() +#include // EFI_MM_CPU_IO_PROTOCOL +#include // EFI_SMM_CPU_SERVICE_PROTOCOL +#include // EFI_STATUS + +#include "ApicId.h" // APIC_ID +#include "QemuCpuhp.h" // QemuCpuhpWriteCpuSelector() +#include "Smbase.h" // SmbaseAllocatePostSmmPen() + +// +// We use this protocol for accessing IO Ports. +// +STATIC EFI_MM_CPU_IO_PROTOCOL *mMmCpuIo; +// +// The following protocol is used to report the addition or removal of a CPU to +// the SMM CPU driver (PiSmmCpuDxeSmm). +// +STATIC EFI_SMM_CPU_SERVICE_PROTOCOL *mMmCpuService; +// +// This structure is a communication side-channel between the +// EFI_SMM_CPU_SERVICE_PROTOCOL consumer (i.e., this driver) and provider +// (i.e., PiSmmCpuDxeSmm). +// +STATIC CPU_HOT_PLUG_DATA *mCpuHotPlugData; +// +// SMRAM arrays for fetching the APIC IDs of processors with pending events (of +// known event types), for the time of just one MMI. +// +// The lifetimes of these arrays match that of this driver only because we +// don't want to allocate SMRAM at OS runtime, and potentially fail (or +// fragment the SMRAM map). +// +// These arrays provide room for ("possible CPU count" minus one) APIC IDs +// each, as we don't expect every possible CPU to appear, or disappear, in a +// single MMI. The numbers of used (populated) elements in the arrays are +// determined on every MMI separately. +// +STATIC APIC_ID *mPluggedApicIds; +STATIC APIC_ID *mToUnplugApicIds; +// +// Address of the non-SMRAM reserved memory page that contains the Post-SMM Pen +// for hot-added CPUs. +// +STATIC UINT32 mPostSmmPenAddress; +// +// Represents the registration of the CPU Hotplug MMI handler. +// +STATIC EFI_HANDLE mDispatchHandle; + + +/** + CPU Hotplug MMI handler function. + + This is a root MMI handler. + + @param[in] DispatchHandle The unique handle assigned to this handler by + EFI_MM_SYSTEM_TABLE.MmiHandlerRegister(). + + @param[in] Context Context passed in by + EFI_MM_SYSTEM_TABLE.MmiManage(). Due to + CpuHotplugMmi() being a root MMI handler, + Context is ASSERT()ed to be NULL. + + @param[in,out] CommBuffer Ignored, due to CpuHotplugMmi() being a root + MMI handler. + + @param[in,out] CommBufferSize Ignored, due to CpuHotplugMmi() being a root + MMI handler. + + @retval EFI_SUCCESS The MMI was handled and the MMI + source was quiesced. When returned + by a non-root MMI handler, + EFI_SUCCESS terminates the + processing of MMI handlers in + EFI_MM_SYSTEM_TABLE.MmiManage(). + For a root MMI handler (i.e., for + the present function too), + EFI_SUCCESS behaves identically to + EFI_WARN_INTERRUPT_SOURCE_QUIESCED, + as further root MMI handlers are + going to be called by + EFI_MM_SYSTEM_TABLE.MmiManage() + anyway. + + @retval EFI_WARN_INTERRUPT_SOURCE_QUIESCED The MMI source has been quiesced, + but other handlers should still + be called. + + @retval EFI_WARN_INTERRUPT_SOURCE_PENDING The MMI source is still pending, + and other handlers should still + be called. + + @retval EFI_INTERRUPT_PENDING The MMI source could not be + quiesced. +**/ +STATIC +EFI_STATUS +EFIAPI +CpuHotplugMmi ( + IN EFI_HANDLE DispatchHandle, + IN CONST VOID *Context OPTIONAL, + IN OUT VOID *CommBuffer OPTIONAL, + IN OUT UINTN *CommBufferSize OPTIONAL + ) +{ + EFI_STATUS Status; + UINT8 ApmControl; + UINT32 PluggedCount; + UINT32 ToUnplugCount; + UINT32 PluggedIdx; + UINT32 NewSlot; + + // + // Assert that we are entering this function due to our root MMI handler + // registration. + // + ASSERT (DispatchHandle == mDispatchHandle); + // + // When MmiManage() is invoked to process root MMI handlers, the caller (the + // MM Core) is expected to pass in a NULL Context. MmiManage() then passes + // the same NULL Context to individual handlers. + // + ASSERT (Context == NULL); + // + // Read the MMI command value from the APM Control Port, to see if this is an + // MMI we should care about. + // + Status = mMmCpuIo->Io.Read (mMmCpuIo, MM_IO_UINT8, ICH9_APM_CNT, 1, + &ApmControl); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: failed to read ICH9_APM_CNT: %r\n", __FUNCTION__, + Status)); + // + // We couldn't even determine if the MMI was for us or not. + // + goto Fatal; + } + + if (ApmControl != ICH9_APM_CNT_CPU_HOTPLUG) { + // + // The MMI is not for us. + // + return EFI_WARN_INTERRUPT_SOURCE_QUIESCED; + } + + // + // Collect the CPUs with pending events. + // + Status = QemuCpuhpCollectApicIds ( + mMmCpuIo, + mCpuHotPlugData->ArrayLength, // PossibleCpuCount + mCpuHotPlugData->ArrayLength - 1, // ApicIdCount + mPluggedApicIds, + &PluggedCount, + mToUnplugApicIds, + &ToUnplugCount + ); + if (EFI_ERROR (Status)) { + goto Fatal; + } + if (ToUnplugCount > 0) { + DEBUG ((DEBUG_ERROR, "%a: hot-unplug is not supported yet\n", + __FUNCTION__)); + goto Fatal; + } + + // + // Process hot-added CPUs. + // + // The Post-SMM Pen need not be reinstalled multiple times within a single + // root MMI handling. Even reinstalling once per root MMI is only prudence; + // in theory installing the pen in the driver's entry point function should + // suffice. + // + SmbaseReinstallPostSmmPen (mPostSmmPenAddress); + + PluggedIdx = 0; + NewSlot = 0; + while (PluggedIdx < PluggedCount) { + APIC_ID NewApicId; + UINT32 CheckSlot; + UINTN NewProcessorNumberByProtocol; + + NewApicId = mPluggedApicIds[PluggedIdx]; + + // + // Check if the supposedly hot-added CPU is already known to us. + // + for (CheckSlot = 0; + CheckSlot < mCpuHotPlugData->ArrayLength; + CheckSlot++) { + if (mCpuHotPlugData->ApicId[CheckSlot] == NewApicId) { + break; + } + } + if (CheckSlot < mCpuHotPlugData->ArrayLength) { + DEBUG ((DEBUG_VERBOSE, "%a: APIC ID " FMT_APIC_ID " was hot-plugged " + "before; ignoring it\n", __FUNCTION__, NewApicId)); + PluggedIdx++; + continue; + } + + // + // Find the first empty slot in CPU_HOT_PLUG_DATA. + // + while (NewSlot < mCpuHotPlugData->ArrayLength && + mCpuHotPlugData->ApicId[NewSlot] != MAX_UINT64) { + NewSlot++; + } + if (NewSlot == mCpuHotPlugData->ArrayLength) { + DEBUG ((DEBUG_ERROR, "%a: no room for APIC ID " FMT_APIC_ID "\n", + __FUNCTION__, NewApicId)); + goto Fatal; + } + + // + // Store the APIC ID of the new processor to the slot. + // + mCpuHotPlugData->ApicId[NewSlot] = NewApicId; + + // + // Relocate the SMBASE of the new CPU. + // + Status = SmbaseRelocate (NewApicId, mCpuHotPlugData->SmBase[NewSlot], + mPostSmmPenAddress); + if (EFI_ERROR (Status)) { + goto RevokeNewSlot; + } + + // + // Add the new CPU with EFI_SMM_CPU_SERVICE_PROTOCOL. + // + Status = mMmCpuService->AddProcessor (mMmCpuService, NewApicId, + &NewProcessorNumberByProtocol); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: AddProcessor(" FMT_APIC_ID "): %r\n", + __FUNCTION__, NewApicId, Status)); + goto RevokeNewSlot; + } + + DEBUG ((DEBUG_INFO, "%a: hot-added APIC ID " FMT_APIC_ID ", SMBASE 0x%Lx, " + "EFI_SMM_CPU_SERVICE_PROTOCOL assigned number %Lu\n", __FUNCTION__, + NewApicId, (UINT64)mCpuHotPlugData->SmBase[NewSlot], + (UINT64)NewProcessorNumberByProtocol)); + + NewSlot++; + PluggedIdx++; + } + + // + // We've handled this MMI. + // + return EFI_SUCCESS; + +RevokeNewSlot: + mCpuHotPlugData->ApicId[NewSlot] = MAX_UINT64; + +Fatal: + ASSERT (FALSE); + CpuDeadLoop (); + // + // We couldn't handle this MMI. + // + return EFI_INTERRUPT_PENDING; +} + + +// +// Entry point function of this driver. +// +EFI_STATUS +EFIAPI +CpuHotplugEntry ( + IN EFI_HANDLE ImageHandle, + IN EFI_SYSTEM_TABLE *SystemTable + ) +{ + EFI_STATUS Status; + UINTN Size; + + // + // This module should only be included when SMM support is required. + // + ASSERT (FeaturePcdGet (PcdSmmSmramRequire)); + // + // This driver depends on the dynamically detected "SMRAM at default SMBASE" + // feature. + // + if (!PcdGetBool (PcdQ35SmramAtDefaultSmbase)) { + return EFI_UNSUPPORTED; + } + + // + // Errors from here on are fatal; we cannot allow the boot to proceed if we + // can't set up this driver to handle CPU hotplug. + // + // First, collect the protocols needed later. All of these protocols are + // listed in our module DEPEX. + // + Status = gMmst->MmLocateProtocol (&gEfiMmCpuIoProtocolGuid, + NULL /* Registration */, (VOID **)&mMmCpuIo); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: locate MmCpuIo: %r\n", __FUNCTION__, Status)); + goto Fatal; + } + Status = gMmst->MmLocateProtocol (&gEfiSmmCpuServiceProtocolGuid, + NULL /* Registration */, (VOID **)&mMmCpuService); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: locate MmCpuService: %r\n", __FUNCTION__, + Status)); + goto Fatal; + } + + // + // Our DEPEX on EFI_SMM_CPU_SERVICE_PROTOCOL guarantees that PiSmmCpuDxeSmm + // has pointed PcdCpuHotPlugDataAddress to CPU_HOT_PLUG_DATA in SMRAM. + // + mCpuHotPlugData = (VOID *)(UINTN)PcdGet64 (PcdCpuHotPlugDataAddress); + if (mCpuHotPlugData == NULL) { + Status = EFI_NOT_FOUND; + DEBUG ((DEBUG_ERROR, "%a: CPU_HOT_PLUG_DATA: %r\n", __FUNCTION__, Status)); + goto Fatal; + } + // + // If the possible CPU count is 1, there's nothing for this driver to do. + // + if (mCpuHotPlugData->ArrayLength == 1) { + return EFI_UNSUPPORTED; + } + // + // Allocate the data structures that depend on the possible CPU count. + // + if (RETURN_ERROR (SafeUintnSub (mCpuHotPlugData->ArrayLength, 1, &Size)) || + RETURN_ERROR (SafeUintnMult (sizeof (APIC_ID), Size, &Size))) { + Status = EFI_ABORTED; + DEBUG ((DEBUG_ERROR, "%a: invalid CPU_HOT_PLUG_DATA\n", __FUNCTION__)); + goto Fatal; + } + Status = gMmst->MmAllocatePool (EfiRuntimeServicesData, Size, + (VOID **)&mPluggedApicIds); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: MmAllocatePool(): %r\n", __FUNCTION__, Status)); + goto Fatal; + } + Status = gMmst->MmAllocatePool (EfiRuntimeServicesData, Size, + (VOID **)&mToUnplugApicIds); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: MmAllocatePool(): %r\n", __FUNCTION__, Status)); + goto ReleasePluggedApicIds; + } + + // + // Allocate the Post-SMM Pen for hot-added CPUs. + // + Status = SmbaseAllocatePostSmmPen (&mPostSmmPenAddress, + SystemTable->BootServices); + if (EFI_ERROR (Status)) { + goto ReleaseToUnplugApicIds; + } + + // + // Sanity-check the CPU hotplug interface. + // + // Both of the following features are part of QEMU 5.0, introduced primarily + // in commit range 3e08b2b9cb64..3a61c8db9d25: + // + // (a) the QEMU_CPUHP_CMD_GET_ARCH_ID command of the modern CPU hotplug + // interface, + // + // (b) the "SMRAM at default SMBASE" feature. + // + // From these, (b) is restricted to 5.0+ machine type versions, while (a) + // does not depend on machine type version. Because we ensured the stricter + // condition (b) through PcdQ35SmramAtDefaultSmbase above, the (a) + // QEMU_CPUHP_CMD_GET_ARCH_ID command must now be available too. While we + // can't verify the presence of precisely that command, we can still verify + // (sanity-check) that the modern interface is active, at least. + // + // Consult the "Typical usecases | Detecting and enabling modern CPU hotplug + // interface" section in QEMU's "docs/specs/acpi_cpu_hotplug.txt", on the + // following. + // + QemuCpuhpWriteCpuSelector (mMmCpuIo, 0); + QemuCpuhpWriteCpuSelector (mMmCpuIo, 0); + QemuCpuhpWriteCommand (mMmCpuIo, QEMU_CPUHP_CMD_GET_PENDING); + if (QemuCpuhpReadCommandData2 (mMmCpuIo) != 0) { + Status = EFI_NOT_FOUND; + DEBUG ((DEBUG_ERROR, "%a: modern CPU hotplug interface: %r\n", + __FUNCTION__, Status)); + goto ReleasePostSmmPen; + } + + // + // Register the handler for the CPU Hotplug MMI. + // + Status = gMmst->MmiHandlerRegister ( + CpuHotplugMmi, + NULL, // HandlerType: root MMI handler + &mDispatchHandle + ); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: MmiHandlerRegister(): %r\n", __FUNCTION__, + Status)); + goto ReleasePostSmmPen; + } + + // + // Install the handler for the hot-added CPUs' first SMI. + // + SmbaseInstallFirstSmiHandler (); + + return EFI_SUCCESS; + +ReleasePostSmmPen: + SmbaseReleasePostSmmPen (mPostSmmPenAddress, SystemTable->BootServices); + mPostSmmPenAddress = 0; + +ReleaseToUnplugApicIds: + gMmst->MmFreePool (mToUnplugApicIds); + mToUnplugApicIds = NULL; + +ReleasePluggedApicIds: + gMmst->MmFreePool (mPluggedApicIds); + mPluggedApicIds = NULL; + +Fatal: + ASSERT (FALSE); + CpuDeadLoop (); + return Status; +} diff --git a/roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf b/roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf new file mode 100644 index 000000000..04322b0d7 --- /dev/null +++ b/roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf @@ -0,0 +1,64 @@ +## @file +# Root SMI handler for VCPU hotplug SMIs. +# +# Copyright (c) 2020, Red Hat, Inc. +# +# SPDX-License-Identifier: BSD-2-Clause-Patent +## + +[Defines] + INF_VERSION = 1.29 + PI_SPECIFICATION_VERSION = 0x00010046 # PI-1.7.0 + BASE_NAME = CpuHotplugSmm + FILE_GUID = 84EEA114-C6BE-4445-8F90-51D97863E363 + MODULE_TYPE = DXE_SMM_DRIVER + ENTRY_POINT = CpuHotplugEntry + +# +# The following information is for reference only and not required by the build +# tools. +# +# VALID_ARCHITECTURES = IA32 X64 +# + +[Sources] + ApicId.h + CpuHotplug.c + FirstSmiHandler.nasm + FirstSmiHandlerContext.h + PostSmmPen.nasm + QemuCpuhp.c + QemuCpuhp.h + Smbase.c + Smbase.h + +[Packages] + MdePkg/MdePkg.dec + OvmfPkg/OvmfPkg.dec + UefiCpuPkg/UefiCpuPkg.dec + +[LibraryClasses] + BaseLib + BaseMemoryLib + DebugLib + LocalApicLib + MmServicesTableLib + PcdLib + SafeIntLib + SynchronizationLib + UefiDriverEntryPoint + +[Protocols] + gEfiMmCpuIoProtocolGuid ## CONSUMES + gEfiSmmCpuServiceProtocolGuid ## CONSUMES + +[Pcd] + gUefiCpuPkgTokenSpaceGuid.PcdCpuHotPlugDataAddress ## CONSUMES + gUefiOvmfPkgTokenSpaceGuid.PcdQ35SmramAtDefaultSmbase ## CONSUMES + +[FeaturePcd] + gUefiOvmfPkgTokenSpaceGuid.PcdSmmSmramRequire ## CONSUMES + +[Depex] + gEfiMmCpuIoProtocolGuid AND + gEfiSmmCpuServiceProtocolGuid diff --git a/roms/edk2/OvmfPkg/CpuHotplugSmm/FirstSmiHandler.nasm b/roms/edk2/OvmfPkg/CpuHotplugSmm/FirstSmiHandler.nasm new file mode 100644 index 000000000..5399b5fa4 --- /dev/null +++ b/roms/edk2/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/roms/edk2/OvmfPkg/CpuHotplugSmm/FirstSmiHandlerContext.h b/roms/edk2/OvmfPkg/CpuHotplugSmm/FirstSmiHandlerContext.h new file mode 100644 index 000000000..029de4cde --- /dev/null +++ b/roms/edk2/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/roms/edk2/OvmfPkg/CpuHotplugSmm/PostSmmPen.nasm b/roms/edk2/OvmfPkg/CpuHotplugSmm/PostSmmPen.nasm new file mode 100644 index 000000000..ef702689b --- /dev/null +++ b/roms/edk2/OvmfPkg/CpuHotplugSmm/PostSmmPen.nasm @@ -0,0 +1,151 @@ +;------------------------------------------------------------------------------ +; @file +; Pen any hot-added CPU in a 16-bit, real mode HLT loop, after it leaves SMM by +; executing the RSM instruction. +; +; Copyright (c) 2020, Red Hat, Inc. +; +; SPDX-License-Identifier: BSD-2-Clause-Patent +; +; The routine implemented here is stored into normal RAM, under 1MB, at the +; beginning of a page that is allocated as EfiReservedMemoryType. On any +; hot-added CPU, it is executed after *at least* the first RSM (i.e., after +; SMBASE relocation). +; +; The first execution of this code occurs as follows: +; +; - The hot-added CPU is in RESET state. +; +; - The ACPI CPU hotplug event handler triggers a broadcast SMI, from the OS. +; +; - Existent CPUs (BSP and APs) enter SMM. +; +; - The hot-added CPU remains in RESET state, but an SMI is pending for it now. +; (See "SYSTEM MANAGEMENT INTERRUPT (SMI)" in the Intel SDM.) +; +; - In SMM, pre-existent CPUs that are not elected SMM Monarch, keep themselves +; busy with their wait loops. +; +; - From the root MMI handler, the SMM Monarch: +; +; - places this routine in the reserved page, +; +; - clears the "about to leave SMM" byte in SMRAM, +; +; - clears the last byte of the reserved page, +; +; - sends an INIT-SIPI-SIPI sequence to the hot-added CPU, +; +; - un-gates the default SMI handler by APIC ID. +; +; - The startup vector in the SIPI that is sent by the SMM Monarch points to +; this code; i.e., to the reserved page. (Example: 0x9_F000.) +; +; - The SMM Monarch starts polling the "about to leave SMM" byte in SMRAM. +; +; - The hot-added CPU boots, and immediately enters SMM due to the pending SMI. +; It starts executing the default SMI handler. +; +; - Importantly, the SMRAM Save State Map captures the following information, +; when the hot-added CPU enters SMM: +; +; - CS selector: assumes the 16 most significant bits of the 20-bit (i.e., +; below 1MB) startup vector from the SIPI. (Example: 0x9F00.) +; +; - CS attributes: Accessed, Readable, User (S=1), CodeSegment (bit#11), +; Present. +; +; - CS limit: 0xFFFF. +; +; - CS base: the CS selector value shifted left by 4 bits. That is, the CS +; base equals the SIPI startup vector. (Example: 0x9_F000.) +; +; - IP: the least significant 4 bits from the SIPI startup vector. Because +; the routine is page-aligned, these bits are zero (hence IP is zero). +; +; - ES, SS, DS, FS, GS selectors: 0. +; +; - ES, SS, DS, FS, GS attributes: same as the CS attributes, minus +; CodeSegment (bit#11). +; +; - ES, SS, DS, FS, GS limits: 0xFFFF. +; +; - ES, SS, DS, FS, GS bases: 0. +; +; - The hot-added CPU sets its new SMBASE value in the SMRAM Save State Map. +; +; - The hot-added CPU sets the "about to leave SMM" byte in SMRAM, then +; executes the RSM instruction immediately after, leaving SMM. +; +; - The SMM Monarch notices that the "about to leave SMM" byte in SMRAM has +; been set, and starts polling the last byte in the reserved page. +; +; - The hot-added CPU jumps ("returns") to the code below (in the reserved +; page), according to the register state listed in the SMRAM Save State Map. +; +; - The hot-added CPU sets the last byte of the reserved page, then halts +; itself. +; +; - The SMM Monarch notices that the hot-added CPU is done with SMBASE +; relocation. +; +; Note that, if the OS is malicious and sends INIT-SIPI-SIPI to the hot-added +; CPU before allowing the ACPI CPU hotplug event handler to trigger a broadcast +; SMI, then said broadcast SMI will yank the hot-added CPU directly into SMM, +; without becoming pending for it (as the hot-added CPU is no longer in RESET +; state). This is OK, because: +; +; - The default SMI handler copes with this, as it is gated by APIC ID. The +; hot-added CPU won't start the actual SMBASE relocation until the SMM +; Monarch lets it. +; +; - The INIT-SIPI-SIPI sequence that the SMM Monarch sends to the hot-added CPU +; will be ignored in this sate (it won't even be latched). See "SMI HANDLER +; EXECUTION ENVIRONMENT" in the Intel SDM: "INIT operations are inhibited +; when the processor enters SMM". +; +; - When the hot-added CPU (e.g., CPU#1) executes the RSM (having relocated +; SMBASE), it returns to the OS. The OS can use CPU#1 to attack the last byte +; of the reserved page, while another CPU (e.g., CPU#2) is relocating SMBASE, +; in order to trick the SMM Monarch (e.g., CPU#0) to open the APIC ID gate +; for yet another CPU (e.g., CPU#3). However, the SMM Monarch won't look at +; the last byte of the reserved page, until CPU#2 sets the "about to leave +; SMM" byte in SMRAM. This leaves a very small window (just one instruction's +; worth before the RSM) for CPU#3 to "catch up" with CPU#2, and overwrite +; CPU#2's SMBASE with its own. +; +; In other words, we do not / need not prevent a malicious OS from booting the +; hot-added CPU early; instead we provide benign OSes with a pen for hot-added +; CPUs. +;------------------------------------------------------------------------------ + +SECTION .data +BITS 16 + +GLOBAL ASM_PFX (mPostSmmPen) ; UINT8[] +GLOBAL ASM_PFX (mPostSmmPenSize) ; UINT16 + +ASM_PFX (mPostSmmPen): + ; + ; Point DS at the same reserved page. + ; + mov ax, cs + mov ds, ax + + ; + ; Inform the SMM Monarch that we're done with SMBASE relocation, by setting + ; the last byte in the reserved page. + ; + mov byte [ds : word 0xFFF], 1 + + ; + ; Halt now, until we get woken by another SMI, or (more likely) the OS + ; reboots us with another INIT-SIPI-SIPI. + ; +HltLoop: + cli + hlt + jmp HltLoop + +ASM_PFX (mPostSmmPenSize): + dw $ - ASM_PFX (mPostSmmPen) diff --git a/roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.c b/roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.c new file mode 100644 index 000000000..8d4a6693c --- /dev/null +++ b/roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.c @@ -0,0 +1,301 @@ +/** @file + Simple wrapper functions and utility functions that access QEMU's modern CPU + hotplug register block. + + These functions manipulate some of the registers described in + "docs/specs/acpi_cpu_hotplug.txt" in the QEMU source. IO Ports are accessed + via EFI_MM_CPU_IO_PROTOCOL. If a protocol call fails, these functions don't + return. + + Copyright (c) 2020, Red Hat, Inc. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include // ICH9_CPU_HOTPLUG_BASE +#include // QEMU_CPUHP_R_CMD_DATA2 +#include // CpuDeadLoop() +#include // DEBUG() + +#include "QemuCpuhp.h" + +UINT32 +QemuCpuhpReadCommandData2 ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo + ) +{ + UINT32 CommandData2; + EFI_STATUS Status; + + CommandData2 = 0; + Status = MmCpuIo->Io.Read ( + MmCpuIo, + MM_IO_UINT32, + ICH9_CPU_HOTPLUG_BASE + QEMU_CPUHP_R_CMD_DATA2, + 1, + &CommandData2 + ); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: %r\n", __FUNCTION__, Status)); + ASSERT (FALSE); + CpuDeadLoop (); + } + return CommandData2; +} + +UINT8 +QemuCpuhpReadCpuStatus ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo + ) +{ + UINT8 CpuStatus; + EFI_STATUS Status; + + CpuStatus = 0; + Status = MmCpuIo->Io.Read ( + MmCpuIo, + MM_IO_UINT8, + ICH9_CPU_HOTPLUG_BASE + QEMU_CPUHP_R_CPU_STAT, + 1, + &CpuStatus + ); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: %r\n", __FUNCTION__, Status)); + ASSERT (FALSE); + CpuDeadLoop (); + } + return CpuStatus; +} + +UINT32 +QemuCpuhpReadCommandData ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo + ) +{ + UINT32 CommandData; + EFI_STATUS Status; + + CommandData = 0; + Status = MmCpuIo->Io.Read ( + MmCpuIo, + MM_IO_UINT32, + ICH9_CPU_HOTPLUG_BASE + QEMU_CPUHP_RW_CMD_DATA, + 1, + &CommandData + ); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: %r\n", __FUNCTION__, Status)); + ASSERT (FALSE); + CpuDeadLoop (); + } + return CommandData; +} + +VOID +QemuCpuhpWriteCpuSelector ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo, + IN UINT32 Selector + ) +{ + EFI_STATUS Status; + + Status = MmCpuIo->Io.Write ( + MmCpuIo, + MM_IO_UINT32, + ICH9_CPU_HOTPLUG_BASE + QEMU_CPUHP_W_CPU_SEL, + 1, + &Selector + ); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: %r\n", __FUNCTION__, Status)); + ASSERT (FALSE); + CpuDeadLoop (); + } +} + +VOID +QemuCpuhpWriteCommand ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo, + IN UINT8 Command + ) +{ + EFI_STATUS Status; + + Status = MmCpuIo->Io.Write ( + MmCpuIo, + MM_IO_UINT8, + ICH9_CPU_HOTPLUG_BASE + QEMU_CPUHP_W_CMD, + 1, + &Command + ); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: %r\n", __FUNCTION__, Status)); + ASSERT (FALSE); + CpuDeadLoop (); + } +} + +/** + Collect the APIC IDs of + - the CPUs that have been hot-plugged, + - the CPUs that are about to be hot-unplugged. + + This function only scans for events -- it does not modify them -- in the + hotplug registers. + + On error, the contents of the output parameters are undefined. + + @param[in] MmCpuIo The EFI_MM_CPU_IO_PROTOCOL instance for + accessing IO Ports. + + @param[in] PossibleCpuCount The number of possible CPUs in the system. Must + be positive. + + @param[in] ApicIdCount The number of elements each one of the + PluggedApicIds and ToUnplugApicIds arrays can + accommodate. Must be positive. + + @param[out] PluggedApicIds The APIC IDs of the CPUs that have been + hot-plugged. + + @param[out] PluggedCount The number of filled-in APIC IDs in + PluggedApicIds. + + @param[out] ToUnplugApicIds The APIC IDs of the CPUs that are about to be + hot-unplugged. + + @param[out] ToUnplugCount The number of filled-in APIC IDs in + ToUnplugApicIds. + + @retval EFI_INVALID_PARAMETER PossibleCpuCount is zero, or ApicIdCount is + zero. + + @retval EFI_PROTOCOL_ERROR Invalid bitmap detected in the + QEMU_CPUHP_R_CPU_STAT register. + + @retval EFI_BUFFER_TOO_SMALL There was an attempt to place more than + ApicIdCount APIC IDs into one of the + PluggedApicIds and ToUnplugApicIds arrays. + + @retval EFI_SUCCESS Output parameters have been set successfully. +**/ +EFI_STATUS +QemuCpuhpCollectApicIds ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo, + IN UINT32 PossibleCpuCount, + IN UINT32 ApicIdCount, + OUT APIC_ID *PluggedApicIds, + OUT UINT32 *PluggedCount, + OUT APIC_ID *ToUnplugApicIds, + OUT UINT32 *ToUnplugCount + ) +{ + UINT32 CurrentSelector; + + if (PossibleCpuCount == 0 || ApicIdCount == 0) { + return EFI_INVALID_PARAMETER; + } + + *PluggedCount = 0; + *ToUnplugCount = 0; + + CurrentSelector = 0; + do { + UINT32 PendingSelector; + UINT8 CpuStatus; + APIC_ID *ExtendIds; + UINT32 *ExtendCount; + APIC_ID NewApicId; + + // + // Write CurrentSelector (which is valid) to the CPU selector register. + // Consequences: + // + // - Other register accesses will be permitted. + // + // - The QEMU_CPUHP_CMD_GET_PENDING command will start scanning for a CPU + // with pending events at CurrentSelector (inclusive). + // + QemuCpuhpWriteCpuSelector (MmCpuIo, CurrentSelector); + // + // Write the QEMU_CPUHP_CMD_GET_PENDING command. Consequences + // (independently of each other): + // + // - If there is a CPU with pending events, starting at CurrentSelector + // (inclusive), the CPU selector will be updated to that CPU. Note that + // the scanning in QEMU may wrap around, because we must never clear the + // event bits. + // + // - The QEMU_CPUHP_RW_CMD_DATA register will return the (possibly updated) + // CPU selector value. + // + QemuCpuhpWriteCommand (MmCpuIo, QEMU_CPUHP_CMD_GET_PENDING); + PendingSelector = QemuCpuhpReadCommandData (MmCpuIo); + if (PendingSelector < CurrentSelector) { + DEBUG ((DEBUG_VERBOSE, "%a: CurrentSelector=%u PendingSelector=%u: " + "wrap-around\n", __FUNCTION__, CurrentSelector, PendingSelector)); + break; + } + CurrentSelector = PendingSelector; + + // + // Check the known status / event bits for the currently selected CPU. + // + CpuStatus = QemuCpuhpReadCpuStatus (MmCpuIo); + if ((CpuStatus & QEMU_CPUHP_STAT_INSERT) != 0) { + // + // The "insert" event guarantees the "enabled" status; plus it excludes + // the "remove" event. + // + if ((CpuStatus & QEMU_CPUHP_STAT_ENABLED) == 0 || + (CpuStatus & QEMU_CPUHP_STAT_REMOVE) != 0) { + DEBUG ((DEBUG_ERROR, "%a: CurrentSelector=%u CpuStatus=0x%x: " + "inconsistent CPU status\n", __FUNCTION__, CurrentSelector, + CpuStatus)); + return EFI_PROTOCOL_ERROR; + } + + DEBUG ((DEBUG_VERBOSE, "%a: CurrentSelector=%u: insert\n", __FUNCTION__, + CurrentSelector)); + + ExtendIds = PluggedApicIds; + ExtendCount = PluggedCount; + } else if ((CpuStatus & QEMU_CPUHP_STAT_REMOVE) != 0) { + DEBUG ((DEBUG_VERBOSE, "%a: CurrentSelector=%u: remove\n", __FUNCTION__, + CurrentSelector)); + + ExtendIds = ToUnplugApicIds; + ExtendCount = ToUnplugCount; + } else { + DEBUG ((DEBUG_VERBOSE, "%a: CurrentSelector=%u: no event\n", + __FUNCTION__, CurrentSelector)); + break; + } + + // + // Save the APIC ID of the CPU with the pending event, to the corresponding + // APIC ID array. + // + if (*ExtendCount == ApicIdCount) { + DEBUG ((DEBUG_ERROR, "%a: APIC ID array too small\n", __FUNCTION__)); + return EFI_BUFFER_TOO_SMALL; + } + QemuCpuhpWriteCommand (MmCpuIo, QEMU_CPUHP_CMD_GET_ARCH_ID); + NewApicId = QemuCpuhpReadCommandData (MmCpuIo); + DEBUG ((DEBUG_VERBOSE, "%a: ApicId=" FMT_APIC_ID "\n", __FUNCTION__, + NewApicId)); + ExtendIds[(*ExtendCount)++] = NewApicId; + + // + // We've processed the CPU with (known) pending events, but we must never + // clear events. Therefore we need to advance past this CPU manually; + // otherwise, QEMU_CPUHP_CMD_GET_PENDING would stick to the currently + // selected CPU. + // + CurrentSelector++; + } while (CurrentSelector < PossibleCpuCount); + + DEBUG ((DEBUG_VERBOSE, "%a: PluggedCount=%u ToUnplugCount=%u\n", + __FUNCTION__, *PluggedCount, *ToUnplugCount)); + return EFI_SUCCESS; +} diff --git a/roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.h b/roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.h new file mode 100644 index 000000000..8adaa0ad9 --- /dev/null +++ b/roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.h @@ -0,0 +1,61 @@ +/** @file + Simple wrapper functions and utility functions that access QEMU's modern CPU + hotplug register block. + + These functions manipulate some of the registers described in + "docs/specs/acpi_cpu_hotplug.txt" in the QEMU source. IO Ports are accessed + via EFI_MM_CPU_IO_PROTOCOL. If a protocol call fails, these functions don't + return. + + Copyright (c) 2020, Red Hat, Inc. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#ifndef QEMU_CPUHP_H_ +#define QEMU_CPUHP_H_ + +#include // EFI_MM_CPU_IO_PROTOCOL +#include // EFI_STATUS + +#include "ApicId.h" // APIC_ID + +UINT32 +QemuCpuhpReadCommandData2 ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo + ); + +UINT8 +QemuCpuhpReadCpuStatus ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo + ); + +UINT32 +QemuCpuhpReadCommandData ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo + ); + +VOID +QemuCpuhpWriteCpuSelector ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo, + IN UINT32 Selector + ); + +VOID +QemuCpuhpWriteCommand ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo, + IN UINT8 Command + ); + +EFI_STATUS +QemuCpuhpCollectApicIds ( + IN CONST EFI_MM_CPU_IO_PROTOCOL *MmCpuIo, + IN UINT32 PossibleCpuCount, + IN UINT32 ApicIdCount, + OUT APIC_ID *PluggedApicIds, + OUT UINT32 *PluggedCount, + OUT APIC_ID *ToUnplugApicIds, + OUT UINT32 *ToUnplugCount + ); + +#endif // QEMU_CPUHP_H_ diff --git a/roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.c b/roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.c new file mode 100644 index 000000000..d8f45c431 --- /dev/null +++ b/roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.c @@ -0,0 +1,290 @@ +/** @file + SMBASE relocation for hot-plugged CPUs. + + Copyright (c) 2020, Red Hat, Inc. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#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 + CPUs. + + This function may only be called from the entry point function of the driver. + + @param[out] PenAddress The address of the allocated (normal RAM) reserved + page. + + @param[in] BootServices Pointer to the UEFI boot services table. Used for + allocating the normal RAM (not SMRAM) reserved page. + + @retval EFI_SUCCESS Allocation successful. + + @retval EFI_BAD_BUFFER_SIZE The Post-SMM Pen template is not smaller than + EFI_PAGE_SIZE. + + @return Error codes propagated from underlying services. + DEBUG_ERROR messages have been logged. No + resources have been allocated. +**/ +EFI_STATUS +SmbaseAllocatePostSmmPen ( + OUT UINT32 *PenAddress, + IN CONST EFI_BOOT_SERVICES *BootServices + ) +{ + EFI_STATUS Status; + EFI_PHYSICAL_ADDRESS Address; + + // + // The pen code must fit in one page, and the last byte must remain free for + // signaling the SMM Monarch. + // + if (mPostSmmPenSize >= EFI_PAGE_SIZE) { + Status = EFI_BAD_BUFFER_SIZE; + DEBUG ((DEBUG_ERROR, "%a: mPostSmmPenSize=%u: %r\n", __FUNCTION__, + mPostSmmPenSize, Status)); + return Status; + } + + Address = BASE_1MB - 1; + Status = BootServices->AllocatePages (AllocateMaxAddress, + EfiReservedMemoryType, 1, &Address); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a: AllocatePages(): %r\n", __FUNCTION__, Status)); + return Status; + } + + DEBUG ((DEBUG_INFO, "%a: Post-SMM Pen at 0x%Lx\n", __FUNCTION__, Address)); + *PenAddress = (UINT32)Address; + return EFI_SUCCESS; +} + +/** + Copy the Post-SMM Pen template code into the reserved page allocated with + SmbaseAllocatePostSmmPen(). + + Note that this effects an "SMRAM to normal RAM" copy. + + The SMM Monarch is supposed to call this function from the root MMI handler. + + @param[in] PenAddress The allocation address returned by + SmbaseAllocatePostSmmPen(). +**/ +VOID +SmbaseReinstallPostSmmPen ( + IN UINT32 PenAddress + ) +{ + CopyMem ((VOID *)(UINTN)PenAddress, mPostSmmPen, mPostSmmPenSize); +} + +/** + Release the reserved page allocated with SmbaseAllocatePostSmmPen(). + + This function may only be called from the entry point function of the driver, + on the error path. + + @param[in] PenAddress The allocation address returned by + SmbaseAllocatePostSmmPen(). + + @param[in] BootServices Pointer to the UEFI boot services table. Used for + releasing the normal RAM (not SMRAM) reserved page. +**/ +VOID +SmbaseReleasePostSmmPen ( + IN UINT32 PenAddress, + IN CONST EFI_BOOT_SERVICES *BootServices + ) +{ + 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. + // + // There are 2*2 cases to consider: + // + // (1) The CPU was hot-added before the SMI was broadcast. + // + // (1.1) The OS is benign. + // + // The hot-added CPU is in RESET state, with the broadcast SMI pending + // for it. The directed SMI below will be ignored (it's idempotent), + // and the INIT-SIPI-SIPI will launch the CPU directly into SMM. + // + // (1.2) The OS is malicious. + // + // The hot-added CPU has been booted, by the OS. Thus, the hot-added + // CPU is spinning on the APIC ID gate. In that case, both the SMI and + // the INIT-SIPI-SIPI below will be ignored. + // + // (2) The CPU was hot-added after the SMI was broadcast. + // + // (2.1) The OS is benign. + // + // The hot-added CPU is in RESET state, with no SMI pending for it. The + // directed SMI will latch the SMI for the CPU. Then the INIT-SIPI-SIPI + // will launch the CPU into SMM. + // + // (2.2) The OS is malicious. + // + // The hot-added CPU is executing OS code. The directed SMI will pull + // the hot-added CPU into SMM, where it will start spinning on the APIC + // ID gate. The INIT-SIPI-SIPI will be ignored. + // + SendSmiIpi (ApicId); + 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/roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.h b/roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.h new file mode 100644 index 000000000..e73730d19 --- /dev/null +++ b/roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.h @@ -0,0 +1,46 @@ +/** @file + SMBASE relocation for hot-plugged CPUs. + + Copyright (c) 2020, Red Hat, Inc. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#ifndef SMBASE_H_ +#define SMBASE_H_ + +#include // EFI_STATUS +#include // EFI_BOOT_SERVICES + +#include "ApicId.h" // APIC_ID + +EFI_STATUS +SmbaseAllocatePostSmmPen ( + OUT UINT32 *PenAddress, + IN CONST EFI_BOOT_SERVICES *BootServices + ); + +VOID +SmbaseReinstallPostSmmPen ( + IN UINT32 PenAddress + ); + +VOID +SmbaseReleasePostSmmPen ( + IN UINT32 PenAddress, + 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_ -- cgit