OvmfPkg/CpuHotplugSmm: introduce First SMI Handler for hot-added CPUs
authorLaszlo Ersek <lersek@redhat.com>
Wed, 26 Feb 2020 22:11:52 +0000 (23:11 +0100)
committermergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Wed, 4 Mar 2020 12:22:07 +0000 (12:22 +0000)
Implement the First SMI Handler for hot-added CPUs, in NASM.

Add the interfacing C-language function that the SMM Monarch calls. This
function launches and coordinates SMBASE relocation for a hot-added CPU.

Cc: Ard Biesheuvel <ard.biesheuvel@linaro.org>
Cc: Igor Mammedov <imammedo@redhat.com>
Cc: Jiewen Yao <jiewen.yao@intel.com>
Cc: Jordan Justen <jordan.l.justen@intel.com>
Cc: Michael Kinney <michael.d.kinney@intel.com>
Cc: Philippe Mathieu-Daudé <philmd@redhat.com>
Ref: https://bugzilla.tianocore.org/show_bug.cgi?id=1512
Signed-off-by: Laszlo Ersek <lersek@redhat.com>
Message-Id: <20200226221156.29589-13-lersek@redhat.com>
Acked-by: Ard Biesheuvel <ard.biesheuvel@linaro.org>
Tested-by: Boris Ostrovsky <boris.ostrovsky@oracle.com>
OvmfPkg/CpuHotplugSmm/CpuHotplugSmm.inf
OvmfPkg/CpuHotplugSmm/FirstSmiHandler.nasm [new file with mode: 0644]
OvmfPkg/CpuHotplugSmm/FirstSmiHandlerContext.h [new file with mode: 0644]
OvmfPkg/CpuHotplugSmm/Smbase.c
OvmfPkg/CpuHotplugSmm/Smbase.h

index bf41622..04322b0 100644 (file)
@@ -24,6 +24,8 @@
 [Sources]\r
   ApicId.h\r
   CpuHotplug.c\r
+  FirstSmiHandler.nasm\r
+  FirstSmiHandlerContext.h\r
   PostSmmPen.nasm\r
   QemuCpuhp.c\r
   QemuCpuhp.h\r
   BaseLib\r
   BaseMemoryLib\r
   DebugLib\r
+  LocalApicLib\r
   MmServicesTableLib\r
   PcdLib\r
   SafeIntLib\r
+  SynchronizationLib\r
   UefiDriverEntryPoint\r
 \r
 [Protocols]\r
