/** @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; | |
} |