| /** @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", | |
| __func__, | |
| 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", __func__, Status)); | |
| return Status; | |
| } | |
| DEBUG ((DEBUG_INFO, "%a: Post-SMM Pen at 0x%Lx\n", __func__, 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", | |
| __func__, | |
| 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", | |
| __func__, | |
| 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; | |
| } |