diff --git a/OvmfPkg/CpuHotplugSmm/FirstSmiHandler.nasm b/OvmfPkg/CpuHotplugSmm/FirstSmiHandler.nasm
new file mode 100644 (file)
index 0000000..5399b5f
--- /dev/null
@@ -0,0 +1,154 @@
+;------------------------------------------------------------------------------\r
+; @file\r
+; Relocate the SMBASE on a hot-added CPU when it services its first SMI.\r
+;\r
+; Copyright (c) 2020, Red Hat, Inc.\r
+;\r
+; SPDX-License-Identifier: BSD-2-Clause-Patent\r
+;\r
+; The routine runs on the hot-added CPU in the following "big real mode",\r
+; 16-bit environment; per "SMI HANDLER EXECUTION ENVIRONMENT" in the Intel SDM\r
+; (table "Processor Register Initialization in SMM"):\r
+;\r
+;  - CS selector: 0x3000 (most significant 16 bits of SMM_DEFAULT_SMBASE).\r
+;\r
+;  - CS limit: 0xFFFF_FFFF.\r
+;\r
+;  - CS base: SMM_DEFAULT_SMBASE (0x3_0000).\r
+;\r
+;  - IP: SMM_HANDLER_OFFSET (0x8000).\r
+;\r
+;  - ES, SS, DS, FS, GS selectors: 0.\r
+;\r
+;  - ES, SS, DS, FS, GS limits: 0xFFFF_FFFF.\r
+;\r
+;  - ES, SS, DS, FS, GS bases: 0.\r
+;\r
+;  - Operand-size and address-size override prefixes can be used to access the\r
+;    address space beyond 1MB.\r
+;------------------------------------------------------------------------------\r
+\r
+SECTION .data\r
+BITS 16\r
+\r
+;\r
+; Bring in SMM_DEFAULT_SMBASE from\r
+; "MdePkg/Include/Register/Intel/SmramSaveStateMap.h".\r
+;\r
+SMM_DEFAULT_SMBASE: equ 0x3_0000\r
+\r
+;\r
+; Field offsets in FIRST_SMI_HANDLER_CONTEXT, which resides at\r
+; SMM_DEFAULT_SMBASE.\r
+;\r
+ApicIdGate:      equ  0 ; UINT64\r
+NewSmbase:       equ  8 ; UINT32\r
+AboutToLeaveSmm: equ 12 ; UINT8\r
+\r
+;\r
+; SMRAM Save State Map field offsets, per the AMD (not Intel) layout that QEMU\r
+; implements. Relative to SMM_DEFAULT_SMBASE.\r
+;\r
+SaveStateRevId:    equ 0xFEFC ; UINT32\r
+SaveStateSmbase:   equ 0xFEF8 ; UINT32\r
+SaveStateSmbase64: equ 0xFF00 ; UINT32\r
+\r
+;\r
+; CPUID constants, from "MdePkg/Include/Register/Intel/Cpuid.h".\r
+;\r
+CPUID_SIGNATURE:         equ 0x00\r
+CPUID_EXTENDED_TOPOLOGY: equ 0x0B\r
+CPUID_VERSION_INFO:      equ 0x01\r
+\r
+GLOBAL ASM_PFX (mFirstSmiHandler)     ; UINT8[]\r
+GLOBAL ASM_PFX (mFirstSmiHandlerSize) ; UINT16\r
+\r
+ASM_PFX (mFirstSmiHandler):\r
+  ;\r
+  ; Get our own APIC ID first, so we can contend for ApicIdGate.\r
+  ;\r
+  ; This basically reimplements GetInitialApicId() from\r
+  ; "UefiCpuPkg/Library/BaseXApicLib/BaseXApicLib.c".\r
+  ;\r
+  mov eax, CPUID_SIGNATURE\r
+  cpuid\r
+  cmp eax, CPUID_EXTENDED_TOPOLOGY\r
+  jb GetApicIdFromVersionInfo\r
+\r
+  mov eax, CPUID_EXTENDED_TOPOLOGY\r
+  mov ecx, 0\r
+  cpuid\r
+  test ebx, 0xFFFF\r
+  jz GetApicIdFromVersionInfo\r
+\r
+  ;\r
+  ; EDX has the APIC ID, save it to ESI.\r
+  ;\r
+  mov esi, edx\r
+  jmp KnockOnGate\r
+\r
+GetApicIdFromVersionInfo:\r
+  mov eax, CPUID_VERSION_INFO\r
+  cpuid\r
+  shr ebx, 24\r
+  ;\r
+  ; EBX has the APIC ID, save it to ESI.\r
+  ;\r
+  mov esi, ebx\r
+\r
+KnockOnGate:\r
+  ;\r
+  ; See if ApicIdGate shows our own APIC ID. If so, swap it to MAX_UINT64\r
+  ; (close the gate), and advance. Otherwise, keep knocking.\r
+  ;\r
+  ; InterlockedCompareExchange64():\r
+  ; - Value                   := &FIRST_SMI_HANDLER_CONTEXT.ApicIdGate\r
+  ; - CompareValue  (EDX:EAX) := APIC ID (from ESI)\r
+  ; - ExchangeValue (ECX:EBX) := MAX_UINT64\r
+  ;\r
+  mov edx, 0\r
+  mov eax, esi\r
+  mov ecx, 0xFFFF_FFFF\r
+  mov ebx, 0xFFFF_FFFF\r
+  lock cmpxchg8b [ds : dword (SMM_DEFAULT_SMBASE + ApicIdGate)]\r
+  jz ApicIdMatch\r
+  pause\r
+  jmp KnockOnGate\r
+\r
+ApicIdMatch:\r
+  ;\r
+  ; Update the SMBASE field in the SMRAM Save State Map.\r
+  ;\r
+  ; First, calculate the address of the SMBASE field, based on the SMM Revision\r
+  ; ID; store the result in EBX.\r
+  ;\r
+  mov eax, dword [ds : dword (SMM_DEFAULT_SMBASE + SaveStateRevId)]\r
+  test eax, 0xFFFF\r
+  jz LegacySaveStateMap\r
+\r
+  mov ebx, SMM_DEFAULT_SMBASE + SaveStateSmbase64\r
+  jmp UpdateSmbase\r
+\r
+LegacySaveStateMap:\r
+  mov ebx, SMM_DEFAULT_SMBASE + SaveStateSmbase\r
+\r
+UpdateSmbase:\r
+  ;\r
+  ; Load the new SMBASE value into EAX.\r
+  ;\r
+  mov eax, dword [ds : dword (SMM_DEFAULT_SMBASE + NewSmbase)]\r
+  ;\r
+  ; Save it to the SMBASE field whose address we calculated in EBX.\r
+  ;\r
+  mov dword [ds : dword ebx], eax\r
+  ;\r
+  ; Set AboutToLeaveSmm.\r
+  ;\r
+  mov byte [ds : dword (SMM_DEFAULT_SMBASE + AboutToLeaveSmm)], 1\r
+  ;\r
+  ; We're done; leave SMM and continue to the pen.\r
+  ;\r
+  rsm\r
+\r
+ASM_PFX (mFirstSmiHandlerSize):\r
+  dw $ - ASM_PFX (mFirstSmiHandler)\r
diff --git a/OvmfPkg/CpuHotplugSmm/FirstSmiHandlerContext.h b/OvmfPkg/CpuHotplugSmm/FirstSmiHandlerContext.h
new file mode 100644 (file)
index 0000000..029de4c
--- /dev/null
@@ -0,0 +1,47 @@
+/** @file\r
+  Define the FIRST_SMI_HANDLER_CONTEXT structure, which is an exchange area\r
+  between the SMM Monarch and the hot-added CPU, for relocating the SMBASE of\r
+  the hot-added CPU.\r
+\r
+  Copyright (c) 2020, Red Hat, Inc.\r
+\r
+  SPDX-License-Identifier: BSD-2-Clause-Patent\r
+**/\r
+\r
+#ifndef FIRST_SMI_HANDLER_CONTEXT_H_\r
+#define FIRST_SMI_HANDLER_CONTEXT_H_\r
+\r
+//\r
+// The following structure is used to communicate between the SMM Monarch\r
+// (running the root MMI handler) and the hot-added CPU (handling its first\r
+// SMI). It is placed at SMM_DEFAULT_SMBASE, which is in SMRAM under QEMU's\r
+// "SMRAM at default SMBASE" feature.\r
+//\r
+#pragma pack (1)\r
+typedef struct {\r
+  //\r
+  // When ApicIdGate is MAX_UINT64, then no hot-added CPU may proceed with\r
+  // SMBASE relocation.\r
+  //\r
+  // Otherwise, the hot-added CPU whose APIC ID equals ApicIdGate may proceed\r
+  // with SMBASE relocation.\r
+  //\r
+  // This field is intentionally wider than APIC_ID (UINT32) because we need a\r
+  // "gate locked" value that is different from all possible APIC_IDs.\r
+  //\r
+  UINT64 ApicIdGate;\r
+  //\r
+  // The new SMBASE value for the hot-added CPU to set in the SMRAM Save State\r
+  // Map, before leaving SMM with the RSM instruction.\r
+  //\r
+  UINT32 NewSmbase;\r
+  //\r
+  // The hot-added CPU sets this field to 1 right before executing the RSM\r
+  // instruction. This tells the SMM Monarch to proceed to polling the last\r
+  // byte of the normal RAM reserved page (Post-SMM Pen).\r
+  //\r
+  UINT8 AboutToLeaveSmm;\r
+} FIRST_SMI_HANDLER_CONTEXT;\r
+#pragma pack ()\r
+\r
+#endif // FIRST_SMI_HANDLER_CONTEXT_H_\r
index ea21153..1705712 100644 (file)
@@ -7,13 +7,21 @@
 **/\r
 \r
 #include <Base.h>                             // BASE_1MB\r
