diff options
Diffstat (limited to 'roms/edk2/OvmfPkg/CpuHotplugSmm')
-rw-r--r-- | roms/edk2/OvmfPkg/CpuHotplugSmm/ApicId.h | 23 | ||||
-rw-r--r-- | roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplug.c | 445 | ||||
-rw-r--r-- | roms/edk2/OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf | 64 | ||||
-rw-r--r-- | roms/edk2/OvmfPkg/CpuHotplugSmm/FirstSmiHandler.nasm | 154 | ||||
-rw-r--r-- | roms/edk2/OvmfPkg/CpuHotplugSmm/FirstSmiHandlerContext.h | 47 | ||||
-rw-r--r-- | roms/edk2/OvmfPkg/CpuHotplugSmm/PostSmmPen.nasm | 151 | ||||
-rw-r--r-- | roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.c | 301 | ||||
-rw-r--r-- | roms/edk2/OvmfPkg/CpuHotplugSmm/QemuCpuhp.h | 61 | ||||
-rw-r--r-- | roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.c | 290 | ||||
-rw-r--r-- | roms/edk2/OvmfPkg/CpuHotplugSmm/Smbase.h | 46 |
10 files changed, 1582 insertions, 0 deletions
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 <CpuHotPlugData.h> // CPU_HOT_PLUG_DATA
+#include <IndustryStandard/Q35MchIch9.h> // ICH9_APM_CNT
+#include <IndustryStandard/QemuCpuHotplug.h> // QEMU_CPUHP_CMD_GET_PENDING
+#include <Library/BaseLib.h> // CpuDeadLoop()
+#include <Library/DebugLib.h> // ASSERT()
+#include <Library/MmServicesTableLib.h> // gMmst
+#include <Library/PcdLib.h> // PcdGetBool()
+#include <Library/SafeIntLib.h> // SafeUintnSub()
+#include <Protocol/MmCpuIo.h> // EFI_MM_CPU_IO_PROTOCOL
+#include <Protocol/SmmCpuService.h> // EFI_SMM_CPU_SERVICE_PROTOCOL
+#include <Uefi/UefiBaseType.h> // 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 <IndustryStandard/Q35MchIch9.h> // ICH9_CPU_HOTPLUG_BASE
+#include <IndustryStandard/QemuCpuHotplug.h> // QEMU_CPUHP_R_CMD_DATA2
+#include <Library/BaseLib.h> // CpuDeadLoop()
+#include <Library/DebugLib.h> // 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 <Protocol/MmCpuIo.h> // EFI_MM_CPU_IO_PROTOCOL
+#include <Uefi/UefiBaseType.h> // 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.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
+ 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 <Uefi/UefiBaseType.h> // EFI_STATUS
+#include <Uefi/UefiSpec.h> // 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_
|