+#include <Library/BaseLib.h>                  // CpuPause()\r
 #include <Library/BaseMemoryLib.h>            // CopyMem()\r
 #include <Library/DebugLib.h>                 // DEBUG()\r
+#include <Library/LocalApicLib.h>             // SendInitSipiSipi()\r
+#include <Library/SynchronizationLib.h>       // InterlockedCompareExchange64()\r
+#include <Register/Intel/SmramSaveStateMap.h> // SMM_DEFAULT_SMBASE\r
+\r
+#include "FirstSmiHandlerContext.h"           // FIRST_SMI_HANDLER_CONTEXT\r
 \r
 #include "Smbase.h"\r
 \r
 extern CONST UINT8 mPostSmmPen[];\r
 extern CONST UINT16 mPostSmmPenSize;\r
+extern CONST UINT8 mFirstSmiHandler[];\r
+extern CONST UINT16 mFirstSmiHandlerSize;\r
 \r
 /**\r
   Allocate a non-SMRAM reserved memory page for the Post-SMM Pen for hot-added\r
@@ -108,3 +116,152 @@ SmbaseReleasePostSmmPen (
 {\r
   BootServices->FreePages (PenAddress, 1);\r
 }\r
+\r
+/**\r
+  Place the handler routine for the first SMIs of hot-added CPUs at\r
+  (SMM_DEFAULT_SMBASE + SMM_HANDLER_OFFSET).\r
+\r
+  Note that this effects an "SMRAM to SMRAM" copy.\r
+\r
+  Additionally, shut the APIC ID gate in FIRST_SMI_HANDLER_CONTEXT.\r
+\r
+  This function may only be called from the entry point function of the driver,\r
+  and only after PcdQ35SmramAtDefaultSmbase has been determined to be TRUE.\r
+**/\r
+VOID\r
+SmbaseInstallFirstSmiHandler (\r
+  VOID\r
+  )\r
+{\r
+  FIRST_SMI_HANDLER_CONTEXT *Context;\r
+\r
+  CopyMem ((VOID *)(UINTN)(SMM_DEFAULT_SMBASE + SMM_HANDLER_OFFSET),\r
+    mFirstSmiHandler, mFirstSmiHandlerSize);\r
+\r
+  Context = (VOID *)(UINTN)SMM_DEFAULT_SMBASE;\r
+  Context->ApicIdGate = MAX_UINT64;\r
+}\r
+\r
+/**\r
+  Relocate the SMBASE on a hot-added CPU. Then pen the hot-added CPU in the\r
+  normal RAM reserved memory page, set up earlier with\r
+  SmbaseAllocatePostSmmPen() and SmbaseReinstallPostSmmPen().\r
+\r
+  The SMM Monarch is supposed to call this function from the root MMI handler.\r
+\r
+  The SMM Monarch is responsible for calling SmbaseInstallFirstSmiHandler(),\r
+  SmbaseAllocatePostSmmPen(), and SmbaseReinstallPostSmmPen() before calling\r
+  this function.\r
+\r
+  If the OS maliciously boots the hot-added CPU ahead of letting the ACPI CPU\r
+  hotplug event handler broadcast the CPU hotplug MMI, then the hot-added CPU\r
+  returns to the OS rather than to the pen, upon RSM. In that case, this\r
+  function will hang forever (unless the OS happens to signal back through the\r
+  last byte of the pen page).\r
+\r
+  @param[in] ApicId      The APIC ID of the hot-added CPU whose SMBASE should\r
+                         be relocated.\r
+\r
+  @param[in] Smbase      The new SMBASE address. The root MMI handler is\r
+                         responsible for passing in a free ("unoccupied")\r
+                         SMBASE address that was pre-configured by\r
+                         PiSmmCpuDxeSmm in CPU_HOT_PLUG_DATA.\r
+\r
+  @param[in] PenAddress  The address of the Post-SMM Pen for hot-added CPUs, as\r
+                         returned by SmbaseAllocatePostSmmPen(), and installed\r
+                         by SmbaseReinstallPostSmmPen().\r
+\r
+  @retval EFI_SUCCESS            The SMBASE of the hot-added CPU with APIC ID\r
+                                 ApicId has been relocated to Smbase. The\r
+                                 hot-added CPU has reported back about leaving\r
+                                 SMM.\r
+\r
+  @retval EFI_PROTOCOL_ERROR     Synchronization bug encountered around\r
+                                 FIRST_SMI_HANDLER_CONTEXT.ApicIdGate.\r
+\r
+  @retval EFI_INVALID_PARAMETER  Smbase does not fit in 32 bits. No relocation\r
+                                 has been attempted.\r
+**/\r
+EFI_STATUS\r
+SmbaseRelocate (\r
+  IN APIC_ID ApicId,\r
+  IN UINTN   Smbase,\r
+  IN UINT32  PenAddress\r
+  )\r
+{\r
+  EFI_STATUS                         Status;\r
+  volatile UINT8                     *SmmVacated;\r
+  volatile FIRST_SMI_HANDLER_CONTEXT *Context;\r
+  UINT64                             ExchangeResult;\r
+\r
+  if (Smbase > MAX_UINT32) {\r
+    Status = EFI_INVALID_PARAMETER;\r
+    DEBUG ((DEBUG_ERROR, "%a: ApicId=" FMT_APIC_ID " Smbase=0x%Lx: %r\n",\r
+      __FUNCTION__, ApicId, (UINT64)Smbase, Status));\r
+    return Status;\r
+  }\r
+\r
+  SmmVacated = (UINT8 *)(UINTN)PenAddress + (EFI_PAGE_SIZE - 1);\r
+  Context = (VOID *)(UINTN)SMM_DEFAULT_SMBASE;\r
+\r
+  //\r
+  // Clear AboutToLeaveSmm, so we notice when the hot-added CPU is just about\r
+  // to reach RSM, and we can proceed to polling the last byte of the reserved\r
+  // page (which could be attacked by the OS).\r
+  //\r
+  Context->AboutToLeaveSmm = 0;\r
+\r
+  //\r
+  // Clear the last byte of the reserved page, so we notice when the hot-added\r
+  // CPU checks back in from the pen.\r
+  //\r
+  *SmmVacated = 0;\r
+\r
+  //\r
+  // Boot the hot-added CPU.\r
+  //\r
+  // If the OS is benign, and so the hot-added CPU is still in RESET state,\r
+  // then the broadcast SMI is still pending for it; it will now launch\r
+  // directly into SMM.\r
+  //\r
+  // If the OS is malicious, the hot-added CPU has been booted already, and so\r
+  // it is already spinning on the APIC ID gate. In that case, the\r
+  // INIT-SIPI-SIPI below will be ignored.\r
+  //\r
+  SendInitSipiSipi (ApicId, PenAddress);\r
+\r
+  //\r
+  // Expose the desired new SMBASE value to the hot-added CPU.\r
+  //\r
+  Context->NewSmbase = (UINT32)Smbase;\r
+\r
+  //\r
+  // Un-gate SMBASE relocation for the hot-added CPU whose APIC ID is ApicId.\r
+  //\r
+  ExchangeResult = InterlockedCompareExchange64 (&Context->ApicIdGate,\r
+                     MAX_UINT64, ApicId);\r
+  if (ExchangeResult != MAX_UINT64) {\r
+    Status = EFI_PROTOCOL_ERROR;\r
+    DEBUG ((DEBUG_ERROR, "%a: ApicId=" FMT_APIC_ID " ApicIdGate=0x%Lx: %r\n",\r
+      __FUNCTION__, ApicId, ExchangeResult, Status));\r
+    return Status;\r
+  }\r
+\r
+  //\r
+  // Wait until the hot-added CPU is just about to execute RSM.\r
+  //\r
+  while (Context->AboutToLeaveSmm == 0) {\r
+    CpuPause ();\r
+  }\r
+\r
+  //\r
+  // Now wait until the hot-added CPU reports back from the pen (or the OS\r
+  // attacks the last byte of the reserved page).\r
+  //\r
+  while (*SmmVacated == 0) {\r
+    CpuPause ();\r
+  }\r
+\r
+  Status = EFI_SUCCESS;\r
+  return Status;\r
+}\r
index cb5aed9..e73730d 100644 (file)
@@ -12,6 +12,8 @@
 #include <Uefi/UefiBaseType.h> // EFI_STATUS\r
 #include <Uefi/UefiSpec.h>     // EFI_BOOT_SERVICES\r
 \r
+#include "ApicId.h"            // APIC_ID\r
+\r
 EFI_STATUS\r
 SmbaseAllocatePostSmmPen (\r
   OUT UINT32                  *PenAddress,\r
@@ -29,4 +31,16 @@ SmbaseReleasePostSmmPen (
   IN CONST EFI_BOOT_SERVICES *BootServices\r
   );\r
 \r
+VOID\r
+SmbaseInstallFirstSmiHandler (\r
+  VOID\r
+  );\r
+\r
+EFI_STATUS\r
+SmbaseRelocate (\r
+  IN APIC_ID ApicId,\r
+  IN UINTN   Smbase,\r
+  IN UINT32  PenAddress\r
+  );\r
+\r
 #endif // SMBASE_H_\r