From 021c1622ebdfb5c140ab907195054684d7541a67 Mon Sep 17 00:00:00 2001 From: Joey Vagedes Date: Mon, 4 Dec 2023 16:26:53 -0800 Subject: [PATCH 01/12] MsCorePkg/SecureBootKeyStoreLib: Add Library implementation (#377) ## Description Adds SecureBootKeyStoreLib which provides secureboot key store options by consuming fixed at build PCDs that represent the Pk, Db, 3PDb, Kek, and Dbx. - [x] Impacts functionality? - Adds 5 new PCDs to represent the Pk, Db, 3PDb, Kek, and Dbx, which are consumed by SedcureBootKeyStoreLibOem when configuring SecureBoot. - [ ] Impacts security? - **Security** - Does the change have a direct security impact on an application, flow, or firmware? - Examples: Crypto algorithm change, buffer overflow fix, parameter validation improvement, ... - [ ] Breaking change? - **Breaking change** - Will anyone consuming this change experience a break in build or boot behavior? - Examples: Add a new library class, move a module to a different repo, call a function in a new library class in a pre-existing module, ... - [ ] Includes tests? - **Tests** - Does the change include any explicit test code? - Examples: Unit tests, integration tests, robot tests, ... - [ ] Includes documentation? - **Documentation** - Does the change contain explicit documentation additions outside direct code modifications (and comments)? - Examples: Update readme file, add feature readme file, link to documentation on an a separate Web page, ... ## How This Was Tested Verified secureboot is properly enabled on Qemu35Pkg for Microsoft and Microsoft 3rd party. ## Integration Instructions Generate the following PCDs for your platform DSC: 1. gOemPkgTokenSpaceGuid.PcdDefaultKek 2. gOemPkgTokenSpaceGuid.PcdDefaultDb 3. gOemPkgTokenSpaceGuid.PcdDefault3PDb 4. gOemPkgTokenSpaceGuid.PcdDefaultDbx 5. gOemPkgTokenSpaceGuid.PcdDefaultPk It is highly suggested, but not required, that each pcd is generated by running BaseTools `BinToPcd` over the binary blobs created in [secureboot_objects](https://github.com/microsoft/secureboot_objects), then including them in the platform's DSC file. --------- Co-authored-by: Michael Kubacki --- .../BaseSecureBootKeyStoreLib.inf | 36 +++++++++ .../SecureBootKeyStoreLib.c | 74 +++++++++++++++++++ MsCorePkg/MsCorePkg.dec | 20 +++++ MsCorePkg/MsCorePkg.dsc | 1 + 4 files changed, 131 insertions(+) create mode 100644 MsCorePkg/Library/BaseSecureBootKeyStoreLib/BaseSecureBootKeyStoreLib.inf create mode 100644 MsCorePkg/Library/BaseSecureBootKeyStoreLib/SecureBootKeyStoreLib.c diff --git a/MsCorePkg/Library/BaseSecureBootKeyStoreLib/BaseSecureBootKeyStoreLib.inf b/MsCorePkg/Library/BaseSecureBootKeyStoreLib/BaseSecureBootKeyStoreLib.inf new file mode 100644 index 0000000000..d577c15ee8 --- /dev/null +++ b/MsCorePkg/Library/BaseSecureBootKeyStoreLib/BaseSecureBootKeyStoreLib.inf @@ -0,0 +1,36 @@ +## @file - BaseSecureBootKeyStoreLib.inf +# Copyright (C) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# + +[Defines] + INF_VERSION = 0x00010005 + BASE_NAME = BaseSecureBootKeyStoreLib + FILE_GUID = 02EEF9DA-5557-4090-BFF5-E07EF0344805 + VERSION_STRING = 1.0 + MODULE_TYPE = BASE + LIBRARY_CLASS = SecureBootKeyStoreLib + +# +# The following information is for reference only and not required by the build tools. +# +# VALID_ARCHITECTURES = IA32 X64 AARCH64 +# + +[Packages] + MdePkg/MdePkg.dec + SecurityPkg/SecurityPkg.dec + MsCorePkg/MsCorePkg.dec + +[Sources] + SecureBootKeyStoreLib.c + +[LibraryClasses] + PcdLib + +[FixedPcd] + gMsCorePkgTokenSpaceGuid.PcdDefaultKek # CONSUMES + gMsCorePkgTokenSpaceGuid.PcdDefaultDb # CONSUMES + gMsCorePkgTokenSpaceGuid.PcdDefault3PDb # CONSUMES + gMsCorePkgTokenSpaceGuid.PcdDefaultDbx # CONSUMES + gMsCorePkgTokenSpaceGuid.PcdDefaultPk # CONSUMES diff --git a/MsCorePkg/Library/BaseSecureBootKeyStoreLib/SecureBootKeyStoreLib.c b/MsCorePkg/Library/BaseSecureBootKeyStoreLib/SecureBootKeyStoreLib.c new file mode 100644 index 0000000000..c8ca803992 --- /dev/null +++ b/MsCorePkg/Library/BaseSecureBootKeyStoreLib/SecureBootKeyStoreLib.c @@ -0,0 +1,74 @@ +/** @file SecureBootKeyStoreLib.c + + + Copyright (C) Microsoft Corporation. All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent + +**/ + +#include +#include + +#include + +#include +#include + +SECURE_BOOT_PAYLOAD_INFO mSecureBootPayload[] = { + { + .SecureBootKeyName = L"Microsoft Only", + .KekPtr = (CONST UINT8 *)FixedPcdGetPtr (PcdDefaultKek), + .KekSize = (CONST UINT32)FixedPcdGetSize (PcdDefaultKek), + .DbPtr = (CONST UINT8 *)FixedPcdGetPtr (PcdDefaultDb), + .DbSize = (CONST UINT32)FixedPcdGetSize (PcdDefaultDb), + .DbxPtr = (CONST UINT8 *)FixedPcdGetPtr (PcdDefaultDbx), + .DbxSize = (CONST UINT32)FixedPcdGetSize (PcdDefaultDbx), + .PkPtr = (CONST UINT8 *)FixedPcdGetPtr (PcdDefaultPk), + .PkSize = (CONST UINT32)FixedPcdGetSize (PcdDefaultPk), + .DbtPtr = NULL, + .DbtSize = 0, + }, + { + .SecureBootKeyName = L"Microsoft Plus 3rd Party", + .KekPtr = (CONST UINT8 *)FixedPcdGetPtr (PcdDefaultKek), + .KekSize = (CONST UINT32)FixedPcdGetSize (PcdDefaultKek), + .DbPtr = (CONST UINT8 *)FixedPcdGetPtr (PcdDefault3PDb), + .DbSize = (CONST UINT32)FixedPcdGetSize (PcdDefault3PDb), + .DbxPtr = (CONST UINT8 *)FixedPcdGetPtr (PcdDefaultDbx), + .DbxSize = (CONST UINT32)FixedPcdGetSize (PcdDefaultDbx), + .PkPtr = (CONST UINT8 *)FixedPcdGetPtr (PcdDefaultPk), + .PkSize = (CONST UINT32)FixedPcdGetSize (PcdDefaultPk), + .DbtPtr = NULL, + .DbtSize = 0, + } +}; + +/** + Interface to fetch platform Secure Boot Certificates, each payload + corresponds to a designated set of db, dbx, dbt, KEK, PK. + + @param[in] Keys Pointer to hold the returned sets of keys. The + returned buffer will be treated as CONST and + permanent pointer. The consumer will NOT free + the buffer after use. + @param[in] KeyCount The number of sets available in the returned Keys. + + @retval EFI_SUCCESS The Keys are properly fetched. + @retval EFI_INVALID_PARAMETER Inputs have NULL pointers. +**/ +EFI_STATUS +EFIAPI +GetPlatformKeyStore ( + OUT SECURE_BOOT_PAYLOAD_INFO **Keys, + OUT UINT8 *KeyCount + ) +{ + if ((Keys == NULL) || (KeyCount == NULL)) { + return EFI_INVALID_PARAMETER; + } + + *Keys = mSecureBootPayload; + *KeyCount = ARRAY_SIZE (mSecureBootPayload); + + return EFI_SUCCESS; +} diff --git a/MsCorePkg/MsCorePkg.dec b/MsCorePkg/MsCorePkg.dec index 0e16bb95cd..074be6d792 100644 --- a/MsCorePkg/MsCorePkg.dec +++ b/MsCorePkg/MsCorePkg.dec @@ -136,6 +136,26 @@ ## Default: 1024 * 4KiB = 4MB gMsCorePkgTokenSpaceGuid.PcdDebugFileLoggerAllocatedPages|1024|UINT32|0x4000001C + ## Pcd value representing the Pk for a platform + # Empty by default. Platform required to set this value to enable setting default secureboot variables. + gMsCorePkgTokenSpaceGuid.PcdDefaultPk |{ 0x0 }|VOID*|0x4000001D + + ## Pcd value representing the Db for a platform + # Empty by default. Platform required to set this value to enable setting default secureboot variables. + gMsCorePkgTokenSpaceGuid.PcdDefaultDb |{ 0x0 }|VOID*|0x4000001E + + ## Pcd value representing the 3PDb for a platform + # Empty by default. Platform required to set this value to enable setting default secureboot variables. + gMsCorePkgTokenSpaceGuid.PcdDefault3PDb | { 0x0 }|VOID*|0x4000001F + + ## Pcd value representing the Dbx for a platform + # Empty by default. Platform required to set this value to enable setting default secureboot variables. + gMsCorePkgTokenSpaceGuid.PcdDefaultDbx | { 0x0 }|VOID*|0x40000020 + + ## Pcd value representing the Kek for a platform + # Empty by default. Platform required to set this value to enable setting default secureboot variables. + gMsCorePkgTokenSpaceGuid.PcdDefaultKek | { 0x0 }|VOID*|0x40000021 + [PcdsDynamic, PcdsDynamicEx] gMsCorePkgTokenSpaceGuid.PcdDeviceStateBitmask|0x00000000|UINT32|0x00010178 diff --git a/MsCorePkg/MsCorePkg.dsc b/MsCorePkg/MsCorePkg.dsc index ae7aa2792b..ac664e0b26 100644 --- a/MsCorePkg/MsCorePkg.dsc +++ b/MsCorePkg/MsCorePkg.dsc @@ -168,6 +168,7 @@ MsCorePkg/Library/DxeIsCapsuleSupportedLib/DxeIsCapsuleSupportedLib.inf MsCorePkg/Library/BaseIsCapsuleSupportedLibNull/BaseIsCapsuleSupportedLibNull.inf MsCorePkg/Library/SecureBootKeyStoreLibNull/SecureBootKeyStoreLibNull.inf + MsCorePkg/Library/BaseSecureBootKeyStoreLib/BaseSecureBootKeyStoreLib.inf MsCorePkg/Library/MuSecureBootKeySelectorLib/MuSecureBootKeySelectorLib.inf MsCorePkg/HelloWorldRustDxe/HelloWorldRustDxe.inf From 0e360b025477b014e97ada1931ff05e1c1fc59d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 09:43:46 -0500 Subject: [PATCH 02/12] pip: bump edk2-pytool-library from 0.19.6 to 0.19.7 (#380) Bumps [edk2-pytool-library](https://github.com/tianocore/edk2-pytool-library) from 0.19.6 to 0.19.7. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pip-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip-requirements.txt b/pip-requirements.txt index 4187317f00..3d5e71acec 100644 --- a/pip-requirements.txt +++ b/pip-requirements.txt @@ -12,7 +12,7 @@ # https://www.python.org/dev/peps/pep-0440/#version-specifiers ## -edk2-pytool-library==0.19.6 +edk2-pytool-library==0.19.7 edk2-pytool-extensions==0.26.2 edk2-basetools==0.1.29 antlr4-python3-runtime==4.13.1 From f5354997aee1fb5caee28d92e232fe638ebf965b Mon Sep 17 00:00:00 2001 From: Taylor Beebe <31827475+TaylorBeebe@users.noreply.github.com> Date: Fri, 8 Dec 2023 11:02:29 -0800 Subject: [PATCH 03/12] Split MemoryProtectionTestApp to DXE and SMM Versions (#379) ## Description MemoryProtectionTestApp was written before Project Mu supported an ARM platform and Standalone MM. The test app generates the test cases for both the SMM and DXE environments. The SMM tests will report failure for Arm platforms and platforms using Standalone MM, so this update separates the test app into SMM and DXE versions. The SMM version will still require a driver to handle the SMI calls. To reduce confusing naming, MemoryProtectionTestSmm (the driver supporting SMM memory protection testing) has been renamed to SmmMemoryProtectionTestDriver. - [x] Impacts functionality? - **Functionality** - Does the change ultimately impact how firmware functions? - Examples: Add a new library, publish a new PPI, update an algorithm, ... - [ ] Impacts security? - **Security** - Does the change have a direct security impact on an application, flow, or firmware? - Examples: Crypto algorithm change, buffer overflow fix, parameter validation improvement, ... - [ ] Breaking change? - **Breaking change** - Will anyone consuming this change experience a break in build or boot behavior? - Examples: Add a new library class, move a module to a different repo, call a function in a new library class in a pre-existing module, ... - [ ] Includes tests? - **Tests** - Does the change include any explicit test code? - Examples: Unit tests, integration tests, robot tests, ... - [ ] Includes documentation? - **Documentation** - Does the change contain explicit documentation additions outside direct code modifications (and comments)? - Examples: Update readme file, add feature readme file, link to documentation on an a separate Web page, ... ## How This Was Tested SMM test was checked on a physical x86 platform. The DXE test was checked on Q35 and SBSA ## Integration Instructions Instances of ` UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/MemoryProtectionTestApp.inf ` will need to be updated to `UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/SmmMemoryProtectionTestApp.inf UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/DxeMemoryProtectionTestApp.inf ` And instances of ` UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Smm/MemoryProtectionTestSmm.inf ` will need to be updated to ` UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Driver/SmmMemoryProtectionTestDriver.inf ` --- .../App/DxeMemoryProtectionTestApp.c | 2059 +++++++++++++++++ .../App/DxeMemoryProtectionTestApp.inf | 81 + .../App/SmmMemoryProtectionTestApp.c | 791 +++++++ .../App/SmmMemoryProtectionTestApp.inf | 81 + .../Driver/SmmMemoryProtectionTestDriver.c | 365 +++ .../Driver/SmmMemoryProtectionTestDriver.inf | 50 + UefiTestingPkg/UefiTestingPkg.dsc | 7 +- 7 files changed, 3432 insertions(+), 2 deletions(-) create mode 100644 UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/DxeMemoryProtectionTestApp.c create mode 100644 UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/DxeMemoryProtectionTestApp.inf create mode 100644 UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/SmmMemoryProtectionTestApp.c create mode 100644 UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/SmmMemoryProtectionTestApp.inf create mode 100644 UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Driver/SmmMemoryProtectionTestDriver.c create mode 100644 UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Driver/SmmMemoryProtectionTestDriver.inf diff --git a/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/DxeMemoryProtectionTestApp.c b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/DxeMemoryProtectionTestApp.c new file mode 100644 index 0000000000..22187361d2 --- /dev/null +++ b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/DxeMemoryProtectionTestApp.c @@ -0,0 +1,2059 @@ +/** @file -- DxeMemoryProtectionTestApp.c + +Tests for page guard, pool guard, NX protections, stack guard, and null pointer detection. + +Copyright (c) Microsoft Corporation. All rights reserved. +SPDX-License-Identifier: BSD-2-Clause-Patent + +**/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../MemoryProtectionTestCommon.h" +#include "ArchSpecificFunctions.h" + +#define UNIT_TEST_APP_NAME "DXE Memory Protection Test" +#define UNIT_TEST_APP_VERSION "3.0" +#define UNIT_TEST_WARM_RESET_STRING L"--Reset" +#define UNIT_TEST_MEMORY_ATTRIBUTE_STRING L"--MemoryAttribute" +#define UNIT_TEST_CLEAR_FAULTS_STRING L"--ClearFaults" + +#define DUMMY_FUNCTION_FOR_CODE_SELF_TEST_GENERIC_SIZE 512 +#define ALIGN_ADDRESS(Address) (((Address) / EFI_PAGE_SIZE) * EFI_PAGE_SIZE) + +DXE_MEMORY_PROTECTION_SETTINGS mDxeMps; +MEMORY_PROTECTION_NONSTOP_MODE_PROTOCOL *mNonstopModeProtocol = NULL; +MEMORY_PROTECTION_DEBUG_PROTOCOL *mMemoryProtectionProtocol = NULL; +EFI_MEMORY_ATTRIBUTE_PROTOCOL *mMemoryAttributeProtocol = NULL; +CPU_MP_DEBUG_PROTOCOL *mCpuMpDebugProtocol = NULL; + +/// ================================================================================================ +/// ================================================================================================ +/// +/// HELPER FUNCTIONS +/// +/// ================================================================================================ +/// ================================================================================================ + +/** + Gets the input EFI_MEMORY_TYPE from the input DXE_HEAP_GUARD_MEMORY_TYPES bitfield + + @param[in] MemoryType Memory type to check. + @param[in] HeapGuardMemoryType DXE_HEAP_GUARD_MEMORY_TYPES bitfield + + @return TRUE The given EFI_MEMORY_TYPE is TRUE in the given DXE_HEAP_GUARD_MEMORY_TYPES + @return FALSE The given EFI_MEMORY_TYPE is FALSE in the given DXE_HEAP_GUARD_MEMORY_TYPES +**/ +BOOLEAN +STATIC +GetDxeMemoryTypeSettingFromBitfield ( + IN EFI_MEMORY_TYPE MemoryType, + IN DXE_HEAP_GUARD_MEMORY_TYPES HeapGuardMemoryType + ) +{ + switch (MemoryType) { + case EfiReservedMemoryType: + return HeapGuardMemoryType.Fields.EfiReservedMemoryType; + case EfiLoaderCode: + return HeapGuardMemoryType.Fields.EfiLoaderCode; + case EfiLoaderData: + return HeapGuardMemoryType.Fields.EfiLoaderData; + case EfiBootServicesCode: + return HeapGuardMemoryType.Fields.EfiBootServicesCode; + case EfiBootServicesData: + return HeapGuardMemoryType.Fields.EfiBootServicesData; + case EfiRuntimeServicesCode: + return HeapGuardMemoryType.Fields.EfiRuntimeServicesCode; + case EfiRuntimeServicesData: + return HeapGuardMemoryType.Fields.EfiRuntimeServicesData; + case EfiConventionalMemory: + return HeapGuardMemoryType.Fields.EfiConventionalMemory; + case EfiUnusableMemory: + return HeapGuardMemoryType.Fields.EfiUnusableMemory; + case EfiACPIReclaimMemory: + return HeapGuardMemoryType.Fields.EfiACPIReclaimMemory; + case EfiACPIMemoryNVS: + return HeapGuardMemoryType.Fields.EfiACPIMemoryNVS; + case EfiMemoryMappedIO: + return HeapGuardMemoryType.Fields.EfiMemoryMappedIO; + case EfiMemoryMappedIOPortSpace: + return HeapGuardMemoryType.Fields.EfiMemoryMappedIOPortSpace; + case EfiPalCode: + return HeapGuardMemoryType.Fields.EfiPalCode; + case EfiPersistentMemory: + return HeapGuardMemoryType.Fields.EfiPersistentMemory; + case EfiUnacceptedMemoryType: + return HeapGuardMemoryType.Fields.EfiUnacceptedMemoryType; + default: + return FALSE; + } +} + +/** + Abstraction layer which fetches the DXE memory protection HOB. + + @retval EFI_SUCCESS The HOB entry has been fetched + @retval EFI_INVALID_PARAMETER The HOB entry could not be found +**/ +EFI_STATUS +STATIC +FetchMemoryProtectionHobEntry ( + VOID + ) +{ + EFI_STATUS Status = EFI_INVALID_PARAMETER; + VOID *Ptr1; + + ZeroMem (&mDxeMps, sizeof (mDxeMps)); + + Ptr1 = GetFirstGuidHob (&gDxeMemoryProtectionSettingsGuid); + + if (Ptr1 != NULL) { + if (*((UINT8 *)GET_GUID_HOB_DATA (Ptr1)) != (UINT8)DXE_MEMORY_PROTECTION_SETTINGS_CURRENT_VERSION) { + DEBUG (( + DEBUG_INFO, + "%a: - Version number of the DXE Memory Protection Settings HOB is invalid.\n", + __FUNCTION__ + )); + } else { + Status = EFI_SUCCESS; + CopyMem (&mDxeMps, GET_GUID_HOB_DATA (Ptr1), sizeof (DXE_MEMORY_PROTECTION_SETTINGS)); + } + } + + return Status; +} + +/** + Populates the heap guard protocol global + + @retval EFI_SUCCESS Protocol is already populated or was successfully populated + @retval other Return value of LocateProtocol +**/ +STATIC +EFI_STATUS +PopulateMemoryProtectionDebugProtocol ( + VOID + ) +{ + if (mMemoryProtectionProtocol != NULL) { + return EFI_SUCCESS; + } + + return gBS->LocateProtocol (&gMemoryProtectionDebugProtocolGuid, NULL, (VOID **)&mMemoryProtectionProtocol); +} + +/** + Populates the memory attribute protocol + + @retval EFI_SUCCESS Protocol is already populated or was successfully populated + @retval other Return value of LocateProtocol +**/ +STATIC +EFI_STATUS +PopulateMemoryAttributeProtocol ( + VOID + ) +{ + if (mMemoryAttributeProtocol != NULL) { + return EFI_SUCCESS; + } + + return gBS->LocateProtocol (&gEfiMemoryAttributeProtocolGuid, NULL, (VOID **)&mMemoryAttributeProtocol); +} + +/** + Populates the CPU MP debug protocol global + + @retval EFI_SUCCESS Protocol is already populated or was successfully populated + @retval other Return value of LocateProtocol +**/ +STATIC +EFI_STATUS +PopulateCpuMpDebugProtocol ( + VOID + ) +{ + if (mCpuMpDebugProtocol != NULL) { + return EFI_SUCCESS; + } + + return gBS->LocateProtocol (&gCpuMpDebugProtocolGuid, NULL, (VOID **)&mCpuMpDebugProtocol); +} + +/** + This helper function returns EFI_SUCCESS if the Nonstop protocol is installed. + + @retval EFI_SUCCESS Nonstop protocol installed + @retval other retval of LocateProtocol() +**/ +STATIC +EFI_STATUS +GetNonstopProtocol ( + VOID + ) +{ + if (mNonstopModeProtocol != NULL) { + return EFI_SUCCESS; + } + + return gBS->LocateProtocol (&gMemoryProtectionNonstopModeProtocolGuid, NULL, (VOID **)&mNonstopModeProtocol); +} + +/** + This helper function returns EFI_SUCCESS if the memory protection exception handler is installed. + + @retval EFI_SUCCESS Memory protection exception handler installed + @retval other retval of LocateProtocol() +**/ +STATIC +EFI_STATUS +CheckMemoryProtectionExceptionHandlerInstallation ( + VOID + ) +{ + VOID *DummyProtocol = NULL; + + return gBS->LocateProtocol (&gMemoryProtectionExceptionHandlerGuid, NULL, (VOID **)&DummyProtocol); +} + +/** + This helper function returns TRUE if the MemProtExGetIgnoreNextException() returns TRUE. + + @retval TRUE IgnoreNextException set + @retval FALSE Otherwise +**/ +STATIC +BOOLEAN +GetIgnoreNextEx ( + VOID + ) +{ + BOOLEAN Result = FALSE; + + ExPersistGetIgnoreNextPageFault (&Result); + + return Result; +} + +/** + The recursion loop for testing stack overflow protection. This function will + recurse until it overflows the stack at which point it's expected that a switch + stack is used and an interrupt is generated. + + @param[in] Count The current recursion depth. + + @retval The sum of Count and the return value of the next recursive call. +**/ +STATIC +UINT64 +Recursion ( + IN UINT64 Count + ) +{ + UINT64 Sum = 0; + volatile BOOLEAN AlwaysTrueBool = TRUE; + + DEBUG ((DEBUG_INFO, "%a - %x\n", __FUNCTION__, Count)); + // This code is meant to be an infinite recursion to trip a page fault. Some compilers will catch + // infinite recursions, so to sidestep those warnings, we block the next recursive call behind + // a boolean check. + if (AlwaysTrueBool) { + Sum = Recursion (++Count); + } + + return Sum + Count; +} + +/** + A recursive stack overflow function which at every recursion level checks if the interrupt handler + has signaled that it ran and cleared the faulting region at which point we unwind the recursion. + + @param[in] Count The current recursion depth. + + @retval The sum of Count and the return value of the next recursive call. +**/ +STATIC +UINT64 +RecursionDynamic ( + IN UINT64 Count + ) +{ + UINT64 Sum = 0; + + DEBUG ((DEBUG_ERROR, "%a - 0x%x\n", __FUNCTION__, Count)); + + if (GetIgnoreNextEx ()) { + Sum = RecursionDynamic (++Count); + } + + return Sum + Count; +} + +/** + Tests the pool guards by allocating a pool and then writing to the guard page. If the testing + method is via reset, this function is expected to fault and reset the system. If the testing + method is via exception clearing, it's expected that this function will return and the value + of ExPersistGetIgnoreNextPageFault() will be FALSE. + + @param[in] ptr The pointer to the pool. + @param[in] AllocationSize The size of the pool. +**/ +VOID +PoolTest ( + IN UINT64 *ptr, + IN UINT64 AllocationSize + ) +{ + UINT64 *ptrLoc; + + DEBUG ((DEBUG_INFO, "%a - Allocated pool at 0x%p\n", __FUNCTION__, ptr)); + + // Check if guard page is going to be at the head or tail + if (mDxeMps.HeapGuardPolicy.Fields.Direction == HEAP_GUARD_ALIGNED_TO_TAIL) { + // Get to the beginning of the page containing the pool + ptrLoc = (UINT64 *)(((UINTN)ptr) + (UINTN)AllocationSize); + ptrLoc = ALIGN_POINTER (ptrLoc, 0x1000); + + // The guard page will be on the next page + ptr = (UINT64 *)(((UINTN)ptr) + 0x1000); + } else { + // Get to the beginning of the page containing the pool + ptrLoc = ALIGN_POINTER (ptr, 0x1000); + + // The guard page will be immediately preceding the page the pool starts on. + ptrLoc = (UINT64 *)(((UINTN)ptrLoc) - 0x1); + } + + DEBUG ((DEBUG_ERROR, "%a - Writing to 0x%p\n", __FUNCTION__, ptrLoc)); + *ptrLoc = 1; +} + +/** + Test the head guard page of the input pointer by writing to the page immediately preceding it. The input + pointer is expected to be page aligned so subtracting 1 from will get the page immediately preceding it. + + @param[in] ptr The pointer to the page expected to have a guard page immediately preceding it. +**/ +VOID +HeadPageTest ( + IN UINT64 *ptr + ) +{ + // Write to the head guard page + ptr = (UINT64 *)(((UINTN)ptr) - 0x1); + DEBUG ((DEBUG_ERROR, "%a - Writing to 0x%p\n", __FUNCTION__, ptr)); + *ptr = 1; +} + +/** + Test the tail guard page of the input pointer by writing to the page immediately following it. The input + pointer can be anywhere within the page with a guard page immediately following it. + + @param[in] ptr The pointer to the page expected to have a guard page immediately following it. +**/ +VOID +TailPageTest ( + IN UINT64 *ptr + ) +{ + // Write to the tail guard page + ptr = (UINT64 *)(((UINTN)ptr) + 0x1000); + DEBUG ((DEBUG_ERROR, "%a - Writing to 0x%p\n", __FUNCTION__, ptr)); + *ptr = 1; +} + +/** + This dummy function definition is used to test no-execute protection on allocated buffers + and the stack. +**/ +typedef +VOID +(*DUMMY_VOID_FUNCTION_FOR_DATA_TEST)( + VOID + ); + +/** + This function serves as a placeholder in the driver code region. +**/ +STATIC +VOID +DummyFunctionForCodeSelfTest ( + VOID + ) +{ + volatile UINT8 DontCompileMeOut = 0; + + DontCompileMeOut++; + return; +} + +/// ================================================================================================ +/// ================================================================================================ +/// +/// PRE REQ FUNCTIONS +/// +/// ================================================================================================ +/// ================================================================================================ + +/** + Checks if any NX protection policy is active. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED NX protection is active. + @retval UNIT_TEST_SKIPPED NX protection is not active. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiHardwareNxProtectionEnabledPreReq ( + IN UNIT_TEST_CONTEXT Context + ) +{ + if (mDxeMps.NxProtectionPolicy.Data) { + return UNIT_TEST_PASSED; + } + + return UNIT_TEST_SKIPPED; +} + +/** + Image protection testing currently requires the Memory Attribute Protocol and the Memory Protection + Debug Protocol to be present. The Memory Protection Debug protocol allows us to retrieve a list of + currently protected images and the Memory Attribute Protocol allows us to check the memory attributes. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED The pre-reqs for image protection testing are met. + @retval UNIT_TEST_SKIPPED The pre-reqs for image protection testing are not met. +**/ +UNIT_TEST_STATUS +EFIAPI +ImageProtectionPreReq ( + IN UNIT_TEST_CONTEXT Context + ) +{ + if (((mDxeMps.ImageProtectionPolicy.Fields.ProtectImageFromFv != 0) || (mDxeMps.ImageProtectionPolicy.Fields.ProtectImageFromUnknown != 0)) && + (!EFI_ERROR (PopulateMemoryAttributeProtocol ())) && + (!EFI_ERROR (PopulateMemoryProtectionDebugProtocol ()))) + { + return UNIT_TEST_PASSED; + } + + return UNIT_TEST_SKIPPED; +} + +/** + This function checks if the NX protection policy is active for the target memory type within Context. Testing + NX protection currently requires that buffers of the relevant memory type can be allocated. If the memory type + is Conventional or Persistent, the test will be skipped as we cannot allocate those types. + + The function also checks if hardware enforced NX protection is active. If it is not, the test will be skipped. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED The pre-reqs for image protection testing are met. + @retval UNIT_TEST_SKIPPED The pre-reqs for image protection testing are not met. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiNxProtectionPreReq ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + + UT_ASSERT_TRUE (MemoryProtectionContext.TargetMemoryType < EfiMaxMemoryType); + if (!GetDxeMemoryTypeSettingFromBitfield ((EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, mDxeMps.NxProtectionPolicy)) { + UT_LOG_WARNING ("Protection for this memory type is disabled: %a\n", MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType]); + return UNIT_TEST_SKIPPED; + } + + // Skip memory types which cannot be allocated + if ((MemoryProtectionContext.TargetMemoryType == EfiConventionalMemory) || + (MemoryProtectionContext.TargetMemoryType == EfiPersistentMemory) || + (MemoryProtectionContext.TargetMemoryType == EfiUnacceptedMemoryType)) + { + UT_LOG_WARNING ("Skipping test of memory type %a -- memory type cannot be allocated\n", MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType]); + return UNIT_TEST_SKIPPED; + } + + // Ensure no-execute protection is possible + if (UefiHardwareNxProtectionEnabled (Context) != UNIT_TEST_PASSED) { + UT_LOG_WARNING ("HardwareNxProtection bit not on. NX Test would not be accurate.\n"); + return UNIT_TEST_SKIPPED; + } + + return UNIT_TEST_PASSED; +} + +/** + This function checks if the page protection policy is active for the target memory type within Context. Testing + page guards currently requires that buffers of the relevant memory type can be allocated. If the memory type + is Conventional or Persistent, the test will be skipped as we cannot allocate those types. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED The pre-reqs for page guard testing are met. + @retval UNIT_TEST_SKIPPED The pre-reqs for page guard testing are not met. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiPageGuardPreReq ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + + UT_ASSERT_TRUE (MemoryProtectionContext.TargetMemoryType < EfiMaxMemoryType); + if (!(mDxeMps.HeapGuardPolicy.Fields.UefiPageGuard && + GetDxeMemoryTypeSettingFromBitfield ((EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, mDxeMps.HeapGuardPageType))) + { + UT_LOG_WARNING ("Protection for this memory type is disabled: %a\n", MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType]); + return UNIT_TEST_SKIPPED; + } + + // Skip memory types which cannot be allocated + if ((MemoryProtectionContext.TargetMemoryType == EfiConventionalMemory) || + (MemoryProtectionContext.TargetMemoryType == EfiPersistentMemory) || + (MemoryProtectionContext.TargetMemoryType == EfiUnacceptedMemoryType)) + { + UT_LOG_WARNING ("Skipping test of memory type %a -- memory type cannot be allocated\n", MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType]); + return UNIT_TEST_SKIPPED; + } + + return UNIT_TEST_PASSED; +} + +/** + This function checks if the pool guard policy is active for the target memory type within Context. Testing + pool guards currently requires that buffers of the relevant memory type can be allocated. If the memory type + is Conventional or Persistent, the test will be skipped as we cannot allocate those types. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED The pre-reqs for pool guard testing are met. + @retval UNIT_TEST_SKIPPED The pre-reqs for pool guard testing are not met. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiPoolGuardPreReq ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + + UT_ASSERT_TRUE (MemoryProtectionContext.TargetMemoryType < EfiMaxMemoryType); + if (!(mDxeMps.HeapGuardPolicy.Fields.UefiPoolGuard && + GetDxeMemoryTypeSettingFromBitfield ((EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, mDxeMps.HeapGuardPoolType))) + { + UT_LOG_WARNING ("Protection for this memory type is disabled: %a\n", MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType]); + return UNIT_TEST_SKIPPED; + } + + // Skip memory types which cannot be allocated + if ((MemoryProtectionContext.TargetMemoryType == EfiConventionalMemory) || + (MemoryProtectionContext.TargetMemoryType == EfiPersistentMemory) || + (MemoryProtectionContext.TargetMemoryType == EfiUnacceptedMemoryType)) + { + UT_LOG_WARNING ("Skipping test of memory type %a -- memory type cannot be allocated\n", MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType]); + return UNIT_TEST_SKIPPED; + } + + return UNIT_TEST_PASSED; +} + +/** + This function checks if the stack guard policy is active. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED The pre-reqs for stack guard testing are met. + @retval UNIT_TEST_SKIPPED The pre-reqs for stack guard testing are not met. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiStackGuardPreReq ( + IN UNIT_TEST_CONTEXT Context + ) +{ + if (!mDxeMps.CpuStackGuard) { + UT_LOG_WARNING ("This feature is disabled\n"); + return UNIT_TEST_SKIPPED; + } + + return UNIT_TEST_PASSED; +} + +/** + This function checks if the NULL pointer detection policy is active. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED The pre-reqs for NULL pointer detection testing are met. + @retval UNIT_TEST_SKIPPED The pre-reqs for NULL pointer detection testing are not met. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiNullPointerPreReq ( + IN UNIT_TEST_CONTEXT Context + ) +{ + if (!mDxeMps.NullPointerDetectionPolicy.Fields.UefiNullDetection) { + UT_LOG_WARNING ("This feature is disabled\n"); + return UNIT_TEST_SKIPPED; + } + + return UNIT_TEST_PASSED; +} + +/// ================================================================================================ +/// ================================================================================================ +/// +/// TEST CASES +/// +/// ================================================================================================ +/// ================================================================================================ + +/** + This test case checks that page guards are present for the target memory type within Context. + + The test can be run in 3 ways: + 1. Using the Memory Attribute Protocol: This version of the test will allocate a page of the target + memory type and check that the page preceding and succeeding the allocated page have the EFI_MEMORY_RP + attribute active and fail otherwise. + 2. By intentionally causing and clearing faults: This version of the test will allocate a page of the + target memory type and write to the page preceding and succeeding the allocated page. Before writing, + the test will set the IgnoreNextPageFault flag using ExceptionPersistenceLib with the expectation that + the interrupt handler will clear the intentional fault, unset the flag, and return. If the flag is still + set after the write, the test will fail. Note that if the handler does not handle the flag and fault, the + interrupt handler will deadloop or reset. This test will simply hang in the case of a deadloop and will + fail upon returning to the test case if the system resets. + 3. By intentionally causing faults and resetting the system: This case is similar to the previous case + except that the system will be reset after the intentional fault is triggered. Prior to causing a fault, + the test will update a counter and save the framework state so when the test resumes after reset it can + move on to the next phase of testing instead of repeating the same test. + + Future Work: + 1. Use the test context to ensure that if the testing method is MemoryProtectionTestClearFaults and the + system still resets that the test will not be attempted again. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED The test completed successfully. + @retval UNIT_TEST_ERROR_TEST_FAILED A condition outlined in the test description was not met. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiPageGuard ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + EFI_PHYSICAL_ADDRESS ptr = (EFI_PHYSICAL_ADDRESS)(UINTN)NULL; + UINT64 Attributes; + + DEBUG ((DEBUG_INFO, "%a - Testing Type: %a\n", __FUNCTION__, MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType])); + + // Test using the Memory Attribute Protocol + if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestMemoryAttributeProtocol) { + UT_ASSERT_NOT_NULL (mMemoryAttributeProtocol); + + // Allocate a page of the target memory type + gBS->AllocatePages ( + AllocateAnyPages, + (EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, + 1, + (EFI_PHYSICAL_ADDRESS *)&ptr + ); + UT_ASSERT_NOT_EQUAL (ptr, (EFI_PHYSICAL_ADDRESS)(UINTN)NULL); + + Attributes = 0; + // Check that the page preceding the allocated page has the EFI_MEMORY_RP attribute set + UT_ASSERT_NOT_EFI_ERROR ( + mMemoryAttributeProtocol->GetMemoryAttributes ( + mMemoryAttributeProtocol, + ALIGN_ADDRESS (ptr) - EFI_PAGE_SIZE, + EFI_PAGE_SIZE, + &Attributes + ) + ); + UT_ASSERT_NOT_EQUAL (Attributes & EFI_MEMORY_RP, 0); + Attributes = 0; + // Check that the page succeeding the allocated page has the EFI_MEMORY_RP attribute set + UT_ASSERT_NOT_EFI_ERROR ( + mMemoryAttributeProtocol->GetMemoryAttributes ( + mMemoryAttributeProtocol, + ALIGN_ADDRESS (ptr) + EFI_PAGE_SIZE, + EFI_PAGE_SIZE, + &Attributes + ) + ); + UT_ASSERT_NOT_EQUAL (Attributes & EFI_MEMORY_RP, 0); + + // Test by intentionally causing and clearing faults + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestClearFaults) { + UT_ASSERT_NOT_NULL (mNonstopModeProtocol); + // Allocate a page of the target memory type + gBS->AllocatePages ( + AllocateAnyPages, + (EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, + 1, + (EFI_PHYSICAL_ADDRESS *)&ptr + ); + UT_ASSERT_NOT_EQUAL (ptr, (EFI_PHYSICAL_ADDRESS)(UINTN)NULL); + + // Set the IgnoreNextPageFault flag + UT_ASSERT_NOT_EFI_ERROR (ExPersistSetIgnoreNextPageFault ()); + + // Write to the head guard page + HeadPageTest ((UINT64 *)(UINTN)ptr); + + // Check that the IgnoreNextPageFault flag was cleared + if (GetIgnoreNextEx ()) { + UT_LOG_ERROR ("Head guard page failed: %p\n", ptr); + UT_ASSERT_FALSE (GetIgnoreNextEx ()); + } + + // Reset the page attributes of the faulted page(s) to their original attributes. + UT_ASSERT_NOT_EFI_ERROR (mNonstopModeProtocol->ResetPageAttributes ()); + UT_ASSERT_NOT_EFI_ERROR (ExPersistSetIgnoreNextPageFault ()); + + // Write to the tail guard page + TailPageTest ((UINT64 *)(UINTN)ptr); + + // Check that the IgnoreNextPageFault flag was cleared + if (GetIgnoreNextEx ()) { + UT_LOG_ERROR ("Tail guard page failed: %p\n", ptr); + UT_ASSERT_FALSE (GetIgnoreNextEx ()); + } + + // Reset the page attributes of the faulted page(s) to their original attributes. + UT_ASSERT_NOT_EFI_ERROR (mNonstopModeProtocol->ResetPageAttributes ()); + + FreePages ((VOID *)(UINTN)ptr, 1); + + // Test by intentionally causing faults and resetting the system + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestReset) { + if (MemoryProtectionContext.TestProgress < 2) { + // + // Context.TestProgress indicates progress within this specific test. + // 0 - Just started. + // 1 - Completed head guard test. + // 2 - Completed tail guard test. + // + // Indicate the test is in progress and save state. + // + MemoryProtectionContext.TestProgress++; + SetBootNextDevice (); + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + + // Allocate a page of the target memory type + gBS->AllocatePages ( + AllocateAnyPages, + (EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, + 1, + (EFI_PHYSICAL_ADDRESS *)&ptr + ); + UT_ASSERT_NOT_EQUAL (ptr, (EFI_PHYSICAL_ADDRESS)(UINTN)NULL); + + // If TestProgress == 1, we are testing the head guard + if (MemoryProtectionContext.TestProgress == 1) { + DEBUG ((DEBUG_ERROR, "%a - Allocated page at 0x%p\n", __FUNCTION__, ptr)); + + // Write to the head guard page + HeadPageTest ((UINT64 *)(UINTN)ptr); + + // Anything executing past this point indicates a failure. + UT_LOG_ERROR ("Head guard page failed: %p\n", ptr); + // If TestProgress == 2, we are testing the tail guard + } else { + DEBUG ((DEBUG_ERROR, "%a - Allocated page at 0x%p\n", __FUNCTION__, ptr)); + + // Write to the tail guard page + TailPageTest ((UINT64 *)(UINTN)ptr); + + // Anything executing past this point indicates a failure. + UT_LOG_ERROR ("Tail guard page failed: %p\n", ptr); + } + + // Reset test progress so failure gets recorded. + MemoryProtectionContext.TestProgress = 0; + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + } + + // TestProgress == 2 indicates we successfully tested the head and tail guard pages. + UT_ASSERT_TRUE (MemoryProtectionContext.TestProgress == 2); + } else { + UT_LOG_ERROR ("Invalid testing method specified: %d\n", MemoryProtectionContext.TestingMethod); + return UNIT_TEST_ERROR_TEST_FAILED; + } + + return UNIT_TEST_PASSED; +} + +/** + This test case checks that pool guards are present for the target memory type within Context. This + test does not currently check that the allocated pool is properly aligned with the head or tail guard + page. + + The test can be run in 3 ways: + 1. Using the Memory Attribute Protocol: This version of the test will allocate a pools of various sizes + of the target memory type and check that the page preceding OR succeeding the page containing the + allocated pool have the EFI_MEMORY_RP attribute active and fail otherwise. Only the head or tail + page will be tested depending on the heap guard direction. + 2. By intentionally causing and clearing faults: This version of the test will allocate a pool of the + target memory type and write to the page preceding OR succeeding the page containing the allocated + pool. Before writing, the test will set the IgnoreNextPageFault flag using ExceptionPersistenceLib + with the expectation that the interrupt handler will clear the intentional fault, unset the flag, + and return. If the flag is still set after the write, the test will fail. Note that if the handler + does not handle the flag and fault, the interrupt handler will deadloop or reset. This test will + simply hang in the case of a deadloop and will fail upon returning to the test case if the system resets. + 3. By intentionally causing faults and resetting the system: This case is similar to the previous case + except that the system will be reset after the intentional fault is triggered. Prior to causing a fault, + the test will update a counter and save the framework state so when the test resumes after reset it can + move on to the next phase of testing instead of repeating the same test. + + Future Work: + 1. Check that the allocated pool is properly aligned with the head or tail guard page. + 2. Use the test context to ensure that if the testing method is MemoryProtectionTestClearFaults and the + system still resets that the test will not be attempted again. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED The test completed successfully. + @retval UNIT_TEST_ERROR_TEST_FAILED A condition outlined in the test description was not met. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiPoolGuard ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + UINT64 *ptr = NULL; + UINTN AllocationSize; + UINT8 Index = 0; + UINT64 Attributes; + EFI_PHYSICAL_ADDRESS PoolGuard; + + DEBUG ((DEBUG_INFO, "%a - Testing Type: %a\n", __FUNCTION__, MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType])); + + // Test using the Memory Attribute Protocol. + if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestMemoryAttributeProtocol) { + UT_ASSERT_NOT_NULL (mMemoryAttributeProtocol); + + // Test each pool size in the pool size table. + for (Index = 0; Index < ARRAY_SIZE (mPoolSizeTable); Index++) { + AllocationSize = mPoolSizeTable[Index]; + + // Allocate a pool of the target memory type. + gBS->AllocatePool ( + (EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, + AllocationSize, + (VOID **)&ptr + ); + UT_ASSERT_NOT_NULL (ptr); + + Attributes = 0; + // Check the head or tail guard page depending on the heap guard direction. + if (mDxeMps.HeapGuardPolicy.Fields.Direction == HEAP_GUARD_ALIGNED_TO_TAIL) { + PoolGuard = ALIGN_ADDRESS (((UINTN)ptr) + AllocationSize + (EFI_PAGE_SIZE - 1)); + } else { + PoolGuard = ALIGN_ADDRESS (((UINTN)ptr) - (EFI_PAGE_SIZE - 1)); + } + + // Check that the guard page is active. + UT_ASSERT_NOT_EFI_ERROR ( + mMemoryAttributeProtocol->GetMemoryAttributes ( + mMemoryAttributeProtocol, + PoolGuard, + EFI_PAGE_SIZE, + &Attributes + ) + ); + + // Check that the guard page has the EFI_MEMORY_RP attribute set. + UT_ASSERT_NOT_EQUAL (Attributes & EFI_MEMORY_RP, 0); + } + + // Test by intentionally causing and clearing faults + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestClearFaults) { + UT_ASSERT_NOT_NULL (mNonstopModeProtocol); + + // Test each pool size in the pool size table. + for (Index = 0; Index < ARRAY_SIZE (mPoolSizeTable); Index++) { + // Set the IgnoreNextPageFault flag. + UT_ASSERT_NOT_EFI_ERROR (ExPersistSetIgnoreNextPageFault ()); + + AllocationSize = mPoolSizeTable[Index]; + + gBS->AllocatePool ( + (EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, + AllocationSize, + (VOID **)&ptr + ); + UT_ASSERT_NOT_NULL (ptr); + + // Check the head OR tail guard page depending on the heap guard direction. + PoolTest ((UINT64 *)ptr, AllocationSize); + + // Check that the IgnoreNextPageFault flag was cleared. + UT_ASSERT_FALSE (GetIgnoreNextEx ()); + // Reset the attributes of the faulting page(s) to their original attributes. + UT_ASSERT_NOT_EFI_ERROR (mNonstopModeProtocol->ResetPageAttributes ()); + } + + // Test by intentionally causing faults and resetting the system. + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestReset) { + // If TestProgress == ARRAY_SIZE (mPoolSizeTable), we have completed all tests. + if (MemoryProtectionContext.TestProgress < ARRAY_SIZE (mPoolSizeTable)) { + // + // Context.TestProgress indicates progress within this specific test. + // The test progressively allocates larger areas to test the guard on. + // These areas are defined in Pool.c as the 13 different sized chunks that are available + // for pool allocation. + // + // Indicate the test is in progress and save state. + // + AllocationSize = mPoolSizeTable[MemoryProtectionContext.TestProgress]; + MemoryProtectionContext.TestProgress++; + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + SetBootNextDevice (); + + // Allocate a pool of the target memory type. + gBS->AllocatePool ( + (EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, + AllocationSize, + (VOID **)&ptr + ); + UT_ASSERT_NOT_NULL (ptr); + + // Write to the pool guard (should cause a fault and reset the system) + PoolTest ((UINT64 *)ptr, AllocationSize); + + // If we reach this point, the fault did not occur and the test has failed. + // Reset test progress so failure gets recorded. + MemoryProtectionContext.TestProgress = 0; + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + UT_LOG_ERROR ("Pool guard failed: %p\n", ptr); + } + + UT_ASSERT_TRUE (MemoryProtectionContext.TestProgress == ARRAY_SIZE (mPoolSizeTable)); + } else { + UT_LOG_ERROR ("Invalid testing method specified: %d\n", MemoryProtectionContext.TestingMethod); + return UNIT_TEST_ERROR_TEST_FAILED; + } + + return UNIT_TEST_PASSED; +} + +/** + Test the stack guard. + + The test can be run in 3 ways: + 1. Using the Memory Attribute Protocol: This version of the test will fetch the HOB list and attempt to + find the stack information identified by gEfiHobMemoryAllocStackGuid. If the HOB is found, the test + will use the Memory Attribute Protocol to check that the page containing the stack base has the + EFI_MEMORY_RP attribute. If the Memory Protection Debug Protocol is also available, the test will + also check the multi-processor stacks using the information collected by the protocol. + 2. By intentionally causing and clearing a fault: This version will test stack guard by overflowing the + stack with an infinite loop. Each loop iteration causes another stack frame to be pushed onto the + stack. Eventually, the stack guard page should be hit and a hardware mechanism will switch to a safe + stack to execute the interrupt handler. Prior to overflowing the stack, the IgnoreNextPageFault flag + will be set using ExceptionPersistenceLib with the expectation that the interrupt handler will clear the + fault, unset the flag, and return. If the stack guard is not properly configured or the handler does not + unset the flag, the test will infinitely recurse and exhibit unknown behavior dependent on the system + configuration. + 3. By intentionally causing a fault and resetting the system: This case is similar to the previous case + except that the system will be reset after the intentional fault is triggered. Prior to causing a fault, + the test will update a counter and save the framework state so when the test resumes after reset it can + move on to the next phase of testing instead of repeating the same test. This version of the test will + also exhibit unknown behavior if the stack guard is not properly configured. + + + Future Work: + 1. Add support for testing the AP stacks without the Memory Attribute Protocol by switching the BSP stack + using MP services and overflowing it. + 2. Use the test context to ensure that if the testing method is MemoryProtectionTestClearFaults and the + system still resets that the test will not be attempted again. + + @param[in] Context The test context. + + @retval UNIT_TEST_PASSED The test was executed successfully. + @retval UNIT_TEST_ERROR_TEST_FAILED A condition outlined in the test description was not met. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiCpuStackGuard ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + EFI_PHYSICAL_ADDRESS StackBase; + EFI_PEI_HOB_POINTERS Hob; + EFI_HOB_MEMORY_ALLOCATION *MemoryHob; + UINT64 Attributes; + LIST_ENTRY *List; + CPU_MP_DEBUG_PROTOCOL *Entry; + + DEBUG ((DEBUG_INFO, "%a - Testing CPU Stack Guard\n", __FUNCTION__)); + + // Test using the Memory Attribute Protocol. + if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestMemoryAttributeProtocol) { + UT_ASSERT_NOT_NULL (mMemoryAttributeProtocol); + + StackBase = 0; + Hob.Raw = GetHobList (); + // Find the BSP stack info from the HOB list. + while ((Hob.Raw = GetNextHob (EFI_HOB_TYPE_MEMORY_ALLOCATION, Hob.Raw)) != NULL) { + MemoryHob = Hob.MemoryAllocation; + if (CompareGuid (&gEfiHobMemoryAllocStackGuid, &MemoryHob->AllocDescriptor.Name)) { + StackBase = MemoryHob->AllocDescriptor.MemoryBaseAddress; + UT_ASSERT_EQUAL (StackBase & EFI_PAGE_MASK, 0); + break; + } + + Hob.Raw = GET_NEXT_HOB (Hob); + } + + // If StackBase == 0, we did not find the stack HOB. + UT_ASSERT_NOT_EQUAL (StackBase, 0); + Attributes = 0; + + // Check that the stack base has the EFI_MEMORY_RP attribute. + UT_ASSERT_NOT_EFI_ERROR ( + mMemoryAttributeProtocol->GetMemoryAttributes ( + mMemoryAttributeProtocol, + StackBase, + EFI_PAGE_SIZE, + &Attributes + ) + ); + UT_ASSERT_NOT_EQUAL (Attributes & EFI_MEMORY_RP, 0); + + // If the Multi-Processor Debug Protocol is available, check the AP stacks. + if (!EFI_ERROR (PopulateCpuMpDebugProtocol ())) { + for (List = mCpuMpDebugProtocol->Link.ForwardLink; List != &mCpuMpDebugProtocol->Link; List = List->ForwardLink) { + Entry = CR ( + List, + CPU_MP_DEBUG_PROTOCOL, + Link, + CPU_MP_DEBUG_SIGNATURE + ); + + // Skip the switch stack (the stack used when a stack overflow occurs). + if (Entry->IsSwitchStack) { + continue; + } + + StackBase = ALIGN_ADDRESS (Entry->ApStackBuffer); + Attributes = 0; + + // Check that the stack base has the EFI_MEMORY_RP attribute. + UT_ASSERT_NOT_EFI_ERROR ( + mMemoryAttributeProtocol->GetMemoryAttributes ( + mMemoryAttributeProtocol, + StackBase, + EFI_PAGE_SIZE, + &Attributes + ) + ); + UT_ASSERT_NOT_EQUAL (Attributes & EFI_MEMORY_RP, 0); + } + } + + // Test by intentionally causing and clearing faults. + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestClearFaults) { + UT_ASSERT_NOT_NULL (mNonstopModeProtocol); + // Set the IgnoreNextPageFault flag. + UT_ASSERT_NOT_EFI_ERROR (ExPersistSetIgnoreNextPageFault ()); + + // Overflow the stack, checking at each level of recursion if the IgnoreNextPageFault + // flag is still set. + RecursionDynamic (1); + + // If the IgnoreNextPageFault flag is still set, the test failed. It's unlikely that + // we'd reach this point in the test if the flag is still set as it implies that the + // interrupt handler did not clear the stack overflow. + UT_ASSERT_FALSE (GetIgnoreNextEx ()); + + // Reset the page attributes to their original attributes. + UT_ASSERT_NOT_EFI_ERROR (mNonstopModeProtocol->ResetPageAttributes ()); + + // Test by intentionally causing a fault and resetting the system. + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestReset) { + if (MemoryProtectionContext.TestProgress < 1) { + // + // Context.TestProgress 0 indicates the test hasn't started yet. + // + // Indicate the test is in progress and save state. + // + MemoryProtectionContext.TestProgress++; + SetBootNextDevice (); + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + + // Overflow the stack. + Recursion (1); + + // If we reach this point, the stack overflow did not cause a system reset and the test + // has failed. Note that it's unlikely that we'd reach this point in the test if the + // stack overflow did not cause a system reset. + MemoryProtectionContext.TestProgress = 0; + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + UT_LOG_ERROR ("System was expected to reboot but didn't.\n"); + } + + UT_ASSERT_TRUE (MemoryProtectionContext.TestProgress == 1); + } else { + UT_LOG_ERROR ("Invalid testing method specified: %d\n", MemoryProtectionContext.TestingMethod); + return UNIT_TEST_ERROR_TEST_FAILED; + } + + return UNIT_TEST_PASSED; +} + +volatile UNIT_TEST_FRAMEWORK *mFw = NULL; + +/** + Test NULL pointer detection. + + The test can be run in 3 ways: + 1. Using the Memory Attribute Protocol: This version of the test will use the Memory Attribute Protocol + to verify that the NULL page has the EFI_MEMORY_RP attribute. + 2. By intentionally causing and clearing a fault: This version will intentionally cause a fault by + dereferencing a NULL pointer (both with a read and a write). Prior to dereferencing, the IgnoreNextPageFault flag + will be set using ExceptionPersistenceLib with the expectation that the interrupt handler will clear the + fault, unset the flag, and return. If the flag is still set after dereferencing NULL, the handler either + was not invoked (implying NULL protection is not active) or the handler did not properly handle the flag. + Both of these cases result in a test failure. + 3. By intentionally causing a fault and resetting the system: This case is similar to the previous case + except that the system will be reset after the intentional fault is triggered. Prior to causing a fault, + the test will update a counter and save the framework state so when the test resumes after reset it can + move on to the next phase of testing instead of repeating the same test. If a reset does not occur, the + test will fail. + + Future Work: + 1. Use the test context to ensure that if the testing method is MemoryProtectionTestClearFaults and the + system still resets that the test will not be attempted again. + + @param[in] Context The test context. + + @retval UNIT_TEST_PASSED The test was executed successfully. + @retval UNIT_TEST_ERROR_TEST_FAILED A condition outlined in the test description was not met. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiNullPointerDetection ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + UINT64 Attributes; + + DEBUG ((DEBUG_INFO, "%a - Testing NULL Pointer Detection\n", __FUNCTION__)); + + // Test using the Memory Attribute Protocol. + if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestMemoryAttributeProtocol) { + UT_ASSERT_NOT_NULL (mMemoryAttributeProtocol); + + // Check that the NULL page has the EFI_MEMORY_RP attribute. + UT_ASSERT_NOT_EFI_ERROR ( + mMemoryAttributeProtocol->GetMemoryAttributes ( + mMemoryAttributeProtocol, + (UINTN)NULL, + EFI_PAGE_SIZE, + &Attributes + ) + ); + UT_ASSERT_NOT_EQUAL (Attributes & EFI_MEMORY_RP, 0); + + // Test by intentionally causing and clearing faults. + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestClearFaults) { + UT_ASSERT_NOT_NULL (mNonstopModeProtocol); + + // Set the IgnoreNextPageFault flag. + UT_ASSERT_NOT_EFI_ERROR (ExPersistSetIgnoreNextPageFault ()); + + // Read from NULL. + if (mFw->Title == NULL) { + DEBUG ((DEBUG_INFO, "NULL pointer read test complete\n")); + } + + // If the IgnoreNextPageFault flag is still set, the read test failed. + if (GetIgnoreNextEx ()) { + UT_LOG_ERROR ("Failed NULL pointer read test.\n"); + UT_ASSERT_FALSE (GetIgnoreNextEx ()); + } + + // Reset the page attributes to their original attributes. + UT_ASSERT_NOT_EFI_ERROR (mNonstopModeProtocol->ResetPageAttributes ()); + + // Set the IgnoreNextPageFault flag. + UT_ASSERT_NOT_EFI_ERROR (ExPersistSetIgnoreNextPageFault ()); + + // Write to NULL. + mFw->Title = "Title"; + + // If the IgnoreNextPageFault flag is still set, the write test failed. + if (GetIgnoreNextEx ()) { + UT_LOG_ERROR ("Failed NULL pointer write test.\n"); + UT_ASSERT_FALSE (GetIgnoreNextEx ()); + } + + // Reset the page attributes to their original attributes. + UT_ASSERT_NOT_EFI_ERROR (mNonstopModeProtocol->ResetPageAttributes ()); + + // Test by intentionally causing a fault and resetting the system. + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestReset) { + if (MemoryProtectionContext.TestProgress < 2) { + // + // Context.TestProgress indicates progress within this specific test. + // 0 - Just started. + // 1 - Completed NULL pointer read test. + // 2 - Completed NULL pointer write test. + // + // Indicate the test is in progress and save state. + // + MemoryProtectionContext.TestProgress++; + SetBootNextDevice (); + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + + if (MemoryProtectionContext.TestProgress == 1) { + if (mFw->Title == NULL) { + DEBUG ((DEBUG_ERROR, "%a - Should have failed \n", __FUNCTION__)); + } + + UT_LOG_ERROR ("Failed NULL pointer read test\n"); + } else { + mFw->Title = "Title"; + UT_LOG_ERROR ("Failed NULL pointer write test.\n"); + } + + // + // At this point, the test has failed. Reset test progress so failure gets recorded. + // + MemoryProtectionContext.TestProgress = 0; + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + } + + UT_ASSERT_TRUE (MemoryProtectionContext.TestProgress == 2); + } else { + UT_LOG_ERROR ("Invalid testing method specified: %d\n", MemoryProtectionContext.TestingMethod); + return UNIT_TEST_ERROR_TEST_FAILED; + } + + return UNIT_TEST_PASSED; +} + +/** + Test stack no-execute protection. + + The test can be run in 3 ways: + 1. Using the Memory Attribute Protocol: This version of the test will use the Memory Attribute Protocol + to verify that the page containing the stack (identified by getting the address of a stack variable) + has the EFI_MEMORY_XP attribute. + 2. By intentionally causing and clearing a fault: This version will intentionally cause a fault by + copying a dummy function into a statically allocated buffer on the stack and executing that function. + Prior to execution, the IgnoreNextPageFault flag will be set using ExceptionPersistenceLib with the + expectation that the interrupt handler will clear the fault, unset the flag, and return. + If the flag is still set after executing from the stack, the handler either was not invoked + (implying stack no-execute protection is not active) or the handler did not properly handle the flag. + Both of these cases result in a test failure. + 3. By intentionally causing a fault and resetting the system: This case is similar to the previous case + except that the system will be reset after the intentional fault is triggered. Prior to causing a fault, + the test will update a counter and save the framework state so when the test resumes after reset it can + move on to the next phase of testing instead of repeating the same test. If a reset does not occur, the + test will fail. + + Future Work: + 1. Use the test context to ensure that if the testing method is MemoryProtectionTestClearFaults and the + system still resets that the test will not be attempted again. + + @param[in] Context The test context. + + @retval UNIT_TEST_PASSED The test was executed successfully. + @retval UNIT_TEST_ERROR_TEST_FAILED A condition outlined in the test description was not met. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiNxStackGuard ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + UINT8 CodeRegionToCopyTo[DUMMY_FUNCTION_FOR_CODE_SELF_TEST_GENERIC_SIZE]; + UINT8 *CodeRegionToCopyFrom = (UINT8 *)DummyFunctionForCodeSelfTest; + UINT64 Attributes; + + DEBUG ((DEBUG_INFO, "%a - NX Stack Guard\n", __FUNCTION__)); + + // Test using the Memory Attribute Protocol. + if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestMemoryAttributeProtocol) { + UT_ASSERT_NOT_NULL (mMemoryAttributeProtocol); + + Attributes = 0; + // Attributes is a stack variable, so get the attributes of the page containing it. + UT_ASSERT_NOT_EFI_ERROR ( + mMemoryAttributeProtocol->GetMemoryAttributes ( + mMemoryAttributeProtocol, + ALIGN_ADDRESS ((UINTN)&Attributes), + EFI_PAGE_SIZE, + &Attributes + ) + ); + + // Verify the page containing Attributes is non-executable. + UT_ASSERT_NOT_EQUAL (Attributes & EFI_MEMORY_XP, 0); + + // Test by intentionally causing and clearing faults. + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestClearFaults) { + UT_ASSERT_NOT_NULL (mNonstopModeProtocol); + + // Set the IgnoreNextPageFault flag. + UT_ASSERT_NOT_EFI_ERROR (ExPersistSetIgnoreNextPageFault ()); + + // Copy the dummy function to a stack variable and execute it. + CopyMem (CodeRegionToCopyTo, CodeRegionToCopyFrom, DUMMY_FUNCTION_FOR_CODE_SELF_TEST_GENERIC_SIZE); + ((DUMMY_VOID_FUNCTION_FOR_DATA_TEST)CodeRegionToCopyTo)(); + + // If the IgnoreNextPageFault flag is still set, the interrupt handler was not invoked or did not handle + // the flag properly. + UT_ASSERT_FALSE (GetIgnoreNextEx ()); + + // Reset the page attributes to their original attributes. + UT_ASSERT_NOT_EFI_ERROR (mNonstopModeProtocol->ResetPageAttributes ()); + + // Test by intentionally causing a fault and resetting the system. + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestReset) { + if (MemoryProtectionContext.TestProgress < 1) { + // + // Context.TestProgress 0 indicates the test hasn't started yet. + // + // Indicate the test is in progress by updating the context and saving state. + // + MemoryProtectionContext.TestProgress++; + SetBootNextDevice (); + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + + // Copy the dummy function to a stack variable and execute it. + CopyMem (CodeRegionToCopyTo, CodeRegionToCopyFrom, DUMMY_FUNCTION_FOR_CODE_SELF_TEST_GENERIC_SIZE); + ((DUMMY_VOID_FUNCTION_FOR_DATA_TEST)CodeRegionToCopyTo)(); + + // If we reach this point, the stack is executable. Log the test failure. + MemoryProtectionContext.TestProgress = 0; + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + UT_LOG_ERROR ("NX Stack Guard Test failed.\n"); + } + + UT_ASSERT_TRUE (MemoryProtectionContext.TestProgress == 1); + } else { + UT_LOG_ERROR ("Invalid testing method specified: %d\n", MemoryProtectionContext.TestingMethod); + return UNIT_TEST_ERROR_TEST_FAILED; + } + + return UNIT_TEST_PASSED; +} + +/** + Test no-execute protection. + + The test can be run in 3 ways: + 1. Using the Memory Attribute Protocol: This version of the test will allocate memory of the type defined + in Context and use the Memory Attribute Protocol to verify that allocated page the EFI_MEMORY_XP attribute. + 2. By intentionally causing and clearing a fault: This version of the test will allocate memory of the + type defined in Context and intentionally cause a fault by copying a dummy function into the allocated + buffer and executing that function. Prior to execution, the IgnoreNextPageFault flag will be set using + ExceptionPersistenceLib with the expectation that the interrupt handler will clear the fault, unset the + flag, and return. If the flag is still set after executing from the stack, the handler either was not + invoked (implying no-execute protection is not active) or the handler did not properly handle the flag. + Both of these cases result in a test failure. + 3. By intentionally causing a fault and resetting the system: This case is similar to the previous case + except that the system will be reset after the intentional fault is triggered. Prior to causing a fault, + the test will update a counter and save the framework state so when the test resumes after reset it can + move on to the next phase of testing instead of repeating the same test. If a reset does not occur, the + test will fail. + + Future Work: + 1. Use the test context to ensure that if the testing method is MemoryProtectionTestClearFaults and the + system still resets that the test will not be attempted again. + + @param[in] Context The test context. + + @retval UNIT_TEST_PASSED The test was executed successfully. + @retval UNIT_TEST_ERROR_TEST_FAILED A condition outlined in the test description was not met. +**/ +UNIT_TEST_STATUS +EFIAPI +UefiNxProtection ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + UINT64 *ptr = NULL; + UINT8 *CodeRegionToCopyFrom = (UINT8 *)DummyFunctionForCodeSelfTest; + UINT64 Attributes; + + DEBUG ((DEBUG_INFO, "%a - Testing Type: %a\n", __FUNCTION__, MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType])); + + // Test using the Memory Attribute Protocol. + if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestMemoryAttributeProtocol) { + UT_ASSERT_NOT_NULL (mMemoryAttributeProtocol); + + // Allocate a page of memory of the type specified in Context. + gBS->AllocatePool ( + (EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, + EFI_PAGE_SIZE, + (VOID **)&ptr + ); + UT_ASSERT_NOT_NULL (ptr); + + Attributes = 0; + // Verify the allocated page is non-executable. + UT_ASSERT_NOT_EFI_ERROR ( + mMemoryAttributeProtocol->GetMemoryAttributes ( + mMemoryAttributeProtocol, + ALIGN_ADDRESS ((UINTN)ptr), + EFI_PAGE_SIZE, + &Attributes + ) + ); + FreePool (ptr); + UT_ASSERT_NOT_EQUAL (Attributes & EFI_MEMORY_XP, 0); + + // Test by intentionally causing and clearing faults. + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestClearFaults) { + UT_ASSERT_NOT_NULL (mNonstopModeProtocol); + + // Set the IgnoreNextPageFault flag. + UT_ASSERT_NOT_EFI_ERROR (ExPersistSetIgnoreNextPageFault ()); + + // Allocate a page of memory of the type specified in Context. + gBS->AllocatePool ( + (EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, + EFI_PAGE_SIZE, + (VOID **)&ptr + ); + UT_ASSERT_NOT_NULL (ptr); + + // Copy the dummy function to the allocated buffer and execute it. + CopyMem (ptr, CodeRegionToCopyFrom, DUMMY_FUNCTION_FOR_CODE_SELF_TEST_GENERIC_SIZE); + ((DUMMY_VOID_FUNCTION_FOR_DATA_TEST)ptr)(); + + FreePool (ptr); + + // Verify the IgnoreNextPageFault flag was cleared. + UT_ASSERT_FALSE (GetIgnoreNextEx ()); + + // Reset the page attributes to their original attributes. + UT_ASSERT_NOT_EFI_ERROR (mNonstopModeProtocol->ResetPageAttributes ()); + + // Test by intentionally causing a fault and resetting the system. + } else if (MemoryProtectionContext.TestingMethod == MemoryProtectionTestReset) { + if (MemoryProtectionContext.TestProgress < 1) { + // + // Context.TestProgress == 0 indicates the test hasn't started yet. + // + // Indicate the test is in progress and save state. + // + MemoryProtectionContext.TestProgress++; + SetBootNextDevice (); + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + + // Allocate a page of memory of the type specified in Context. + gBS->AllocatePool ( + (EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, + EFI_PAGE_SIZE, + (VOID **)&ptr + ); + UT_ASSERT_NOT_NULL (ptr); + + // Copy the dummy function to the allocated buffer and execute it. + CopyMem (ptr, CodeRegionToCopyFrom, DUMMY_FUNCTION_FOR_CODE_SELF_TEST_GENERIC_SIZE); + ((DUMMY_VOID_FUNCTION_FOR_DATA_TEST)ptr)(); + + // If the test reaches this point, the above function invocation did not cause a fault. + // The test has failed. + MemoryProtectionContext.TestProgress = 0; + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + UT_LOG_ERROR ("NX Test failed.\n"); + } + + UT_ASSERT_TRUE (MemoryProtectionContext.TestProgress == 1); + } else { + UT_LOG_ERROR ("Invalid testing method specified: %d\n", MemoryProtectionContext.TestingMethod); + return UNIT_TEST_ERROR_TEST_FAILED; + } + + return UNIT_TEST_PASSED; +} + +/** + Test image protection by using the Memory Protection Debug Protocol to get a list of currently + protected images and using the Memory Attribute Protocol to check that the code sections of the + image have the EFI_MEMORY_RP attribute and the data sections have the EFI_MEMORY_XP attribute. + + @param[in] Context The test context. + + @retval UNIT_TEST_PASSED The test was executed successfully. + @retval UNIT_TEST_ERROR_TEST_FAILED Failed to fetch the image list + @retval UNIT_TEST_ERROR_TEST_FAILED A condition outlined in the test description was not met. +**/ +UNIT_TEST_STATUS +EFIAPI +ImageProtection ( + IN UNIT_TEST_CONTEXT Context + ) +{ + EFI_STATUS Status; + IMAGE_RANGE_DESCRIPTOR *ImageRangeDescriptorHead = NULL; + LIST_ENTRY *ImageRangeDescriptorLink = NULL; + IMAGE_RANGE_DESCRIPTOR *ImageRangeDescriptor = NULL; + BOOLEAN TestFailed = FALSE; + UINT64 Attributes = 0; + + DEBUG ((DEBUG_INFO, "%a() - Enter\n", __FUNCTION__)); + + // Ensure the Memory Protection Protocol and Memory Attribute Protocol are available. + UT_ASSERT_NOT_NULL (mMemoryProtectionProtocol); + UT_ASSERT_NOT_NULL (mMemoryAttributeProtocol); + + // Use the Memory Protection Protocol to get a list of protected images. Each descriptor in the + // output list will be a code or data section of a protected image. + UT_ASSERT_NOT_EFI_ERROR (mMemoryProtectionProtocol->GetImageList (&ImageRangeDescriptorHead, Protected)); + + // Walk through each image + for (ImageRangeDescriptorLink = ImageRangeDescriptorHead->Link.ForwardLink; + ImageRangeDescriptorLink != &ImageRangeDescriptorHead->Link; + ImageRangeDescriptorLink = ImageRangeDescriptorLink->ForwardLink) + { + ImageRangeDescriptor = CR ( + ImageRangeDescriptorLink, + IMAGE_RANGE_DESCRIPTOR, + Link, + IMAGE_RANGE_DESCRIPTOR_SIGNATURE + ); + if (ImageRangeDescriptor != NULL) { + // Get the attributes of the image range. + Status = mMemoryAttributeProtocol->GetMemoryAttributes ( + mMemoryAttributeProtocol, + ImageRangeDescriptor->Base, + ImageRangeDescriptor->Length, + &Attributes + ); + + if (EFI_ERROR (Status)) { + UT_LOG_ERROR ( + "Unable to get attributes of memory range 0x%llx - 0x%llx! Status: %r\n", + ImageRangeDescriptor->Base, + ImageRangeDescriptor->Base + ImageRangeDescriptor->Length, + Status + ); + TestFailed = TRUE; + continue; + } + + // Check that the code sections have the EFI_MEMORY_RO attribute and the data sections have + // the EFI_MEMORY_XP attribute. + if ((ImageRangeDescriptor->Type == Code) && ((Attributes & EFI_MEMORY_RO) == 0)) { + TestFailed = TRUE; + UT_LOG_ERROR ( + "Memory Range 0x%llx - 0x%llx should be non-writeable!\n", + ImageRangeDescriptor->Base, + ImageRangeDescriptor->Base + ImageRangeDescriptor->Length + ); + } else if ((ImageRangeDescriptor->Type == Data) && ((Attributes & EFI_MEMORY_XP) == 0)) { + TestFailed = TRUE; + UT_LOG_ERROR ( + "Memory Range 0x%llx - 0x%llx should be non-executable!\n", + ImageRangeDescriptor->Base, + ImageRangeDescriptor->Base + ImageRangeDescriptor->Length + ); + } + } + } + + // Free the list of image range descriptors. + while (!IsListEmpty (&ImageRangeDescriptorHead->Link)) { + ImageRangeDescriptor = CR ( + ImageRangeDescriptorHead->Link.ForwardLink, + IMAGE_RANGE_DESCRIPTOR, + Link, + IMAGE_RANGE_DESCRIPTOR_SIGNATURE + ); + + RemoveEntryList (&ImageRangeDescriptor->Link); + FreePool (ImageRangeDescriptor); + } + + FreePool (ImageRangeDescriptorHead); + + // If TestFailed is TRUE, the test has failed. + UT_ASSERT_FALSE (TestFailed); + + return UNIT_TEST_PASSED; +} + +/// ================================================================================================ +/// ================================================================================================ +/// +/// TEST ENGINE +/// +/// ================================================================================================ +/// ================================================================================================ + +/** + This function adds a test case for each memory type with no-execute protection enabled. + + @param[in] TestSuite The test suite to add the test cases to. + @param[in] TestingMethod The method to use for testing (Memory Attribute, Clear Faults, etc.) +**/ +VOID +AddUefiNxTest ( + UNIT_TEST_SUITE_HANDLE TestSuite, + MEMORY_PROTECTION_TESTING_METHOD TestingMethod + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT *MemoryProtectionContext = NULL; + UINT8 Index; + CHAR8 NameStub[] = "Security.NxProtection.Uefi"; + CHAR8 DescriptionStub[] = "Execution of a page of the following memory type should fail. Memory type: "; + CHAR8 *TestName = NULL; + CHAR8 *TestDescription = NULL; + UINTN TestNameSize; + UINTN TestDescriptionSize; + + DEBUG ((DEBUG_INFO, "%a() - Enter\n", __FUNCTION__)); + + // Need to generate a test case for each memory type. + for (Index = 0; Index < EfiMaxMemoryType; Index++) { + MemoryProtectionContext = (MEMORY_PROTECTION_TEST_CONTEXT *)AllocateZeroPool (sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + if (MemoryProtectionContext == NULL) { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test context failed.\n", __FUNCTION__)); + return; + } + + // Set the context for this test case. + MemoryProtectionContext->TargetMemoryType = Index; + MemoryProtectionContext->GuardAlignment = mDxeMps.HeapGuardPolicy.Fields.Direction; + MemoryProtectionContext->TestingMethod = TestingMethod; + + // Set the test name and description. + TestNameSize = sizeof (CHAR8) * (1 + AsciiStrnLenS (NameStub, UNIT_TEST_MAX_STRING_LENGTH) + AsciiStrnLenS (MEMORY_TYPES[Index], UNIT_TEST_MAX_STRING_LENGTH)); + TestName = AllocateZeroPool (TestNameSize); + TestDescriptionSize = sizeof (CHAR8) * (1 + AsciiStrnLenS (DescriptionStub, UNIT_TEST_MAX_STRING_LENGTH) + AsciiStrnLenS (MEMORY_TYPES[Index], UNIT_TEST_MAX_STRING_LENGTH)); + TestDescription = (CHAR8 *)AllocateZeroPool (TestDescriptionSize); + + if ((TestName != NULL) && (TestDescription != NULL) && (MemoryProtectionContext != NULL)) { + // Name of the test is Security.PageGuard.Uefi + Memory Type Name (from MEMORY_TYPES) + AsciiStrCatS (TestName, UNIT_TEST_MAX_STRING_LENGTH, NameStub); + AsciiStrCatS (TestName, UNIT_TEST_MAX_STRING_LENGTH, MEMORY_TYPES[Index]); + + // Description of this test is DescriptionStub + Memory Type Name (from MEMORY_TYPES) + AsciiStrCatS (TestDescription, UNIT_TEST_MAX_STRING_LENGTH, DescriptionStub); + AsciiStrCatS (TestDescription, UNIT_TEST_MAX_STRING_LENGTH, MEMORY_TYPES[Index]); + + // Add the test case. This test case will only run if UefiNxProtectionPreReq passes (which checks the protection policy for + // the memory type). + AddTestCase (TestSuite, TestDescription, TestName, UefiNxProtection, UefiNxProtectionPreReq, NULL, MemoryProtectionContext); + + // Free the memory allocated for the test name and description. + FreePool (TestName); + FreePool (TestDescription); + } else { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test creation failed.\n", __FUNCTION__)); + return; + } + } +} + +/** + This function adds a test case for each memory type with pool guards enabled. + + @param[in] TestSuite The test suite to add the test cases to. + @param[in] TestingMethod The method to use for testing (Memory Attribute, Clear Faults, etc.) +**/ +VOID +AddUefiPoolTest ( + UNIT_TEST_SUITE_HANDLE TestSuite, + MEMORY_PROTECTION_TESTING_METHOD TestingMethod + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT *MemoryProtectionContext = NULL; + UINT8 Index; + CHAR8 NameStub[] = "Security.PoolGuard.Uefi"; + CHAR8 DescriptionStub[] = "Accesses before/after the pool should hit a guard page. Memory type: "; + CHAR8 *TestName = NULL; + CHAR8 *TestDescription = NULL; + UINTN TestNameSize; + UINTN TestDescriptionSize; + + DEBUG ((DEBUG_INFO, "%a() - Enter\n", __FUNCTION__)); + + // Need to generate a test case for each memory type. + for (Index = 0; Index < EfiMaxMemoryType; Index++) { + MemoryProtectionContext = (MEMORY_PROTECTION_TEST_CONTEXT *)AllocateZeroPool (sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + if (MemoryProtectionContext == NULL) { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test context failed.\n", __FUNCTION__)); + return; + } + + // Set the context for this test case. + MemoryProtectionContext->TargetMemoryType = Index; + MemoryProtectionContext->GuardAlignment = mDxeMps.HeapGuardPolicy.Fields.Direction; + MemoryProtectionContext->TestingMethod = TestingMethod; + + // Set the test name and description. + TestNameSize = sizeof (CHAR8) * (1 + AsciiStrnLenS (NameStub, UNIT_TEST_MAX_STRING_LENGTH) + AsciiStrnLenS (MEMORY_TYPES[Index], UNIT_TEST_MAX_STRING_LENGTH)); + TestName = (CHAR8 *)AllocateZeroPool (TestNameSize); + TestDescriptionSize = sizeof (CHAR8) * (1 + AsciiStrnLenS (DescriptionStub, UNIT_TEST_MAX_STRING_LENGTH) + AsciiStrnLenS (MEMORY_TYPES[Index], UNIT_TEST_MAX_STRING_LENGTH)); + TestDescription = (CHAR8 *)AllocateZeroPool (TestDescriptionSize); + + if ((TestName != NULL) && (TestDescription != NULL) && (MemoryProtectionContext != NULL)) { + // Name of the test is Security.PoolGuard.Uefi + Memory Type Name (from MEMORY_TYPES) + AsciiStrCatS (TestName, UNIT_TEST_MAX_STRING_LENGTH, NameStub); + AsciiStrCatS (TestName, UNIT_TEST_MAX_STRING_LENGTH, MEMORY_TYPES[Index]); + + // Description of this test is DescriptionStub + Memory Type Name (from MEMORY_TYPES) + AsciiStrCatS (TestDescription, UNIT_TEST_MAX_STRING_LENGTH, DescriptionStub); + AsciiStrCatS (TestDescription, UNIT_TEST_MAX_STRING_LENGTH, MEMORY_TYPES[Index]); + + // Add the test case. This test case will only run if UefiPoolGuardPreReq passes (which checks the protection policy for + // the memory type). + AddTestCase (TestSuite, TestDescription, TestName, UefiPoolGuard, UefiPoolGuardPreReq, NULL, MemoryProtectionContext); + + FreePool (TestName); + FreePool (TestDescription); + } else { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test creation failed.\n", __FUNCTION__)); + return; + } + } +} + +/** + This function adds a test case for each memory type with page guards enabled. + + @param[in] TestSuite The test suite to add the test cases to. + @param[in] TestingMethod The method to use for testing (Memory Attribute, Clear Faults, etc.) +**/ +VOID +AddUefiPageTest ( + UNIT_TEST_SUITE_HANDLE TestSuite, + MEMORY_PROTECTION_TESTING_METHOD TestingMethod + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT *MemoryProtectionContext = NULL; + UINT8 Index; + CHAR8 NameStub[] = "Security.PageGuard.Uefi"; + CHAR8 DescriptionStub[] = "Accesses before and after an allocated page should hit a guard page. Memory type: "; + CHAR8 *TestName = NULL; + CHAR8 *TestDescription = NULL; + UINTN TestNameSize; + UINTN TestDescriptionSize; + + DEBUG ((DEBUG_INFO, "%a() - Enter\n", __FUNCTION__)); + + // Need to generate a test case for each memory type. + for (Index = 0; Index < EfiMaxMemoryType; Index++) { + MemoryProtectionContext = (MEMORY_PROTECTION_TEST_CONTEXT *)AllocateZeroPool (sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + if (MemoryProtectionContext == NULL) { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test context failed.\n", __FUNCTION__)); + return; + } + + // Set the context for this test case. + MemoryProtectionContext->TargetMemoryType = Index; + MemoryProtectionContext->GuardAlignment = mDxeMps.HeapGuardPolicy.Fields.Direction; + MemoryProtectionContext->TestingMethod = TestingMethod; + + // Set the test name and description. + TestNameSize = sizeof (CHAR8) * (1 + AsciiStrnLenS (NameStub, UNIT_TEST_MAX_STRING_LENGTH) + AsciiStrnLenS (MEMORY_TYPES[Index], UNIT_TEST_MAX_STRING_LENGTH)); + TestName = (CHAR8 *)AllocateZeroPool (TestNameSize); + TestDescriptionSize = sizeof (CHAR8) * (1 + AsciiStrnLenS (DescriptionStub, UNIT_TEST_MAX_STRING_LENGTH) + AsciiStrnLenS (MEMORY_TYPES[Index], UNIT_TEST_MAX_STRING_LENGTH)); + TestDescription = (CHAR8 *)AllocateZeroPool (TestDescriptionSize); + + if ((TestName != NULL) && (TestDescription != NULL) && (MemoryProtectionContext != NULL)) { + // Name of the test is Security.PageGuard.Uefi + Memory Type Name (from MEMORY_TYPES) + AsciiStrCatS (TestName, UNIT_TEST_MAX_STRING_LENGTH, NameStub); + AsciiStrCatS (TestName, UNIT_TEST_MAX_STRING_LENGTH, MEMORY_TYPES[Index]); + + // Description of this test is DescriptionStub + Memory Type Name (from MEMORY_TYPES) + AsciiStrCatS (TestDescription, UNIT_TEST_MAX_STRING_LENGTH, DescriptionStub); + AsciiStrCatS (TestDescription, UNIT_TEST_MAX_STRING_LENGTH, MEMORY_TYPES[Index]); + + // Add the test case. This test case will only run if UefiPageGuardPreReq passes (which checks the protection policy for + // the memory type). + AddTestCase (TestSuite, TestDescription, TestName, UefiPageGuard, UefiPageGuardPreReq, NULL, MemoryProtectionContext); + + FreePool (TestName); + FreePool (TestDescription); + } else { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test creation failed.\n", __FUNCTION__)); + return; + } + } +} + +/** + Determine the test method which will be used to run this unit test. If a preferred test method is specified, + that test method MUST be usable or this function will return an error. If no preferred test method is + specified, the test will run with the first available test method in the following order: + 1. Memory Attribute Protocol + 2. Clear Faults + 3. Reset System + + @param[in] PreferredTestingMethod The preferred testing method to use. If this method is not usable, this + function will return an error. + @param[out] TestingMethod The testing method which will be used to run this test. + + @retval EFI_SUCCESS The testing method was successfully determined. + @retval EFI_UNSUPPORTED None of the testing methods were usable. + @retval EFI_INVALID_PARAMETER The preferred testing method could not be used. +**/ +STATIC +EFI_STATUS +DetermineTestMethod ( + IN MEMORY_PROTECTION_TESTING_METHOD PreferredTestingMethod, + OUT MEMORY_PROTECTION_TESTING_METHOD *TestingMethod + ) +{ + MEMORY_PROTECTION_TESTING_METHOD DeterminedTestingMethod = MemoryProtectionTestMax; + + // Use a switch based on the preferred testing method. MemoryProtectionTestMax implies that there + // is no perferred testing method in which case we will fall through the switch to find the first + // available testing method based on the order in the description above. Otherwise, we will check + // the testing method specified by PreferredTestingMethod. + switch (PreferredTestingMethod) { + default: + case MemoryProtectionTestMax: + case MemoryProtectionTestMemoryAttributeProtocol: + // Check if the Memory Attribute Protocol is installed + if (!EFI_ERROR (PopulateMemoryAttributeProtocol ())) { + DeterminedTestingMethod = MemoryProtectionTestMemoryAttributeProtocol; + break; + } + + case MemoryProtectionTestClearFaults: + // Check if the Project Mu page fault handler is installed. This handler will warm-reset on page faults + // unless the Nonstop Protocol is installed to clear intentional page faults. + if (!EFI_ERROR (CheckMemoryProtectionExceptionHandlerInstallation ())) { + // Clear the memory protection early store in case a fault was previously tripped and was not cleared + ExPersistClearAll (); + + // Check if a read/write to the early store works and the Nonstop Protocol is installed + if (!EFI_ERROR (ExPersistSetIgnoreNextPageFault ()) && + !EFI_ERROR (ExPersistClearIgnoreNextPageFault ()) && + !EFI_ERROR (GetNonstopProtocol ())) + { + DeterminedTestingMethod = MemoryProtectionTestClearFaults; + break; + } + } + + case MemoryProtectionTestReset: + if (!EFI_ERROR (RegisterMemoryProtectionTestAppInterruptHandler ())) { + DeterminedTestingMethod = MemoryProtectionTestReset; + break; + } + } + + // DeterminedTestingMethod will be MemoryProtectionTestMax if none of the testing + // methods were usable. + if (DeterminedTestingMethod == MemoryProtectionTestMax) { + DEBUG ((DEBUG_ERROR, "Could not find a suitable testing method.\n")); + return EFI_UNSUPPORTED; + } + + // If a preferred testing method was specified, make sure that the determined testing method + // matches. Otherwise, return an error. + if ((PreferredTestingMethod != MemoryProtectionTestMax) && (PreferredTestingMethod != DeterminedTestingMethod)) { + DEBUG ((DEBUG_ERROR, "Could not use desired testing method.\n")); + return EFI_INVALID_PARAMETER; + } + + // Print the testing method that will be used + switch (DeterminedTestingMethod) { + case MemoryProtectionTestReset: + DEBUG ((DEBUG_INFO, "Testing with a reset after each protection violation.\n")); + break; + + case MemoryProtectionTestMemoryAttributeProtocol: + DEBUG ((DEBUG_INFO, "Testing with the Memory Attribute Protocol.\n")); + break; + + case MemoryProtectionTestClearFaults: + DEBUG ((DEBUG_INFO, "Testing with the Nonstop Protocol.\n")); + break; + + default: + // Should never get here + DEBUG ((DEBUG_ERROR, "Invalid testing method.\n")); + return EFI_INVALID_PARAMETER; + } + + // Set the output parameter and return + *TestingMethod = DeterminedTestingMethod; + return EFI_SUCCESS; +} + +/** + MemoryProtectionTestAppEntryPoint + + Future Work: + 1. Enable running the reset method on ARM platforms by installing a synchronous handler. + + @param[in] ImageHandle The firmware allocated handle for the EFI image. + @param[in] SystemTable A pointer to the EFI System Table. + + @retval EFI_SUCCESS The entry point executed successfully. + @retval other Some error occurred when executing this entry point. + +**/ +EFI_STATUS +EFIAPI +MemoryProtectionTestAppEntryPoint ( + IN EFI_HANDLE ImageHandle, + IN EFI_SYSTEM_TABLE *SystemTable + ) +{ + EFI_STATUS Status; + UNIT_TEST_FRAMEWORK_HANDLE Fw = NULL; + UNIT_TEST_SUITE_HANDLE PageGuard = NULL; + UNIT_TEST_SUITE_HANDLE PoolGuard = NULL; + UNIT_TEST_SUITE_HANDLE NxProtection = NULL; + UNIT_TEST_SUITE_HANDLE Misc = NULL; + MEMORY_PROTECTION_TEST_CONTEXT *MemoryProtectionContext; + MEMORY_PROTECTION_TESTING_METHOD TestingMethod; + MEMORY_PROTECTION_TESTING_METHOD PreferredTestingMethod; + EFI_SHELL_PARAMETERS_PROTOCOL *ShellParams; + + DEBUG ((DEBUG_ERROR, "%a()\n", __FUNCTION__)); + + DEBUG ((DEBUG_ERROR, "%a v%a\n", UNIT_TEST_APP_NAME, UNIT_TEST_APP_VERSION)); + + MemoryProtectionContext = (MEMORY_PROTECTION_TEST_CONTEXT *)AllocateZeroPool (sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + if (MemoryProtectionContext == NULL) { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test context failed.\n", __FUNCTION__)); + return EFI_OUT_OF_RESOURCES; + } + + Status = gBS->HandleProtocol ( + gImageHandle, + &gEfiShellParametersProtocolGuid, + (VOID **)&ShellParams + ); + + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a - Could not retrieve command line args!\n", __FUNCTION__)); + goto EXIT; + } + + Status = FetchMemoryProtectionHobEntry (); + ASSERT_EFI_ERROR (Status); + + // Set up the test framework for running the tests. + Status = InitUnitTestFramework (&Fw, UNIT_TEST_APP_NAME, gEfiCallerBaseName, UNIT_TEST_APP_VERSION); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "Failed in InitUnitTestFramework. Status = %r\n", Status)); + goto EXIT; + } + + // Create separate test suites for Page, Pool, and NX tests. The Misc test suite is for stack guard + // and null pointer testing. + CreateUnitTestSuite (&Misc, Fw, "Stack Guard and Null Pointer Detection", "Security.HeapGuardMisc", NULL, NULL); + CreateUnitTestSuite (&PageGuard, Fw, "Page Guard Tests", "Security.PageGuard", NULL, NULL); + CreateUnitTestSuite (&PoolGuard, Fw, "Pool Guard Tests", "Security.PoolGuard", NULL, NULL); + CreateUnitTestSuite (&NxProtection, Fw, "NX Protection Tests", "Security.NxProtection", NULL, NULL); + + if ((PageGuard == NULL) || (PoolGuard == NULL) || (NxProtection == NULL) || (Misc == NULL)) { + DEBUG ((DEBUG_ERROR, "%a - Failed in CreateUnitTestSuite for TestSuite\n", __FUNCTION__)); + Status = EFI_OUT_OF_RESOURCES; + goto EXIT; + } + + PreferredTestingMethod = MemoryProtectionTestMax; + + // Check the command line arguments to see if a preferred testing method was specified. + if (ShellParams->Argc > 1) { + if (StrnCmp (ShellParams->Argv[1], UNIT_TEST_WARM_RESET_STRING, StrLen (UNIT_TEST_WARM_RESET_STRING)) == 0) { + PreferredTestingMethod = MemoryProtectionTestReset; + } else if (StrnCmp (ShellParams->Argv[1], UNIT_TEST_MEMORY_ATTRIBUTE_STRING, StrLen (UNIT_TEST_MEMORY_ATTRIBUTE_STRING)) == 0) { + PreferredTestingMethod = MemoryProtectionTestMemoryAttributeProtocol; + } else if (StrnCmp (ShellParams->Argv[1], UNIT_TEST_CLEAR_FAULTS_STRING, StrLen (UNIT_TEST_CLEAR_FAULTS_STRING)) == 0) { + PreferredTestingMethod = MemoryProtectionTestClearFaults; + } else { + if (StrnCmp (ShellParams->Argv[1], L"-h", 4) != 0) { + DEBUG ((DEBUG_INFO, "Invalid argument!\n\n")); + } + + DEBUG ((DEBUG_INFO, "--Reset : Attempt to run the test by violating memory protections and performing a warm reset on faults.\n")); + DEBUG ((DEBUG_INFO, "--MemoryAttribute : Attempt to run the test by using the memory attribute protocol to check attributes.\n")); + DEBUG ((DEBUG_INFO, "--ClearFaults : Attempt to run the test by violating memory protections and expecting the exception handler to clear the faults.\n")); + + Status = EFI_ABORTED; + goto EXIT; + } + } + + // Determine the testing method to use. + if (EFI_ERROR (DetermineTestMethod (PreferredTestingMethod, &TestingMethod))) { + goto EXIT; + } + + // Set the testing method in the test context. + MemoryProtectionContext->TestingMethod = TestingMethod; + + // Add a unit test for each memory type for pool, page, and NX protection. + AddUefiPoolTest (PoolGuard, TestingMethod); + AddUefiPageTest (PageGuard, TestingMethod); + AddUefiNxTest (NxProtection, TestingMethod); + + // Add NULL protection, stack protection, and Image protection tests to the Misc test suite. + AddTestCase (Misc, "Null pointer access should trigger a page fault", "Security.HeapGuardMisc.UefiNullPointerDetection", UefiNullPointerDetection, UefiNullPointerPreReq, NULL, MemoryProtectionContext); + AddTestCase (Misc, "Blowing the stack should trigger a page fault", "Security.HeapGuardMisc.UefiCpuStackGuard", UefiCpuStackGuard, UefiStackGuardPreReq, NULL, MemoryProtectionContext); + AddTestCase (Misc, "Check that loaded images have proper attributes set", "Security.HeapGuardMisc.ImageProtectionEnabled", ImageProtection, ImageProtectionPreReq, NULL, MemoryProtectionContext); + AddTestCase (NxProtection, "Check hardware configuration of HardwareNxProtection bit", "Security.HeapGuardMisc.UefiHardwareNxProtectionEnabled", UefiHardwareNxProtectionEnabled, UefiHardwareNxProtectionEnabledPreReq, NULL, MemoryProtectionContext); + AddTestCase (NxProtection, "Stack NX Protection", "Security.HeapGuardMisc.UefiNxStackGuard", UefiNxStackGuard, NULL, NULL, MemoryProtectionContext); + + // Execute the tests. + Status = RunAllTestSuites (Fw); + +EXIT: + + if (Fw) { + FreeUnitTestFramework (Fw); + } + + if (MemoryProtectionContext) { + FreePool (MemoryProtectionContext); + } + + return Status; +} diff --git a/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/DxeMemoryProtectionTestApp.inf b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/DxeMemoryProtectionTestApp.inf new file mode 100644 index 0000000000..3a2384df81 --- /dev/null +++ b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/DxeMemoryProtectionTestApp.inf @@ -0,0 +1,81 @@ +## @file DxeMemoryProtectionTestApp.inf +# +# Tests for page guard, pool guard, NX protections, stack guard, and null pointer detection. +## +# Copyright (C) Microsoft Corporation. All rights reserved. +## SPDX-License-Identifier: BSD-2-Clause-Patent +## + +[Defines] + INF_VERSION = 0x00010017 + BASE_NAME = DxeMemoryProtectionTestApp + FILE_GUID = 28C4CBBB-188F-460A-944A-DBE4A19D279E + VERSION_STRING = 3.0 + MODULE_TYPE = UEFI_APPLICATION + ENTRY_POINT = MemoryProtectionTestAppEntryPoint + +# +# The following information is for reference only and not required by the build tools. +# +# VALID_ARCHITECTURES = X64 AARCH64 +# + +[Sources] + DxeMemoryProtectionTestApp.c + +[Sources.X64] + X64/X64Functions.c + +[Sources.AARCH64] + AArch64/AArch64Functions.c + +[Packages] + MdePkg/MdePkg.dec + MdeModulePkg/MdeModulePkg.dec + UefiCpuPkg/UefiCpuPkg.dec + UnitTestFrameworkPkg/UnitTestFrameworkPkg.dec + ShellPkg/ShellPkg.dec + MsCorePkg/MsCorePkg.dec + +[LibraryClasses] + DebugLib + UefiApplicationEntryPoint + UefiBootServicesTableLib + UnitTestLib + UnitTestPersistenceLib + UnitTestBootLib + PrintLib + MemoryAllocationLib + BaseLib + ShellLib + UefiLib + HobLib + ExceptionPersistenceLib + +[LibraryClasses.X64] + HwResetSystemLib + +[Guids] + gMemoryProtectionExceptionHandlerGuid ## CONSUMES + gEfiHobMemoryAllocStackGuid ## CONSUMES + +[Protocols] + gEfiCpuArchProtocolGuid ## CONSUMES + gMemoryProtectionNonstopModeProtocolGuid ## CONSUMES + gMemoryProtectionDebugProtocolGuid ## CONSUMES + gEfiMemoryAttributeProtocolGuid ## CONSUMES + gCpuMpDebugProtocolGuid ## CONSUMES + +[Guids] + gEfiDebugImageInfoTableGuid ## SOMETIMES_CONSUMES ## GUID + gEfiMemoryAttributesTableGuid + gDxeMemoryProtectionSettingsGuid + +[BuildOptions] + GCC:*_CLANG40WIN_AARCH64_CC_FLAGS = -Wno-infinite-recursion + MSFT:*_*_*_CC_FLAGS = /wd4054 /wd4055 /wd4717 + # An invalid opcode exception can be triggered during the NULL detection test on GCC5 builds + # due to the instruction "ud2" being inserted by the compiler after a NULL pointer dereference. + # Removing optimization prevents the invalid opcode instruction from being inserted and enables + # the interrupt handler to clear the fault and return to the test. + GCC:*_GCC5_X64_CC_FLAGS = -O0 diff --git a/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/SmmMemoryProtectionTestApp.c b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/SmmMemoryProtectionTestApp.c new file mode 100644 index 0000000000..aa52296392 --- /dev/null +++ b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/SmmMemoryProtectionTestApp.c @@ -0,0 +1,791 @@ +/** @file -- SmmMemoryProtectionTestApp.c + +Tests for page guard, pool guard, NX protections, stack guard, and null pointer detection. + +Copyright (c) Microsoft Corporation. All rights reserved. +SPDX-License-Identifier: BSD-2-Clause-Patent + +**/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../MemoryProtectionTestCommon.h" +#include "ArchSpecificFunctions.h" + +#define UNIT_TEST_APP_NAME "SMM Memory Protection Test" +#define UNIT_TEST_APP_VERSION "3.0" + +#define DUMMY_FUNCTION_FOR_CODE_SELF_TEST_GENERIC_SIZE 512 +#define ALIGN_ADDRESS(Address) (((Address) / EFI_PAGE_SIZE) * EFI_PAGE_SIZE) + +UINTN mPiSmmCommonCommBufferSize; +MM_MEMORY_PROTECTION_SETTINGS mMmMps; +DXE_MEMORY_PROTECTION_SETTINGS mDxeMps; +VOID *mPiSmmCommonCommBufferAddress = NULL; + +/// ================================================================================================ +/// ================================================================================================ +/// +/// HELPER FUNCTIONS +/// +/// ================================================================================================ +/// ================================================================================================ + +/** + Gets the input EFI_MEMORY_TYPE from the input MM_HEAP_GUARD_MEMORY_TYPES bitfield + + @param[in] MemoryType Memory type to check. + @param[in] HeapGuardMemoryType MM_HEAP_GUARD_MEMORY_TYPES bitfield + + @return TRUE The given EFI_MEMORY_TYPE is TRUE in the given MM_HEAP_GUARD_MEMORY_TYPES + @return FALSE The given EFI_MEMORY_TYPE is FALSE in the given MM_HEAP_GUARD_MEMORY_TYPES +**/ +BOOLEAN +STATIC +GetMmMemoryTypeSettingFromBitfield ( + IN EFI_MEMORY_TYPE MemoryType, + IN MM_HEAP_GUARD_MEMORY_TYPES HeapGuardMemoryType + ) +{ + switch (MemoryType) { + case EfiReservedMemoryType: + return HeapGuardMemoryType.Fields.EfiReservedMemoryType; + case EfiLoaderCode: + return HeapGuardMemoryType.Fields.EfiLoaderCode; + case EfiLoaderData: + return HeapGuardMemoryType.Fields.EfiLoaderData; + case EfiBootServicesCode: + return HeapGuardMemoryType.Fields.EfiBootServicesCode; + case EfiBootServicesData: + return HeapGuardMemoryType.Fields.EfiBootServicesData; + case EfiRuntimeServicesCode: + return HeapGuardMemoryType.Fields.EfiRuntimeServicesCode; + case EfiRuntimeServicesData: + return HeapGuardMemoryType.Fields.EfiRuntimeServicesData; + case EfiConventionalMemory: + return HeapGuardMemoryType.Fields.EfiConventionalMemory; + case EfiUnusableMemory: + return HeapGuardMemoryType.Fields.EfiUnusableMemory; + case EfiACPIReclaimMemory: + return HeapGuardMemoryType.Fields.EfiACPIReclaimMemory; + case EfiACPIMemoryNVS: + return HeapGuardMemoryType.Fields.EfiACPIMemoryNVS; + case EfiMemoryMappedIO: + return HeapGuardMemoryType.Fields.EfiMemoryMappedIO; + case EfiMemoryMappedIOPortSpace: + return HeapGuardMemoryType.Fields.EfiMemoryMappedIOPortSpace; + case EfiPalCode: + return HeapGuardMemoryType.Fields.EfiPalCode; + case EfiPersistentMemory: + return HeapGuardMemoryType.Fields.EfiPersistentMemory; + case EfiUnacceptedMemoryType: + return HeapGuardMemoryType.Fields.EfiUnacceptedMemoryType; + default: + return FALSE; + } +} + +/** + Abstraction layer which fetches the DXE and MM memory protection HOBs. + + @retval EFI_SUCCESS Both HOB entries have been fetched + @retval EFI_INVALID_PARAMETER A HOB entry could not be found +**/ +EFI_STATUS +STATIC +FetchMemoryProtectionHobEntries ( + VOID + ) +{ + VOID *Ptr1; + VOID *Ptr2; + + ZeroMem (&mMmMps, sizeof (mMmMps)); + ZeroMem (&mDxeMps, sizeof (mDxeMps)); + + Ptr1 = GetFirstGuidHob (&gMmMemoryProtectionSettingsGuid); + Ptr2 = GetFirstGuidHob (&gDxeMemoryProtectionSettingsGuid); + + if (Ptr1 != NULL) { + if (*((UINT8 *)GET_GUID_HOB_DATA (Ptr1)) != (UINT8)MM_MEMORY_PROTECTION_SETTINGS_CURRENT_VERSION) { + DEBUG (( + DEBUG_INFO, + "%a: - Version number of the MM Memory Protection Settings HOB is invalid.\n", + __FUNCTION__ + )); + } else { + CopyMem (&mMmMps, GET_GUID_HOB_DATA (Ptr1), sizeof (MM_MEMORY_PROTECTION_SETTINGS)); + } + } + + if (Ptr2 != NULL) { + if (*((UINT8 *)GET_GUID_HOB_DATA (Ptr2)) != (UINT8)DXE_MEMORY_PROTECTION_SETTINGS_CURRENT_VERSION) { + DEBUG (( + DEBUG_INFO, + "%a: - Version number of the DXE Memory Protection Settings HOB is invalid.\n", + __FUNCTION__ + )); + } else { + CopyMem (&mDxeMps, GET_GUID_HOB_DATA (Ptr2), sizeof (DXE_MEMORY_PROTECTION_SETTINGS)); + } + } + + return (Ptr1 != NULL && Ptr2 != NULL) ? EFI_SUCCESS : EFI_INVALID_PARAMETER; +} + +/** + This helper function actually sends the requested communication + to the SMM driver. + + @param[in] RequestedFunction The test function to request the SMM driver run. + @param[in] Context The context of the test. + + @retval EFI_SUCCESS Communication was successful. + @retval EFI_ABORTED Some error occurred. + +**/ +STATIC +EFI_STATUS +SmmMemoryProtectionsDxeToSmmCommunicate ( + IN UINT16 RequestedFunction, + IN MEMORY_PROTECTION_TEST_CONTEXT *Context + ) +{ + EFI_STATUS Status = EFI_SUCCESS; + EFI_SMM_COMMUNICATE_HEADER *CommHeader; + MEMORY_PROTECTION_TEST_COMM_BUFFER *VerificationCommBuffer; + static EFI_SMM_COMMUNICATION_PROTOCOL *SmmCommunication = NULL; + UINTN CommBufferSize; + + if (mPiSmmCommonCommBufferAddress == NULL) { + DEBUG ((DEBUG_ERROR, "%a - Communication buffer not found!\n", __FUNCTION__)); + return EFI_ABORTED; + } + + // Zero the comm buffer + CommHeader = (EFI_SMM_COMMUNICATE_HEADER *)mPiSmmCommonCommBufferAddress; + CommBufferSize = sizeof (MEMORY_PROTECTION_TEST_COMM_BUFFER) + OFFSET_OF (EFI_SMM_COMMUNICATE_HEADER, Data); + if (CommBufferSize > mPiSmmCommonCommBufferSize) { + DEBUG ((DEBUG_ERROR, "%a - Communication buffer is too small!\n", __FUNCTION__)); + return EFI_ABORTED; + } + + ZeroMem (CommHeader, CommBufferSize); + + // Update the SMM communication parameters + CopyGuid (&CommHeader->HeaderGuid, &gMemoryProtectionTestSmiHandlerGuid); + CommHeader->MessageLength = sizeof (MEMORY_PROTECTION_TEST_COMM_BUFFER); + + // Update parameters Specific to this implementation + VerificationCommBuffer = (MEMORY_PROTECTION_TEST_COMM_BUFFER *)CommHeader->Data; + VerificationCommBuffer->Function = RequestedFunction; + VerificationCommBuffer->Status = EFI_NOT_FOUND; + CopyMem (&VerificationCommBuffer->Context, Context, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + + // Locate the protocol if necessary + if (!SmmCommunication) { + Status = gBS->LocateProtocol (&gEfiSmmCommunicationProtocolGuid, NULL, (VOID **)&SmmCommunication); + } + + // Signal MM + if (!EFI_ERROR (Status)) { + Status = SmmCommunication->Communicate ( + SmmCommunication, + CommHeader, + &CommBufferSize + ); + DEBUG ((DEBUG_INFO, "%a - Communicate() = %r\n", __FUNCTION__, Status)); + } + + return VerificationCommBuffer->Status; +} + +/** + Locates and stores address of comm buffer +**/ +STATIC +VOID +LocateSmmCommonCommBuffer ( + VOID + ) +{ + EFI_STATUS Status; + EDKII_PI_SMM_COMMUNICATION_REGION_TABLE *PiSmmCommunicationRegionTable; + EFI_MEMORY_DESCRIPTOR *SmmCommMemRegion; + UINTN Index, BufferSize; + + if (mPiSmmCommonCommBufferAddress == NULL) { + Status = EfiGetSystemConfigurationTable (&gEdkiiPiSmmCommunicationRegionTableGuid, (VOID **)&PiSmmCommunicationRegionTable); + if (EFI_ERROR (Status)) { + return; + } + + // Only need a region large enough to hold a MEMORY_PROTECTION_TEST_COMM_BUFFER + BufferSize = 0; + SmmCommMemRegion = (EFI_MEMORY_DESCRIPTOR *)(PiSmmCommunicationRegionTable + 1); + for (Index = 0; Index < PiSmmCommunicationRegionTable->NumberOfEntries; Index++) { + if (SmmCommMemRegion->Type == EfiConventionalMemory) { + BufferSize = EFI_PAGES_TO_SIZE ((UINTN)SmmCommMemRegion->NumberOfPages); + if (BufferSize >= (sizeof (MEMORY_PROTECTION_TEST_COMM_BUFFER) + OFFSET_OF (EFI_SMM_COMMUNICATE_HEADER, Data))) { + break; + } + } + + SmmCommMemRegion = (EFI_MEMORY_DESCRIPTOR *)((UINT8 *)SmmCommMemRegion + PiSmmCommunicationRegionTable->DescriptorSize); + } + + if (SmmCommMemRegion->PhysicalStart > MAX_UINTN) { + return; + } + + mPiSmmCommonCommBufferAddress = (VOID *)(UINTN)SmmCommMemRegion->PhysicalStart; + mPiSmmCommonCommBufferSize = BufferSize; + } +} + +/** + This dummy function definition is used to test no-execute protection on allocated buffers + and the stack. +**/ +typedef +VOID +(*DUMMY_VOID_FUNCTION_FOR_DATA_TEST)( + VOID + ); + +/// ================================================================================================ +/// ================================================================================================ +/// +/// PRE REQ FUNCTIONS +/// +/// ================================================================================================ +/// ================================================================================================ + +/** + This function checks if the MM page guard policy is active for the target memory type within Context. Testing + page guards currently requires that buffers of the relevant memory type can be allocated. If the memory type + is Conventional, Persistent, or Unaccepted, the test will be skipped as we cannot allocate those types. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED The pre-reqs for page guard testing are met. + @retval UNIT_TEST_SKIPPED The pre-reqs for page guard testing are not met. +**/ +UNIT_TEST_STATUS +EFIAPI +SmmPageGuardPreReq ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + + UT_ASSERT_TRUE (MemoryProtectionContext.TargetMemoryType < EfiMaxMemoryType); + if (!(mMmMps.HeapGuardPolicy.Fields.MmPageGuard && + GetMmMemoryTypeSettingFromBitfield ((EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, mMmMps.HeapGuardPageType))) + { + UT_LOG_WARNING ("Protection for this memory type is disabled: %a\n", MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType]); + return UNIT_TEST_SKIPPED; + } + + // Skip memory types which cannot be allocated + if ((MemoryProtectionContext.TargetMemoryType == EfiConventionalMemory) || + (MemoryProtectionContext.TargetMemoryType == EfiPersistentMemory) || + (MemoryProtectionContext.TargetMemoryType == EfiUnacceptedMemoryType)) + { + UT_LOG_WARNING ("Skipping test of memory type %a -- memory type cannot be allocated\n", MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType]); + return UNIT_TEST_SKIPPED; + } + + return UNIT_TEST_PASSED; +} + +/** + This function checks if the MM pool guard policy is active for the target memory type within Context. Testing + pool guards currently requires that buffers of the relevant memory type can be allocated. If the memory type + is Conventional, Persistent, or Unaccepted the test will be skipped as we cannot allocate those types. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED The pre-reqs for pool guard testing are met. + @retval UNIT_TEST_SKIPPED The pre-reqs for pool guard testing are not met. +**/ +UNIT_TEST_STATUS +EFIAPI +SmmPoolGuardPreReq ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + + UT_ASSERT_TRUE (MemoryProtectionContext.TargetMemoryType < EfiMaxMemoryType); + if (!(mMmMps.HeapGuardPolicy.Fields.MmPoolGuard && + GetMmMemoryTypeSettingFromBitfield ((EFI_MEMORY_TYPE)MemoryProtectionContext.TargetMemoryType, mMmMps.HeapGuardPoolType))) + { + UT_LOG_WARNING ("Protection for this memory type is disabled: %a\n", MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType]); + return UNIT_TEST_SKIPPED; + } + + // Skip memory types which cannot be allocated + if ((MemoryProtectionContext.TargetMemoryType == EfiConventionalMemory) || + (MemoryProtectionContext.TargetMemoryType == EfiPersistentMemory) || + (MemoryProtectionContext.TargetMemoryType == EfiUnacceptedMemoryType)) + { + UT_LOG_WARNING ("Skipping test of memory type %a -- memory type cannot be allocated\n", MEMORY_TYPES[MemoryProtectionContext.TargetMemoryType]); + return UNIT_TEST_SKIPPED; + } + + return UNIT_TEST_PASSED; +} + +/** + This function checks if the NULL pointer detection policy for MM is active. + + @param[in] Context The context of the test. + + @retval UNIT_TEST_PASSED The pre-reqs for NULL pointer testing are met. + @retval UNIT_TEST_SKIPPED The pre-reqs for NULL pointer testing are not met. +**/ +UNIT_TEST_STATUS +EFIAPI +SmmNullPointerPreReq ( + IN UNIT_TEST_CONTEXT Context + ) +{ + if (!mMmMps.NullPointerDetectionPolicy) { + UT_LOG_WARNING ("This feature is disabled\n"); + return UNIT_TEST_SKIPPED; + } + + return UNIT_TEST_PASSED; +} + +/// ================================================================================================ +/// ================================================================================================ +/// +/// TEST CASES +/// +/// ================================================================================================ +/// ================================================================================================ + +/** + This test requires that the MM memory protection driver is present. This test will use the mailbox + to pass the test context to the MM driver. The MM driver will allocate a page of the target memory + type described in Context and attempt to write to the page immediately preceding and succeeding + the allocated page. Prior to communicating with the MM driver, this test will update a counter + and save the framework state so when the test resumes after reset it can move on to the next phase + of testing instead of repeating the same test. If a reset does not occur, the test will fail. + + @param[in] Context The test context. + + @retval UNIT_TEST_PASSED The test was executed successfully. + @retval UNIT_TEST_ERROR_TEST_FAILED A condition outlined in the test description was not met. +**/ +UNIT_TEST_STATUS +EFIAPI +SmmPageGuard ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + EFI_STATUS Status; + + if (MemoryProtectionContext.TestProgress < 2) { + // + // Context.TestProgress indicates progress within this specific test. + // 0 - Just started. + // 1 - Completed head guard test. + // 2 - Completed tail guard test. + // + // Indicate the test is in progress and save state. + // + MemoryProtectionContext.TestProgress++; + SetBootNextDevice (); + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + + // Communicate to the MM driver to run the page guard test based on MemoryProtectionContext. + Status = SmmMemoryProtectionsDxeToSmmCommunicate (MEMORY_PROTECTION_TEST_PAGE, &MemoryProtectionContext); + if (Status == EFI_NOT_FOUND) { + UT_LOG_WARNING ("SMM test driver is not loaded.\n"); + return UNIT_TEST_SKIPPED; + } else { + UT_LOG_ERROR ("System was expected to reboot, but didn't.\n"); + } + + // If the test reaches this point, the MM driver did not not cause a fault and reset. + // The test has failed. + MemoryProtectionContext.TestProgress = 0; + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + } + + // TestProgress will be 2 if the test has completed successfully. + UT_ASSERT_TRUE (MemoryProtectionContext.TestProgress == 2); + + return UNIT_TEST_PASSED; +} + +/** + This test requires that the MM memory protection driver is present. This test will use the mailbox + to pass the test context to the MM driver. The MM driver will allocate a pool of the target memory + type described in Context and attempt to write to the page immediately preceding and succeeding + the page containing the allocated pool which should cause the system to reset. The MM driver does + not test that the pool is properly aligned to the head or tail of the guard. Prior to communicating + with the MM driver, this test will update a counter and save the framework state so when the test + resumes after reset it can move on to the next phase of testing instead of repeating the same test. + If a reset does not occur, the test will fail. + + @param[in] Context The test context. + + @retval UNIT_TEST_PASSED The test was executed successfully. + @retval UNIT_TEST_ERROR_TEST_FAILED A condition outlined in the test description was not met. +**/ +UNIT_TEST_STATUS +EFIAPI +SmmPoolGuard ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + EFI_STATUS Status; + + if (MemoryProtectionContext.TestProgress < ARRAY_SIZE (mPoolSizeTable)) { + // + // Context.TestProgress indicates progress within this specific test. + // The test progressively allocates larger areas to test the guard on. + // These areas are defined in Pool.c as the 13 different sized chunks that are available + // for pool allocation. + // + // Indicate the test is in progress and save state. + // + MemoryProtectionContext.TestProgress++; + SetBootNextDevice (); + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + + // Communicate to the MM driver to run the pool guard test based on MemoryProtectionContext. + Status = SmmMemoryProtectionsDxeToSmmCommunicate (MEMORY_PROTECTION_TEST_POOL, &MemoryProtectionContext); + + if (Status == EFI_NOT_FOUND) { + UT_LOG_WARNING ("SMM test driver is not loaded.\n"); + return UNIT_TEST_SKIPPED; + } else { + UT_LOG_ERROR ("System was expected to reboot, but didn't.\n"); + } + + // If the test reaches this point, the MM driver did not not cause a fault and reset. + // The test has failed. + MemoryProtectionContext.TestProgress = 0; + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + } + + // TestProgress will be 1 if the test has completed successfully. + UT_ASSERT_TRUE (MemoryProtectionContext.TestProgress == 1); + + return UNIT_TEST_PASSED; +} + +/** + This test requires that the MM memory protection driver is present. This test will use the mailbox + to pass the test context to the MM driver. The MM driver will dereference NULL via write and read which + should cause a fault and reset. Prior to communicating with the MM driver, this test will update a counter + and save the framework state so when the test resumes after reset it can move on to the next phase + of testing instead of repeating the same test. If a reset does not occur, the test will fail. + + @param[in] Context The test context. + + @retval UNIT_TEST_PASSED The test was executed successfully. + @retval UNIT_TEST_ERROR_TEST_FAILED A condition outlined in the test description was not met. +**/ +UNIT_TEST_STATUS +EFIAPI +SmmNullPointerDetection ( + IN UNIT_TEST_CONTEXT Context + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT MemoryProtectionContext = (*(MEMORY_PROTECTION_TEST_CONTEXT *)Context); + EFI_STATUS Status; + + if (MemoryProtectionContext.TestProgress < 1) { + // + // Context.TestProgress 0 indicates the test hasn't started yet. + // + // Indicate the test is in progress and save state. + // + MemoryProtectionContext.TestProgress++; + SetBootNextDevice (); + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + + // Communicate to the MM driver to run the NULL pointer test based on MemoryProtectionContext. + Status = SmmMemoryProtectionsDxeToSmmCommunicate (MEMORY_PROTECTION_TEST_NULL_POINTER, &MemoryProtectionContext); + + if (Status == EFI_NOT_FOUND) { + UT_LOG_WARNING ("SMM test driver is not loaded.\n"); + return UNIT_TEST_SKIPPED; + } else { + UT_LOG_ERROR ("System was expected to reboot, but didn't. %r\n", Status); + } + + // If the test reaches this point, the MM driver did not not cause a fault and reset. + // The test has failed. + MemoryProtectionContext.TestProgress = 0; + SaveFrameworkState (&MemoryProtectionContext, sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + } + + // TestProgress will be 1 if the test has completed successfully. + UT_ASSERT_TRUE (MemoryProtectionContext.TestProgress == 1); + + return UNIT_TEST_PASSED; +} + +/// ================================================================================================ +/// ================================================================================================ +/// +/// TEST ENGINE +/// +/// ================================================================================================ +/// ================================================================================================ + +/** + This function adds an MM test case for each memory type with pool guards enabled. + + Future Work: + 1. Update the SMM testing structure to allocate the tested pools in SMM so the MM guard + alignment setting can be used. + + @param[in] TestSuite The test suite to add the test cases to. + @param[in] TestingMethod The method to use for testing (Memory Attribute, Clear Faults, etc.) +**/ +VOID +AddSmmPoolTest ( + UNIT_TEST_SUITE_HANDLE TestSuite + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT *MemoryProtectionContext = NULL; + UINT8 Index; + CHAR8 NameStub[] = "Security.PoolGuard.Smm"; + CHAR8 DescriptionStub[] = "Accesses before/after the pool should hit a guard page in SMM. Memory type: "; + CHAR8 *TestName = NULL; + CHAR8 *TestDescription = NULL; + UINTN TestNameSize; + UINTN TestDescriptionSize; + + // Need to generate a test case for each memory type. + for (Index = 0; Index < EfiMaxMemoryType; Index++) { + MemoryProtectionContext = (MEMORY_PROTECTION_TEST_CONTEXT *)AllocateZeroPool (sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + if (MemoryProtectionContext == NULL) { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test context failed.\n", __FUNCTION__)); + return; + } + + // Set the context for this test case. + MemoryProtectionContext->TargetMemoryType = Index; + MemoryProtectionContext->GuardAlignment = mDxeMps.HeapGuardPolicy.Fields.Direction; + + // Set the test name and description. + TestNameSize = sizeof (CHAR8) * (1 + AsciiStrnLenS (NameStub, UNIT_TEST_MAX_STRING_LENGTH) + AsciiStrnLenS (MEMORY_TYPES[Index], UNIT_TEST_MAX_STRING_LENGTH)); + TestName = (CHAR8 *)AllocateZeroPool (TestNameSize); + TestDescriptionSize = sizeof (CHAR8) * (1 + AsciiStrnLenS (DescriptionStub, UNIT_TEST_MAX_STRING_LENGTH) + AsciiStrnLenS (MEMORY_TYPES[Index], UNIT_TEST_MAX_STRING_LENGTH)); + TestDescription = (CHAR8 *)AllocateZeroPool (TestDescriptionSize); + + if ((TestName != NULL) && (TestDescription != NULL) && (MemoryProtectionContext != NULL)) { + // Name of the test is Security.PoolGuard.Smm + Memory Type Name (from MEMORY_TYPES) + AsciiStrCatS (TestName, UNIT_TEST_MAX_STRING_LENGTH, NameStub); + AsciiStrCatS (TestName, UNIT_TEST_MAX_STRING_LENGTH, MEMORY_TYPES[Index]); + + // Description of this test is DescriptionStub + Memory Type Name (from MEMORY_TYPES) + AsciiStrCatS (TestDescription, UNIT_TEST_MAX_STRING_LENGTH, DescriptionStub); + AsciiStrCatS (TestDescription, UNIT_TEST_MAX_STRING_LENGTH, MEMORY_TYPES[Index]); + + // Add the test case. This test case will only run if SmmPoolGuardPreReq passes (which checks the protection policy for + // the memory type). + AddTestCase (TestSuite, TestDescription, TestName, SmmPoolGuard, SmmPoolGuardPreReq, NULL, MemoryProtectionContext); + + FreePool (TestName); + FreePool (TestDescription); + } else { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test creation failed.\n", __FUNCTION__)); + return; + } + } +} + +/** + This function adds an MM test case for each memory type with page guards enabled. + + Future Work: + 1. Update the SMM testing structure to allocate the tested pages in SMM so the MM guard + alignment setting can be used. + + @param[in] TestSuite The test suite to add the test cases to. + @param[in] TestingMethod The method to use for testing (Memory Attribute, Clear Faults, etc.) +**/ +VOID +AddSmmPageTest ( + UNIT_TEST_SUITE_HANDLE TestSuite + ) +{ + MEMORY_PROTECTION_TEST_CONTEXT *MemoryProtectionContext = NULL; + UINT8 Index; + CHAR8 NameStub[] = "Security.PageGuard.Smm"; + CHAR8 DescriptionStub[] = "Accesses before and after an allocated page should hit a guard page in SMM. Memory type: "; + CHAR8 *TestName = NULL; + CHAR8 *TestDescription = NULL; + UINTN TestNameSize; + UINTN TestDescriptionSize; + + // Need to generate a test case for each memory type. + for (Index = 0; Index < EfiMaxMemoryType; Index++) { + MemoryProtectionContext = (MEMORY_PROTECTION_TEST_CONTEXT *)AllocateZeroPool (sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + if (MemoryProtectionContext == NULL) { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test context failed.\n", __FUNCTION__)); + return; + } + + // Set the context for this test case. + MemoryProtectionContext->TargetMemoryType = Index; + + // Because the pages/pools will be allocated in the UEFI context, use the DXE guard direction + MemoryProtectionContext->GuardAlignment = mDxeMps.HeapGuardPolicy.Fields.Direction; + + // Set the test name and description. + TestNameSize = sizeof (CHAR8) * (1 + AsciiStrnLenS (NameStub, UNIT_TEST_MAX_STRING_LENGTH) + AsciiStrnLenS (MEMORY_TYPES[Index], UNIT_TEST_MAX_STRING_LENGTH)); + TestName = (CHAR8 *)AllocateZeroPool (TestNameSize); + TestDescriptionSize = sizeof (CHAR8) * (1 + AsciiStrnLenS (DescriptionStub, UNIT_TEST_MAX_STRING_LENGTH) + AsciiStrnLenS (MEMORY_TYPES[Index], UNIT_TEST_MAX_STRING_LENGTH)); + TestDescription = (CHAR8 *)AllocateZeroPool (TestDescriptionSize); + + if ((TestName != NULL) && (TestDescription != NULL) && (MemoryProtectionContext != NULL)) { + // Name of the test is Security.PageGuard.Smm + Memory Type Name (from MEMORY_TYPES) + AsciiStrCatS (TestName, UNIT_TEST_MAX_STRING_LENGTH, NameStub); + AsciiStrCatS (TestName, UNIT_TEST_MAX_STRING_LENGTH, MEMORY_TYPES[Index]); + + // Description of this test is DescriptionStub + Memory Type Name (from MEMORY_TYPES) + AsciiStrCatS (TestDescription, UNIT_TEST_MAX_STRING_LENGTH, DescriptionStub); + AsciiStrCatS (TestDescription, UNIT_TEST_MAX_STRING_LENGTH, MEMORY_TYPES[Index]); + + // Add the test case. This test case will only run if SmmPageGuardPreReq passes (which checks the protection policy for + // the memory type). + AddTestCase (TestSuite, TestDescription, TestName, SmmPageGuard, SmmPageGuardPreReq, NULL, MemoryProtectionContext); + + FreePool (TestName); + FreePool (TestDescription); + } else { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test creation failed.\n", __FUNCTION__)); + return; + } + } +} + +/** + MemoryProtectionTestAppEntryPoint + + @param[in] ImageHandle The firmware allocated handle for the EFI image. + @param[in] SystemTable A pointer to the EFI System Table. + + @retval EFI_SUCCESS The entry point executed successfully. + @retval other Some error occurred when executing this entry point. + +**/ +EFI_STATUS +EFIAPI +SmmMemoryProtectionTestAppEntryPoint ( + IN EFI_HANDLE ImageHandle, + IN EFI_SYSTEM_TABLE *SystemTable + ) +{ + EFI_STATUS Status; + UNIT_TEST_FRAMEWORK_HANDLE Fw = NULL; + UNIT_TEST_SUITE_HANDLE PageGuard = NULL; + UNIT_TEST_SUITE_HANDLE PoolGuard = NULL; + UNIT_TEST_SUITE_HANDLE Misc = NULL; + MEMORY_PROTECTION_TEST_CONTEXT *MemoryProtectionContext; + + DEBUG ((DEBUG_ERROR, "%a()\n", __FUNCTION__)); + + DEBUG ((DEBUG_ERROR, "%a v%a\n", UNIT_TEST_APP_NAME, UNIT_TEST_APP_VERSION)); + + MemoryProtectionContext = (MEMORY_PROTECTION_TEST_CONTEXT *)AllocateZeroPool (sizeof (MEMORY_PROTECTION_TEST_CONTEXT)); + if (MemoryProtectionContext == NULL) { + DEBUG ((DEBUG_ERROR, "%a - Allocating memory for test context failed.\n", __FUNCTION__)); + return EFI_OUT_OF_RESOURCES; + } + + LocateSmmCommonCommBuffer (); + + Status = FetchMemoryProtectionHobEntries (); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a - FetchMemoryProtectionHobEntries() failed. %r\n", __FUNCTION__, Status)); + goto EXIT; + } + + Status = RegisterMemoryProtectionTestAppInterruptHandler (); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a - RegisterMemoryProtectionTestAppInterruptHandler() failed. %r\n", __FUNCTION__, Status)); + goto EXIT; + } + + // Set up the test framework for running the tests. + Status = InitUnitTestFramework (&Fw, UNIT_TEST_APP_NAME, gEfiCallerBaseName, UNIT_TEST_APP_VERSION); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "Failed in InitUnitTestFramework. Status = %r\n", Status)); + goto EXIT; + } + + // Create separate test suites for Page, Pool, and NX tests. The Misc test suite is for stack guard + // and null pointer testing. + CreateUnitTestSuite (&Misc, Fw, "Stack Guard and Null Pointer Detection", "Security.HeapGuardMisc", NULL, NULL); + CreateUnitTestSuite (&PageGuard, Fw, "Page Guard Tests", "Security.PageGuard", NULL, NULL); + CreateUnitTestSuite (&PoolGuard, Fw, "Pool Guard Tests", "Security.PoolGuard", NULL, NULL); + + if ((PageGuard == NULL) || (PoolGuard == NULL) || (Misc == NULL)) { + DEBUG ((DEBUG_ERROR, "%a - Failed in CreateUnitTestSuite for TestSuite\n", __func__)); + Status = EFI_OUT_OF_RESOURCES; + goto EXIT; + } + + AddSmmPageTest (PageGuard); + AddSmmPoolTest (PoolGuard); + + // Add NULL protection to the Misc test suite. + AddTestCase (Misc, "Null pointer access in SMM should trigger a page fault", "Security.HeapGuardMisc.SmmNullPointerDetection", SmmNullPointerDetection, SmmNullPointerPreReq, NULL, MemoryProtectionContext); + + // Execute the tests. + Status = RunAllTestSuites (Fw); + +EXIT: + + if (Fw) { + FreeUnitTestFramework (Fw); + } + + if (MemoryProtectionContext) { + FreePool (MemoryProtectionContext); + } + + return Status; +} diff --git a/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/SmmMemoryProtectionTestApp.inf b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/SmmMemoryProtectionTestApp.inf new file mode 100644 index 0000000000..d94920a395 --- /dev/null +++ b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/SmmMemoryProtectionTestApp.inf @@ -0,0 +1,81 @@ +## @file SmmMemoryProtectionTestApp.inf +# +# Tests for page guard, pool guard, NX protections, and null pointer detection. +## +# Copyright (C) Microsoft Corporation. All rights reserved. +## SPDX-License-Identifier: BSD-2-Clause-Patent +## + +[Defines] + INF_VERSION = 0x00010017 + BASE_NAME = SmmMemoryProtectionTestApp + FILE_GUID = 64A62B1B-8487-466C-82BC-73EC289910A0 + VERSION_STRING = 3.0 + MODULE_TYPE = UEFI_APPLICATION + ENTRY_POINT = SmmMemoryProtectionTestAppEntryPoint + +# +# The following information is for reference only and not required by the build tools. +# +# VALID_ARCHITECTURES = X64 +# + +[Sources] + SmmMemoryProtectionTestApp.c + +[Sources.X64] + X64/X64Functions.c + +[Packages] + MdePkg/MdePkg.dec + MdeModulePkg/MdeModulePkg.dec + UefiCpuPkg/UefiCpuPkg.dec + UnitTestFrameworkPkg/UnitTestFrameworkPkg.dec + ShellPkg/ShellPkg.dec + MsCorePkg/MsCorePkg.dec + +[LibraryClasses] + DebugLib + UefiApplicationEntryPoint + UefiBootServicesTableLib + UnitTestLib + UnitTestPersistenceLib + UnitTestBootLib + PrintLib + MemoryAllocationLib + BaseLib + ShellLib + UefiLib + HobLib + ExceptionPersistenceLib + +[LibraryClasses.X64] + HwResetSystemLib + +[Guids] + gMemoryProtectionExceptionHandlerGuid ## CONSUMES + gEfiHobMemoryAllocStackGuid ## CONSUMES + +[Protocols] + gEfiSmmCommunicationProtocolGuid + gEfiCpuArchProtocolGuid ## CONSUMES + gMemoryProtectionNonstopModeProtocolGuid ## CONSUMES + gMemoryProtectionDebugProtocolGuid ## CONSUMES + gEfiMemoryAttributeProtocolGuid ## CONSUMES + gCpuMpDebugProtocolGuid ## CONSUMES + +[Guids] + gEdkiiPiSmmCommunicationRegionTableGuid + gEfiDebugImageInfoTableGuid ## SOMETIMES_CONSUMES ## GUID + gEfiMemoryAttributesTableGuid + gMmMemoryProtectionSettingsGuid + gDxeMemoryProtectionSettingsGuid + +[BuildOptions] + GCC:*_CLANG40WIN_AARCH64_CC_FLAGS = -Wno-infinite-recursion + MSFT:*_*_*_CC_FLAGS = /wd4054 /wd4055 /wd4717 + # An invalid opcode exception can be triggered during the NULL detection test on GCC5 builds + # due to the instruction "ud2" being inserted by the compiler after a NULL pointer dereference. + # Removing optimization prevents the invalid opcode instruction from being inserted and enables + # the interrupt handler to clear the fault and return to the test. + GCC:*_GCC5_X64_CC_FLAGS = -O0 diff --git a/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Driver/SmmMemoryProtectionTestDriver.c b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Driver/SmmMemoryProtectionTestDriver.c new file mode 100644 index 0000000000..629d9ca2aa --- /dev/null +++ b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Driver/SmmMemoryProtectionTestDriver.c @@ -0,0 +1,365 @@ +/** @file -- SmmMemoryProtectionTestDriver.c + +Tests for page guard, pool guard, and null pointer detection in SMM. + +Copyright (c) Microsoft Corporation. All rights reserved. +SPDX-License-Identifier: BSD-2-Clause-Patent + +**/ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "../MemoryProtectionTestCommon.h" + +// ============================================================================= +// TEST HELPERS +// ============================================================================= + +/** + + Trigger reboot on interrupt instead of hang. + +**/ +VOID +EnableExceptionTestMode ( + VOID + ) +{ + EFI_STATUS Status; + static SMM_EXCEPTION_TEST_PROTOCOL *SmmExceptionTestProtocol = NULL; + + // If we haven't found the protocol yet, do that now. + if (SmmExceptionTestProtocol == NULL) { + Status = gSmst->SmmLocateProtocol (&gSmmExceptionTestProtocolGuid, NULL, (VOID **)&SmmExceptionTestProtocol); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a - Failed to locate SmmExceptionTestProtocol! %r\n", __FUNCTION__, Status)); + SmmExceptionTestProtocol = NULL; + } + } + + // If we have, request test mode. + if (SmmExceptionTestProtocol != NULL) { + Status = SmmExceptionTestProtocol->EnableTestMode (); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a - Failed to enable test mode!\n", __FUNCTION__)); + } + } + + return; +} // EnableExceptionTestMode() + +// ============================================================================= +// TEST ASSETS +// These resources are used (and abused) by the test cases. +// ============================================================================= + +VOID +PoolTest ( + IN UINT64 *ptr, + IN UINT64 AllocationSize, + IN UINT8 Alignment + ) +{ + UINT64 *ptrLoc; + + DEBUG ((DEBUG_ERROR, "%a Allocated pool at 0x%p\n", __FUNCTION__, ptr)); + + // + // Check if guard page is going to be at the head or tail. + // + if ((Alignment == HEAP_GUARD_ALIGNED_TO_TAIL)) { + // + // Get to the beginning of the page the pool tail is on. + // + ptrLoc = (UINT64 *)(((UINTN)ptr) + (UINTN)AllocationSize); + ptrLoc = ALIGN_POINTER (ptrLoc, 0x1000); + + // + // The guard page will be on the next page. + // + ptr = (UINT64 *)(((UINTN)ptr) + 0x1000); + } else { + // + // Get to the beginning of the page the pool head is on. + // + ptrLoc = ALIGN_POINTER (ptr, 0x1000); + + // + // The guard page will be immediately preceding the page the pool starts on. + // + ptrLoc = (UINT64 *)(((UINTN)ptrLoc) - 0x1); + } + + DEBUG ((DEBUG_ERROR, "%a Writing to 0x%p\n", __FUNCTION__, ptrLoc)); + *ptrLoc = 1; + DEBUG ((DEBUG_ERROR, "%a failure \n", __FUNCTION__)); +} // PoolTest() + +VOID +HeadPageTest ( + IN UINT64 *ptr + ) +{ + DEBUG ((DEBUG_ERROR, "%a Allocated page at 0x%p\n", __FUNCTION__, ptr)); + + // Hit the head guard page + ptr = (UINT64 *)(((UINTN)ptr) - 0x1); + DEBUG ((DEBUG_ERROR, "%a Writing to 0x%p\n", __FUNCTION__, ptr)); + *ptr = 1; + + DEBUG ((DEBUG_ERROR, "%a failure \n", __FUNCTION__)); +} + +VOID +TailPageTest ( + IN UINT64 *ptr + ) +{ + DEBUG ((DEBUG_ERROR, "%a Allocated page at 0x%p\n", __FUNCTION__, ptr)); + + // Hit the tail guard page + ptr = (UINT64 *)(((UINTN)ptr) + 0x1000); + DEBUG ((DEBUG_ERROR, "%a Writing to 0x%p\n", __FUNCTION__, ptr)); + *ptr = 1; + DEBUG ((DEBUG_ERROR, "%a failure \n", __FUNCTION__)); +} // HeadPageTest() + +// ============================================================================= +// TEST CASES +// ============================================================================= + +/** + Page Guard + Tests to make sure accessing the guard page at the head and the guard page + at the tail result in a page fault. +**/ +VOID +SmmPageGuard ( + IN MEMORY_PROTECTION_TEST_CONTEXT *Context + ) +{ + EFI_PHYSICAL_ADDRESS ptr; + EFI_STATUS Status; + UINT64 MemoryType; + + DEBUG ((DEBUG_ERROR, "%a\n", __FUNCTION__)); + + // + // Memory type refers to the bitmask for the Heap Guard Page Type, + // we need to RShift 1 to get it to reflect the correct EFI_MEMORY_TYPE. + // + MemoryType = Context->TargetMemoryType; + MemoryType = RShiftU64 (MemoryType, 1); + Status = gBS->AllocatePages (AllocateAnyPages, (EFI_MEMORY_TYPE)MemoryType, 1, &ptr); + + // + // Context.TestProgress indicates progress within this specific test. + // 1 - Complete head guard test. + // 2 - Complete tail guard test. + // + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a Memory allocation failed for %x- %r\n", __FUNCTION__, MemoryType, Status)); + } else if (Context->TestProgress == 1) { + HeadPageTest ((UINT64 *)(UINTN)ptr); + DEBUG ((DEBUG_ERROR, "Head guard page failed.\n")); + } else { + TailPageTest ((UINT64 *)(UINTN)ptr); + DEBUG ((DEBUG_ERROR, "Tail guard page failed\n")); + } +} // SmmPageGuard() + +/** + Pool Guard + Tests to make sure accessing the guard page at the head/tail of the pool + triggers a page fault. +**/ +VOID +SmmPoolGuard ( + IN MEMORY_PROTECTION_TEST_CONTEXT *Context + ) +{ + EFI_PHYSICAL_ADDRESS ptr; + EFI_STATUS Status; + UINTN AllocationSize; + UINT64 MemoryType; + + DEBUG ((DEBUG_ERROR, "%a\n", __FUNCTION__)); + + // + // Memory type refers to the bitmask for the Heap Guard Page Type, + // we need to RShift 1 to get it to reflect the correct EFI_MEMORY_TYPE. + // + MemoryType = Context->TargetMemoryType; + MemoryType = RShiftU64 (MemoryType, 1); + + // + // Context.TestProgress indicates progress within this specific test. + // The test progressively allocates larger areas to test the guard on. + // These areas are defined in Pool.c as the 13 different sized chunks that are available + // for pool allocation. + // + AllocationSize = mPoolSizeTable[Context->TestProgress]; + Status = gBS->AllocatePool ((EFI_MEMORY_TYPE)MemoryType, AllocationSize, (VOID **)&ptr); + + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "%a Memory allocation failed for %x- %r\n", __FUNCTION__, MemoryType, Status)); + } else { + PoolTest ((UINT64 *)(UINTN)ptr, AllocationSize, Context->GuardAlignment); + DEBUG ((DEBUG_ERROR, "Pool test failed.")); + } +} // SmmPoolGuard() + +volatile MEMORY_PROTECTION_TEST_CONTEXT *mContext = NULL; + +/** + Null Pointer Detection + Test checks to make sure reading and writing from a null pointer + results in a page fault. +**/ +VOID +SmmNullPointerDetection ( + IN MEMORY_PROTECTION_TEST_CONTEXT *Context + ) +{ + if (Context->TestProgress == 1) { + if (mContext->TargetMemoryType == 0) { + } + } else { + mContext->TargetMemoryType = 1; + } + + DEBUG ((DEBUG_ERROR, "%a should have failed \n", __FUNCTION__)); +} // SmmNullPointerDetection() + +/** + Communication service SMI Handler entry. + + This handler takes requests to probe specific areas of memory and prove + whether the SMM memory protections are covering the expected regions. + + Caution: This function may receive untrusted input. + Communicate buffer and buffer size are external input, so this function will do basic validation. + + @param[in] DispatchHandle The unique handle assigned to this handler by SmiHandlerRegister(). + @param[in] RegisterContext Points to an optional handler context which was specified when the + handler was registered. + @param[in, out] CommBuffer A pointer to a collection of data in memory that will + be conveyed from a non-SMM environment into an SMM environment. + @param[in, out] CommBufferSize The size of the CommBuffer. + + @retval EFI_SUCCESS The interrupt was handled and quiesced. No other handlers + should still be called. + @retval EFI_UNSUPPORTED An unknown test function was requested. + @retval EFI_ACCESS_DENIED Part of the communication buffer lies in an invalid region. + +**/ +EFI_STATUS +EFIAPI +MemoryProtectionTestHandler ( + IN EFI_HANDLE DispatchHandle, + IN CONST VOID *RegisterContext, + IN OUT VOID *CommBuffer, + IN OUT UINTN *CommBufferSize + ) +{ + EFI_STATUS Status; + UINTN TempCommBufferSize; + MEMORY_PROTECTION_TEST_COMM_BUFFER *CommParams; + + DEBUG ((DEBUG_ERROR, "%a()\n", __FUNCTION__)); + + // + // If input is invalid, stop processing this SMI + // + if ((CommBuffer == NULL) || (CommBufferSize == NULL)) { + return EFI_SUCCESS; + } + + TempCommBufferSize = *CommBufferSize; + + if (TempCommBufferSize != sizeof (MEMORY_PROTECTION_TEST_COMM_BUFFER)) { + DEBUG ((DEBUG_ERROR, "%a: SMM Communication buffer size is invalid for this handler!\n", __FUNCTION__)); + return EFI_ACCESS_DENIED; + } + + if (!SmmIsBufferOutsideSmmValid ((UINTN)CommBuffer, TempCommBufferSize)) { + DEBUG ((DEBUG_ERROR, "%a: SMM Communication buffer in invalid location!\n", __FUNCTION__)); + return EFI_ACCESS_DENIED; + } + + // + // Farm out the job to individual functions based on what was requested. + // + CommParams = (MEMORY_PROTECTION_TEST_COMM_BUFFER *)CommBuffer; + CommParams->Status = EFI_SUCCESS; + Status = EFI_SUCCESS; + switch (CommParams->Function) { + case MEMORY_PROTECTION_TEST_POOL: + DEBUG ((DEBUG_ERROR, "%a - Function Requested - MEMORY_PROTECTION_TEST_POOL\n", __FUNCTION__)); + SmmPageGuard (&CommParams->Context); + break; + + case MEMORY_PROTECTION_TEST_PAGE: + DEBUG ((DEBUG_ERROR, "%a - Function Requested - MEMORY_PROTECTION_TEST_PAGE\n", __FUNCTION__)); + SmmPoolGuard (&CommParams->Context); + break; + + case MEMORY_PROTECTION_TEST_NULL_POINTER: + DEBUG ((DEBUG_ERROR, "%a - Function Requested - MEMORY_PROTECTION_TEST_NULL_POINTER\n", __FUNCTION__)); + SmmNullPointerDetection (&CommParams->Context); + break; + + default: + DEBUG ((DEBUG_INFO, "%a - Unknown function - %d\n", __FUNCTION__, CommParams->Function)); + Status = EFI_UNSUPPORTED; + break; + } + + return Status; +} // MemoryProtectionTestHandler() + +/** + The module Entry Point of the driver. + + @param[in] ImageHandle The firmware allocated handle for the EFI image. + @param[in] SystemTable A pointer to the EFI System Table. + + @retval EFI_SUCCESS The entry point is executed successfully. + @retval Other Some error occurs when executing this entry point. + +**/ +EFI_STATUS +EFIAPI +SmmMemoryProtectionTestDriverEntryPoint ( + IN EFI_HANDLE ImageHandle, + IN EFI_SYSTEM_TABLE *SystemTable + ) +{ + EFI_STATUS Status; + EFI_HANDLE DiscardedHandle; + + // + // Register SMI handler. + // + DiscardedHandle = NULL; + Status = gSmst->SmiHandlerRegister (MemoryProtectionTestHandler, &gMemoryProtectionTestSmiHandlerGuid, &DiscardedHandle); + ASSERT_EFI_ERROR (Status); + + return Status; +} // MemoryProtectionTestEntryPoint() diff --git a/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Driver/SmmMemoryProtectionTestDriver.inf b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Driver/SmmMemoryProtectionTestDriver.inf new file mode 100644 index 0000000000..ac4db06a5a --- /dev/null +++ b/UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Driver/SmmMemoryProtectionTestDriver.inf @@ -0,0 +1,50 @@ +## @file SmmMemoryProtectionTestDriver.inf +# +# Tests for page guard, pool guard, and null pointer detection in SMM. +## +# Copyright (C) Microsoft Corporation. All rights reserved. +## SPDX-License-Identifier: BSD-2-Clause-Patent +## + +[Defines] + INF_VERSION = 0x00010005 + BASE_NAME = SmmMemoryProtectionTestDriver + FILE_GUID = 4531C1F1-8358-42D8-8EB3-FD12622F63ED + MODULE_TYPE = DXE_SMM_DRIVER + VERSION_STRING = 1.0 + PI_SPECIFICATION_VERSION = 0x0001000A + ENTRY_POINT = SmmMemoryProtectionTestDriverEntryPoint + +# +# The following information is for reference only and not required by the build tools. +# +# VALID_ARCHITECTURES = IA32 X64 +# + + +[Sources] + SmmMemoryProtectionTestDriver.c + +[Packages] + MdePkg/MdePkg.dec + MdeModulePkg/MdeModulePkg.dec + UefiCpuPkg/UefiCpuPkg.dec + +[LibraryClasses] + UefiDriverEntryPoint + SmmServicesTableLib + DebugLib + SmmMemLib + PcdLib + BaseMemoryLib + UefiBootServicesTableLib + HobLib + + +[Protocols] + gSmmExceptionTestProtocolGuid ## CONSUMES +[Guids] + gMmMemoryProtectionSettingsGuid + +[Depex] + gSmmExceptionTestProtocolGuid diff --git a/UefiTestingPkg/UefiTestingPkg.dsc b/UefiTestingPkg/UefiTestingPkg.dsc index 2b5d0c70a7..efd8c1b438 100644 --- a/UefiTestingPkg/UefiTestingPkg.dsc +++ b/UefiTestingPkg/UefiTestingPkg.dsc @@ -138,7 +138,6 @@ UefiTestingPkg/AuditTests/DMAProtectionAudit/UEFI/DMAIVRSProtectionUnitTestApp.inf UefiTestingPkg/AuditTests/DMAProtectionAudit/UEFI/DMAVTdProtectionUnitTestApp.inf UefiTestingPkg/AuditTests/PagingAudit/UEFI/SmmPagingAuditDriver.inf - UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Smm/MemoryProtectionTestSmm.inf UefiTestingPkg/FunctionalSystemTests/SmmPagingProtectionsTest/App/SmmPagingProtectionsTestApp.inf UefiTestingPkg/FunctionalSystemTests/SmmPagingProtectionsTest/Smm/SmmPagingProtectionsTestSmm.inf UefiTestingPkg/FunctionalSystemTests/SmmPagingProtectionsTest/Smm/SmmPagingProtectionsTestStandaloneMm.inf @@ -151,7 +150,11 @@ UefiTestingPkg/AuditTests/PagingAudit/UEFI/DxePagingAuditDriver.inf UefiTestingPkg/AuditTests/PagingAudit/UEFI/DxePagingAuditTestApp.inf UefiTestingPkg/AuditTests/PagingAudit/UEFI/SmmPagingAuditTestApp.inf - UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/MemoryProtectionTestApp.inf + UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/MemoryProtectionTestApp.inf # DEPRECATED + UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Smm/MemoryProtectionTestSmm.inf # DEPRECATED + UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/SmmMemoryProtectionTestApp.inf + UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/App/DxeMemoryProtectionTestApp.inf + UefiTestingPkg/FunctionalSystemTests/MemoryProtectionTest/Driver/SmmMemoryProtectionTestDriver.inf [Components.AARCH64] # NOTE: These currently have source files that are only implemented for AARCH64. From 36cec67a40fe53f0986e838e1070fa255674ac84 Mon Sep 17 00:00:00 2001 From: kuqin12 <42554914+kuqin12@users.noreply.github.com> Date: Fri, 8 Dec 2023 12:35:34 -0800 Subject: [PATCH 04/12] Minor debug print updates for DxePagingAuditTestApp (#382) ## Description This change updated some print levels and added the PDB name for images being looked at. - [ ] Impacts functionality? - **Functionality** - Does the change ultimately impact how firmware functions? - Examples: Add a new library, publish a new PPI, update an algorithm, ... - [ ] Impacts security? - **Security** - Does the change have a direct security impact on an application, flow, or firmware? - Examples: Crypto algorithm change, buffer overflow fix, parameter validation improvement, ... - [ ] Breaking change? - **Breaking change** - Will anyone consuming this change experience a break in build or boot behavior? - Examples: Add a new library class, move a module to a different repo, call a function in a new library class in a pre-existing module, ... - [x] Includes tests? - **Tests** - Does the change include any explicit test code? - Examples: Unit tests, integration tests, robot tests, ... - [ ] Includes documentation? - **Documentation** - Does the change contain explicit documentation additions outside direct code modifications (and comments)? - Examples: Update readme file, add feature readme file, link to documentation on an a separate Web page, ... ## How This Was Tested Tested on proprietary virtual ARM platform and verified updated prints works as expected. ## Integration Instructions N/A --- .../UEFI/Dxe/App/DxePagingAuditTestApp.c | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/UefiTestingPkg/AuditTests/PagingAudit/UEFI/Dxe/App/DxePagingAuditTestApp.c b/UefiTestingPkg/AuditTests/PagingAudit/UEFI/Dxe/App/DxePagingAuditTestApp.c index bbfce0cf06..5be101ff0d 100644 --- a/UefiTestingPkg/AuditTests/PagingAudit/UEFI/Dxe/App/DxePagingAuditTestApp.c +++ b/UefiTestingPkg/AuditTests/PagingAudit/UEFI/Dxe/App/DxePagingAuditTestApp.c @@ -1211,6 +1211,7 @@ ImageCodeSectionsRoDataSectionsXp ( BOOLEAN TestFailure; UINT64 SectionStart; UINT64 SectionEnd; + CHAR8 *PdbFileName; DEBUG ((DEBUG_INFO, "%a Enter...\n", __FUNCTION__)); @@ -1243,6 +1244,17 @@ ImageCodeSectionsRoDataSectionsXp ( } // Check PE/COFF image + PdbFileName = PeCoffLoaderGetPdbPointer (LoadedImage->ImageBase); + if (PdbFileName == NULL) { + DEBUG (( + DEBUG_WARN, + "%a Could not get name of image loaded at 0x%llx - 0x%llx...\n", + __func__, + (UINTN)LoadedImage->ImageBase, + (UINTN)LoadedImage->ImageBase + LoadedImage->ImageSize + )); + } + DosHdr = (EFI_IMAGE_DOS_HEADER *)(UINTN)LoadedImage->ImageBase; PeCoffHeaderOffset = 0; if (DosHdr->e_magic == EFI_IMAGE_DOS_SIGNATURE) { @@ -1260,7 +1272,8 @@ ImageCodeSectionsRoDataSectionsXp ( if (!IsLoadedImageSectionAligned (SectionAlignment, LoadedImage->ImageCodeType)) { UT_LOG_ERROR ( - "Image 0x%llx - 0x%llx is not aligned\n", + "Image %a: 0x%llx - 0x%llx is not aligned\n", + PdbFileName, (UINTN)LoadedImage->ImageBase, (UINTN)LoadedImage->ImageBase + LoadedImage->ImageSize ); @@ -1287,7 +1300,8 @@ ImageCodeSectionsRoDataSectionsXp ( (EFI_IMAGE_SCN_CNT_INITIALIZED_DATA || EFI_IMAGE_SCN_CNT_UNINITIALIZED_DATA)) != 0)) { UT_LOG_ERROR ( - "Image Section 0x%llx-0x%llx contains code and data\n", + "Image %a: Section 0x%llx-0x%llx contains code and data\n", + PdbFileName, SectionStart, SectionEnd ); @@ -1313,7 +1327,8 @@ ImageCodeSectionsRoDataSectionsXp ( { if ((Attributes & EFI_MEMORY_RO) == 0) { UT_LOG_ERROR ( - "Image Section 0x%llx-0x%llx is not EFI_MEMORY_RO\n", + "Image %a: Section 0x%llx-0x%llx is not EFI_MEMORY_RO\n", + PdbFileName, SectionStart, SectionEnd ); @@ -1322,7 +1337,8 @@ ImageCodeSectionsRoDataSectionsXp ( } else { if ((Attributes & EFI_MEMORY_XP) == 0) { UT_LOG_ERROR ( - "Image Section 0x%llx-0x%llx is not EFI_MEMORY_XP\n", + "Image %a: Section 0x%llx-0x%llx is not EFI_MEMORY_XP\n", + PdbFileName, SectionStart, SectionEnd ); @@ -1377,7 +1393,7 @@ BspStackIsXpAndHasGuardPage ( StackBase = (EFI_PHYSICAL_ADDRESS)((MemoryHob->AllocDescriptor.MemoryBaseAddress / EFI_PAGE_SIZE) * EFI_PAGE_SIZE); StackLength = (EFI_PHYSICAL_ADDRESS)(EFI_PAGES_TO_SIZE (EFI_SIZE_TO_PAGES (MemoryHob->AllocDescriptor.MemoryLength))); - UT_LOG_INFO ("BSP stack located at 0x%llx - 0x%llx", StackBase, StackBase + StackLength); + UT_LOG_INFO ("BSP stack located at 0x%llx - 0x%llx\n", StackBase, StackBase + StackLength); Attributes = 0; Status = GetRegionCommonAccessAttributes ( @@ -1545,7 +1561,7 @@ DxePagingAuditTestAppEntryPoint ( ); if (EFI_ERROR (Status)) { - DEBUG ((DEBUG_INFO, "%a Could not retrieve command line args!\n", __FUNCTION__)); + DEBUG ((DEBUG_ERROR, "%a Could not retrieve command line args!\n", __FUNCTION__)); return EFI_PROTOCOL_ERROR; } @@ -1563,13 +1579,13 @@ DxePagingAuditTestAppEntryPoint ( } } else { if (StrnCmp (ShellParams->Argv[1], L"-h", MAX_CHARS_TO_READ) != 0) { - DEBUG ((DEBUG_INFO, "Invalid argument!\n")); + DEBUG ((DEBUG_ERROR, "Invalid argument!\n")); } - DEBUG ((DEBUG_INFO, "-h : Print available flags\n")); - DEBUG ((DEBUG_INFO, "-d : Dump the page table files\n")); - DEBUG ((DEBUG_INFO, "-r : Run the application tests\n")); - DEBUG ((DEBUG_INFO, "NOTE: Combined flags (i.e. -rd) is not supported\n")); + Print (L"-h : Print available flags\n"); + Print (L"-d : Dump the page table files\n"); + Print (L"-r : Run the application tests\n"); + Print (L"NOTE: Combined flags (i.e. -rd) is not supported\n"); } } From ef5218ab095e2d86b1903d62185ba9892607663f Mon Sep 17 00:00:00 2001 From: Taylor Beebe <31827475+TaylorBeebe@users.noreply.github.com> Date: Fri, 8 Dec 2023 12:56:20 -0800 Subject: [PATCH 05/12] DxePagingAudit: Update MemoryOutsideEfiMemoryMapIsInaccessible Test (#381) ## Description 1. MemoryOutsideEfiMemoryMapIsInaccessible checks if memory not present in the EFI memory map has the EFI_MEMORY_RP attribute. The previous version of this test assumed that the memory range spanned by the EFI memory map was contiguous which is sometimes not the case on platforms. This update changes the flow of the test to look at interstitial gaps in the memory map and not just those on the flanks. 2. The EFI memory map returned through the boot services table is sometimes out of order. This update sorts the memory map and memory space map whenever they're populated for a test. 3. The X64 MemoryOutsideEfiMemoryMapIsInaccessible HTML test had a typo which is fixed in this update. - [x] Impacts functionality? - **Functionality** - Does the change ultimately impact how firmware functions? - Examples: Add a new library, publish a new PPI, update an algorithm, ... - [ ] Impacts security? - **Security** - Does the change have a direct security impact on an application, flow, or firmware? - Examples: Crypto algorithm change, buffer overflow fix, parameter validation improvement, ... - [ ] Breaking change? - **Breaking change** - Will anyone consuming this change experience a break in build or boot behavior? - Examples: Add a new library class, move a module to a different repo, call a function in a new library class in a pre-existing module, ... - [x] Includes tests? - **Tests** - Does the change include any explicit test code? - Examples: Unit tests, integration tests, robot tests, ... - [ ] Includes documentation? - **Documentation** - Does the change contain explicit documentation additions outside direct code modifications (and comments)? - Examples: Update readme file, add feature readme file, link to documentation on an a separate Web page, ... ## How This Was Tested Tested on Q35 and ARM ## Integration Instructions N/A --- .../UEFI/Dxe/App/DxePagingAuditTestApp.c | 103 +++++++++++++----- .../PagingAudit/UEFI/PagingAuditCommon.c | 4 +- .../PagingAudit/UEFI/PagingAuditCommon.h | 32 ++++++ .../Windows/DxePaging_template_X64.html | 2 +- 4 files changed, 108 insertions(+), 33 deletions(-) diff --git a/UefiTestingPkg/AuditTests/PagingAudit/UEFI/Dxe/App/DxePagingAuditTestApp.c b/UefiTestingPkg/AuditTests/PagingAudit/UEFI/Dxe/App/DxePagingAuditTestApp.c index 5be101ff0d..16165bf622 100644 --- a/UefiTestingPkg/AuditTests/PagingAudit/UEFI/Dxe/App/DxePagingAuditTestApp.c +++ b/UefiTestingPkg/AuditTests/PagingAudit/UEFI/Dxe/App/DxePagingAuditTestApp.c @@ -353,6 +353,8 @@ PopulateMemorySpaceMap ( mMemorySpaceMap = NULL; } + SortMemorySpaceMap (mMemorySpaceMap, mMemorySpaceMapCount, sizeof (EFI_GCD_MEMORY_SPACE_DESCRIPTOR)); + return Status; } @@ -424,6 +426,8 @@ PopulateEfiMemoryMap ( } } while (Status == EFI_BUFFER_TOO_SMALL); + SortMemoryMap (mEfiMemoryMap, mEfiMemoryMapSize, mEfiMemoryMapDescriptorSize); + return Status; } @@ -1473,13 +1477,12 @@ MemoryOutsideEfiMemoryMapIsInaccessible ( { UINT64 StartOfAddressSpace; UINT64 EndOfAddressSpace; - UINT64 StartOfEfiMemoryMap; - UINT64 EndOfEfiMemoryMap; - EFI_MEMORY_DESCRIPTOR *FinalEfiMemoryMapDescriptor; - UINTN Index; + EFI_MEMORY_DESCRIPTOR *EndOfEfiMemoryMap; + EFI_MEMORY_DESCRIPTOR *CurrentEfiMemoryMapEntry; BOOLEAN TestFailure; - UINT64 StartOfMapEntry; - UINT64 EndOfMapEntry; + EFI_PHYSICAL_ADDRESS LastMemoryMapEntryEnd; + UINT64 Attributes; + EFI_STATUS Status; DEBUG ((DEBUG_INFO, "%a Enter...\n", __FUNCTION__)); @@ -1491,35 +1494,75 @@ MemoryOutsideEfiMemoryMapIsInaccessible ( UT_ASSERT_NOT_NULL (mMap.Entries); StartOfAddressSpace = mMemorySpaceMap[0].BaseAddress; - EndOfAddressSpace = mMemorySpaceMap[mMemorySpaceMapCount - 1].BaseAddress + mMemorySpaceMap[mMemorySpaceMapCount - 1].Length; - TestFailure = FALSE; - - StartOfEfiMemoryMap = mEfiMemoryMap->PhysicalStart; - FinalEfiMemoryMapDescriptor = (EFI_MEMORY_DESCRIPTOR *)(((UINT8 *)mEfiMemoryMap + mEfiMemoryMapSize) - mEfiMemoryMapDescriptorSize); - EndOfEfiMemoryMap = FinalEfiMemoryMapDescriptor->PhysicalStart + (FinalEfiMemoryMapDescriptor->NumberOfPages * EFI_PAGE_SIZE); - - for (Index = 0; Index < mMap.EntryCount; Index++) { - StartOfMapEntry = mMap.Entries[Index].LinearAddress; - EndOfMapEntry = mMap.Entries[Index].LinearAddress + mMap.Entries[Index].Length; - if (CHECK_OVERLAP (StartOfMapEntry, EndOfMapEntry, StartOfAddressSpace, StartOfEfiMemoryMap)) { - if (IsPageReadable (mMap.Entries[Index].PageEntry)) { - UT_LOG_ERROR ( - "Memory Range 0x%llx-0x%llx is accessible\n", - StartOfMapEntry, - EndOfMapEntry - ); - TestFailure = TRUE; - } - } else if (CHECK_OVERLAP (StartOfMapEntry, EndOfMapEntry, EndOfEfiMemoryMap, EndOfAddressSpace)) { - if (IsPageReadable (mMap.Entries[Index].PageEntry)) { + EndOfAddressSpace = mMemorySpaceMap[mMemorySpaceMapCount - 1].BaseAddress + + mMemorySpaceMap[mMemorySpaceMapCount - 1].Length; + TestFailure = FALSE; + EndOfEfiMemoryMap = (EFI_MEMORY_DESCRIPTOR *)(((UINT8 *)mEfiMemoryMap + mEfiMemoryMapSize)); + CurrentEfiMemoryMapEntry = mEfiMemoryMap; + + if (CurrentEfiMemoryMapEntry->PhysicalStart > StartOfAddressSpace) { + Attributes = 0; + Status = GetRegionCommonAccessAttributes ( + &mMap, + StartOfAddressSpace, + CurrentEfiMemoryMapEntry->PhysicalStart - StartOfAddressSpace, + &Attributes + ); + + if ((Status != EFI_NOT_FOUND) && ((Attributes & EFI_MEMORY_RP) == 0)) { + UT_LOG_ERROR ( + "Memory Range 0x%llx-0x%llx is not EFI_MEMORY_RP\n", + StartOfAddressSpace, + CurrentEfiMemoryMapEntry->PhysicalStart + ); + TestFailure = TRUE; + } + } + + LastMemoryMapEntryEnd = CurrentEfiMemoryMapEntry->PhysicalStart + + (CurrentEfiMemoryMapEntry->NumberOfPages * EFI_PAGE_SIZE); + CurrentEfiMemoryMapEntry = NEXT_MEMORY_DESCRIPTOR (CurrentEfiMemoryMapEntry, mEfiMemoryMapDescriptorSize); + + while ((UINTN)CurrentEfiMemoryMapEntry < (UINTN)EndOfEfiMemoryMap) { + if (CurrentEfiMemoryMapEntry->PhysicalStart > LastMemoryMapEntryEnd) { + Attributes = 0; + Status = GetRegionCommonAccessAttributes ( + &mMap, + LastMemoryMapEntryEnd, + CurrentEfiMemoryMapEntry->PhysicalStart - LastMemoryMapEntryEnd, + &Attributes + ); + if ((Status != EFI_NOT_FOUND) && ((Attributes & EFI_MEMORY_RP) == 0)) { UT_LOG_ERROR ( - "Memory Range 0x%llx-0x%llx is accessible\n", - StartOfMapEntry, - EndOfMapEntry + "Memory Range 0x%llx-0x%llx is not EFI_MEMORY_RP\n", + LastMemoryMapEntryEnd, + CurrentEfiMemoryMapEntry->PhysicalStart ); TestFailure = TRUE; } } + + LastMemoryMapEntryEnd = CurrentEfiMemoryMapEntry->PhysicalStart + + (CurrentEfiMemoryMapEntry->NumberOfPages * EFI_PAGE_SIZE); + CurrentEfiMemoryMapEntry = NEXT_MEMORY_DESCRIPTOR (CurrentEfiMemoryMapEntry, mEfiMemoryMapDescriptorSize); + } + + if (LastMemoryMapEntryEnd < EndOfAddressSpace) { + Attributes = 0; + Status = GetRegionCommonAccessAttributes ( + &mMap, + LastMemoryMapEntryEnd, + EndOfAddressSpace - LastMemoryMapEntryEnd, + &Attributes + ); + if ((Status != EFI_NOT_FOUND) && ((Attributes & EFI_MEMORY_RP) == 0)) { + UT_LOG_ERROR ( + "Memory Range 0x%llx-0x%llx is not EFI_MEMORY_RP\n", + LastMemoryMapEntryEnd, + EndOfAddressSpace + ); + TestFailure = TRUE; + } } UT_ASSERT_FALSE (TestFailure); diff --git a/UefiTestingPkg/AuditTests/PagingAudit/UEFI/PagingAuditCommon.c b/UefiTestingPkg/AuditTests/PagingAudit/UEFI/PagingAuditCommon.c index 4c6c4e261e..5fdcaa3a0e 100644 --- a/UefiTestingPkg/AuditTests/PagingAudit/UEFI/PagingAuditCommon.c +++ b/UefiTestingPkg/AuditTests/PagingAudit/UEFI/PagingAuditCommon.c @@ -387,8 +387,8 @@ MemoryAttributesTableDump ( @param[in] DescriptorSize Size, in bytes, of each descriptor region in the array NOTE: This is not sizeof (EFI_MEMORY_DESCRIPTOR) **/ -STATIC VOID +EFIAPI SortMemoryMap ( IN OUT EFI_MEMORY_DESCRIPTOR *MemoryMap, IN UINTN MemoryMapSize, @@ -428,8 +428,8 @@ SortMemoryMap ( @param[in] MemoryMapSize Size, in bytes, of the MemoryMap buffer @param[in] DescriptorSize Size, in bytes, of an individual EFI_GCD_MEMORY_SPACE_DESCRIPTOR **/ -STATIC VOID +EFIAPI SortMemorySpaceMap ( IN OUT EFI_GCD_MEMORY_SPACE_DESCRIPTOR *MemoryMap, IN UINTN MemoryMapSize, diff --git a/UefiTestingPkg/AuditTests/PagingAudit/UEFI/PagingAuditCommon.h b/UefiTestingPkg/AuditTests/PagingAudit/UEFI/PagingAuditCommon.h index 1400de68e5..8d74cd1656 100644 --- a/UefiTestingPkg/AuditTests/PagingAudit/UEFI/PagingAuditCommon.h +++ b/UefiTestingPkg/AuditTests/PagingAudit/UEFI/PagingAuditCommon.h @@ -200,4 +200,36 @@ DumpPlatforminfo ( VOID ); +/** + Sort memory map entries based upon PhysicalStart, from low to high. + + @param[in, out] MemoryMap A pointer to the buffer in which firmware places + the current memory map + @param[in] MemoryMapSize Size, in bytes, of the MemoryMap buffer + @param[in] DescriptorSize Size, in bytes, of each descriptor region in the array + NOTE: This is not sizeof (EFI_MEMORY_DESCRIPTOR) +**/ +VOID +EFIAPI +SortMemoryMap ( + IN OUT EFI_MEMORY_DESCRIPTOR *MemoryMap, + IN UINTN MemoryMapSize, + IN UINTN DescriptorSize + ); + +/** + Sort memory map entries based upon PhysicalStart, from low to high. + + @param[in, out] MemoryMap A pointer to the buffer containing the current memory map + @param[in] MemoryMapSize Size, in bytes, of the MemoryMap buffer + @param[in] DescriptorSize Size, in bytes, of an individual EFI_GCD_MEMORY_SPACE_DESCRIPTOR +**/ +VOID +EFIAPI +SortMemorySpaceMap ( + IN OUT EFI_GCD_MEMORY_SPACE_DESCRIPTOR *MemoryMap, + IN UINTN MemoryMapSize, + IN UINTN DescriptorSize + ); + #endif // _PAGING_AUDIT_COMMON_H_ diff --git a/UefiTestingPkg/AuditTests/PagingAudit/Windows/DxePaging_template_X64.html b/UefiTestingPkg/AuditTests/PagingAudit/Windows/DxePaging_template_X64.html index 8403a9c241..866e776c05 100644 --- a/UefiTestingPkg/AuditTests/PagingAudit/Windows/DxePaging_template_X64.html +++ b/UefiTestingPkg/AuditTests/PagingAudit/Windows/DxePaging_template_X64.html @@ -659,7 +659,7 @@

External Licenses

"Description": "Memory not in the EFI memory map should cause a fault if accessed", "Filter": function (mrObject) { var isTargetType = mrObject["Memory Type"] === "None"; - var hasInvalidAttributes = mrObject["Present"] !== "Yes"; + var hasInvalidAttributes = mrObject["Present"] !== "No"; return isTargetType && hasInvalidAttributes; }, //end of Filter function "ConfigureFilter": function () { From 045991a8b4cca4d4a27f187447873f8971232da3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 13:22:35 -0500 Subject: [PATCH 06/12] GitHub Action: Bump actions/setup-python from 4 to 5 (#383) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 78ec8b9c60..acc6180f4e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: uses: actions/checkout@v4 - name: Install Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.12' @@ -92,7 +92,7 @@ jobs: uses: actions/checkout@v4 - name: Install Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip' From c67a13efe7a932aba65a7fda7b3c2eebec06f71c Mon Sep 17 00:00:00 2001 From: kuqin12 <42554914+kuqin12@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:48:47 -0800 Subject: [PATCH 07/12] Adding policy check for advanced file logger (#384) # Preface Please ensure you have read the [contribution docs](https://github.com/microsoft/mu/blob/master/CONTRIBUTING.md) prior to submitting the pull request. In particular, [pull request guidelines](https://github.com/microsoft/mu/blob/master/CONTRIBUTING.md#pull-request-best-practices). ## Description This change added support for enablement/disablement of advanced file logger through policy services. Platforms intending to support this functionality should produce policy prior to the driver load. The default configuration is set to file logger enabled. For each item, place an "x" in between `[` and `]` if true. Example: `[x]`. _(you can also check items in the GitHub UI)_ - [x] Impacts functionality? - **Functionality** - Does the change ultimately impact how firmware functions? - Examples: Add a new library, publish a new PPI, update an algorithm, ... - [ ] Impacts security? - **Security** - Does the change have a direct security impact on an application, flow, or firmware? - Examples: Crypto algorithm change, buffer overflow fix, parameter validation improvement, ... - [ ] Breaking change? - **Breaking change** - Will anyone consuming this change experience a break in build or boot behavior? - Examples: Add a new library class, move a module to a different repo, call a function in a new library class in a pre-existing module, ... - [ ] Includes tests? - **Tests** - Does the change include any explicit test code? - Examples: Unit tests, integration tests, robot tests, ... - [x] Includes documentation? - **Documentation** - Does the change contain explicit documentation additions outside direct code modifications (and comments)? - Examples: Update readme file, add feature readme file, link to documentation on an a separate Web page, ... ## How This Was Tested This is tested on QEMU Q35 and verified that not creating policy will remain producing log files, and disabling the policy will prevent the driver from logging any files to ESP. ## Integration Instructions In platform code, use `SetPolicy` with `gAdvancedFileLoggerPolicyGuid` to produce a policy entry for file logger driver to consume. --- AdvLoggerPkg/AdvLoggerPkg.ci.yaml | 2 +- AdvLoggerPkg/AdvLoggerPkg.dec | 3 + AdvLoggerPkg/AdvLoggerPkg.dsc | 3 + .../AdvancedFileLogger/AdvancedFileLogger.c | 55 ++++++++++------- .../AdvancedFileLogger/AdvancedFileLogger.h | 2 + .../AdvancedFileLogger/AdvancedFileLogger.inf | 3 + AdvLoggerPkg/AdvancedFileLogger/ReadMe.md | 59 ++++++++++++++++++- .../Include/Guid/AdvancedFileLoggerPolicy.h | 29 +++++++++ 8 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 AdvLoggerPkg/Include/Guid/AdvancedFileLoggerPolicy.h diff --git a/AdvLoggerPkg/AdvLoggerPkg.ci.yaml b/AdvLoggerPkg/AdvLoggerPkg.ci.yaml index a7730b9235..b02dab1e8b 100644 --- a/AdvLoggerPkg/AdvLoggerPkg.ci.yaml +++ b/AdvLoggerPkg/AdvLoggerPkg.ci.yaml @@ -26,7 +26,7 @@ "MdeModulePkg/MdeModulePkg.dec", "AdvLoggerPkg/AdvLoggerPkg.dec", "MsWheaPkg/MsWheaPkg.dec", - "UnitTestFrameworkPkg/UnitTestFrameworkPkg.dec", + "PolicyServicePkg/PolicyServicePkg.dec", "ShellPkg/ShellPkg.dec" ], "AcceptableDependencies-HOST_APPLICATION":[ # for host based unit tests diff --git a/AdvLoggerPkg/AdvLoggerPkg.dec b/AdvLoggerPkg/AdvLoggerPkg.dec index eefeab624c..e7876b8712 100644 --- a/AdvLoggerPkg/AdvLoggerPkg.dec +++ b/AdvLoggerPkg/AdvLoggerPkg.dec @@ -46,6 +46,9 @@ # gAdvancedFileLoggerWriteLogFiles = { 0xc60e894d, 0x29a4, 0x4e0b, {0xb7, 0x44, 0x77, 0x84, 0x22, 0x6e, 0x81, 0x80 }} + ## GUID for Advanced file logger policy data + # + gAdvancedFileLoggerPolicyGuid = { 0x6c3fd4f1, 0xb596, 0x438e, { 0x8a, 0xb8, 0x53, 0xf1, 0xc, 0x9c, 0x27, 0x74 } } [Ppis] ## Advanced Logger Ppi - Communication from PEIM to PEI_CORE library implementation diff --git a/AdvLoggerPkg/AdvLoggerPkg.dsc b/AdvLoggerPkg/AdvLoggerPkg.dsc index 1500cef7c7..40e3865d87 100644 --- a/AdvLoggerPkg/AdvLoggerPkg.dsc +++ b/AdvLoggerPkg/AdvLoggerPkg.dsc @@ -87,6 +87,9 @@ [LibraryClasses.common.DXE_CORE] +[LibraryClasses.common.DXE_DRIVER] + PolicyLib|PolicyServicePkg/Library/DxePolicyLib/DxePolicyLib.inf + [LibraryClasses.common.DXE_SMM_DRIVER] AdvancedLoggerLib|AdvLoggerPkg/Library/AdvancedLoggerLib/Smm/AdvancedLoggerLib.inf SmmServicesTableLib|MdePkg/Library/SmmServicesTableLib/SmmServicesTableLib.inf diff --git a/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.c b/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.c index 9b3b8e0595..baa12673df 100644 --- a/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.c +++ b/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.c @@ -142,17 +142,17 @@ OnResetNotificationProtocolInstalled ( // // Register our reset notification request // - DEBUG ((DEBUG_INFO, "%a: Located Reset notification protocol. Registering handler\n", __FUNCTION__)); + DEBUG ((DEBUG_INFO, "%a: Located Reset notification protocol. Registering handler\n", __func__)); Status = ResetNotificationProtocol->RegisterResetNotify (ResetNotificationProtocol, OnResetNotification); if (EFI_ERROR (Status)) { - DEBUG ((DEBUG_ERROR, "%a: failed to register Reset Notification handler (%r)\n", __FUNCTION__, Status)); + DEBUG ((DEBUG_ERROR, "%a: failed to register Reset Notification handler (%r)\n", __func__, Status)); } if (Event != NULL) { gBS->CloseEvent (Event); } } else { - DEBUG ((DEBUG_ERROR, "%a: Unable to locate Reset Notification Protocol.\n", __FUNCTION__)); + DEBUG ((DEBUG_ERROR, "%a: Unable to locate Reset Notification Protocol.\n", __func__)); } return; @@ -183,7 +183,7 @@ RegisterLogDevice ( LogDevice = (LOG_DEVICE *)AllocateZeroPool (sizeof (LOG_DEVICE)); ASSERT (LogDevice); if (NULL == LogDevice) { - DEBUG ((DEBUG_ERROR, "%a: Out of memory:\n", __FUNCTION__)); + DEBUG ((DEBUG_ERROR, "%a: Out of memory:\n", __func__)); return; } @@ -233,7 +233,7 @@ OnFileSystemNotification ( EFI_HANDLE *HandleBuffer; EFI_STATUS Status; - DEBUG ((DEBUG_INFO, "%a: Entry...\n", __FUNCTION__)); + DEBUG ((DEBUG_INFO, "%a: Entry...\n", __func__)); for ( ; ;) { // @@ -256,7 +256,7 @@ OnFileSystemNotification ( // Spec says we only get one at a time using ByRegisterNotify ASSERT (HandleCount == 1); - DEBUG ((DEBUG_INFO, "%a: processing a potential log device on handle %p\n", __FUNCTION__, HandleBuffer[0])); + DEBUG ((DEBUG_INFO, "%a: processing a potential log device on handle %p\n", __func__, HandleBuffer[0])); RegisterLogDevice (HandleBuffer[0]); @@ -363,7 +363,7 @@ ProcessFileSystemRegistration ( ); if (EFI_ERROR (Status)) { - DEBUG ((DEBUG_ERROR, "%a: failed to create callback event (%r)\n", __FUNCTION__, Status)); + DEBUG ((DEBUG_ERROR, "%a: failed to create callback event (%r)\n", __func__, Status)); goto Cleanup; } @@ -374,7 +374,7 @@ ProcessFileSystemRegistration ( ); if (EFI_ERROR (Status)) { - DEBUG ((DEBUG_ERROR, "%a: failed to register for file system notifications (%r)\n", __FUNCTION__, Status)); + DEBUG ((DEBUG_ERROR, "%a: failed to register for file system notifications (%r)\n", __func__, Status)); gBS->CloseEvent (FileSystemCallBackEvent); goto Cleanup; } @@ -424,13 +424,13 @@ ProcessResetEventRegistration ( // // Register our reset notification request // - DEBUG ((DEBUG_INFO, "%a: Located Reset notification protocol. Registering handler\n", __FUNCTION__)); + DEBUG ((DEBUG_INFO, "%a: Located Reset notification protocol. Registering handler\n", __func__)); Status = ResetNotificationProtocol->RegisterResetNotify (ResetNotificationProtocol, OnResetNotification); if (EFI_ERROR (Status)) { - DEBUG ((DEBUG_ERROR, "%a: failed to register Reset Notification handler (%r)\n", __FUNCTION__, Status)); + DEBUG ((DEBUG_ERROR, "%a: failed to register Reset Notification handler (%r)\n", __func__, Status)); } } else { - DEBUG ((DEBUG_INFO, "%a: Reset Notification protocol not installed. Registering for notification\n", __FUNCTION__)); + DEBUG ((DEBUG_INFO, "%a: Reset Notification protocol not installed. Registering for notification\n", __func__)); Status = gBS->CreateEvent ( EVT_NOTIFY_SIGNAL, TPL_CALLBACK, @@ -440,7 +440,7 @@ ProcessResetEventRegistration ( ); if (EFI_ERROR (Status)) { - DEBUG ((DEBUG_ERROR, "%a: failed to create Reset Protocol protocol callback event (%r)\n", __FUNCTION__, Status)); + DEBUG ((DEBUG_ERROR, "%a: failed to create Reset Protocol protocol callback event (%r)\n", __func__, Status)); } else { Status = gBS->RegisterProtocolNotify ( &gEdkiiPlatformSpecificResetFilterProtocolGuid, @@ -449,7 +449,7 @@ ProcessResetEventRegistration ( ); if (EFI_ERROR (Status)) { - DEBUG ((DEBUG_ERROR, "%a: failed to register for Reset Protocol notification (%r)\n", __FUNCTION__, Status)); + DEBUG ((DEBUG_ERROR, "%a: failed to register for Reset Protocol notification (%r)\n", __func__, Status)); gBS->CloseEvent (ResetNotificationEvent); } } @@ -491,7 +491,7 @@ ProcessSyncRequestRegistration ( ); if (EFI_ERROR (Status)) { - DEBUG ((DEBUG_ERROR, "%a - Create Event Ex for file logger write. Code = %r\n", __FUNCTION__, Status)); + DEBUG ((DEBUG_ERROR, "%a - Create Event Ex for file logger write. Code = %r\n", __func__, Status)); } return Status; @@ -534,7 +534,7 @@ ProcessReadyToBootRegistration ( ); if (EFI_ERROR (Status)) { - DEBUG ((DEBUG_ERROR, "%a - Create Event Ex for ReadyToBoot. Code = %r\n", __FUNCTION__, Status)); + DEBUG ((DEBUG_ERROR, "%a - Create Event Ex for ReadyToBoot. Code = %r\n", __func__, Status)); } } @@ -577,7 +577,7 @@ ProcessPreExitBootServicesRegistration ( ); if (EFI_ERROR (Status)) { - DEBUG ((DEBUG_ERROR, "%a - Create Event Ex for ExitBootServices. Code = %r\n", __FUNCTION__, Status)); + DEBUG ((DEBUG_ERROR, "%a - Create Event Ex for ExitBootServices. Code = %r\n", __func__, Status)); } } @@ -601,9 +601,24 @@ AdvancedFileLoggerEntry ( IN EFI_SYSTEM_TABLE *SystemTable ) { - EFI_STATUS Status; + EFI_STATUS Status; + ADVANCED_FILE_LOGGER_POLICY AdvFileLoggerPolicy; + UINT16 PolicySize = ADVANCED_FILE_LOGGER_POLICY_SIZE; + + DEBUG ((DEBUG_INFO, "%a: enter...\n", __func__)); + + // Step 0. Check advanced file logger policy, default to enabled. - DEBUG ((DEBUG_INFO, "%a: enter...\n", __FUNCTION__)); + Status = GetPolicy (&gAdvancedFileLoggerPolicyGuid, NULL, (VOID *)&AdvFileLoggerPolicy, &PolicySize); + if (EFI_ERROR (Status)) { + // If we fail to get policy, default to enabled. + DEBUG ((DEBUG_WARN, "%a: Unable to get file logger - %r defaulting to enabled!\n", __func__, Status)); + } else if (AdvFileLoggerPolicy.FileLoggerEnable == FALSE) { + DEBUG ((DEBUG_INFO, "%a: File logger disabled in policy, exiting.\n", __func__)); + return EFI_SUCCESS; + } else { + DEBUG ((DEBUG_INFO, "%a: File logger enabled in policy.\n", __func__)); + } // // Step 1. Register for file system notifications @@ -645,9 +660,9 @@ AdvancedFileLoggerEntry ( Exit: if (EFI_ERROR (Status)) { - DEBUG ((DEBUG_ERROR, "%a: Leaving, code = %r\n", __FUNCTION__, Status)); + DEBUG ((DEBUG_ERROR, "%a: Leaving, code = %r\n", __func__, Status)); } else { - DEBUG ((DEBUG_INFO, "%a: Leaving, code = %r\n", __FUNCTION__, Status)); + DEBUG ((DEBUG_INFO, "%a: Leaving, code = %r\n", __func__, Status)); } // Always return EFI_SUCCESS. This means any partial registration of functions diff --git a/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.h b/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.h index 854935b534..922f0b7d8b 100644 --- a/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.h +++ b/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.h @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -34,6 +35,7 @@ #include #include #include +#include #define LOG_DEVICE_SIGNATURE SIGNATURE_32('D','L','o','g') diff --git a/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.inf b/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.inf index ac12d4b710..19a27e3a0b 100644 --- a/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.inf +++ b/AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.inf @@ -30,6 +30,7 @@ MdePkg/MdePkg.dec MdeModulePkg/MdeModulePkg.dec AdvLoggerPkg/AdvLoggerPkg.dec + PolicyServicePkg/PolicyServicePkg.dec [LibraryClasses] AdvancedLoggerAccessLib @@ -46,11 +47,13 @@ TimerLib UefiDriverEntryPoint UefiRuntimeServicesTableLib + PolicyLib [Guids] gAdvancedFileLoggerWriteLogFiles gEfiEventReadyToBootGuid gMuEventPreExitBootServicesGuid + gAdvancedFileLoggerPolicyGuid [Protocols] gEdkiiPlatformSpecificResetFilterProtocolGuid ## CONSUMES diff --git a/AdvLoggerPkg/AdvancedFileLogger/ReadMe.md b/AdvLoggerPkg/AdvancedFileLogger/ReadMe.md index 4a87771ed0..8619bba6c4 100644 --- a/AdvLoggerPkg/AdvancedFileLogger/ReadMe.md +++ b/AdvLoggerPkg/AdvancedFileLogger/ReadMe.md @@ -6,7 +6,7 @@ The Advanced File Logger monitors for file systems mounted during boot. When an eligible file system is detected, the log is flushed to the file system. The log is flushed if the system is reset during POST, and at Exit Boot Services. -An eligible file system is one with a Logs directory in the root of the file system. +An eligible file system is one with a `UefiLogs` directory in the root of the file system. If no log files are present, the Advanced File Logger will create a log index file which contains the index of the last log file written, and nine log files each PcdAdvancedLoggerPages in size. These files are pre allocated at one time to reduce interference with other users of the filesystem. @@ -25,6 +25,63 @@ and the follow change is needed in the .fdf: INF AdvLoggerPkg/AdvancedFileLogger/AdvancedFileLogger.inf ``` +## Driver Usage Configuration + +This section describes the configuration options of the Advanced File Logger driver. + +### Advanced file logger enforcement + +The Advanced File Logger can be configured to enforce the storing of memory logs when setting +`PcdAdvancedFileLoggerForceEnable` to `TRUE`. + +### Advanced file logger enlightenment + +The Advanced File Logger can be set to store memory logs with a `UefiLogs` directory in the ESP partition. +Without such a directory **AND** the enforcement is not enabled, the Advanced File Logger will not store +memory logs. + +### Advanced file logger enablement through Policy + +The Advanced File Logger can be configured to be enabled through policy service by producing a policy under[`gAdvancedFileLoggerPolicyGuid`](../Include/Guid/AdvancedFileLoggerPolicy.h) +and setting the `FileLoggerEnable` field to `TRUE`. For the sake of backwards compatibility, if a platform does not +produce such policy or not support policy services at all, the Advanced File Logger will default to be enabled. + +Note: The above enablement, enforcement and enlightenment are in serial, the general pseudo code is as follows: + +```c + if (PolicyNotFound) { + Enabled = TRUE; + } else { + Enabled = Policy.FileLoggerEnable; + } + + if (Enable) { + if (PcdGet (PcdAdvancedFileLoggerForceEnable)) { + StoreLogs = TRUE; + } else if (DirectoryExists (L"UefiLogs")) { + StoreLogs = TRUE; + } else { + StoreLogs = FALSE; + } + } + + if (StoreLogs) { + // Store logs + // The UEFI_Index.txt file will indicate the last log file written + StoreLogs ("UefiLogs/Log#.txt"); + } +``` + +### Advanced file logger Flush events + +The advanced file logger can be flushed or configured to flush on the following events: + +| Event | Description | +| --- | --- | +| `gEfiEventReadyToBootGuid` | By setting `BIT0` of `PcdAdvancedFileLoggerFlush`, logger will flushed to ESP at "Ready To Boot" event | +| `gEfiEventExitBootServicesGuid` | By setting `BIT1` of `PcdAdvancedFileLoggerFlush`, logger will flushed to ESP at "Exit Boot Services" event | +| System Reset | This will always be enabled as long as the system supports `gEdkiiPlatformSpecificResetFilterProtocolGuid` | + --- ## Copyright diff --git a/AdvLoggerPkg/Include/Guid/AdvancedFileLoggerPolicy.h b/AdvLoggerPkg/Include/Guid/AdvancedFileLoggerPolicy.h new file mode 100644 index 0000000000..271b5a7e3e --- /dev/null +++ b/AdvLoggerPkg/Include/Guid/AdvancedFileLoggerPolicy.h @@ -0,0 +1,29 @@ +/** @file + This file defines GUIDs and data structure used for Advanced file logger policy. + + Copyright (C) Microsoft Corporation. + SPDX-License-Identifier: BSD-2-Clause-Patent + +**/ + +#ifndef ADVANCED_FILE_LOGGER_POLICY_H_ +#define ADVANCED_FILE_LOGGER_POLICY_H_ + +#define ADVANCED_FILE_LOGGER_POLICY_GUID \ + { \ + 0x6c3fd4f1, 0xb596, 0x438e, { 0x8a, 0xb8, 0x53, 0xf1, 0xc, 0x9c, 0x27, 0x74 } \ + } + +#define ADVANCED_FILE_LOGGER_POLICY_SIZE sizeof(ADVANCED_FILE_LOGGER_POLICY) + +#pragma pack(1) + +typedef struct { + BOOLEAN FileLoggerEnable; +} ADVANCED_FILE_LOGGER_POLICY; + +#pragma pack() + +extern EFI_GUID gAdvancedFileLoggerPolicyGuid; + +#endif //ADVANCED_FILE_LOGGER_POLICY_H_ From 3d3b828364b087bb79ea208988b1ba9176dbf34b Mon Sep 17 00:00:00 2001 From: Michael Kubacki Date: Tue, 12 Dec 2023 14:42:15 -0500 Subject: [PATCH 08/12] Add header guards missing in some files Some header files, such as those which define structures or classes, cannot be included more than once within a translation unit, as doing so would cause a redefinition error. Such headers must be guarded to prevent ill-effects from multiple inclusion. Similarly, if header files include other header files, and this inclusion graph contains a cycle, then at least one file within the cycle must contain header guards in order to break the cycle. Because of cases like these, all headers should be guarded as a matter of good practice, even if they do not strictly need to be. Furthermore, most modern compilers contain optimizations which are triggered by header guards. If the header guard strictly conforms to the pattern that compilers expect, then inclusions of that header other than the first have absolutely no effect: the file isn't re-read from disk, nor is it re-tokenised or re-preprocessed. This can result in a noticeable, albeit minor, improvement to compilation time. Signed-off-by: Michael Kubacki --- MsCorePkg/Include/Library/DeviceSpecificBusInfoLib.h | 5 +++++ MsWheaPkg/HwhMenu/CreatorIDParser.h | 5 +++++ MsWheaPkg/HwhMenu/PlatformIDParser.h | 5 +++++ MsWheaPkg/Include/Library/CheckHwErrRecHeaderLib.h | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/MsCorePkg/Include/Library/DeviceSpecificBusInfoLib.h b/MsCorePkg/Include/Library/DeviceSpecificBusInfoLib.h index add2eefc89..25779a5711 100644 --- a/MsCorePkg/Include/Library/DeviceSpecificBusInfoLib.h +++ b/MsCorePkg/Include/Library/DeviceSpecificBusInfoLib.h @@ -12,6 +12,9 @@ SPDX-License-Identifier: BSD-2-Clause-Patent **/ +#ifndef DEVICE_SPECIFIC_BUS_INFO_LIB_H_ +#define DEVICE_SPECIFIC_BUS_INFO_LIB_H_ + typedef enum { Ignore, // Do not check link speed Gen1, // 2.5 GT/s @@ -72,3 +75,5 @@ ProcessPciDeviceResults ( IN UINTN ResultCount, IN DEVICE_PCI_CHECK_RESULT *Results ); + +#endif diff --git a/MsWheaPkg/HwhMenu/CreatorIDParser.h b/MsWheaPkg/HwhMenu/CreatorIDParser.h index 1c96655909..ed5204f28f 100644 --- a/MsWheaPkg/HwhMenu/CreatorIDParser.h +++ b/MsWheaPkg/HwhMenu/CreatorIDParser.h @@ -8,6 +8,9 @@ Copyright (c) Microsoft Corporation. All rights reserved. SPDX-License-Identifier: BSD-2-Clause-Patent **/ +#ifndef CREATOR_ID_PARSER_H_ +#define CREATOR_ID_PARSER_H_ + /** * Parses the Creator ID which, for now, just prints the GUID * @@ -19,3 +22,5 @@ VOID ParseCreatorID ( IN CONST EFI_GUID *CreatorID ); + +#endif diff --git a/MsWheaPkg/HwhMenu/PlatformIDParser.h b/MsWheaPkg/HwhMenu/PlatformIDParser.h index 0da55f7f35..8a198b99c0 100644 --- a/MsWheaPkg/HwhMenu/PlatformIDParser.h +++ b/MsWheaPkg/HwhMenu/PlatformIDParser.h @@ -9,6 +9,9 @@ SPDX-License-Identifier: BSD-2-Clause-Patent **/ +#ifndef PLATFORM_ID_PARSER_H_ +#define PLATFORM_ID_PARSER_H_ + /** * Parses the Platform/Source ID * @@ -20,3 +23,5 @@ VOID ParseSourceID ( IN CONST EFI_GUID *SourceID ); + +#endif diff --git a/MsWheaPkg/Include/Library/CheckHwErrRecHeaderLib.h b/MsWheaPkg/Include/Library/CheckHwErrRecHeaderLib.h index f88e9e963d..883cc78661 100644 --- a/MsWheaPkg/Include/Library/CheckHwErrRecHeaderLib.h +++ b/MsWheaPkg/Include/Library/CheckHwErrRecHeaderLib.h @@ -4,6 +4,9 @@ Copyright (C) Microsoft Corporation. All rights reserved. SPDX-License-Identifier: BSD-2-Clause-Patent **/ +#ifndef CHECK_HW_ERR_REC_HEADER_LIB_H_ +#define CHECK_HW_ERR_REC_HEADER_LIB_H_ + /** * Checks that all length and offset fields within the HWErrRec fall within the bounds of * the buffer, all section data is accounted for in their respective section headers, and that @@ -21,3 +24,5 @@ ValidateCperHeader ( IN CONST EFI_COMMON_ERROR_RECORD_HEADER *Err, IN CONST UINTN Size ); + +#endif From fd620ac233d2862d4ab22af3ac4832976b665c4b Mon Sep 17 00:00:00 2001 From: Michael Kubacki Date: Tue, 12 Dec 2023 14:44:26 -0500 Subject: [PATCH 09/12] MsWheaPkg/MsWheaEarlyStorageLib: Remove unused static function Removes `MsWheaCMOSStoreClearAll()` which is scoped to the file and not used. Improves code readability and maintenance. Signed-off-by: Michael Kubacki --- .../MsWheaEarlyStorageLib/MsWheaEarlyStorageLib.c | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/MsWheaPkg/Library/MsWheaEarlyStorageLib/MsWheaEarlyStorageLib.c b/MsWheaPkg/Library/MsWheaEarlyStorageLib/MsWheaEarlyStorageLib.c index 85ec293f82..d38d01f97c 100644 --- a/MsWheaPkg/Library/MsWheaEarlyStorageLib/MsWheaEarlyStorageLib.c +++ b/MsWheaPkg/Library/MsWheaEarlyStorageLib/MsWheaEarlyStorageLib.c @@ -176,20 +176,6 @@ __MsWheaCMOSRawClear ( /** -This routine clears all bytes in Data region - -**/ -STATIC -VOID -MsWheaCMOSStoreClearAll ( - VOID - ) -{ - __MsWheaCMOSRawClear (MsWheaEarlyStorageGetMaxSize (), MS_WHEA_EARLY_STORAGE_OFFSET); -} - -/** - This routine returns the maximum number of bytes that can be stored in the early storage area. @retval Count The maximum number of bytes that can be stored in the MS WHEA store. From d9ed9de9e2d6358963256b5442e2d84fe76b749a Mon Sep 17 00:00:00 2001 From: Michael Kubacki Date: Tue, 12 Dec 2023 14:49:53 -0500 Subject: [PATCH 10/12] MsWheaPkg/MsWheaEarlyStorageLib: Remove unsigned comparisons to zero Removes an unnecessary condition since unsigned values are always greater than or equal to 0. Signed-off-by: Michael Kubacki --- .../Library/MsWheaEarlyStorageLib/MsWheaEarlyStorageLib.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MsWheaPkg/Library/MsWheaEarlyStorageLib/MsWheaEarlyStorageLib.c b/MsWheaPkg/Library/MsWheaEarlyStorageLib/MsWheaEarlyStorageLib.c index d38d01f97c..8ac6c527ea 100644 --- a/MsWheaPkg/Library/MsWheaEarlyStorageLib/MsWheaEarlyStorageLib.c +++ b/MsWheaPkg/Library/MsWheaEarlyStorageLib/MsWheaEarlyStorageLib.c @@ -63,7 +63,7 @@ __MsWheaCMOSRawRead ( for (i = 0; i < Size; i++) { mIndex = Offset + i; - if ((mIndex >= 0) && (mIndex <= 127)) { + if (mIndex <= 127) { IoWrite8 (PCAT_RTC_LO_ADDRESS_PORT, mIndex); mBuf[i] = IoRead8 (PCAT_RTC_LO_DATA_PORT); } else { @@ -114,7 +114,7 @@ __MsWheaCMOSRawWrite ( for (i = 0; i < Size; i++) { mIndex = Offset + i; - if ((mIndex >= 0) && (mIndex <= 127)) { + if (mIndex <= 127) { IoWrite8 (PCAT_RTC_LO_ADDRESS_PORT, mIndex); IoWrite8 (PCAT_RTC_LO_DATA_PORT, mBuf[i]); } else { @@ -160,7 +160,7 @@ __MsWheaCMOSRawClear ( for (i = 0; i < Size; i++) { mIndex = Offset + i; - if ((mIndex >= 0) && (mIndex <= 127)) { + if (mIndex <= 127) { IoWrite8 (PCAT_RTC_LO_ADDRESS_PORT, mIndex); IoWrite8 (PCAT_RTC_LO_DATA_PORT, PcdGet8 (PcdMsWheaEarlyStorageDefaultValue)); } else { From bb8910fe1004e30cc0504611b40155a7a7c18fd5 Mon Sep 17 00:00:00 2001 From: Michael Kubacki Date: Tue, 12 Dec 2023 21:01:02 -0500 Subject: [PATCH 11/12] MsGraphicsPkg/SimpleUIToolKit: Move array bounds check before access (#386) ## Description Checks the index is within expected bounds before accessing the array. - [ ] Impacts functionality? - **Functionality** - Does the change ultimately impact how firmware functions? - Examples: Add a new library, publish a new PPI, update an algorithm, ... - [ ] Impacts security? - **Security** - Does the change have a direct security impact on an application, flow, or firmware? - Examples: Crypto algorithm change, buffer overflow fix, parameter validation improvement, ... - [ ] Breaking change? - **Breaking change** - Will anyone consuming this change experience a break in build or boot behavior? - Examples: Add a new library class, move a module to a different repo, call a function in a new library class in a pre-existing module, ... - [ ] Includes tests? - **Tests** - Does the change include any explicit test code? - Examples: Unit tests, integration tests, robot tests, ... - [ ] Includes documentation? - **Documentation** - Does the change contain explicit documentation additions outside direct code modifications (and comments)? - Examples: Update readme file, add feature readme file, link to documentation on an a separate Web page, ... ## How This Was Tested CodeQL and package compilation. ## Integration Instructions N/A Signed-off-by: Michael Kubacki --- MsGraphicsPkg/Library/SimpleUIToolKit/ListBox.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MsGraphicsPkg/Library/SimpleUIToolKit/ListBox.c b/MsGraphicsPkg/Library/SimpleUIToolKit/ListBox.c index cbd8c23aea..249810a221 100644 --- a/MsGraphicsPkg/Library/SimpleUIToolKit/ListBox.c +++ b/MsGraphicsPkg/Library/SimpleUIToolKit/ListBox.c @@ -946,7 +946,7 @@ Ctor ( // Iterate through cell list and compute bounding rectangle, checkbox bounding rectangle (optional) and string bitmap rectangle. // - for (Index = 0; CellData[Index].CellText != NULL && Index < this->m_NumberOfCells; Index++) { + for (Index = 0; Index < this->m_NumberOfCells && CellData[Index].CellText != NULL; Index++) { this->m_pCells[Index].OriginalOrder = Index; this->m_pCells[Index].pCellText = AllocateCopyPool (StrSize (CellData[Index].CellText), CellData[Index].CellText); this->m_pCells[Index].CheckboxSelected = CellData[Index].CheckBoxSelected; From 298478fa554538131cc88ed00fafe8fac6bfe852 Mon Sep 17 00:00:00 2001 From: John Schock Date: Wed, 13 Dec 2023 18:55:47 -0800 Subject: [PATCH 12/12] Implement a new version of UefiHidDxeV2 with significant improvements to unit test capability and general architecture. (#374) ## Description This PR introduces a "v2" implementation of UefiHidDxe that has significant refactoring to support unit tests and architectural improvements. - UefiHidDxeV2 code coverage with unit tests is ~80% or greater for all modules of the UefiHidDxe crate - Better separation between FFI layers and native rust layers The long term plan is to eventually remove the original UefiHidDxe in favor of this implementation. - [x] Impacts functionality? - Driver provides the same functionality as UefiHidDxe which should be considered deprecated going forward. - [ ] Impacts security? - [ ] Breaking change? - [x] Includes tests? - Includes Rust unit tests covering all new modules. - [x] Includes documentation? - Standard rustdocs included. ## How This Was Tested Tested under QEMU using simulated USB keyboard and mouse. ## Integration Instructions UefiHidDxeV2 is a drop-in replacement for UefiHidDxe; platforms that wish to move to the new implementation can do so by pulling in the new UefiHidDxeV2 and adding the necessary mockall crate to their workspace Cargo.toml. --- .../Crates/RustAdvancedLoggerDxe/src/lib.rs | 6 +- Cargo.toml | 8 +- HidPkg/Crates/HidIo/src/lib.rs | 1 + HidPkg/HidPkg.dsc | 1 + HidPkg/UefiHidDxeV2/Cargo.toml | 33 + HidPkg/UefiHidDxeV2/UefiHidDxeV2.inf | 22 + HidPkg/UefiHidDxeV2/src/boot_services.rs | 369 +++++ HidPkg/UefiHidDxeV2/src/driver_binding.rs | 322 ++++ HidPkg/UefiHidDxeV2/src/hid.rs | 422 ++++++ HidPkg/UefiHidDxeV2/src/hid_io.rs | 460 ++++++ HidPkg/UefiHidDxeV2/src/keyboard/key_queue.rs | 731 +++++++++ HidPkg/UefiHidDxeV2/src/keyboard/mod.rs | 1328 +++++++++++++++++ .../src/keyboard/simple_text_in.rs | 639 ++++++++ .../src/keyboard/simple_text_in_ex.rs | 1014 +++++++++++++ HidPkg/UefiHidDxeV2/src/lib.rs | 41 + HidPkg/UefiHidDxeV2/src/main.rs | 90 ++ .../src/pointer/absolute_pointer.rs | 597 ++++++++ HidPkg/UefiHidDxeV2/src/pointer/mod.rs | 668 +++++++++ 18 files changed, 6745 insertions(+), 7 deletions(-) create mode 100644 HidPkg/UefiHidDxeV2/Cargo.toml create mode 100644 HidPkg/UefiHidDxeV2/UefiHidDxeV2.inf create mode 100644 HidPkg/UefiHidDxeV2/src/boot_services.rs create mode 100644 HidPkg/UefiHidDxeV2/src/driver_binding.rs create mode 100644 HidPkg/UefiHidDxeV2/src/hid.rs create mode 100644 HidPkg/UefiHidDxeV2/src/hid_io.rs create mode 100644 HidPkg/UefiHidDxeV2/src/keyboard/key_queue.rs create mode 100644 HidPkg/UefiHidDxeV2/src/keyboard/mod.rs create mode 100644 HidPkg/UefiHidDxeV2/src/keyboard/simple_text_in.rs create mode 100644 HidPkg/UefiHidDxeV2/src/keyboard/simple_text_in_ex.rs create mode 100644 HidPkg/UefiHidDxeV2/src/lib.rs create mode 100644 HidPkg/UefiHidDxeV2/src/main.rs create mode 100644 HidPkg/UefiHidDxeV2/src/pointer/absolute_pointer.rs create mode 100644 HidPkg/UefiHidDxeV2/src/pointer/mod.rs diff --git a/AdvLoggerPkg/Crates/RustAdvancedLoggerDxe/src/lib.rs b/AdvLoggerPkg/Crates/RustAdvancedLoggerDxe/src/lib.rs index 4c3d977978..8b58758e12 100644 --- a/AdvLoggerPkg/Crates/RustAdvancedLoggerDxe/src/lib.rs +++ b/AdvLoggerPkg/Crates/RustAdvancedLoggerDxe/src/lib.rs @@ -29,7 +29,7 @@ //! #![no_std] -#[cfg(doc)] +#[cfg(any(doc, feature = "std"))] extern crate std; //allow rustdoc links to reference std (e.g. println docs below). use core::{ @@ -173,8 +173,6 @@ mod no_std_debug { #[cfg(feature = "std")] mod std_debug { - extern crate std; - /// Prints to the console log. /// /// This macro uses the same syntax as rust std [`std::println!`] macro, with the addition of a level argument that @@ -203,7 +201,7 @@ mod std_debug { macro_rules! debug { ($level:expr, $($arg:tt)*) => { let _ = $level; - print!($($arg)*) + std::print!($($arg)*) } } } diff --git a/Cargo.toml b/Cargo.toml index 2d18013d26..031eb6881e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,12 @@ [workspace] +resolver = "2" # Add packages that generate binaries here members = [ "HidPkg/Crates/HidIo", "HidPkg/Crates/HiiKeyboardLayout", "HidPkg/UefiHidDxe", + "HidPkg/UefiHidDxeV2", "MsCorePkg/HelloWorldRustDxe" ] @@ -16,10 +18,10 @@ HidIo = {path = "HidPkg/Crates/HidIo"} hidparser = {git = "https://github.com/microsoft/mu_rust_hid.git", branch = "main"} HiiKeyboardLayout = {path = "HidPkg/Crates/HiiKeyboardLayout"} +memoffset = "0.9.0" +num-traits = { version = "0.2", default-features = false} +num-derive = { version = "0.4", default-features = false} r-efi = "4.3.0" rustversion = "1.0.14" spin = "0.9.8" -memoffset = "0.9.0" scroll = { version = "0.11", default-features = false, features = ["derive"]} -num-traits = { version = "0.2", default-features = false} -num-derive = { version = "0.4", default-features = false} \ No newline at end of file diff --git a/HidPkg/Crates/HidIo/src/lib.rs b/HidPkg/Crates/HidIo/src/lib.rs index 538ba76f18..fbaceb64f0 100644 --- a/HidPkg/Crates/HidIo/src/lib.rs +++ b/HidPkg/Crates/HidIo/src/lib.rs @@ -16,6 +16,7 @@ pub mod protocol { pub const GUID: Guid = Guid::from_fields(0x3ea93936, 0x6bf4, 0x49d6, 0xaa, 0x50, &[0xd9, 0xf5, 0xb9, 0xad, 0x8c, 0xff]); + #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[repr(C)] pub enum HidReportType { InputReport = 1, diff --git a/HidPkg/HidPkg.dsc b/HidPkg/HidPkg.dsc index bb0a4bf3e2..9974cb7e4d 100644 --- a/HidPkg/HidPkg.dsc +++ b/HidPkg/HidPkg.dsc @@ -47,6 +47,7 @@ HidPkg/UsbMouseHidDxe/UsbMouseHidDxe.inf HidPkg/UsbHidDxe/UsbHidDxe.inf HidPkg/UefiHidDxe/UefiHidDxe.inf + HidPkg/UefiHidDxeV2/UefiHidDxeV2.inf [BuildOptions] #force deprecated interfaces off diff --git a/HidPkg/UefiHidDxeV2/Cargo.toml b/HidPkg/UefiHidDxeV2/Cargo.toml new file mode 100644 index 0000000000..21d88ff0b8 --- /dev/null +++ b/HidPkg/UefiHidDxeV2/Cargo.toml @@ -0,0 +1,33 @@ +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +[package] +name = "UefiHidDxeV2" +version = "0.1.0" +edition = "2021" +authors = ["Microsoft"] + +[lib] +name = "uefi_hid_dxe_v2" +path = "src/lib.rs" + +[[bin]] +name = "UefiHidDxeV2" +path = "src/main.rs" + +[dependencies] +HidIo = {workspace=true} +hidparser = {workspace=true} +HiiKeyboardLayout = {workspace=true} +r-efi = {workspace=true} +rustversion = {workspace=true} +RustAdvancedLoggerDxe = {workspace=true} +scroll = {workspace=true} + +[target.'cfg(target_os = "uefi")'.dependencies] +RustBootServicesAllocatorDxe = {workspace=true} + +[dev-dependencies] +mockall = "0.11.4" +RustAdvancedLoggerDxe = {workspace=true, features=["std"]} diff --git a/HidPkg/UefiHidDxeV2/UefiHidDxeV2.inf b/HidPkg/UefiHidDxeV2/UefiHidDxeV2.inf new file mode 100644 index 0000000000..bfeb350053 --- /dev/null +++ b/HidPkg/UefiHidDxeV2/UefiHidDxeV2.inf @@ -0,0 +1,22 @@ +### @file +# +# UEFI HID - this driver consumes lower-level HID device support via the HidIo +# protocol abstraction and produces the UEFI spec input protocols for console support. +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +### + +[Defines] + INF_VERSION = 1.27 + BASE_NAME = UefiHidDxeV2 + FILE_GUID = 0DB81E33-8EF5-487E-8C24-FE4B6DF08503 + MODULE_TYPE = DXE_DRIVER + RUST_MODULE = TRUE + +[Sources] + Cargo.toml + +[Depex] + TRUE diff --git a/HidPkg/UefiHidDxeV2/src/boot_services.rs b/HidPkg/UefiHidDxeV2/src/boot_services.rs new file mode 100644 index 0000000000..55d38ca03d --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/boot_services.rs @@ -0,0 +1,369 @@ +//! Provides a trait abstraction for the UEFI Boot Services used in this crate. +//! +//! This module provides a trait definition to abstract the UEFI services used +//! by this crate and provides a concrete implementation of that trait that uses +//! real boot services. +//! +//! ## License +//! +//! Copyright (C) Microsoft Corporation. All rights reserved. +//! +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! +use core::{ffi::c_void, fmt::Debug, sync::atomic::AtomicPtr}; +#[cfg(test)] +use mockall::automock; +use r_efi::efi; + +/// Abstracts UEFI Boot Services used in this crate. +#[cfg_attr(test, automock)] +pub trait UefiBootServices { + fn create_event( + &self, + r#type: u32, + notify_tpl: efi::Tpl, + notify_function: Option, + notify_context: *mut c_void, + event: *mut efi::Event, + ) -> efi::Status; + + fn create_event_ex( + &self, + r#type: u32, + notify_tpl: efi::Tpl, + notify_function: Option, + notify_context: *const c_void, + event_group: *const efi::Guid, + event: *mut efi::Event, + ) -> efi::Status; + + fn close_event(&self, event: efi::Event) -> efi::Status; + + fn signal_event(&self, event: efi::Event) -> efi::Status; + + fn raise_tpl(&self, new_tpl: efi::Tpl) -> efi::Tpl; + + fn restore_tpl(&self, old_tpl: efi::Tpl); + + fn install_protocol_interface( + &self, + handle: *mut efi::Handle, + protocol: *mut efi::Guid, + interface_type: efi::InterfaceType, + interface: *mut c_void, + ) -> efi::Status; + + fn uninstall_protocol_interface( + &self, + handle: efi::Handle, + protocol: *mut efi::Guid, + interface: *mut c_void, + ) -> efi::Status; + + fn open_protocol( + &self, + handle: efi::Handle, + protocol: *mut efi::Guid, + interface: *mut *mut c_void, + agent_handle: efi::Handle, + controller_handle: efi::Handle, + attributes: u32, + ) -> efi::Status; + + fn close_protocol( + &self, + handle: efi::Handle, + protocol: *mut efi::Guid, + agent_handle: efi::Handle, + controller_handle: efi::Handle, + ) -> efi::Status; + + fn locate_protocol( + &self, + protocol: *mut efi::Guid, + registration: *mut c_void, + interface: *mut *mut c_void, + ) -> efi::Status; +} + +/// Provides a concrete implementation of the [`UefiBootServices`] trait. +#[derive(Debug)] +pub struct StandardUefiBootServices { + boot_services: AtomicPtr, +} + +impl StandardUefiBootServices { + /// Creates a new StandardUefiBootServices. + /// Note: attempts to use methods on this instance will panic until [`Self::initialize`] is called. + pub const fn new() -> Self { + Self { boot_services: AtomicPtr::new(core::ptr::null_mut()) } + } + + /// Initializes this instance of [`StandardUefiBootServices`] with a pointer to the boot services table. + pub fn initialize(&self, boot_services: *mut efi::BootServices) { + self.boot_services.store(boot_services, core::sync::atomic::Ordering::SeqCst) + } + + // Returns a reference to the boot services table. Panics if uninitialized. + fn boot_services(&self) -> &efi::BootServices { + let boot_services_ptr = self.boot_services.load(core::sync::atomic::Ordering::SeqCst); + unsafe { boot_services_ptr.as_ref().expect("invalid boot_services pointer") } + } +} + +unsafe impl Sync for StandardUefiBootServices {} +unsafe impl Send for StandardUefiBootServices {} + +impl UefiBootServices for StandardUefiBootServices { + fn create_event( + &self, + r#type: u32, + notify_tpl: efi::Tpl, + notify_function: Option, + notify_context: *mut c_void, + event: *mut efi::Event, + ) -> efi::Status { + (self.boot_services().create_event)(r#type, notify_tpl, notify_function, notify_context, event) + } + fn create_event_ex( + &self, + r#type: u32, + notify_tpl: efi::Tpl, + notify_function: Option, + notify_context: *const c_void, + event_group: *const efi::Guid, + event: *mut efi::Event, + ) -> efi::Status { + (self.boot_services().create_event_ex)(r#type, notify_tpl, notify_function, notify_context, event_group, event) + } + fn close_event(&self, event: efi::Event) -> efi::Status { + (self.boot_services().close_event)(event) + } + fn signal_event(&self, event: efi::Event) -> efi::Status { + (self.boot_services().signal_event)(event) + } + fn raise_tpl(&self, new_tpl: efi::Tpl) -> efi::Tpl { + (self.boot_services().raise_tpl)(new_tpl) + } + fn restore_tpl(&self, old_tpl: efi::Tpl) { + (self.boot_services().restore_tpl)(old_tpl) + } + fn install_protocol_interface( + &self, + handle: *mut efi::Handle, + protocol: *mut efi::Guid, + interface_type: efi::InterfaceType, + interface: *mut c_void, + ) -> efi::Status { + (self.boot_services().install_protocol_interface)(handle, protocol, interface_type, interface) + } + fn uninstall_protocol_interface( + &self, + handle: efi::Handle, + protocol: *mut efi::Guid, + interface: *mut c_void, + ) -> efi::Status { + (self.boot_services().uninstall_protocol_interface)(handle, protocol, interface) + } + fn open_protocol( + &self, + handle: efi::Handle, + protocol: *mut efi::Guid, + interface: *mut *mut c_void, + agent_handle: efi::Handle, + controller_handle: efi::Handle, + attributes: u32, + ) -> efi::Status { + (self.boot_services().open_protocol)(handle, protocol, interface, agent_handle, controller_handle, attributes) + } + fn close_protocol( + &self, + handle: efi::Handle, + protocol: *mut efi::Guid, + agent_handle: efi::Handle, + controller_handle: efi::Handle, + ) -> efi::Status { + (self.boot_services().close_protocol)(handle, protocol, agent_handle, controller_handle) + } + fn locate_protocol( + &self, + protocol: *mut efi::Guid, + registration: *mut c_void, + interface: *mut *mut c_void, + ) -> efi::Status { + (self.boot_services().locate_protocol)(protocol, registration, interface) + } +} + +#[cfg(test)] +mod test { + use core::{ffi::c_void, mem::MaybeUninit}; + + use r_efi::efi; + + use super::{StandardUefiBootServices, UefiBootServices}; + + extern "efiapi" fn mock_create_event( + _type: u32, + _notify_tpl: efi::Tpl, + _notify_function: Option, + _notify_context: *mut c_void, + _event: *mut efi::Event, + ) -> efi::Status { + efi::Status::SUCCESS + } + + extern "efiapi" fn mock_create_event_ex( + _type: u32, + _notify_tpl: efi::Tpl, + _notify_function: Option, + _notify_context: *const c_void, + _event_group: *const efi::Guid, + _event: *mut efi::Event, + ) -> efi::Status { + efi::Status::SUCCESS + } + extern "efiapi" fn mock_close_event(_event: efi::Event) -> efi::Status { + efi::Status::SUCCESS + } + + extern "efiapi" fn mock_signal_event(_event: efi::Event) -> efi::Status { + efi::Status::SUCCESS + } + + extern "efiapi" fn mock_raise_tpl(_new_tpl: efi::Tpl) -> efi::Tpl { + efi::TPL_APPLICATION + } + + extern "efiapi" fn mock_restore_tpl(_new_tpl: efi::Tpl) {} + + extern "efiapi" fn mock_install_protocol_interface( + _handle: *mut efi::Handle, + _protocol: *mut efi::Guid, + _interface_type: efi::InterfaceType, + _interface: *mut c_void, + ) -> efi::Status { + efi::Status::SUCCESS + } + + extern "efiapi" fn mock_uninstall_protocol_interface( + _handle: efi::Handle, + _protocol: *mut efi::Guid, + _interface: *mut c_void, + ) -> efi::Status { + efi::Status::SUCCESS + } + + extern "efiapi" fn mock_open_protocol( + _handle: efi::Handle, + _protocol: *mut efi::Guid, + _interface: *mut *mut c_void, + _agent_handle: efi::Handle, + _controller_handle: efi::Handle, + _attributes: u32, + ) -> efi::Status { + efi::Status::SUCCESS + } + + extern "efiapi" fn mock_close_protocol( + _handle: efi::Handle, + _protocol: *mut efi::Guid, + _agent_handle: efi::Handle, + _controller_handle: efi::Handle, + ) -> efi::Status { + efi::Status::SUCCESS + } + + extern "efiapi" fn mock_locate_protocol( + _protocol: *mut efi::Guid, + _registration: *mut c_void, + _interface: *mut *mut c_void, + ) -> efi::Status { + efi::Status::SUCCESS + } + + #[test] + fn standard_uefi_boot_services_should_wrap_boot_services() { + let boot_services = MaybeUninit::::zeroed(); + let mut boot_services = unsafe { boot_services.assume_init() }; + boot_services.create_event = mock_create_event; + boot_services.create_event_ex = mock_create_event_ex; + boot_services.close_event = mock_close_event; + boot_services.signal_event = mock_signal_event; + boot_services.raise_tpl = mock_raise_tpl; + boot_services.restore_tpl = mock_restore_tpl; + boot_services.install_protocol_interface = mock_install_protocol_interface; + boot_services.uninstall_protocol_interface = mock_uninstall_protocol_interface; + boot_services.open_protocol = mock_open_protocol; + boot_services.close_protocol = mock_close_protocol; + boot_services.locate_protocol = mock_locate_protocol; + + const TEST_GUID: efi::Guid = efi::Guid::from_fields(0, 0, 0, 0, 0, &[0, 0, 0, 0, 0, 0]); + let mut event = 1 as efi::Event; + let mut handle = 2 as efi::Handle; + + let test_boot_services = StandardUefiBootServices::new(); + test_boot_services.initialize(&mut boot_services as *mut efi::BootServices); + assert_eq!( + test_boot_services.create_event(0, efi::TPL_NOTIFY, None, core::ptr::null_mut(), core::ptr::addr_of_mut!(event)), + efi::Status::SUCCESS + ); + + assert_eq!( + test_boot_services.create_event_ex( + 0, + efi::TPL_NOTIFY, + None, + core::ptr::null_mut(), + &TEST_GUID as *const efi::Guid, + core::ptr::addr_of_mut!(event), + ), + efi::Status::SUCCESS + ); + + assert_eq!(test_boot_services.close_event(event), efi::Status::SUCCESS); + assert_eq!(test_boot_services.signal_event(event), efi::Status::SUCCESS); + assert_eq!(test_boot_services.raise_tpl(efi::TPL_HIGH_LEVEL), efi::TPL_APPLICATION); + test_boot_services.restore_tpl(efi::TPL_APPLICATION); + assert_eq!( + test_boot_services.install_protocol_interface( + core::ptr::addr_of_mut!(handle), + &TEST_GUID as *const efi::Guid as *mut efi::Guid, + efi::NATIVE_INTERFACE, + core::ptr::null_mut() + ), + efi::Status::SUCCESS + ); + assert_eq!( + test_boot_services.uninstall_protocol_interface( + handle, + &TEST_GUID as *const efi::Guid as *mut efi::Guid, + core::ptr::null_mut() + ), + efi::Status::SUCCESS + ); + assert_eq!( + test_boot_services.open_protocol( + handle, + &TEST_GUID as *const efi::Guid as *mut efi::Guid, + core::ptr::null_mut(), + handle, + handle, + 0 + ), + efi::Status::SUCCESS + ); + assert_eq!( + test_boot_services.close_protocol(handle, &TEST_GUID as *const efi::Guid as *mut efi::Guid, handle, handle), + efi::Status::SUCCESS + ); + assert_eq!( + test_boot_services.locate_protocol( + &TEST_GUID as *const efi::Guid as *mut efi::Guid, + core::ptr::null_mut(), + core::ptr::null_mut() + ), + efi::Status::SUCCESS + ); + } +} diff --git a/HidPkg/UefiHidDxeV2/src/driver_binding.rs b/HidPkg/UefiHidDxeV2/src/driver_binding.rs new file mode 100644 index 0000000000..5432d5bdd6 --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/driver_binding.rs @@ -0,0 +1,322 @@ +//! Provides a trait interface and FFI support for the UEFI Driver Binding used +//! in this crate. +//! +//! ## License +//! +//! Copyright (C) Microsoft Corporation. All rights reserved. +//! +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! +use alloc::boxed::Box; +use core::ffi::c_void; + +#[cfg(test)] +use mockall::automock; +use r_efi::{efi, protocols}; + +use crate::boot_services::UefiBootServices; + +/// Abstracts the UEFI driver binding interface. +/// +/// Clients of this module can implement this interface and pass it to an +/// instance of [`UefiDriverBinding`] which will manage FFI for the driver +/// binding with the UEFI core. +/// +/// Reference: +#[cfg_attr(test, automock)] +pub trait DriverBinding { + /// Reference: + fn driver_binding_supported( + &mut self, + boot_services: &'static dyn UefiBootServices, + controller: efi::Handle, + ) -> Result<(), efi::Status>; + /// Reference: + fn driver_binding_start( + &mut self, + boot_services: &'static dyn UefiBootServices, + controller: efi::Handle, + ) -> Result<(), efi::Status>; + /// Reference: + fn driver_binding_stop( + &mut self, + boot_services: &'static dyn UefiBootServices, + controller: efi::Handle, + ) -> Result<(), efi::Status>; +} + +/// Manages FFI for a driver binding instance with UEFI core. +/// +/// ## Examples and Usage +/// +/// ```ignore +/// use uefi_hid_dxe::driver_binding::*; +/// +/// let driver_binding = Box::new(MyDriverBinding::new()); +/// let uefi_driver_binding = UefiDriverBinding::new( +/// boot_services, +/// driver_binding, +/// driver_handle +/// ); +/// let raw_binding = uefi_driver_binding.install().unwrap(); +/// // driver_binding can now be invoked by UEFI core. +/// let driver_binding = uninstall(raw_binding).unwrap(); +/// // driver_binding is now uninstalled from core and can be safely dropped. +///``` +#[repr(C)] +pub struct UefiDriverBinding { + uefi_binding: protocols::driver_binding::Protocol, + boot_services: &'static dyn UefiBootServices, + binding: Box, +} + +impl UefiDriverBinding { + /// Creates a new UefiDriverBinding that manages the given binding. + pub fn new( + boot_services: &'static dyn UefiBootServices, + binding: Box, + handle: efi::Handle, + ) -> Self { + let uefi_binding = protocols::driver_binding::Protocol { + supported: Self::driver_binding_supported, + start: Self::driver_binding_start, + stop: Self::driver_binding_stop, + version: 1, + image_handle: handle, + driver_binding_handle: handle, + }; + Self { uefi_binding, boot_services, binding } + } + + /// Installs the binding with the UEFI core. + pub fn install(self) -> Result<*mut UefiDriverBinding, efi::Status> { + let mut handle = self.uefi_binding.driver_binding_handle; + let uefi_driver_binding_mgr_ptr = Box::into_raw(Box::new(self)); + let boot_services = &unsafe { uefi_driver_binding_mgr_ptr.as_ref().unwrap() }.boot_services; + let status = boot_services.install_protocol_interface( + core::ptr::addr_of_mut!(handle), + &protocols::driver_binding::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + efi::NATIVE_INTERFACE, + uefi_driver_binding_mgr_ptr as *mut c_void, + ); + if status.is_error() { + Err(status) + } else { + Ok(uefi_driver_binding_mgr_ptr) + } + } + + /// Uninstalls the binding from the UEFI core. + /// # Safety + /// uefi_binding must be the same pointer returned from [`Self::install`]. + pub unsafe fn uninstall(uefi_binding: *mut UefiDriverBinding) -> Result { + let ptr = uefi_binding; + let binding = Box::from_raw(uefi_binding); + let status = binding.boot_services.uninstall_protocol_interface( + binding.uefi_binding.driver_binding_handle, + &protocols::driver_binding::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + ptr as *mut c_void, + ); + if status.is_error() { + Err(status) + } else { + Ok(*binding) + } + } + + // Driver Binding Supported FFI function + // Handles driver_binding_supported calls from the UEFI core by passing them to the trait implementation. + extern "efiapi" fn driver_binding_supported( + this: *mut protocols::driver_binding::Protocol, + controller: efi::Handle, + _remaining_device_path: *mut protocols::device_path::Protocol, + ) -> efi::Status { + let uefi_binding = unsafe { (this as *mut UefiDriverBinding).as_mut() }.expect("bad this pointer"); + match uefi_binding.binding.driver_binding_supported(uefi_binding.boot_services, controller) { + Ok(_) => efi::Status::SUCCESS, + Err(err) => err, + } + } + + // Driver Binding Start FFI function + // Handles driver_binding_start calls from the UEFI core by passing them to the trait implementation. + extern "efiapi" fn driver_binding_start( + this: *mut protocols::driver_binding::Protocol, + controller: efi::Handle, + _remaining_device_path: *mut protocols::device_path::Protocol, + ) -> efi::Status { + let uefi_binding = unsafe { (this as *mut UefiDriverBinding).as_mut() }.expect("bad this pointer"); + match uefi_binding.binding.driver_binding_start(uefi_binding.boot_services, controller) { + Ok(_) => efi::Status::SUCCESS, + Err(err) => err, + } + } + + // Driver Binding Stop FFI function + // Handles driver_binding_stop calls from the UEFI core by passing them to the trait implementation. + extern "efiapi" fn driver_binding_stop( + this: *mut protocols::driver_binding::Protocol, + controller: efi::Handle, + _num_children: usize, + _child_handle_buffer: *mut efi::Handle, + ) -> efi::Status { + let uefi_binding = unsafe { (this as *mut UefiDriverBinding).as_mut() }.expect("bad this pointer"); + match uefi_binding.binding.driver_binding_stop(uefi_binding.boot_services, controller) { + Ok(_) => efi::Status::SUCCESS, + Err(err) => err, + } + } +} + +#[cfg(test)] +mod test { + + use super::{MockDriverBinding, UefiDriverBinding}; + use crate::boot_services::MockUefiBootServices; + use r_efi::{efi, protocols}; + + // In this module, the usage model for boot_services is global static, and so &'static dyn UefiBootServices is used + // throughout the API. For testing, each test will have a different set of expectations on the UefiBootServices mock + // object, and the mock object itself expects to be "mut", which makes it hard to handle as a single global static. + // Instead, raw pointers are used to simulate a MockUefiBootServices instance with 'static lifetime. + // This object needs to outlive anything that uses it - once created, it will live until the end of the program. + fn create_fake_static_boot_service() -> &'static mut MockUefiBootServices { + unsafe { Box::into_raw(Box::new(MockUefiBootServices::new())).as_mut().unwrap() } + } + + #[test] + fn new_should_instantiate_new_uefi_driver_binding() { + let boot_services = create_fake_static_boot_service(); + let binding = MockDriverBinding::new(); + let handle = 0x1234 as efi::Handle; + let driver_binding = UefiDriverBinding::new(boot_services, Box::new(binding), handle); + + assert!(driver_binding.uefi_binding.supported == UefiDriverBinding::driver_binding_supported); + assert!(driver_binding.uefi_binding.start == UefiDriverBinding::driver_binding_start); + assert!(driver_binding.uefi_binding.stop == UefiDriverBinding::driver_binding_stop); + assert_eq!(driver_binding.uefi_binding.version, 1); + assert_eq!(driver_binding.uefi_binding.image_handle, handle); + assert_eq!(driver_binding.uefi_binding.driver_binding_handle, handle); + } + + #[test] + fn install_should_install_the_driver_binding() { + let boot_services = create_fake_static_boot_service(); + //expect a call to install_protocol_interface + boot_services + .expect_install_protocol_interface() + .withf(|handle, protocol, interface_type, interface| { + assert_ne!(*handle, core::ptr::null_mut()); + assert_eq!(unsafe { **protocol }, protocols::driver_binding::PROTOCOL_GUID); + assert_eq!(*interface_type, efi::NATIVE_INTERFACE); + assert_ne!(*interface, core::ptr::null_mut()); + true + }) + .returning(|_, _, _, _| efi::Status::SUCCESS); + + let handle = 0x1234 as efi::Handle; + + let binding = MockDriverBinding::new(); + let driver_binding = UefiDriverBinding::new(boot_services, Box::new(binding), handle); + driver_binding.install().unwrap(); + } + + #[test] + fn install_should_report_failures_to_install_the_driver_binding() { + let boot_services = create_fake_static_boot_service(); + + //expect a call to install_protocol_interface + boot_services.expect_install_protocol_interface().returning(|_, _, _, _| efi::Status::OUT_OF_RESOURCES); + + let handle = 0x1234 as efi::Handle; + + let binding = MockDriverBinding::new(); + let driver_binding = UefiDriverBinding::new(boot_services, Box::new(binding), handle); + assert_eq!(driver_binding.install(), Err(efi::Status::OUT_OF_RESOURCES)); + } + + #[test] + fn uninstall_should_uninstall_the_driver_binding() { + let boot_services = create_fake_static_boot_service(); + //expect a call to install_protocol_interface + boot_services.expect_install_protocol_interface().returning(|handle, protocol, interface_type, interface| { + unsafe { + assert_ne!(handle.read(), core::ptr::null_mut()); + assert_eq!(protocol.read(), protocols::driver_binding::PROTOCOL_GUID); + assert_eq!(interface_type, efi::NATIVE_INTERFACE); + assert_ne!(interface, core::ptr::null_mut()); + } + efi::Status::SUCCESS + }); + boot_services.expect_uninstall_protocol_interface().returning(|handle, protocol, interface| { + assert_ne!(handle, core::ptr::null_mut()); + assert_eq!(unsafe { protocol.read() }, protocols::driver_binding::PROTOCOL_GUID); + assert_ne!(interface, core::ptr::null_mut()); + efi::Status::SUCCESS + }); + let handle = 0x1234 as efi::Handle; + + let binding = MockDriverBinding::new(); + let driver_binding = UefiDriverBinding::new(boot_services, Box::new(binding), handle); + let binding_ptr = driver_binding.install().unwrap(); + + let driver_binding = unsafe { UefiDriverBinding::uninstall(binding_ptr) }.unwrap(); + + assert!(driver_binding.uefi_binding.supported == UefiDriverBinding::driver_binding_supported); + assert!(driver_binding.uefi_binding.start == UefiDriverBinding::driver_binding_start); + assert!(driver_binding.uefi_binding.stop == UefiDriverBinding::driver_binding_stop); + assert_eq!(driver_binding.uefi_binding.version, 1); + assert_eq!(driver_binding.uefi_binding.image_handle, handle); + assert_eq!(driver_binding.uefi_binding.driver_binding_handle, handle); + } + + #[test] + fn uninstall_should_report_failures_to_uninstall_the_driver() { + let boot_services = create_fake_static_boot_service(); + + //expect a call to install_protocol_interface + boot_services.expect_install_protocol_interface().returning(|_, _, _, _| efi::Status::SUCCESS); + boot_services.expect_uninstall_protocol_interface().returning(|_, _, _| efi::Status::INVALID_PARAMETER); + + let handle = 0x1234 as efi::Handle; + + let binding = MockDriverBinding::new(); + let driver_binding = UefiDriverBinding::new(boot_services, Box::new(binding), handle); + let binding_ptr = driver_binding.install().unwrap(); + + assert_eq!(unsafe { UefiDriverBinding::uninstall(binding_ptr) }.err(), Some(efi::Status::INVALID_PARAMETER)); + } + + #[test] + fn driver_binding_should_call_driver_binding_routines() { + let boot_services = create_fake_static_boot_service(); + //expect a call to install_protocol_interface + boot_services.expect_install_protocol_interface().returning(|_, _, _, _| efi::Status::SUCCESS); + + let mut binding = MockDriverBinding::new(); + binding.expect_driver_binding_supported().returning(|_, _| Ok(())); + binding.expect_driver_binding_start().returning(|_, _| Ok(())); + binding.expect_driver_binding_stop().returning(|_, _| Ok(())); + + let handle = 0x1234 as efi::Handle; + let driver_binding = UefiDriverBinding::new(boot_services, Box::new(binding), handle); + let binding_ptr = driver_binding.install().unwrap(); + + let driver_binding_ref = unsafe { binding_ptr.as_ref().unwrap() }; + let this_ptr = binding_ptr as *mut protocols::driver_binding::Protocol; + + let controller_handle = 0x4321 as efi::Handle; + assert_eq!( + (driver_binding_ref.uefi_binding.supported)(this_ptr, controller_handle, core::ptr::null_mut()), + efi::Status::SUCCESS + ); + assert_eq!( + (driver_binding_ref.uefi_binding.start)(this_ptr, controller_handle, core::ptr::null_mut()), + efi::Status::SUCCESS + ); + assert_eq!( + (driver_binding_ref.uefi_binding.stop)(this_ptr, controller_handle, 0, core::ptr::null_mut()), + efi::Status::SUCCESS + ); + } +} diff --git a/HidPkg/UefiHidDxeV2/src/hid.rs b/HidPkg/UefiHidDxeV2/src/hid.rs new file mode 100644 index 0000000000..1652019101 --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/hid.rs @@ -0,0 +1,422 @@ +//! Creates and manages HID instances. +//! +//! The [`HidFactory`] in this module provides an implementation of the +//! [`crate::driver_binding::DriverBinding`] trait. +//! +//! When this driver is asked to manage a controller that supports HidIo, +//! the [`crate::hid_io::HidIoFactory`] associated with the factory is used +//! to create and manage a HidIo abstraction for the controller, and the +//! [`HidReceiverFactory`] is used to create a set of receivers for reports +//! from the HidIo device. +//! +//! ## Example +//! ```ignore +//! //Create a receiver factory that creates Pointer and Keyboard Handlers as receivers. +//! struct UefiReceivers { +//! boot_services: &'static dyn UefiBootServices, +//! agent: efi::Handle, +//! } +//! impl HidReceiverFactory for UefiReceivers { +//! fn new_hid_receiver_list(&self, _controller: efi::Handle) -> Result>, efi::Status> { +//! let mut receivers: Vec> = Vec::new(); +//! receivers.push(Box::new(PointerHidHandler::new(self.boot_services, self.agent))); +//! receivers.push(Box::new(KeyboardHidHandler::new(self.boot_services, self.agent))); +//! Ok(receivers) +//! } +//! } +//! +//! // Create new factories for HidIo, Receivers, and Hid +//! let hid_io_factory = Box::new(UefiHidIoFactory::new(&BOOT_SERVICES, image_handle)); +//! let receiver_factory = Box::new(UefiReceivers { boot_services: &BOOT_SERVICES, agent: image_handle }); +//! let hid_factory = Box::new(HidFactory::new(hid_io_factory, receiver_factory, image_handle)); +//! +//! // start up the driver binding for the hid_factory and install it with the core. +//! let hid_binding = UefiDriverBinding::new(&BOOT_SERVICES, hid_factory, image_handle); +//! hid_binding.install().expect("failed to install HID driver binding"); +//! ``` +//! +//! ## License +//! +//! Copyright (C) Microsoft Corporation. All rights reserved. +//! +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! +use core::ffi::c_void; + +use crate::{ + boot_services::UefiBootServices, + driver_binding::DriverBinding, + hid_io::{HidIo, HidIoFactory, HidReportReceiver}, +}; + +use alloc::{boxed::Box, vec::Vec}; + +use r_efi::efi; +use rust_advanced_logger_dxe::{debugln, DEBUG_ERROR}; + +#[cfg(test)] +use mockall::automock; + +/// This trait defines an abstraction for getting a list of receivers for HID reports. +/// +/// This is used to specify to a HidFactory how it should instantiate new receivers for HID reports. +#[cfg_attr(test, automock)] +pub trait HidReceiverFactory { + /// Generates a vector of [`crate::hid_io::HidReportReceiver`] trait objects that can handle reports from the given controller. + fn new_hid_receiver_list(&self, controller: efi::Handle) -> Result>, efi::Status>; +} + +// Context structure used to track HID instances being managed. +// This is installed as a private interface on the controller handle to associate the HID instance with the controller. +// Note: a concrete structure is used here because Box is a fat pointer that doesn't work well for FFI. +// Wrapping it in HidInstance makes it a fixed size type: *mut HidInstance is a thin pointer that can be cast back and +// forth to c_void. +struct HidInstance { + _hid_io: Box, +} + +impl HidInstance { + // This guid {fb719b29-fda7-4359-ac68-0d46c31a7a7e} is used to associate a HidInstance with a given controller by + // installing it as a protocol on the controller handle. + const PRIVATE_HID_CONTEXT_GUID: efi::Guid = + efi::Guid::from_fields(0xfb719b29, 0xfda7, 0x4359, 0xac, 0x68, &[0x0d, 0x46, 0xc3, 0x1a, 0x7a, 0x7e]); + + //create a new hid instance from + fn new(hid_io: Box) -> Self { + HidInstance { _hid_io: hid_io } + } +} + +// Structure used to manage multiple receivers and split reports between them. +struct HidSplitter { + receivers: Vec>, +} + +impl HidReportReceiver for HidSplitter { + //initialize is not expected, since hid splitter is generic for all + // controllers and is fully initialized when constructed. + fn initialize(&mut self, _controller: efi::Handle, _hid_io: &dyn HidIo) -> Result<(), efi::Status> { + panic!("initialize not expected for HidSplitter") + } + + //iterates over the receivers and passes the report to each one. + fn receive_report(&mut self, report: &[u8], hid_io: &dyn HidIo) { + for receiver in &mut self.receivers { + receiver.receive_report(report, hid_io) + } + } +} + +/// This structure implements provides an implementation of +/// [`crate::driver_binding::DriverBinding`] that spans private "HidInstances" +/// whenever [`HidFactory::driver_binding_start`] is called to manage a given +/// controller. +pub struct HidFactory { + hid_io_factory: Box, + receiver_factory: Box, + agent: efi::Handle, +} + +impl HidFactory { + /// Creates a new "HidFactory". + /// + /// When a new HidInstance is spawned by + /// [`HidFactory::driver_binding_start`], `hid_io_factory` is used to create a + /// new [`crate::hid_io::HidIo`] instance to interact with the controller, and + /// `receiver_factory` is used to create new + /// [`crate::hid_io::HidReportReceiver`] that will process reports from the + /// managed controller. + /// + /// `agent` is the handle on which the driver binding instance should be + /// installed (expected to be the image_handle for this driver). + pub fn new( + hid_io_factory: Box, + receiver_factory: Box, + agent: efi::Handle, + ) -> Self { + HidFactory { hid_io_factory, receiver_factory, agent } + } +} + +impl DriverBinding for HidFactory { + /// Verifies the given controller supports HidIo + /// + /// This is done by attempting the instantiation of a HidIo instance on it + /// using the HidIoFactory provided at construction - if that succeeds, the + /// controller is considered supported. Note that the actual HidIo instance + /// constructed for the test is dropped on return. + fn driver_binding_supported( + &mut self, + _boot_services: &'static dyn UefiBootServices, + controller: r_efi::efi::Handle, + ) -> Result<(), efi::Status> { + self.hid_io_factory.new_hid_io(controller, true).map(|_| ()) + } + + /// Starts a new HID instance. + /// + /// Starts Hid support for the given controller. The HidIoFactory provided at + /// construction is used to create a new HidIo trait object to manage the + /// controller, and new receivers are instantiated using the + /// HidReceiverFactory provided at construction. A private "HidInstance" + /// structure is created and associated with the controller to own these + /// objects as long as the instance is "running" - i.e. until + /// [`Self::driver_binding_stop`] is invoked for the controller. + fn driver_binding_start( + &mut self, + boot_services: &'static dyn UefiBootServices, + controller: r_efi::efi::Handle, + ) -> Result<(), efi::Status> { + let mut hid_io = self.hid_io_factory.new_hid_io(controller, true)?; + + let mut hid_splitter = Box::new(HidSplitter { receivers: Vec::new() }); + + for mut receiver in self.receiver_factory.new_hid_receiver_list(controller)? { + if receiver.initialize(controller, hid_io.as_mut()).is_ok() { + hid_splitter.receivers.push(receiver); + } + } + + if hid_splitter.receivers.is_empty() { + return Err(efi::Status::UNSUPPORTED); + } + + hid_io.set_report_receiver(hid_splitter)?; + + let hid_instance = Box::into_raw(Box::new(HidInstance::new(hid_io))); + + let mut handle = controller; + let status = boot_services.install_protocol_interface( + core::ptr::addr_of_mut!(handle), + &HidInstance::PRIVATE_HID_CONTEXT_GUID as *const efi::Guid as *mut efi::Guid, + efi::NATIVE_INTERFACE, + hid_instance as *mut c_void, + ); + if status != efi::Status::SUCCESS { + drop(unsafe { Box::from_raw(hid_instance) }); + return Err(status); + } + Ok(()) + } + + /// Stops a running HID instance. + /// + /// Stops Hid support for the given controller. The private "HidInstance" + /// created by [`Self::driver_binding_start`] is reclaimed and dropped. + fn driver_binding_stop( + &mut self, + boot_services: &'static dyn UefiBootServices, + controller: r_efi::efi::Handle, + ) -> Result<(), efi::Status> { + let mut hid_instance: *mut HidInstance = core::ptr::null_mut(); + + let status = boot_services.open_protocol( + controller, + &HidInstance::PRIVATE_HID_CONTEXT_GUID as *const efi::Guid as *mut efi::Guid, + core::ptr::addr_of_mut!(hid_instance) as *mut *mut c_void, + self.agent, + controller, + efi::OPEN_PROTOCOL_GET_PROTOCOL, + ); + if status != efi::Status::SUCCESS { + return Err(status); + } + + let status = boot_services.uninstall_protocol_interface( + controller, + &HidInstance::PRIVATE_HID_CONTEXT_GUID as *const efi::Guid as *mut efi::Guid, + hid_instance as *mut c_void, + ); + if status != efi::Status::SUCCESS { + debugln!(DEBUG_ERROR, "hid::driver_binding_stop: unexpected failure return: {:x?}", status); + } + + drop(unsafe { Box::from_raw(hid_instance) }); + Ok(()) + } +} + +#[cfg(test)] +mod test { + use core::ffi::c_void; + + use r_efi::efi; + + use crate::{ + boot_services::MockUefiBootServices, + driver_binding::DriverBinding, + hid_io::{HidReportReceiver, MockHidIo, MockHidIoFactory, MockHidReportReceiver}, + }; + + use super::{HidFactory, HidSplitter, MockHidReceiverFactory}; + + // In this module, the usage model for boot_services is global static, and so &'static dyn UefiBootServices is used + // throughout the API. For testing, each test will have a different set of expectations on the UefiBootServices mock + // object, and the mock object itself expects to be "mut", which makes it hard to handle as a single global static. + // Instead, raw pointers are used to simulate a MockUefiBootServices instance with 'static lifetime. + // This object needs to outlive anything that uses it - once created, it will live until the end of the program. + fn create_fake_static_boot_service() -> &'static mut MockUefiBootServices { + unsafe { Box::into_raw(Box::new(MockUefiBootServices::new())).as_mut().unwrap() } + } + + #[test] + fn driver_binding_supported_should_indicate_support() { + let boot_services = create_fake_static_boot_service(); + + let mut hid_io_factory = Box::new(MockHidIoFactory::new()); + //handle 0x3 should return success. + hid_io_factory + .expect_new_hid_io() + .withf_st(|controller, _| *controller == 0x3 as efi::Handle) + .returning(|_, _| Ok(Box::new(MockHidIo::new()))); + //default for any other handles + hid_io_factory.expect_new_hid_io().returning(|_, _| Err(efi::Status::UNSUPPORTED)); + + let receiver_factory = Box::new(MockHidReceiverFactory::new()); + let agent = 0x1 as efi::Handle; + let mut hid_factory = HidFactory::new(hid_io_factory, receiver_factory, agent); + + let controller = 0x2 as efi::Handle; + assert_eq!(hid_factory.driver_binding_supported(boot_services, controller), Err(efi::Status::UNSUPPORTED)); + + let controller = 0x3 as efi::Handle; + assert!(hid_factory.driver_binding_supported(boot_services, controller).is_ok()); + } + + #[test] + fn driver_binding_start_should_not_start_when_not_supported() { + let boot_services = create_fake_static_boot_service(); + let mut hid_io_factory = Box::new(MockHidIoFactory::new()); + hid_io_factory + .expect_new_hid_io() + .withf_st(|controller, _| *controller == 0x3 as efi::Handle) + .returning(|_, _| Ok(Box::new(MockHidIo::new()))); + hid_io_factory + .expect_new_hid_io() + .withf_st(|controller, _| *controller == 0x4 as efi::Handle) + .returning(|_, _| Ok(Box::new(MockHidIo::new()))); + //default for any other handles + hid_io_factory.expect_new_hid_io().returning(|_, _| Err(efi::Status::UNSUPPORTED)); + + let mut receiver_factory = Box::new(MockHidReceiverFactory::new()); + receiver_factory + .expect_new_hid_receiver_list() + .withf_st(|controller| *controller == 0x4 as efi::Handle) + .returning(|_| Ok(Vec::new())); + receiver_factory.expect_new_hid_receiver_list().returning(|_| Err(efi::Status::UNSUPPORTED)); + + let agent = 0x1 as efi::Handle; + let mut hid_factory = HidFactory::new(hid_io_factory, receiver_factory, agent); + + // test: no hid_io on the handle. + let controller = 0x02 as efi::Handle; + assert_eq!(hid_factory.driver_binding_start(boot_services, controller), Err(efi::Status::UNSUPPORTED)); + + // test: hid_io present, but failed to retrieve receivers. + let controller = 0x03 as efi::Handle; + assert_eq!(hid_factory.driver_binding_start(boot_services, controller), Err(efi::Status::UNSUPPORTED)); + + // test: hid_io present, empty receiver list. + let controller = 0x04 as efi::Handle; + assert_eq!(hid_factory.driver_binding_start(boot_services, controller), Err(efi::Status::UNSUPPORTED)); + + let boot_services = create_fake_static_boot_service(); + + //test: hid_io present, receiver present, receiver init indicates no support. + let mut hid_io_factory = Box::new(MockHidIoFactory::new()); + hid_io_factory.expect_new_hid_io().returning(|_, _| Ok(Box::new(MockHidIo::new()))); + + let mut receiver_factory = Box::new(MockHidReceiverFactory::new()); + receiver_factory.expect_new_hid_receiver_list().returning(|_| { + let mut hid_receiver = MockHidReportReceiver::new(); + hid_receiver.expect_initialize().returning(|_, _| Err(efi::Status::UNSUPPORTED)); + Ok(vec![Box::new(hid_receiver)]) + }); + + let mut hid_factory = HidFactory::new(hid_io_factory, receiver_factory, agent); + let controller = 0x02 as efi::Handle; + assert_eq!(hid_factory.driver_binding_start(boot_services, controller), Err(efi::Status::UNSUPPORTED)); + } + + #[test] + fn driver_binding_start_should_start_when_supported() { + let boot_services = create_fake_static_boot_service(); + let agent = 0x1 as efi::Handle; + + let mut hid_io_factory = Box::new(MockHidIoFactory::new()); + hid_io_factory.expect_new_hid_io().returning(|_, _| { + let mut hid_io = MockHidIo::new(); + hid_io.expect_set_report_receiver().returning(|_| Ok(())); + Ok(Box::new(hid_io)) + }); + + let mut receiver_factory = Box::new(MockHidReceiverFactory::new()); + receiver_factory.expect_new_hid_receiver_list().returning(|_| { + let mut hid_receiver = MockHidReportReceiver::new(); + hid_receiver.expect_initialize().returning(|_, _| Ok(())); + Ok(vec![Box::new(hid_receiver)]) + }); + + boot_services.expect_install_protocol_interface().returning(|_, _, _, _| efi::Status::SUCCESS); + + let mut hid_factory = HidFactory::new(hid_io_factory, receiver_factory, agent); + let controller = 0x02 as efi::Handle; + hid_factory.driver_binding_start(boot_services, controller).unwrap(); + + //test note: this will leak a HidInstance. + } + + #[test] + fn driver_binding_start_should_stop_after_start() { + let boot_services = create_fake_static_boot_service(); + let agent = 0x1 as efi::Handle; + + let mut hid_io_factory = Box::new(MockHidIoFactory::new()); + hid_io_factory.expect_new_hid_io().returning(|_, _| { + let mut hid_io = MockHidIo::new(); + hid_io.expect_set_report_receiver().returning(|_| Ok(())); + Ok(Box::new(hid_io)) + }); + + let mut receiver_factory = Box::new(MockHidReceiverFactory::new()); + receiver_factory.expect_new_hid_receiver_list().returning(|_| { + let mut hid_receiver = MockHidReportReceiver::new(); + hid_receiver.expect_initialize().returning(|_, _| Ok(())); + Ok(vec![Box::new(hid_receiver)]) + }); + + static mut HID_INSTANCE_PTR: *mut c_void = core::ptr::null_mut(); + boot_services.expect_install_protocol_interface().returning(|_, _, _, instance| { + unsafe { HID_INSTANCE_PTR = instance }; + efi::Status::SUCCESS + }); + + boot_services.expect_open_protocol().returning(|_, _, interface, _, _, _| { + unsafe { *interface = HID_INSTANCE_PTR }; + efi::Status::SUCCESS + }); + + boot_services.expect_uninstall_protocol_interface().returning(|_, _, _| efi::Status::SUCCESS); + + let mut hid_factory = HidFactory::new(hid_io_factory, receiver_factory, agent); + let controller = 0x02 as efi::Handle; + hid_factory.driver_binding_start(boot_services, controller).unwrap(); + + assert_ne!(unsafe { HID_INSTANCE_PTR }, core::ptr::null_mut()); + + hid_factory.driver_binding_stop(boot_services, controller).unwrap(); + } + + #[test] + fn hid_splitter_should_split_things() { + let mut mock_hid_receiver1 = MockHidReportReceiver::new(); + mock_hid_receiver1.expect_receive_report().returning(|_, _| ()); + let mut mock_hid_receiver2 = MockHidReportReceiver::new(); + mock_hid_receiver2.expect_receive_report().returning(|_, _| ()); + let receivers: Vec> = vec![Box::new(mock_hid_receiver1), Box::new(mock_hid_receiver2)]; + + let mut hid_splitter = HidSplitter { receivers }; + let mock_hid_io = MockHidIo::new(); + hid_splitter.receive_report(&[0, 0, 0, 0], &mock_hid_io); + } +} diff --git a/HidPkg/UefiHidDxeV2/src/hid_io.rs b/HidPkg/UefiHidDxeV2/src/hid_io.rs new file mode 100644 index 0000000000..2d0b2fd3be --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/hid_io.rs @@ -0,0 +1,460 @@ +//! Provides HidIo interface support. +//! +//! This module defines traits for abstractions that support the HidIo protocol +//! as well as concrete implementations that provide those traits by wrapping +//! the HidIo protocol from external devices. +//! +//! ## License +//! +//! Copyright (C) Microsoft Corporation. All rights reserved. +//! +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! +use core::{ffi::c_void, slice::from_raw_parts_mut}; + +use alloc::{boxed::Box, vec}; + +use hid_io::protocol::HidReportType; +use hidparser::ReportDescriptor; +use r_efi::efi; +use rust_advanced_logger_dxe::{debugln, DEBUG_ERROR}; + +#[cfg(test)] +use mockall::automock; + +use crate::boot_services::UefiBootServices; + +/// Defines an interface to be implemented by logic that wants to receive hid reports. +#[cfg_attr(test, automock)] +pub trait HidReportReceiver { + /// Initializes the receiver. After this call, the receiver should be prepared to receive reports. + fn initialize(&mut self, controller: efi::Handle, hid_io: &dyn HidIo) -> Result<(), efi::Status>; + /// Called to pass a report to the receiver. + fn receive_report(&mut self, report: &[u8], hid_io: &dyn HidIo); +} + +/// Defines an interface to abstract interaction with the HidIo protocol. +/// +/// Refer to: +#[cfg_attr(test, automock)] +pub trait HidIo { + /// Returns the parsed report descriptor for the device. + fn get_report_descriptor(&self) -> Result; + /// sends an output report to the device. + fn set_output_report(&self, id: Option, report: &[u8]) -> Result<(), efi::Status>; + /// configures a receiver to receive reports from the device and configures the device to send reports. + fn set_report_receiver(&mut self, receiver: Box) -> Result<(), efi::Status>; + /// removes the receiver and stops the device from sending reports. + fn take_report_receiver(&mut self) -> Option>; +} + +/// Defines a factory interface for producing HidIo instances on a given controller. +#[cfg_attr(test, automock)] +pub trait HidIoFactory { + /// Creates a new instance of HidIo on the given controller. If `owned` is set, then the implementation + /// expects to have ownership of the device (to be released when dropped). If not `owned`, then the + /// HidIo instance is being opened for a transient non-owned usage and does not need to be released. + fn new_hid_io(&self, controller: efi::Handle, owned: bool) -> Result, efi::Status>; +} + +/// Implements the HidIoFactory interface using UEFI boot services to open HidIo protocols on supported controllers. +pub struct UefiHidIoFactory { + boot_services: &'static dyn UefiBootServices, + agent: efi::Handle, +} + +impl UefiHidIoFactory { + /// Creates a new UefiHidIoFactory. `agent` represents the owner of the factory (typically the image_handle). + pub fn new(boot_services: &'static dyn UefiBootServices, agent: efi::Handle) -> Self { + UefiHidIoFactory { boot_services, agent } + } +} + +impl HidIoFactory for UefiHidIoFactory { + /// instantiate a new UefiHidIo instance on the given controller. + fn new_hid_io(&self, controller: efi::Handle, owned: bool) -> Result, efi::Status> { + let hid_io = UefiHidIo::new(self.boot_services, self.agent, controller, owned)?; + Ok(Box::new(hid_io)) + } +} + +/// Implements the HidIo interface on top of the HidIo protocol. +pub struct UefiHidIo { + hid_io: &'static mut hid_io::protocol::Protocol, + boot_services: &'static dyn UefiBootServices, + controller: efi::Handle, + agent: efi::Handle, + receiver: Option>, + owned: bool, +} + +impl UefiHidIo { + // creates a new HidIo - private, intended only to be invoked by the [`UefiHidIoFactory`] implementation. + // if `owned`, then the interface is opened BY_DRIVER, otherwise it is opened with GET_PROTOCOL. + fn new( + boot_services: &'static dyn UefiBootServices, + agent: efi::Handle, + controller: efi::Handle, + owned: bool, + ) -> Result { + let mut hid_io_ptr: *mut hid_io::protocol::Protocol = core::ptr::null_mut(); + + let attributes = { + if owned { + efi::OPEN_PROTOCOL_BY_DRIVER + } else { + efi::OPEN_PROTOCOL_GET_PROTOCOL + } + }; + + let status = boot_services.open_protocol( + controller, + &hid_io::protocol::GUID as *const efi::Guid as *mut efi::Guid, + core::ptr::addr_of_mut!(hid_io_ptr) as *mut *mut c_void, + agent, + controller, + attributes, + ); + + if status.is_error() { + return Err(status); + } + + let hid_io = unsafe { hid_io_ptr.as_mut().expect("bad hid_io ptr") }; + Ok(Self { hid_io, boot_services, controller, agent, receiver: None, owned }) + } + + // the report callback FFI interface that is submitted to the HidIo instance to receive callbacks for reports. + extern "efiapi" fn report_callback(report_buffer_size: u16, report_buffer: *mut c_void, context: *mut c_void) { + let hid_io = unsafe { (context as *mut Self).as_mut().expect("bad context") }; + if let Some(mut receiver) = hid_io.receiver.take() { + let report = unsafe { from_raw_parts_mut(report_buffer as *mut u8, report_buffer_size as usize) }; + receiver.receive_report(report, hid_io); + hid_io.receiver = Some(receiver); + } + } +} + +impl Drop for UefiHidIo { + // Closes the HidIo interface if owned. + fn drop(&mut self) { + if self.owned { + let _ = self.take_report_receiver(); + let status = self.boot_services.close_protocol( + self.controller, + &hid_io::protocol::GUID as *const efi::Guid as *mut efi::Guid, + self.agent, + self.controller, + ); + if status.is_error() { + debugln!(DEBUG_ERROR, "Unexpected error closing hid_io: {:x?}", status); + } + } + } +} + +impl HidIo for UefiHidIo { + fn get_report_descriptor(&self) -> Result { + let mut report_descriptor_size: usize = 0; + match (self.hid_io.get_report_descriptor)( + self.hid_io, + core::ptr::addr_of_mut!(report_descriptor_size), + core::ptr::null_mut(), + ) { + efi::Status::BUFFER_TOO_SMALL => (), + efi::Status::SUCCESS => return Err(efi::Status::DEVICE_ERROR), + err => return Err(err), + } + + let mut report_descriptor_buffer = vec![0u8; report_descriptor_size]; + let report_descriptor_buffer_ptr = report_descriptor_buffer.as_mut_ptr(); + + match (self.hid_io.get_report_descriptor)( + self.hid_io, + core::ptr::addr_of_mut!(report_descriptor_size), + report_descriptor_buffer_ptr as *mut c_void, + ) { + efi::Status::SUCCESS => (), + err => return Err(err), + } + + hidparser::parse_report_descriptor(&report_descriptor_buffer).map_err(|_| efi::Status::DEVICE_ERROR) + } + + fn set_output_report(&self, id: Option, report: &[u8]) -> Result<(), efi::Status> { + match (self.hid_io.set_report)( + self.hid_io, + id.unwrap_or(0), + HidReportType::OutputReport, + report.len(), + report.as_ptr() as *mut c_void, + ) { + efi::Status::SUCCESS => Ok(()), + err => Err(err), + } + } + + fn set_report_receiver(&mut self, receiver: Box) -> Result<(), efi::Status> { + if !self.owned { + return Err(efi::Status::ACCESS_DENIED); + } + let self_ptr = self as *mut UefiHidIo; + + //always attempt uninstall. Failure is ok if not already installed. This shuts down report callback generation + //(if any) so that callbacks are not occurring while the new receiver is installed. + let _ = (self.hid_io.unregister_report_callback)(self.hid_io, Self::report_callback); + + match (self.hid_io.register_report_callback)(self.hid_io, Self::report_callback, self_ptr as *mut c_void) { + efi::Status::SUCCESS => (), + err => return Err(err), + } + self.receiver = Some(receiver); + + Ok(()) + } + fn take_report_receiver(&mut self) -> Option> { + if !self.owned { + return None; + } + //always attempt uninstall. Failure is ok if not already installed. This shuts down report callback generation + //(if any) so that callbacks are not occurring before the receiver is removed. + let _ = (self.hid_io.unregister_report_callback)(self.hid_io, Self::report_callback); + self.receiver.take() + } +} + +#[cfg(test)] +mod test { + use core::{ + ffi::c_void, + slice::{from_raw_parts, from_raw_parts_mut}, + }; + + use super::{HidIo, MockHidReportReceiver, UefiHidIo}; + + use crate::boot_services::MockUefiBootServices; + + use r_efi::efi; + + static MINIMAL_BOOT_KEYBOARD_REPORT_DESCRIPTOR: &[u8] = &[ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x06, // USAGE (Keyboard) + 0xa1, 0x01, // COLLECTION (Application) + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, 0x08, // REPORT_COUNT (8) + 0x05, 0x07, // USAGE_PAGE (Key Codes) + 0x19, 0xE0, // USAGE_MINIMUM (224) + 0x29, 0xE7, // USAGE_MAXIMUM (231) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x81, 0x02, // INPUT (Data, Var, Abs) (Modifier Byte) + 0xc0, // END_COLLECTION + ]; + + static TEST_REPORT0: &[u8] = &[0x0, 0x1, 0x2, 0x3, 0x4]; + static TEST_REPORT1: &[u8] = &[0x4, 0x3, 0x2, 0x1, 0x0]; + + // In this module, the usage model for boot_services is global static, and so &'static dyn UefiBootServices is used + // throughout the API. For testing, each test will have a different set of expectations on the UefiBootServices mock + // object, and the mock object itself expects to be "mut", which makes it hard to handle as a single global static. + // Instead, raw pointers are used to simulate a MockUefiBootServices instance with 'static lifetime. + // This object needs to outlive anything that uses it - once created, it will live until the end of the program. + fn create_fake_static_boot_service() -> &'static mut MockUefiBootServices { + unsafe { Box::into_raw(Box::new(MockUefiBootServices::new())).as_mut().unwrap() } + } + + // Mock the HidIo FFI interface. + fn mock_hid_io() -> hid_io::protocol::Protocol { + extern "efiapi" fn mock_get_report_descriptor( + this: *const hid_io::protocol::Protocol, + report_descriptor_size: *mut usize, + report_descriptor_buffer: *mut c_void, + ) -> efi::Status { + assert_ne!(this, core::ptr::null()); + unsafe { + if *report_descriptor_size < MINIMAL_BOOT_KEYBOARD_REPORT_DESCRIPTOR.len() { + *report_descriptor_size = MINIMAL_BOOT_KEYBOARD_REPORT_DESCRIPTOR.len(); + return efi::Status::BUFFER_TOO_SMALL; + } else { + *report_descriptor_size = MINIMAL_BOOT_KEYBOARD_REPORT_DESCRIPTOR.len(); + let slice = from_raw_parts_mut(report_descriptor_buffer as *mut u8, *report_descriptor_size); + slice.copy_from_slice(MINIMAL_BOOT_KEYBOARD_REPORT_DESCRIPTOR); + return efi::Status::SUCCESS; + } + } + } + + extern "efiapi" fn mock_get_report( + _this: *const hid_io::protocol::Protocol, + _report_id: u8, + _report_type: hid_io::protocol::HidReportType, + _report_buffer_size: usize, + _report_buffer: *mut c_void, + ) -> efi::Status { + panic!("This implementation does not use get_report."); + } + + extern "efiapi" fn mock_set_report( + this: *const hid_io::protocol::Protocol, + report_id: u8, + report_type: hid_io::protocol::HidReportType, + report_buffer_size: usize, + report_buffer: *mut c_void, + ) -> efi::Status { + assert_ne!(this, core::ptr::null()); + assert_eq!(report_type, hid_io::protocol::HidReportType::OutputReport); + assert_ne!(report_buffer_size, 0); + assert_ne!(report_buffer, core::ptr::null_mut()); + + let report_slice = unsafe { from_raw_parts(report_buffer as *mut u8, report_buffer_size) }; + + match report_id { + 0 => { + assert_eq!(report_slice, TEST_REPORT0); + efi::Status::SUCCESS + } + 1 => { + assert_eq!(report_slice, TEST_REPORT1); + efi::Status::SUCCESS + } + _ => efi::Status::UNSUPPORTED, + } + } + + extern "efiapi" fn mock_register_report_callback( + this: *const hid_io::protocol::Protocol, + callback: hid_io::protocol::HidIoReportCallback, + context: *mut c_void, + ) -> efi::Status { + assert_ne!(this, core::ptr::null()); + assert_ne!(context, core::ptr::null_mut()); + assert!(callback == UefiHidIo::report_callback); + + callback(TEST_REPORT0.len() as u16, TEST_REPORT0.as_ptr() as *mut c_void, context); + + efi::Status::SUCCESS + } + + extern "efiapi" fn mock_unregister_report_callback( + this: *const hid_io::protocol::Protocol, + callback: hid_io::protocol::HidIoReportCallback, + ) -> efi::Status { + assert_ne!(this, core::ptr::null()); + assert!(callback == UefiHidIo::report_callback); + efi::Status::SUCCESS + } + + hid_io::protocol::Protocol { + get_report_descriptor: mock_get_report_descriptor, + get_report: mock_get_report, + set_report: mock_set_report, + register_report_callback: mock_register_report_callback, + unregister_report_callback: mock_unregister_report_callback, + } + } + + #[test] + fn new_should_instantiate_new_uefi_hid_io() { + let boot_services = create_fake_static_boot_service(); + let controller: efi::Handle = 0x1234 as efi::Handle; + let agent: efi::Handle = 0x4321 as efi::Handle; + + boot_services.expect_open_protocol().returning(|handle, protocol, interface, agent, controller, attributes| { + assert_eq!(handle, 0x1234 as efi::Handle); + assert_eq!(unsafe { *protocol }, hid_io::protocol::GUID); + assert_ne!(interface, core::ptr::null_mut()); + assert_eq!(agent, 0x4321 as efi::Handle); + assert_eq!(controller, 0x1234 as efi::Handle); + assert_eq!(attributes, efi::OPEN_PROTOCOL_BY_DRIVER); + + //note: this leaks; but easier than trying to share it between the closure and the environment. + let hid_io = Box::into_raw(Box::new(mock_hid_io())); + unsafe { *interface = hid_io as *mut c_void }; + efi::Status::SUCCESS + }); + + boot_services.expect_close_protocol().returning(|handle, protocol, agent, controller| { + assert_eq!(handle, 0x1234 as efi::Handle); + assert_eq!(unsafe { *protocol }, hid_io::protocol::GUID); + assert_eq!(agent, 0x4321 as efi::Handle); + assert_eq!(controller, 0x1234 as efi::Handle); + efi::Status::SUCCESS + }); + + let uefi_hid_io = UefiHidIo::new(boot_services, agent, controller, true).unwrap(); + drop(uefi_hid_io); + } + + #[test] + fn get_report_descriptor_should_return_report_descriptor() { + let boot_services = create_fake_static_boot_service(); + let controller: efi::Handle = 0x1234 as efi::Handle; + let agent: efi::Handle = 0x4321 as efi::Handle; + + boot_services.expect_open_protocol().returning(|_, _, interface, _, _, _| { + let hid_io = mock_hid_io(); + //note: this leaks; but easier than trying to share it between the closure and the environment. + unsafe { *interface = Box::into_raw(Box::new(hid_io)) as *mut c_void }; + efi::Status::SUCCESS + }); + + boot_services.expect_close_protocol().returning(|_, _, _, _| efi::Status::SUCCESS); + + let uefi_hid_io = UefiHidIo::new(boot_services, agent, controller, true).unwrap(); + let descriptor = uefi_hid_io.get_report_descriptor().unwrap(); + assert_eq!(descriptor, hidparser::parse_report_descriptor(&MINIMAL_BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap()); + drop(uefi_hid_io); + } + #[test] + fn set_report_should_set_report() { + let boot_services = create_fake_static_boot_service(); + let controller: efi::Handle = 0x1234 as efi::Handle; + let agent: efi::Handle = 0x4321 as efi::Handle; + + boot_services.expect_open_protocol().returning(|_, _, interface, _, _, _| { + let hid_io = mock_hid_io(); + unsafe { *interface = Box::into_raw(Box::new(hid_io)) as *mut c_void }; + efi::Status::SUCCESS + }); + + boot_services.expect_close_protocol().returning(|_, _, _, _| efi::Status::SUCCESS); + + let uefi_hid_io = UefiHidIo::new(boot_services, agent, controller, true).unwrap(); + + uefi_hid_io.set_output_report(None, &TEST_REPORT0).unwrap(); + uefi_hid_io.set_output_report(Some(1), &TEST_REPORT1).unwrap(); + assert_eq!(uefi_hid_io.set_output_report(Some(2), &TEST_REPORT0), Err(efi::Status::UNSUPPORTED)); + + drop(uefi_hid_io); + } + + #[test] + fn set_receiver_should_install_receiver() { + let boot_services = create_fake_static_boot_service(); + let controller: efi::Handle = 0x1234 as efi::Handle; + let agent: efi::Handle = 0x4321 as efi::Handle; + + boot_services.expect_open_protocol().returning(|_, _, interface, _, _, _| { + let hid_io = mock_hid_io(); + unsafe { *interface = Box::into_raw(Box::new(hid_io)) as *mut c_void }; + efi::Status::SUCCESS + }); + + boot_services.expect_close_protocol().returning(|_, _, _, _| efi::Status::SUCCESS); + + let mut uefi_hid_io = UefiHidIo::new(boot_services, agent, controller, true).unwrap(); + + let mut mock_receiver = MockHidReportReceiver::new(); + mock_receiver + .expect_receive_report() + .withf(|report, _| { + assert_eq!(report, TEST_REPORT0); + true + }) + .returning(|_, _| ()); + + uefi_hid_io.set_report_receiver(Box::new(mock_receiver)).unwrap(); + + drop(uefi_hid_io); + } +} diff --git a/HidPkg/UefiHidDxeV2/src/keyboard/key_queue.rs b/HidPkg/UefiHidDxeV2/src/keyboard/key_queue.rs new file mode 100644 index 0000000000..0ae881ed43 --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/keyboard/key_queue.rs @@ -0,0 +1,731 @@ +//! Key queue support for HID driver. +//! +//! This module manages a queue of pending keystrokes and current active keyboard state and provides support for +//! translating between HID usages and EFI keyboard primitives. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. All rights reserved. +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! + +use core::sync::atomic::Ordering; + +use alloc::{ + collections::{BTreeSet, VecDeque}, + vec::Vec, +}; +use hidparser::report_data_types::Usage; +use hii_keyboard_layout::{EfiKey, HiiKey, HiiKeyDescriptor, HiiKeyboardLayout, HiiNsKeyDescriptor}; +use r_efi::{ + efi, + protocols::{self, hii_database::*, simple_text_input::InputKey, simple_text_input_ex::*}, +}; + +use rust_advanced_logger_dxe::{debugln, DEBUG_WARN}; + +use crate::RUNTIME_SERVICES; + +// The set of HID usages that represent modifier keys this driver is interested in. +#[rustfmt::skip] +const KEYBOARD_MODIFIERS: &[u16] = &[ + LEFT_CONTROL_MODIFIER, RIGHT_CONTROL_MODIFIER, LEFT_SHIFT_MODIFIER, RIGHT_SHIFT_MODIFIER, LEFT_ALT_MODIFIER, + RIGHT_ALT_MODIFIER, LEFT_LOGO_MODIFIER, RIGHT_LOGO_MODIFIER, MENU_MODIFIER, PRINT_MODIFIER, SYS_REQUEST_MODIFIER, + ALT_GR_MODIFIER]; + +// The set of HID usages that represent modifier keys that toggle state (as opposed to remain active while pressed). +const TOGGLE_MODIFIERS: &[u16] = &[NUM_LOCK_MODIFIER, CAPS_LOCK_MODIFIER, SCROLL_LOCK_MODIFIER]; + +// Control, Shift, and Alt modifiers. +const CTRL_MODIFIERS: &[u16] = &[LEFT_CONTROL_MODIFIER, RIGHT_CONTROL_MODIFIER]; +const SHIFT_MODIFIERS: &[u16] = &[LEFT_SHIFT_MODIFIER, RIGHT_SHIFT_MODIFIER]; +const ALT_MODIFIERS: &[u16] = &[LEFT_ALT_MODIFIER, RIGHT_ALT_MODIFIER]; + +// Defines whether a key stroke represents a key being pressed (KeyDown) or released (KeyUp) +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum KeyAction { + // Key is being pressed + KeyUp, + // Key is being released + KeyDown, +} + +// A wrapper for the KeyData type that allows definition of the Ord trait and additional registration matching logic. +#[derive(Debug, Clone)] +pub(crate) struct OrdKeyData(pub protocols::simple_text_input_ex::KeyData); + +impl Ord for OrdKeyData { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + let e = self.0.key.unicode_char.cmp(&other.0.key.unicode_char); + if !e.is_eq() { + return e; + } + let e = self.0.key.scan_code.cmp(&other.0.key.scan_code); + if !e.is_eq() { + return e; + } + let e = self.0.key_state.key_shift_state.cmp(&other.0.key_state.key_shift_state); + if !e.is_eq() { + return e; + } + self.0.key_state.key_toggle_state.cmp(&other.0.key_state.key_toggle_state) + } +} + +impl PartialOrd for OrdKeyData { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for OrdKeyData { + fn eq(&self, other: &Self) -> bool { + self.cmp(other).is_eq() + } +} + +impl Eq for OrdKeyData {} + +impl OrdKeyData { + // Returns whether this key matches the given registration. Note that this is not a straight compare - UEFI spec + // allows for some degree of wildcard matching. Refer to UEFI spec 2.10 section 12.2.5. + pub(crate) fn matches_registered_key(&self, registration: &Self) -> bool { + // assign names here for brevity below. + let self_char = self.0.key.unicode_char; + let self_scan = self.0.key.scan_code; + let self_shift = self.0.key_state.key_shift_state; + let self_toggle = self.0.key_state.key_toggle_state; + let register_char = registration.0.key.unicode_char; + let register_scan = registration.0.key.scan_code; + let register_shift = registration.0.key_state.key_shift_state; + let register_toggle = registration.0.key_state.key_toggle_state; + + //char and scan must match (per the reference implementation in the EDK2 C code). + if !(register_char == self_char && register_scan == self_scan) { + return false; + } + + //shift state must be zero or must match. + if !(register_shift == 0 || register_shift == self_shift) { + return false; + } + + //toggle state must be zero or must match. + if !(register_toggle == 0 || register_toggle == self_toggle) { + return false; + } + true + } +} + +// This structure manages the queue of pending keystrokes +#[derive(Debug, Default)] +pub(crate) struct KeyQueue { + layout: Option, + active_modifiers: BTreeSet, + active_ns_key: Option, + partial_key_support_active: bool, + key_queue: VecDeque, + registered_keys: BTreeSet, + notified_key_queue: VecDeque, +} + +impl KeyQueue { + // resets the KeyQueue to initial state + pub(crate) fn reset(&mut self, extended_reset: bool) { + if extended_reset { + self.active_modifiers.clear(); + } else { + let active_leds = self.get_active_led_modifiers(); + self.active_modifiers.retain(|x| active_leds.contains(x)); + } + self.active_ns_key = None; + self.partial_key_support_active = false; + self.key_queue.clear(); + } + + // Processes the given keystroke and updates the KeyQueue accordingly. + pub(crate) fn keystroke(&mut self, key: Usage, action: KeyAction) { + let Some(ref active_layout) = self.layout else { + //nothing to do if no layout. This is unexpected: layout should be initialized with default if not present. + debugln!(DEBUG_WARN, "key_queue::keystroke: Received keystroke without layout."); + return; + }; + + let Some(efi_key) = usage_to_efi_key(key) else { + //unsupported key usage, nothing to do. + return; + }; + + // Check if it is a dependent key of a currently active "non-spacing" (ns) key. + // Non-spacing key handling is described in UEFI spec 2.10 section 33.2.4.3. + let mut current_descriptor: Option = None; + if let Some(ref ns_key) = self.active_ns_key { + for descriptor in &ns_key.dependent_keys { + if descriptor.key == efi_key { + // found a dependent key for a previously active ns key. + // de-activate the ns key and process the dependent descriptor. + current_descriptor = Some(*descriptor); + self.active_ns_key = None; + break; + } + } + } + + // If it is not a dependent key of a currently active ns key, then check if it is a regular or ns key. + if current_descriptor.is_none() { + for key in &active_layout.keys { + match key { + HiiKey::Key(descriptor) => { + if descriptor.key == efi_key { + current_descriptor = Some(*descriptor); + break; + } + } + HiiKey::NsKey(ns_descriptor) => { + if ns_descriptor.descriptor.key == efi_key { + // if it is an ns_key, set it as the active ns key, and no further processing is needed. + self.active_ns_key = Some(ns_descriptor.clone()); + return; + } + } + } + } + } + + if current_descriptor.is_none() { + return; //could not find descriptor, nothing to do. + } + + let current_descriptor = current_descriptor.unwrap(); + + //handle modifiers that are active as long as they are pressed + if KEYBOARD_MODIFIERS.contains(¤t_descriptor.modifier) { + match action { + KeyAction::KeyUp => { + self.active_modifiers.remove(¤t_descriptor.modifier); + } + KeyAction::KeyDown => { + self.active_modifiers.insert(current_descriptor.modifier); + } + } + } + + //handle modifiers that toggle each time the key is pressed. + if TOGGLE_MODIFIERS.contains(¤t_descriptor.modifier) && action == KeyAction::KeyDown { + if self.active_modifiers.contains(¤t_descriptor.modifier) { + self.active_modifiers.remove(¤t_descriptor.modifier); + } else { + self.active_modifiers.insert(current_descriptor.modifier); + } + } + + //handle ctrl-alt-delete + if CTRL_MODIFIERS.iter().any(|x| self.active_modifiers.contains(x)) + && ALT_MODIFIERS.iter().any(|x| self.active_modifiers.contains(x)) + && current_descriptor.modifier == DELETE_MODIFIER + { + debugln!(DEBUG_WARN, "Ctrl-Alt-Del pressed, resetting system."); + if let Some(runtime_services) = unsafe { RUNTIME_SERVICES.load(Ordering::SeqCst).as_mut() } { + (runtime_services.reset_system)(efi::RESET_WARM, efi::Status::SUCCESS, 0, core::ptr::null_mut()); + } + panic!("Reset failed."); + } + + if action == KeyAction::KeyUp { + //nothing else to do. + return; + } + + // process the keystroke to construct a KeyData item to add to the queue. + let mut key_data = protocols::simple_text_input_ex::KeyData { + key: InputKey { + unicode_char: current_descriptor.unicode, + scan_code: modifier_to_scan(current_descriptor.modifier), + }, + ..Default::default() + }; + + // retrieve relevant modifier state that may need to be applied to the key data. + let shift_active = SHIFT_MODIFIERS.iter().any(|x| self.active_modifiers.contains(x)); + let alt_gr_active = self.active_modifiers.contains(&ALT_GR_MODIFIER); + let caps_lock_active = self.active_modifiers.contains(&CAPS_LOCK_MODIFIER); + let num_lock_active = self.active_modifiers.contains(&NUM_LOCK_MODIFIER); + + // Apply the shift modifier if needed. Track whether it was applied as the shift modifier is removed from the key + // state if it was applied here. + let mut shift_applied: bool = false; + if (current_descriptor.affected_attribute & AFFECTED_BY_STANDARD_SHIFT) != 0 { + if shift_active { + //shift active + if alt_gr_active { + key_data.key.unicode_char = current_descriptor.shifted_alt_gr_unicode; + } else { + key_data.key.unicode_char = current_descriptor.shifted_unicode; + } + shift_applied = true; + } else { + //not shifted. + if alt_gr_active { + key_data.key.unicode_char = current_descriptor.alt_gr_unicode; + } + } + } + + // if capslock is active, then invert the shift state of the key. + if (current_descriptor.affected_attribute & AFFECTED_BY_CAPS_LOCK) != 0 && caps_lock_active { + //Note: reference EDK2 implementation does not apply capslock to alt_gr. + if key_data.key.unicode_char == current_descriptor.unicode { + key_data.key.unicode_char = current_descriptor.shifted_unicode; + } else if key_data.key.unicode_char == current_descriptor.shifted_unicode { + key_data.key.unicode_char = current_descriptor.unicode; + } + } + + // for the num pad, numlock (and shift state) controls whether a number key or a control key (e.g. arrow) is queued. + if (current_descriptor.affected_attribute & AFFECTED_BY_NUM_LOCK) != 0 { + if num_lock_active && !shift_active { + key_data.key.scan_code = SCAN_NULL; + } else { + key_data.key.unicode_char = 0x0000; + } + } + + //special handling for unicode 0x1B (ESC). + if key_data.key.unicode_char == 0x01B && key_data.key.scan_code == SCAN_NULL { + key_data.key.scan_code = SCAN_ESC; + key_data.key.unicode_char = 0x0000; + } + + if !self.partial_key_support_active && key_data.key.unicode_char == 0 && key_data.key.scan_code == SCAN_NULL { + return; // no further processing required if there is no key or scancode and partial support is not active. + } + + //initialize key state from active modifiers + key_data.key_state = self.init_key_state(); + + // if shift was applied above, then remove shift from key state. See UEFI spec 2.10 section 12.2.3. + if shift_applied { + key_data.key_state.key_shift_state &= !(LEFT_SHIFT_PRESSED | RIGHT_SHIFT_PRESSED); + } + + // if a callback has been registered matching this key, enqueue it in the callback queue. + if self.is_registered_key(key_data) { + self.notified_key_queue.push_back(key_data); + } + + // enqueue the key data. + self.key_queue.push_back(key_data); + } + + fn is_registered_key(&self, current_key: KeyData) -> bool { + for registered_key in &self.registered_keys { + if OrdKeyData(current_key).matches_registered_key(registered_key) { + return true; + } + } + false + } + + // Creates a KeyState instance initialized based on the current modifier state. + pub(crate) fn init_key_state(&self) -> KeyState { + let mut key_shift_state = SHIFT_STATE_VALID; + let mut key_toggle_state = TOGGLE_STATE_VALID; + + if self.partial_key_support_active { + key_toggle_state |= KEY_STATE_EXPOSED; + } + + for modifier in &self.active_modifiers { + match *modifier { + LEFT_CONTROL_MODIFIER => key_shift_state |= LEFT_CONTROL_PRESSED, + RIGHT_CONTROL_MODIFIER => key_shift_state |= RIGHT_CONTROL_PRESSED, + LEFT_ALT_MODIFIER => key_shift_state |= LEFT_ALT_PRESSED, + RIGHT_ALT_MODIFIER => key_shift_state |= RIGHT_ALT_PRESSED, + LEFT_SHIFT_MODIFIER => key_shift_state |= LEFT_SHIFT_PRESSED, + RIGHT_SHIFT_MODIFIER => key_shift_state |= RIGHT_SHIFT_PRESSED, + LEFT_LOGO_MODIFIER => key_shift_state |= LEFT_LOGO_PRESSED, + RIGHT_LOGO_MODIFIER => key_shift_state |= RIGHT_LOGO_PRESSED, + MENU_MODIFIER => key_shift_state |= MENU_KEY_PRESSED, + SYS_REQUEST_MODIFIER | PRINT_MODIFIER => key_shift_state |= SYS_REQ_PRESSED, + SCROLL_LOCK_MODIFIER => key_toggle_state |= SCROLL_LOCK_ACTIVE, + NUM_LOCK_MODIFIER => key_toggle_state |= NUM_LOCK_ACTIVE, + CAPS_LOCK_MODIFIER => key_toggle_state |= CAPS_LOCK_ACTIVE, + _ => (), + } + } + KeyState { key_shift_state, key_toggle_state } + } + + // pops and returns the front of the key queue + pub(crate) fn pop_key(&mut self) -> Option { + self.key_queue.pop_front() + } + + // returns a copy of the key at the front of the queue + pub(crate) fn peek_key(&self) -> Option { + self.key_queue.front().cloned() + } + + // pops and returns the front of the notify queue + pub(crate) fn pop_notify_key(&mut self) -> Option { + self.notified_key_queue.pop_front() + } + + // returns a copy of the key at the front of the notify queue + pub(crate) fn peek_notify_key(&self) -> Option { + self.key_queue.front().cloned() + } + + // set the key toggle state. This allows control of scroll/caps/num locks, as well as whether partial key state is + // exposed. + pub(crate) fn set_key_toggle_state(&mut self, toggle_state: KeyToggleState) { + if (toggle_state & SCROLL_LOCK_ACTIVE) != 0 { + self.active_modifiers.insert(SCROLL_LOCK_MODIFIER); + } else { + self.active_modifiers.remove(&SCROLL_LOCK_MODIFIER); + } + + if (toggle_state & NUM_LOCK_ACTIVE) != 0 { + self.active_modifiers.insert(NUM_LOCK_MODIFIER); + } else { + self.active_modifiers.remove(&NUM_LOCK_MODIFIER); + } + + if (toggle_state & CAPS_LOCK_ACTIVE) != 0 { + self.active_modifiers.insert(CAPS_LOCK_MODIFIER); + } else { + self.active_modifiers.remove(&CAPS_LOCK_MODIFIER); + } + + self.partial_key_support_active = (toggle_state & KEY_STATE_EXPOSED) != 0; + } + + fn get_active_led_modifiers(&self) -> Vec { + self.active_modifiers.iter().cloned().filter(|x| modifier_to_led_usage(*x).is_some()).collect() + } + // Returns a vector of HID usages corresponding to the active LEDs based on the active modifier state. + pub(crate) fn get_active_leds(&self) -> Vec { + self.active_modifiers.iter().cloned().filter_map(modifier_to_led_usage).collect() + } + + // Returns the current keyboard layout that the KeyQueue is using. + pub(crate) fn get_layout(&self) -> Option { + self.layout.clone() + } + + // Sets the current keyboard layout that the KeyQueue should use. + pub(crate) fn set_layout(&mut self, new_layout: Option) { + self.layout = new_layout; + } + + // Add a registration key for notifications; if a keystroke matches this key data, it will be added to the notify + // queue in addition to the normal key queue. + pub(crate) fn add_notify_key(&mut self, key_data: OrdKeyData) { + self.registered_keys.insert(key_data); + } + + // Remove a previously added notify key; keystrokes matching this key data will no longer be added to the notify + // queue. + pub(crate) fn remove_notify_key(&mut self, key_data: &OrdKeyData) { + self.registered_keys.remove(key_data); + } +} + +// Helper routine that converts a HID Usage to the corresponding EfiKey. +fn usage_to_efi_key(usage: Usage) -> Option { + //Refer to UEFI spec version 2.10 figure 34.3 + match usage.into() { + 0x00070001..=0x00070003 => None, //Keyboard error codes. + 0x00070004 => Some(EfiKey::C1), + 0x00070005 => Some(EfiKey::B5), + 0x00070006 => Some(EfiKey::B3), + 0x00070007 => Some(EfiKey::C3), + 0x00070008 => Some(EfiKey::D3), + 0x00070009 => Some(EfiKey::C4), + 0x0007000A => Some(EfiKey::C5), + 0x0007000B => Some(EfiKey::C6), + 0x0007000C => Some(EfiKey::D8), + 0x0007000D => Some(EfiKey::C7), + 0x0007000E => Some(EfiKey::C8), + 0x0007000F => Some(EfiKey::C9), + 0x00070010 => Some(EfiKey::B7), + 0x00070011 => Some(EfiKey::B6), + 0x00070012 => Some(EfiKey::D9), + 0x00070013 => Some(EfiKey::D10), + 0x00070014 => Some(EfiKey::D1), + 0x00070015 => Some(EfiKey::D4), + 0x00070016 => Some(EfiKey::C2), + 0x00070017 => Some(EfiKey::D5), + 0x00070018 => Some(EfiKey::D7), + 0x00070019 => Some(EfiKey::B4), + 0x0007001A => Some(EfiKey::D2), + 0x0007001B => Some(EfiKey::B2), + 0x0007001C => Some(EfiKey::D6), + 0x0007001D => Some(EfiKey::B1), + 0x0007001E => Some(EfiKey::E1), + 0x0007001F => Some(EfiKey::E2), + 0x00070020 => Some(EfiKey::E3), + 0x00070021 => Some(EfiKey::E4), + 0x00070022 => Some(EfiKey::E5), + 0x00070023 => Some(EfiKey::E6), + 0x00070024 => Some(EfiKey::E7), + 0x00070025 => Some(EfiKey::E8), + 0x00070026 => Some(EfiKey::E9), + 0x00070027 => Some(EfiKey::E10), + 0x00070028 => Some(EfiKey::Enter), + 0x00070029 => Some(EfiKey::Esc), + 0x0007002A => Some(EfiKey::BackSpace), + 0x0007002B => Some(EfiKey::Tab), + 0x0007002C => Some(EfiKey::SpaceBar), + 0x0007002D => Some(EfiKey::E11), + 0x0007002E => Some(EfiKey::E12), + 0x0007002F => Some(EfiKey::D11), + 0x00070030 => Some(EfiKey::D12), + 0x00070031 => Some(EfiKey::D13), + 0x00070032 => Some(EfiKey::C12), + 0x00070033 => Some(EfiKey::C10), + 0x00070034 => Some(EfiKey::C11), + 0x00070035 => Some(EfiKey::E0), + 0x00070036 => Some(EfiKey::B8), + 0x00070037 => Some(EfiKey::B9), + 0x00070038 => Some(EfiKey::B10), + 0x00070039 => Some(EfiKey::CapsLock), + 0x0007003A => Some(EfiKey::F1), + 0x0007003B => Some(EfiKey::F2), + 0x0007003C => Some(EfiKey::F3), + 0x0007003D => Some(EfiKey::F4), + 0x0007003E => Some(EfiKey::F5), + 0x0007003F => Some(EfiKey::F6), + 0x00070040 => Some(EfiKey::F7), + 0x00070041 => Some(EfiKey::F8), + 0x00070042 => Some(EfiKey::F9), + 0x00070043 => Some(EfiKey::F10), + 0x00070044 => Some(EfiKey::F11), + 0x00070045 => Some(EfiKey::F12), + 0x00070046 => Some(EfiKey::Print), + 0x00070047 => Some(EfiKey::SLck), + 0x00070048 => Some(EfiKey::Pause), + 0x00070049 => Some(EfiKey::Ins), + 0x0007004A => Some(EfiKey::Home), + 0x0007004B => Some(EfiKey::PgUp), + 0x0007004C => Some(EfiKey::Del), + 0x0007004D => Some(EfiKey::End), + 0x0007004E => Some(EfiKey::PgDn), + 0x0007004F => Some(EfiKey::RightArrow), + 0x00070050 => Some(EfiKey::LeftArrow), + 0x00070051 => Some(EfiKey::DownArrow), + 0x00070052 => Some(EfiKey::UpArrow), + 0x00070053 => Some(EfiKey::NLck), + 0x00070054 => Some(EfiKey::Slash), + 0x00070055 => Some(EfiKey::Asterisk), + 0x00070056 => Some(EfiKey::Minus), + 0x00070057 => Some(EfiKey::Plus), + 0x00070058 => Some(EfiKey::Enter), + 0x00070059 => Some(EfiKey::One), + 0x0007005A => Some(EfiKey::Two), + 0x0007005B => Some(EfiKey::Three), + 0x0007005C => Some(EfiKey::Four), + 0x0007005D => Some(EfiKey::Five), + 0x0007005E => Some(EfiKey::Six), + 0x0007005F => Some(EfiKey::Seven), + 0x00070060 => Some(EfiKey::Eight), + 0x00070061 => Some(EfiKey::Nine), + 0x00070062 => Some(EfiKey::Zero), + 0x00070063 => Some(EfiKey::Period), + 0x00070064 => Some(EfiKey::B0), + 0x00070065 => Some(EfiKey::A4), + 0x00070066..=0x000700DF => None, // not used by EFI keyboard layout. + 0x000700E0 => Some(EfiKey::LCtrl), + 0x000700E1 => Some(EfiKey::LShift), + 0x000700E2 => Some(EfiKey::LAlt), + 0x000700E3 => Some(EfiKey::A0), + 0x000700E4 => Some(EfiKey::RCtrl), + 0x000700E5 => Some(EfiKey::RShift), + 0x000700E6 => Some(EfiKey::A2), + 0x000700E7 => Some(EfiKey::A3), + _ => None, // all other usages not used by EFI keyboard layout. + } +} + +//These should be defined in r_efi::protocols::simple_text_input +const SCAN_NULL: u16 = 0x0000; +const SCAN_UP: u16 = 0x0001; +const SCAN_DOWN: u16 = 0x0002; +const SCAN_RIGHT: u16 = 0x0003; +const SCAN_LEFT: u16 = 0x0004; +const SCAN_HOME: u16 = 0x0005; +const SCAN_END: u16 = 0x0006; +const SCAN_INSERT: u16 = 0x0007; +const SCAN_DELETE: u16 = 0x0008; +const SCAN_PAGE_UP: u16 = 0x0009; +const SCAN_PAGE_DOWN: u16 = 0x000A; +const SCAN_F1: u16 = 0x000B; +const SCAN_F2: u16 = 0x000C; +const SCAN_F3: u16 = 0x000D; +const SCAN_F4: u16 = 0x000E; +const SCAN_F5: u16 = 0x000F; +const SCAN_F6: u16 = 0x0010; +const SCAN_F7: u16 = 0x0011; +const SCAN_F8: u16 = 0x0012; +const SCAN_F9: u16 = 0x0013; +const SCAN_F10: u16 = 0x0014; +const SCAN_F11: u16 = 0x0015; +const SCAN_F12: u16 = 0x0016; +const SCAN_ESC: u16 = 0x0017; +const SCAN_PAUSE: u16 = 0x0048; + +// helper routine that converts the given modifier to the corresponding SCAN code +fn modifier_to_scan(modifier: u16) -> u16 { + match modifier { + INSERT_MODIFIER => SCAN_INSERT, + DELETE_MODIFIER => SCAN_DELETE, + PAGE_DOWN_MODIFIER => SCAN_PAGE_DOWN, + PAGE_UP_MODIFIER => SCAN_PAGE_UP, + HOME_MODIFIER => SCAN_HOME, + END_MODIFIER => SCAN_END, + LEFT_ARROW_MODIFIER => SCAN_LEFT, + RIGHT_ARROW_MODIFIER => SCAN_RIGHT, + DOWN_ARROW_MODIFIER => SCAN_DOWN, + UP_ARROW_MODIFIER => SCAN_UP, + FUNCTION_KEY_ONE_MODIFIER => SCAN_F1, + FUNCTION_KEY_TWO_MODIFIER => SCAN_F2, + FUNCTION_KEY_THREE_MODIFIER => SCAN_F3, + FUNCTION_KEY_FOUR_MODIFIER => SCAN_F4, + FUNCTION_KEY_FIVE_MODIFIER => SCAN_F5, + FUNCTION_KEY_SIX_MODIFIER => SCAN_F6, + FUNCTION_KEY_SEVEN_MODIFIER => SCAN_F7, + FUNCTION_KEY_EIGHT_MODIFIER => SCAN_F8, + FUNCTION_KEY_NINE_MODIFIER => SCAN_F9, + FUNCTION_KEY_TEN_MODIFIER => SCAN_F10, + FUNCTION_KEY_ELEVEN_MODIFIER => SCAN_F11, + FUNCTION_KEY_TWELVE_MODIFIER => SCAN_F12, + PAUSE_MODIFIER => SCAN_PAUSE, + _ => SCAN_NULL, + } +} + +// helper routine that converts the given modifier to the corresponding HID Usage. +fn modifier_to_led_usage(modifier: u16) -> Option { + match modifier { + NUM_LOCK_MODIFIER => Some(Usage::from(0x00080001)), + CAPS_LOCK_MODIFIER => Some(Usage::from(0x00080002)), + SCROLL_LOCK_MODIFIER => Some(Usage::from(0x00080003)), + _ => None, + } +} + +#[cfg(test)] +mod test { + + use hidparser::report_data_types::Usage; + use hii_keyboard_layout::{EfiKey, HiiKey, HiiKeyDescriptor, HiiNsKeyDescriptor}; + use r_efi::protocols::{ + self, + hii_database::{AFFECTED_BY_CAPS_LOCK, AFFECTED_BY_STANDARD_SHIFT, NS_KEY_DEPENDENCY_MODIFIER, NS_KEY_MODIFIER}, + }; + + use crate::keyboard::key_queue::{OrdKeyData, SCAN_DOWN}; + + use super::KeyQueue; + + // convenience macro for defining HiiKeyDescriptor structures. + // note: for unicode characters, these are encoded as u16 for compliance with UEFI spec. UEFI only supports UCS-2 + // encoding - so unicode characters that require more than two bytes under UTF-16 are not supported (and will panic). + macro_rules! key_descriptor { + ($key:expr, $unicode:literal, $shifted:literal, $alt_gr:literal, $shifted_alt_gr:literal, $modifier:expr, $affected:expr ) => { + HiiKeyDescriptor { + key: $key, + unicode: $unicode.encode_utf16(&mut [0u16; 1])[0], + shifted_unicode: $shifted.encode_utf16(&mut [0u16; 1])[0], + alt_gr_unicode: $alt_gr.encode_utf16(&mut [0u16; 1])[0], + shifted_alt_gr_unicode: $shifted_alt_gr.encode_utf16(&mut [0u16; 1])[0], + modifier: $modifier, + affected_attribute: $affected, + } + }; + } + + //most key_queue functionality is covered by tests in keyboard.rs. Here follows a few misc test cases. + + #[test] + fn test_ord_key_comparisons() { + let mut key_data1: protocols::simple_text_input_ex::KeyData = Default::default(); + let mut key_data2: protocols::simple_text_input_ex::KeyData = Default::default(); + + assert_eq!(OrdKeyData(key_data1), OrdKeyData(key_data2)); + key_data1.key.unicode_char = 'a' as u16; + assert_ne!(OrdKeyData(key_data1), OrdKeyData(key_data2)); + key_data2.key.unicode_char = 'a' as u16; + assert_eq!(OrdKeyData(key_data1), OrdKeyData(key_data2)); + + key_data1.key.scan_code = SCAN_DOWN; + assert_ne!(OrdKeyData(key_data1), OrdKeyData(key_data2)); + key_data2.key.scan_code = SCAN_DOWN; + assert_eq!(OrdKeyData(key_data1), OrdKeyData(key_data2)); + + key_data1.key_state.key_shift_state = + protocols::simple_text_input_ex::SHIFT_STATE_VALID | protocols::simple_text_input_ex::LEFT_SHIFT_PRESSED; + assert_ne!(OrdKeyData(key_data1), OrdKeyData(key_data2)); + key_data2.key_state.key_shift_state = + protocols::simple_text_input_ex::SHIFT_STATE_VALID | protocols::simple_text_input_ex::LEFT_SHIFT_PRESSED; + assert_eq!(OrdKeyData(key_data1), OrdKeyData(key_data2)); + + key_data1.key_state.key_toggle_state = + protocols::simple_text_input_ex::TOGGLE_STATE_VALID | protocols::simple_text_input_ex::CAPS_LOCK_ACTIVE; + assert_ne!(OrdKeyData(key_data1), OrdKeyData(key_data2)); + key_data2.key_state.key_toggle_state = + protocols::simple_text_input_ex::TOGGLE_STATE_VALID | protocols::simple_text_input_ex::CAPS_LOCK_ACTIVE; + assert_eq!(OrdKeyData(key_data1), OrdKeyData(key_data2)); + + assert_eq!(OrdKeyData(key_data1).partial_cmp(&OrdKeyData(key_data2)), Some(core::cmp::Ordering::Equal)); + } + + #[test] + fn test_ns_keystroke() { + let mut key_queue = KeyQueue::default(); + + let mut ns_key_layout = hii_keyboard_layout::get_default_keyboard_layout(); + + let keys = &mut ns_key_layout.keys; + + let (index, _) = keys + .iter() + .enumerate() + .find(|(_, element)| if let HiiKey::Key(key) = element { key.key == EfiKey::E0 } else { false }) + .unwrap(); + + #[rustfmt::skip] + let ns_key = HiiKey::NsKey(HiiNsKeyDescriptor { + descriptor: + key_descriptor!(EfiKey::E0, '\0', '\0', '\0', '\0', NS_KEY_MODIFIER, 0), + dependent_keys: vec![ + key_descriptor!(EfiKey::C1, '\u{00E2}', '\u{00C2}', '\0', '\0', NS_KEY_DEPENDENCY_MODIFIER, AFFECTED_BY_STANDARD_SHIFT | AFFECTED_BY_CAPS_LOCK), + key_descriptor!(EfiKey::D3, '\u{00EA}', '\u{00CA}', '\0', '\0', NS_KEY_DEPENDENCY_MODIFIER, AFFECTED_BY_STANDARD_SHIFT | AFFECTED_BY_CAPS_LOCK), + key_descriptor!(EfiKey::D8, '\u{00EC}', '\u{00CC}', '\0', '\0', NS_KEY_DEPENDENCY_MODIFIER, AFFECTED_BY_STANDARD_SHIFT | AFFECTED_BY_CAPS_LOCK), + key_descriptor!(EfiKey::D9, '\u{00F4}', '\u{00D4}', '\0', '\0', NS_KEY_DEPENDENCY_MODIFIER, AFFECTED_BY_STANDARD_SHIFT | AFFECTED_BY_CAPS_LOCK), + key_descriptor!(EfiKey::D7, '\u{00FB}', '\u{00CB}', '\0', '\0', NS_KEY_DEPENDENCY_MODIFIER, AFFECTED_BY_STANDARD_SHIFT | AFFECTED_BY_CAPS_LOCK) + ]}); + + keys[index] = ns_key.clone(); + + key_queue.set_layout(Some(ns_key_layout)); + + let key = Usage::from(0x00070035); //E0 + + key_queue.keystroke(key, super::KeyAction::KeyDown); + key_queue.keystroke(key, super::KeyAction::KeyUp); + + let HiiKey::NsKey(expected_key) = ns_key else { panic!() }; + assert_eq!(key_queue.active_ns_key, Some(expected_key)); + + assert!(key_queue.peek_key().is_none()); + + let key = Usage::from(0x00070004); //C1 + key_queue.keystroke(key, super::KeyAction::KeyDown); + key_queue.keystroke(key, super::KeyAction::KeyUp); + + let stroke = key_queue.pop_key().unwrap(); + assert_eq!(stroke.key.unicode_char, '\u{00E2}' as u16); + } +} diff --git a/HidPkg/UefiHidDxeV2/src/keyboard/mod.rs b/HidPkg/UefiHidDxeV2/src/keyboard/mod.rs new file mode 100644 index 0000000000..77ec5cf70c --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/keyboard/mod.rs @@ -0,0 +1,1328 @@ +//! Provides Keyboard HID support. +//! +//! This module handles the core logic for processing keystrokes from HID +//! devices. +//! +//! ## License +//! +//! Copyright (C) Microsoft Corporation. All rights reserved. +//! +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! +mod key_queue; +mod simple_text_in; +mod simple_text_in_ex; + +use core::ffi::c_void; + +use alloc::{ + boxed::Box, + collections::{BTreeMap, BTreeSet}, + vec, + vec::Vec, +}; +use hidparser::{ + report_data_types::{ReportId, Usage}, + ArrayField, ReportDescriptor, ReportField, VariableField, +}; +use r_efi::{efi, hii, protocols}; + +use crate::{ + boot_services::UefiBootServices, + hid_io::{HidIo, HidReportReceiver}, + keyboard::key_queue::OrdKeyData, +}; + +use rust_advanced_logger_dxe::{debugln, function, DEBUG_ERROR, DEBUG_WARN}; + +// usages supported by this module +const KEYBOARD_MODIFIER_USAGE_MIN: u32 = 0x000700E0; +const KEYBOARD_MODIFIER_USAGE_MAX: u32 = 0x000700E7; +const KEYBOARD_USAGE_MIN: u32 = 0x00070001; +const KEYBOARD_USAGE_MAX: u32 = 0x00070065; +const LED_USAGE_MIN: u32 = 0x00080001; +const LED_USAGE_MAX: u32 = 0x00080005; + +// maps a given field to a routine that handles input from it. +#[derive(Debug, Clone)] +struct ReportFieldWithHandler { + field: T, + report_handler: fn(handler: &mut KeyboardHidHandler, field: T, report: &[u8]), +} + +// maps a given field to a routine that builds output reports from it. +#[derive(Debug, Clone)] +struct ReportFieldBuilder { + field: T, + field_builder: fn(&mut KeyboardHidHandler, field: T, report: &mut [u8]), +} + +// Defines an input report and the fields of interest in it. +#[derive(Debug, Default, Clone)] +struct KeyboardReportData { + report_id: Option, + report_size: usize, + relevant_variable_fields: Vec>, + relevant_array_fields: Vec>, +} + +// Defines an output report and the fields of interest in it. +#[derive(Debug, Default, Clone)] +struct KeyboardOutputReportBuilder { + report_id: Option, + report_size: usize, + relevant_variable_fields: Vec>, +} + +#[repr(C)] +struct LayoutChangeContext { + boot_services: &'static dyn UefiBootServices, + keyboard_handler: *mut KeyboardHidHandler, +} + +/// Keyboard HID Handler +pub struct KeyboardHidHandler { + boot_services: &'static dyn UefiBootServices, + agent: efi::Handle, + controller: Option, + input_reports: BTreeMap, KeyboardReportData>, + output_builders: Vec, + report_id_present: bool, + last_keys: BTreeSet, + current_keys: BTreeSet, + led_state: BTreeSet, + key_queue: key_queue::KeyQueue, + notification_callbacks: BTreeMap, + next_notify_handle: usize, + key_notify_event: efi::Event, + layout_change_event: efi::Event, + layout_context: *mut LayoutChangeContext, +} + +impl KeyboardHidHandler { + /// Instantiates a new Keyboard HID handler. `agent` is the handle that owns the handler (typically image_handle) + pub fn new(boot_services: &'static dyn UefiBootServices, agent: efi::Handle) -> Self { + Self { + boot_services, + agent, + controller: None, + input_reports: BTreeMap::new(), + output_builders: Vec::new(), + report_id_present: false, + last_keys: BTreeSet::new(), + current_keys: BTreeSet::new(), + led_state: BTreeSet::new(), + key_queue: Default::default(), + notification_callbacks: BTreeMap::new(), + next_notify_handle: 0, + key_notify_event: core::ptr::null_mut(), + layout_change_event: core::ptr::null_mut(), + layout_context: core::ptr::null_mut(), + } + } + + // Processes the report descriptor to determine whether this is a supported device, and if so, extract the information + // required to process reports. + fn process_descriptor(&mut self, descriptor: ReportDescriptor) -> Result<(), efi::Status> { + let multiple_reports = + descriptor.input_reports.len() > 1 || descriptor.output_reports.len() > 1 || descriptor.features.len() > 1; + + for report in &descriptor.input_reports { + let mut report_data = KeyboardReportData { report_id: report.report_id, ..Default::default() }; + + self.report_id_present = report.report_id.is_some(); + + if multiple_reports && !self.report_id_present { + //Invalid to have None ReportId if multiple reports present. + Err(efi::Status::DEVICE_ERROR)?; + } + + report_data.report_size = report.size_in_bits.div_ceil(8); + + for field in &report.fields { + match field { + //Variable fields (typically used for modifier Usages) + ReportField::Variable(field) => { + if let KEYBOARD_MODIFIER_USAGE_MIN..=KEYBOARD_MODIFIER_USAGE_MAX = field.usage.into() { + report_data.relevant_variable_fields.push(ReportFieldWithHandler:: { + field: field.clone(), + report_handler: Self::handle_variable_key, + }); + } + } + //Array fields (typically used for key strokes) + ReportField::Array(field) => { + for usage_list in &field.usage_list { + if usage_list.contains(Usage::from(KEYBOARD_USAGE_MIN)) + || usage_list.contains(Usage::from(KEYBOARD_USAGE_MAX)) + { + report_data.relevant_array_fields.push(ReportFieldWithHandler:: { + field: field.clone(), + report_handler: Self::handle_array_key, + }); + break; + } + } + } + ReportField::Padding(_) => (), // padding irrelevant. + } + } + if !(report_data.relevant_variable_fields.is_empty() && report_data.relevant_array_fields.is_empty()) { + self.input_reports.insert(report_data.report_id, report_data); + } + } + + for report in &descriptor.output_reports { + let mut report_builder = KeyboardOutputReportBuilder { report_id: report.report_id, ..Default::default() }; + + self.report_id_present = report.report_id.is_some(); + + if multiple_reports && !self.report_id_present { + //invalid to have None ReportId if multiple reports present. + Err(efi::Status::DEVICE_ERROR)?; + } + + report_builder.report_size = report.size_in_bits / 8; + if (report.size_in_bits % 8) != 0 { + report_builder.report_size += 1; + } + + for field in &report.fields { + match field { + //Variable fields in output reports (typically used for LEDs). + ReportField::Variable(field) => { + if let LED_USAGE_MIN..=LED_USAGE_MAX = field.usage.into() { + report_builder.relevant_variable_fields.push( + ReportFieldBuilder { + field: field.clone(), + field_builder: Self::build_led_report + } + ) + } + }, + ReportField::Array(_) | // No support for array field report outputs; could be added if required. + ReportField::Padding(_) => (), // padding fields irrelevant. + } + } + if !report_builder.relevant_variable_fields.is_empty() { + self.output_builders.push(report_builder); + } + } + + if self.input_reports.is_empty() && self.output_builders.is_empty() { + Err(efi::Status::UNSUPPORTED) + } else { + Ok(()) + } + } + + // Helper routine to handle variable keyboard input report fields + fn handle_variable_key(&mut self, field: VariableField, report: &[u8]) { + match field.field_value(report) { + Some(x) if x != 0 => { + self.current_keys.insert(field.usage); + } + None | Some(_) => (), + } + } + + // Helper routine to handle array keyboard input report fields + fn handle_array_key(&mut self, field: ArrayField, report: &[u8]) { + match field.field_value(report) { + Some(index) if index != 0 => { + let mut index = (index as u32 - u32::from(field.logical_minimum)) as usize; + let usage = field.usage_list.iter().find_map(|x| { + let range_size = (x.end() - x.start()) as usize; + if index <= range_size { + x.range().nth(index) + } else { + index -= range_size; + None + } + }); + if let Some(usage) = usage { + self.current_keys.insert(Usage::from(usage)); + } + } + None | Some(_) => (), + } + } + + // Helper routine that updates the fields in the given report buffer for the given field (called for each field for + // every LED usage that was discovered in the output report descriptor). + fn build_led_report(&mut self, field: VariableField, report: &mut [u8]) { + let status = field.set_field_value(self.led_state.contains(&field.usage).into(), report); + if status.is_err() { + debugln!(DEBUG_WARN, "{:}: failed to set field value: {:?}", function!(), status); + } + } + + // Generates LED output reports - usually there is only one, but this implementation handles an arbitrary number of + // possible output reports. If more than one output report is defined, all will be sent whenever there is a change in + // LEDs. + fn generate_led_output_reports(&mut self) -> Vec<(Option, Vec)> { + let mut output_vec = Vec::new(); + let current_leds: BTreeSet = self.key_queue.get_active_leds().iter().cloned().collect(); + if current_leds != self.led_state { + self.led_state = current_leds; + for output_builder in self.output_builders.clone() { + let mut report_buffer = vec![0u8; output_builder.report_size]; + for field_builder in &output_builder.relevant_variable_fields { + (field_builder.field_builder)(self, field_builder.field.clone(), report_buffer.as_mut_slice()); + } + output_vec.push((output_builder.report_id, report_buffer)); + } + } + output_vec + } + + // Installs the FFI interfaces that provide keyboard support to the rest of the system + fn install_protocol_interfaces(&mut self, controller: efi::Handle) -> Result<(), efi::Status> { + simple_text_in::SimpleTextInFfi::install(self.boot_services, controller, self)?; + simple_text_in_ex::SimpleTextInExFfi::install(self.boot_services, controller, self)?; + self.controller = Some(controller); + + // after this point, access to self must be guarded by raising TPL to NOTIFY. + Ok(()) + } + + // Installs an event to be notified when a new layout is installed. This allows the driver to respond dynamically to + // installation of new layouts and handle keys accordingly. + fn install_layout_change_event(&mut self) -> Result<(), efi::Status> { + let context = LayoutChangeContext { boot_services: self.boot_services, keyboard_handler: self as *mut Self }; + let context_ptr = Box::into_raw(Box::new(context)); + + let mut layout_change_event: efi::Event = core::ptr::null_mut(); + let status = self.boot_services.create_event_ex( + efi::EVT_NOTIFY_SIGNAL, + efi::TPL_NOTIFY, + Some(on_layout_update), + context_ptr as *mut c_void, + &protocols::hii_database::SET_KEYBOARD_LAYOUT_EVENT_GUID, + core::ptr::addr_of_mut!(layout_change_event), + ); + if status.is_error() { + Err(status)?; + } + + self.layout_change_event = layout_change_event; + self.layout_context = context_ptr; + + Ok(()) + } + + // Closes the layout change event. + fn uninstall_layout_change_event(&mut self) -> Result<(), efi::Status> { + if !self.layout_change_event.is_null() { + let layout_change_event: efi::Handle = self.layout_change_event; + let status = self.boot_services.close_event(layout_change_event); + if status.is_error() { + //An error here means the event was not closed, so in theory the notification_callback on it could still be fired. + //Mark the instance invalid by setting the keyboard_handler raw pointer to null, but leak the LayoutContext + //instance. Leaking context allows context usage in the callback should it fire. + debugln!(DEBUG_ERROR, "Failed to close layout_change_event event, status: {:x?}", status); + unsafe { + (*self.layout_context).keyboard_handler = core::ptr::null_mut(); + } + return Err(status); + } + // safe to drop layout change context. + drop(unsafe { Box::from_raw(self.layout_context) }); + self.layout_context = core::ptr::null_mut(); + self.layout_change_event = core::ptr::null_mut(); + } + Ok(()) + } + + // Installs a default keyboard layout. + fn install_default_layout(&mut self) -> Result<(), efi::Status> { + let mut hii_database_protocol_ptr: *mut protocols::hii_database::Protocol = core::ptr::null_mut(); + + let status = self.boot_services.locate_protocol( + &protocols::hii_database::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + core::ptr::null_mut(), + core::ptr::addr_of_mut!(hii_database_protocol_ptr) as *mut *mut c_void, + ); + if status.is_error() { + debugln!( + DEBUG_ERROR, + "keyboard::install_default_layout: Could not locate hii_database protocol to install keyboard layout: {:x?}", + status + ); + Err(status)?; + } + + let hii_database_protocol = + unsafe { hii_database_protocol_ptr.as_mut().expect("Bad pointer returned from successful locate protocol.") }; + + let mut hii_handle: hii::Handle = core::ptr::null_mut(); + let status = (hii_database_protocol.new_package_list)( + hii_database_protocol_ptr, + hii_keyboard_layout::get_default_keyboard_pkg_list_buffer().as_ptr() as *const hii::PackageListHeader, + core::ptr::null_mut(), + core::ptr::addr_of_mut!(hii_handle), + ); + + if status.is_error() { + debugln!( + DEBUG_ERROR, + "keyboard::install_default_layout: Failed to install keyboard layout package: {:x?}", + status + ); + Err(status)?; + } + + let status = (hii_database_protocol.set_keyboard_layout)( + hii_database_protocol_ptr, + &hii_keyboard_layout::DEFAULT_KEYBOARD_LAYOUT_GUID as *const efi::Guid as *mut efi::Guid, + ); + + if status.is_error() { + debugln!(DEBUG_ERROR, "keyboard::install_default_layout: Failed to set keyboard layout: {:x?}", status); + Err(status)?; + } + + Ok(()) + } + + // Initializes the keyboard layout. If no layout is already installed in the system, a default layout is installed. + fn initialize_keyboard_layout(&mut self) -> Result<(), efi::Status> { + self.install_layout_change_event()?; + + //signal event to pick up any existing layout + self.boot_services.signal_event(self.layout_change_event); + + //install a default layout if no layout is installed. + if self.key_queue.get_layout().is_none() { + self.install_default_layout()?; + } + Ok(()) + } + + /// Resets the keyboard driver state. Clears any pending key state. `extended verification` will also reset toggle + /// state. + pub fn reset(&mut self, hid_io: &dyn HidIo, extended_verification: bool) -> Result<(), efi::Status> { + self.last_keys.clear(); + self.current_keys.clear(); + self.key_queue.reset(extended_verification); + if extended_verification { + self.update_leds(hid_io)?; + } + Ok(()) + } + + /// Called to send LED state to the device if there has been a change in LEDs. + pub fn update_leds(&mut self, hid_io: &dyn HidIo) -> Result<(), efi::Status> { + for (id, output_report) in self.generate_led_output_reports() { + let result = hid_io.set_output_report(id.map(|x| u32::from(x) as u8), &output_report); + if let Err(result) = result { + debugln!(DEBUG_ERROR, "unexpected error sending output report: {:?}", result); + return Err(result); + } + } + Ok(()) + } + + /// Returns a clone of the keystroke at the front of the keystroke queue. + pub fn peek_key(&mut self) -> Option { + self.key_queue.peek_key() + } + + /// Removes and returns the keystroke at the front of the keystroke queue. + pub fn pop_key(&mut self) -> Option { + self.key_queue.pop_key() + } + + /// Returns the current key state (i.e. the SHIFT and TOGGLE state). + pub fn get_key_state(&mut self) -> protocols::simple_text_input_ex::KeyState { + self.key_queue.init_key_state() + } + + /// Sets the current key state. + pub fn set_key_toggle_state(&mut self, toggle_state: u8) { + self.key_queue.set_key_toggle_state(toggle_state); + self.generate_led_output_reports(); + } + + /// Registers a new key notify callback function to be invoked on the specified `key_data` press. + /// + /// Returns a handle that is used to unregister the callback if desired. + pub fn insert_key_notify_callback( + &mut self, + key_data: protocols::simple_text_input_ex::KeyData, + key_notification_function: protocols::simple_text_input_ex::KeyNotifyFunction, + ) -> usize { + let key_data = OrdKeyData(key_data); + for (handle, entry) in &self.notification_callbacks { + if entry.0 == key_data && entry.1 == key_notification_function { + //this callback already exists for this key, so return the current handle. + return *handle; + } + } + // key_data/callback combo doesn't exist, create a new registration for it. + self.next_notify_handle += 1; + self.notification_callbacks.insert(self.next_notify_handle, (key_data.clone(), key_notification_function)); + self.key_queue.add_notify_key(key_data); + self.next_notify_handle + } + + /// Unregisters a previously registered key notify callback function. + pub fn remove_key_notify_callback(&mut self, notification_handle: usize) -> Result<(), efi::Status> { + if let Some(entry) = self.notification_callbacks.remove(¬ification_handle) { + let removed_key = entry.0; + if !self.notification_callbacks.values().any(|(key, _)| *key == removed_key) { + // no other handlers exist for the key in the removed entry, so remove it from the key_queue as well. + self.key_queue.remove_notify_key(&removed_key); + } + Ok(()) + } else { + Err(efi::Status::INVALID_PARAMETER) + } + } + + /// Returns the set of keys that have pending callbacks, along with the vector of callback functions associated with + /// each key. + pub fn get_pending_callbacks( + &mut self, + ) -> (Option, Vec) { + if let Some(pending_notify_key) = self.key_queue.pop_notify_key() { + let mut pending_callbacks = Vec::new(); + for (key, callback) in self.notification_callbacks.values() { + if OrdKeyData(pending_notify_key).matches_registered_key(key) { + pending_callbacks.push(*callback); + } + } + (Some(pending_notify_key), pending_callbacks) + } else { + (None, Vec::new()) + } + } + + /// Returns the agent associated with this KeyboardHidHandler + pub fn get_agent(&self) -> efi::Handle { + self.agent + } + + /// Returns the controller associated with this KeyboardHidHandler. + pub fn get_controller(&self) -> Option { + self.controller + } + + #[cfg(test)] + pub fn set_controller(&mut self, controller: Option) { + self.controller = controller; + } + + #[cfg(test)] + pub fn set_layout(&mut self, layout: Option) { + self.key_queue.set_layout(layout) + } + #[cfg(test)] + pub fn set_notify_event(&mut self, event: efi::Event) { + self.key_notify_event = event; + } +} + +impl HidReportReceiver for KeyboardHidHandler { + fn initialize(&mut self, controller: efi::Handle, hid_io: &dyn HidIo) -> Result<(), efi::Status> { + let descriptor = hid_io.get_report_descriptor()?; + self.process_descriptor(descriptor)?; + + self.install_protocol_interfaces(controller)?; + + self.initialize_keyboard_layout()?; + + Ok(()) + } + + fn receive_report(&mut self, report: &[u8], hid_io: &dyn HidIo) { + let old_tpl = self.boot_services.raise_tpl(efi::TPL_NOTIFY); + + let mut output_reports = Vec::new(); + 'report_processing: { + if report.is_empty() { + break 'report_processing; + } + // determine whether report includes report id byte and adjust the buffer as needed. + let (report_id, report) = match self.report_id_present { + true => (Some(ReportId::from(&report[0..1])), &report[1..]), + false => (None, &report[0..]), + }; + + if report.is_empty() { + break 'report_processing; + } + + if let Some(report_data) = self.input_reports.get(&report_id).cloned() { + if report.len() != report_data.report_size { + break 'report_processing; + } + + //reset currently active keys to empty set. + self.current_keys.clear(); + + // hand the report data to the handler for each relevant field for field-specific processing. + for field in report_data.relevant_variable_fields { + (field.report_handler)(self, field.field, report); + } + + for field in report_data.relevant_array_fields { + (field.report_handler)(self, field.field, report); + } + + //check if any key state has changed. + if self.last_keys != self.current_keys { + // process keys that are not in both sets: that is the set of keys that have changed. + // XOR on the sets yields a set of keys that are in either last or current keys, but not both. + + // Modifier keys need to be processed first so that normal key processing includes modifiers that showed up in + // the same report. The key sets are sorted by Usage, and modifier keys all have higher usages than normal keys + // - so use a reverse iterator to process the modifier keys first. In addition, all released keys should be + // processed first so that pressed keys (which typically generate key stroke events) have the most recent + // key state associated with them. + let mut released_keys = Vec::new(); + let mut pressed_keys = Vec::new(); + for changed_key in (&self.last_keys ^ &self.current_keys).into_iter().rev() { + if self.last_keys.contains(&changed_key) { + //In the last key list, but not in current. This is a key release. + released_keys.push(changed_key); + } else { + //Not in last, so must be in current. This is a key press. + pressed_keys.push(changed_key); + } + } + for key in released_keys { + self.key_queue.keystroke(key, key_queue::KeyAction::KeyUp); + } + for key in pressed_keys { + self.key_queue.keystroke(key, key_queue::KeyAction::KeyDown); + } + + //after processing all the key strokes, check if any keys were pressed that should trigger the notifier callback + //and if so, signal the event to trigger notify processing at the appropriate TPL. + if self.key_queue.peek_notify_key().is_some() { + self.boot_services.signal_event(self.key_notify_event); + } + + //after processing all the key strokes, send updated LED state if required. + output_reports = self.generate_led_output_reports(); + } + //after all key handling is complete for this report, update the last key set to match the current key set. + self.last_keys = self.current_keys.clone(); + } + } + + self.boot_services.restore_tpl(old_tpl); + + // if any output reports, send them after releasing handler. + for (id, output_report) in output_reports { + let result = hid_io.set_output_report(id.map(|x| u32::from(x) as u8), &output_report); + if let Err(result) = result { + debugln!(DEBUG_ERROR, "unexpected error sending output report: {:?}", result); + let _ = result; + } + } + } +} + +impl Drop for KeyboardHidHandler { + fn drop(&mut self) { + if let Some(controller) = self.controller { + let status = simple_text_in::SimpleTextInFfi::uninstall(self.boot_services, self.agent, controller); + if status.is_err() { + debugln!(DEBUG_ERROR, "KeyboardHidHandler::drop: Failed to uninstall simple_text_in: {:?}", status); + } + let status = simple_text_in_ex::SimpleTextInExFfi::uninstall(self.boot_services, self.agent, controller); + if status.is_err() { + debugln!(DEBUG_ERROR, "KeyboardHidHandler::drop: Failed to uninstall simple_text_in: {:?}", status); + } + } + let status = self.uninstall_layout_change_event(); + if status.is_err() { + debugln!(DEBUG_ERROR, "KeyboardHidHandler::drop: Failed to close layout_change_event: {:?}", status); + } + } +} + +// handles keyboard layout change event that occurs when a new keyboard layout is set. +extern "efiapi" fn on_layout_update(_event: efi::Event, context: *mut c_void) { + let context = unsafe { (context as *mut LayoutChangeContext).as_mut() }.expect("bad context pointer"); + let old_tpl = context.boot_services.raise_tpl(efi::TPL_NOTIFY); + + 'layout_processing: { + if context.keyboard_handler.is_null() { + debugln!(DEBUG_ERROR, "on_layout_update invoked with invalid handler"); + break 'layout_processing; + } + + let keyboard_handler = unsafe { context.keyboard_handler.as_mut() }.expect("bad keyboard handler"); + + let mut hii_database_protocol_ptr: *mut protocols::hii_database::Protocol = core::ptr::null_mut(); + let status = context.boot_services.locate_protocol( + &protocols::hii_database::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + core::ptr::null_mut(), + core::ptr::addr_of_mut!(hii_database_protocol_ptr) as *mut *mut c_void, + ); + + if status.is_error() { + //nothing to do if there is no hii protocol. + break 'layout_processing; + } + + let hii_database_protocol = + unsafe { hii_database_protocol_ptr.as_mut().expect("Bad pointer returned from successful locate protocol.") }; + + // retrieve keyboard layout size + let mut layout_buffer_len: u16 = 0; + match (hii_database_protocol.get_keyboard_layout)( + hii_database_protocol_ptr, + core::ptr::null_mut(), + &mut layout_buffer_len as *mut u16, + core::ptr::null_mut(), + ) { + efi::Status::NOT_FOUND => break 'layout_processing, + status if status != efi::Status::BUFFER_TOO_SMALL => { + debugln!( + DEBUG_ERROR, + "{:}: unexpected return from get_keyboard_layout when trying to determine length: {:x?}", + function!(), + status + ); + break 'layout_processing; + } + _ => (), + } + + let mut keyboard_layout_buffer = vec![0u8; layout_buffer_len as usize]; + let status = (hii_database_protocol.get_keyboard_layout)( + hii_database_protocol_ptr, + core::ptr::null_mut(), + &mut layout_buffer_len as *mut u16, + keyboard_layout_buffer.as_mut_ptr() as *mut protocols::hii_database::KeyboardLayout<0>, + ); + + if status.is_error() { + debugln!(DEBUG_ERROR, "Unexpected return from get_keyboard_layout: {:x?}", status); + break 'layout_processing; + } + + let keyboard_layout = hii_keyboard_layout::keyboard_layout_from_buffer(&keyboard_layout_buffer); + match keyboard_layout { + Ok(keyboard_layout) => { + keyboard_handler.key_queue.set_layout(Some(keyboard_layout)); + } + Err(_) => { + debugln!(DEBUG_ERROR, "keyboard::on_layout_update: Could not parse keyboard layout buffer."); + break 'layout_processing; + } + } + } + + context.boot_services.restore_tpl(old_tpl); +} + +#[cfg(test)] +mod test { + + use core::{ffi::c_void, mem::MaybeUninit, slice::from_raw_parts_mut}; + + use hii_keyboard_layout::HiiKeyboardLayout; + use r_efi::{efi, hii, protocols}; + use scroll::Pwrite; + + use crate::{ + boot_services::MockUefiBootServices, + hid_io::{HidReportReceiver, MockHidIo}, + keyboard::{key_queue::OrdKeyData, on_layout_update, KeyboardHidHandler, LayoutChangeContext}, + }; + + static BOOT_KEYBOARD_REPORT_DESCRIPTOR: &[u8] = &[ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x06, // USAGE (Keyboard) + 0xa1, 0x01, // COLLECTION (Application) + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, 0x08, // REPORT_COUNT (8) + 0x05, 0x07, // USAGE_PAGE (Key Codes) + 0x19, 0xE0, // USAGE_MINIMUM (224) + 0x29, 0xE7, // USAGE_MAXIMUM (231) + 0x15, 0x00, // LOGICAL_MAXIMUM (0) + 0x25, 0x01, // LOGICAL_MINIMUM (1) + 0x81, 0x02, // INPUT (Data, Var, Abs) (Modifier Byte) + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x08, // REPORT_SIZE (8) + 0x81, 0x03, // INPUT (Const) (Reserved Byte) + 0x95, 0x05, // REPORT_COUNT (5) + 0x75, 0x01, // REPORT_SIZE (1) + 0x05, 0x08, // USAGE_PAGE (LEDs) + 0x19, 0x01, // USAGE_MINIMUM (1) + 0x29, 0x05, // USAGE_MAXIMUM (5) + 0x91, 0x02, // OUTPUT (Data, Var, Abs) (LED report) + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x03, // REPORT_SIZE (3) + 0x91, 0x02, // OUTPUT (Constant) (LED report padding) + 0x95, 0x06, // REPORT_COUNT (6) + 0x75, 0x08, // REPORT_SIZE (8) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x26, 0xff, 00, // LOGICAL_MAXIMUM (255) + 0x05, 0x07, // USAGE_PAGE (Key Codes) + 0x19, 0x00, // USAGE_MINIMUM (0) + 0x2a, 0xff, 00, // USAGE_MAXIMUM (255) + 0x81, 0x00, // INPUT (Data, Array) + 0xc0, // END_COLLECTION + ]; + + static MOUSE_REPORT_DESCRIPTOR: &[u8] = &[ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x02, // USAGE (Mouse) + 0xa1, 0x01, // COLLECTION (Application) + 0x09, 0x01, // USAGE(Pointer) + 0xa1, 0x00, // COLLECTION (Physical) + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM(1) + 0x29, 0x05, // USAGE_MAXIMUM(5) + 0x15, 0x00, // LOGICAL_MINIMUM(0) + 0x25, 0x01, // LOGICAL_MAXIMUM(1) + 0x95, 0x05, // REPORT_COUNT(5) + 0x75, 0x01, // REPORT_SIZE(1) + 0x81, 0x02, // INPUT(Data, Variable, Absolute) + 0x95, 0x01, // REPORT_COUNT(1) + 0x75, 0x03, // REPORT_SIZE(3) + 0x81, 0x01, // INPUT(Constant, Array, Absolute) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x09, 0x38, // USAGE (Wheel) + 0x15, 0x81, // LOGICAL_MINIMUM (-127) + 0x25, 0x7f, // LOGICAL_MAXIMUM (127) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x03, // REPORT_COUNT (3) + 0x81, 0x06, // INPUT(Data, Variable, Relative) + 0xc0, // END_COLLECTION + 0xc0, // END_COLLECTION + ]; + + // In this module, the usage model for boot_services is global static, and so &'static dyn UefiBootServices is used + // throughout the API. For testing, each test will have a different set of expectations on the UefiBootServices mock + // object, and the mock object itself expects to be "mut", which makes it hard to handle as a single global static. + // Instead, raw pointers are used to simulate a MockUefiBootServices instance with 'static lifetime. + // This object needs to outlive anything that uses it - once created, it will live until the end of the program. + fn create_fake_static_boot_service() -> &'static mut MockUefiBootServices { + unsafe { Box::into_raw(Box::new(MockUefiBootServices::new())).as_mut().unwrap() } + } + + #[test] + fn keyboard_initialize_should_fail_for_unsupported_descriptors() { + let boot_services = create_fake_static_boot_service(); + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&MOUSE_REPORT_DESCRIPTOR).unwrap())); + + assert_eq!(keyboard_handler.initialize(2 as efi::Handle, &hid_io), Err(efi::Status::UNSUPPORTED)); + } + + #[test] + fn keyboard_initialization_should_succeed_for_supported_descriptors() { + let boot_services = create_fake_static_boot_service(); + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, _| efi::Status::SUCCESS); + boot_services.expect_signal_event().returning(|_| efi::Status::SUCCESS); + boot_services.expect_open_protocol().returning(|_, _, _, _, _, _| efi::Status::NOT_FOUND); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + keyboard_handler.key_queue.set_layout(Some(hii_keyboard_layout::get_default_keyboard_layout())); + + assert_eq!(keyboard_handler.initialize(2 as efi::Handle, &hid_io), Ok(())); + } + + #[test] + fn keyboard_should_process_input_reports() { + let boot_services = create_fake_static_boot_service(); + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, _| efi::Status::SUCCESS); + boot_services.expect_signal_event().returning(|_| efi::Status::SUCCESS); + boot_services.expect_open_protocol().returning(|_, _, _, _, _, _| efi::Status::NOT_FOUND); + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + keyboard_handler.key_queue.set_layout(Some(hii_keyboard_layout::get_default_keyboard_layout())); + keyboard_handler.initialize(2 as efi::Handle, &hid_io).unwrap(); + + // press the 'a' key. + let report: &[u8] = &[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + assert_eq!(keyboard_handler.key_queue.pop_key().unwrap().key.unicode_char, 'a' as u16); + assert!(keyboard_handler.key_queue.peek_key().is_none()); + + // press the 'shift' key while 'a' remains pressed. + let report: &[u8] = &[0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + assert!(keyboard_handler.key_queue.peek_key().is_none()); + + // holding the 'shift' key while releasing 'a'. + let report: &[u8] = &[0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + assert!(keyboard_handler.key_queue.peek_key().is_none()); + + // holding the 'shift' key while pressing 'a' again. + let report: &[u8] = &[0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + assert_eq!(keyboard_handler.key_queue.pop_key().unwrap().key.unicode_char, 'A' as u16); + assert!(keyboard_handler.key_queue.peek_key().is_none()); + + // release the 'shift' key, press the 'ctrl' key, continue pressing 'a', and press 'b' + let report: &[u8] = &[0x01, 0x00, 0x04, 0x05, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + let key_data = keyboard_handler.key_queue.pop_key().unwrap(); + assert_eq!(key_data.key.unicode_char, 'b' as u16); + assert_eq!( + key_data.key_state.key_shift_state, + protocols::simple_text_input_ex::SHIFT_STATE_VALID | protocols::simple_text_input_ex::LEFT_CONTROL_PRESSED + ); + assert!(keyboard_handler.key_queue.peek_key().is_none()); + + // enable partial keystrokes + keyboard_handler.key_queue.set_key_toggle_state(protocols::simple_text_input_ex::KEY_STATE_EXPOSED); + + // release the 'ctrl' key, press 'alt', continue pressing 'a' and 'b' + let report: &[u8] = &[0x04, 0x00, 0x04, 0x05, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + let key_data = keyboard_handler.key_queue.pop_key().unwrap(); + assert_eq!(key_data.key.unicode_char, 0); + assert_eq!( + key_data.key_state.key_shift_state, + protocols::simple_text_input_ex::SHIFT_STATE_VALID | protocols::simple_text_input_ex::LEFT_ALT_PRESSED + ); + assert!(keyboard_handler.key_queue.peek_key().is_none()); + + // release all the keys + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + assert!(keyboard_handler.key_queue.peek_key().is_none()); + + // press the right logo key + let report: &[u8] = &[0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + let key_data = keyboard_handler.key_queue.pop_key().unwrap(); + assert_eq!(key_data.key.unicode_char, 0); + assert_eq!( + key_data.key_state.key_shift_state, + protocols::simple_text_input_ex::SHIFT_STATE_VALID | protocols::simple_text_input_ex::RIGHT_LOGO_PRESSED + ); + assert!(keyboard_handler.key_queue.peek_key().is_none()); + + // simulate rollover + let report: &[u8] = &[0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01]; + keyboard_handler.receive_report(report, &hid_io); + assert!(keyboard_handler.key_queue.peek_key().is_none()); + + // pass some unsupported keys + let report: &[u8] = &[0x00, 0x00, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0]; + keyboard_handler.receive_report(report, &hid_io); + assert!(keyboard_handler.key_queue.peek_key().is_none()); + + // try all possible modifiers and all possible keys and make sure it doesn't panic. + // some of these may generate LED reports + let mut hid_io = MockHidIo::new(); + hid_io.expect_set_output_report().returning(|_, _| Ok(())); + for i in 0..0xff { + // avoid sending "Delete" keys as it will cause reset if CTRL-ALT are pressed. + if (i == 0x4C) || (i == 0x63) { + continue; + } + let report: &[u8] = &[i, 0x00, i, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + } + } + + #[test] + fn keyboard_should_install_layout_if_not_already_present() { + let boot_services = create_fake_static_boot_service(); + boot_services.expect_create_event().returning(|_, _, _, _, event| unsafe { + event.write(3 as efi::Event); + efi::Status::SUCCESS + }); + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, event| unsafe { + event.write(4 as efi::Event); + efi::Status::SUCCESS + }); + boot_services.expect_install_protocol_interface().returning(|_, _, _, _| efi::Status::SUCCESS); + boot_services.expect_signal_event().returning(|_| efi::Status::SUCCESS); + boot_services.expect_open_protocol().returning(|_, _, _, _, _, _| efi::Status::NOT_FOUND); + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + static mut HANDLER: *mut KeyboardHidHandler = core::ptr::null_mut(); + + extern "efiapi" fn new_package_list( + _this: *const protocols::hii_database::Protocol, + _package_list: *const hii::PackageListHeader, + _driver_handle: efi::Handle, + _handle: *mut hii::Handle, + ) -> efi::Status { + efi::Status::SUCCESS + } + + extern "efiapi" fn set_keyboard_layout( + _this: *const protocols::hii_database::Protocol, + _key_guid: *mut efi::Guid, + ) -> efi::Status { + let boot_services = create_fake_static_boot_service(); + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + boot_services.expect_signal_event().returning(|_| efi::Status::SUCCESS); + + const TEST_KEYBOARD_GUID: efi::Guid = + efi::Guid::from_fields(0xf1796c10, 0xdafb, 0x4989, 0xa0, 0x82, &[0x75, 0xe9, 0x65, 0x76, 0xbe, 0x52]); + + static mut TEST_KEYBOARD_LAYOUT: HiiKeyboardLayout = + HiiKeyboardLayout { keys: Vec::new(), guid: TEST_KEYBOARD_GUID, descriptions: Vec::new() }; + unsafe { + //make a test keyboard layout that is different than the default. + TEST_KEYBOARD_LAYOUT = hii_keyboard_layout::get_default_keyboard_layout(); + TEST_KEYBOARD_LAYOUT.guid = TEST_KEYBOARD_GUID; + TEST_KEYBOARD_LAYOUT.keys.pop(); + TEST_KEYBOARD_LAYOUT.keys.pop(); + TEST_KEYBOARD_LAYOUT.keys.pop(); + TEST_KEYBOARD_LAYOUT.descriptions[0].description = "Test Keyboard Layout".to_string(); + TEST_KEYBOARD_LAYOUT.descriptions[0].language = "ts-TS".to_string(); + } + + extern "efiapi" fn get_keyboard_layout( + _this: *const protocols::hii_database::Protocol, + _key_guid: *const efi::Guid, + keyboard_layout_length: *mut u16, + keyboard_layout_ptr: *mut protocols::hii_database::KeyboardLayout, + ) -> efi::Status { + let mut keyboard_layout_buffer = vec![0u8; 4096]; + let buffer_size = keyboard_layout_buffer.pwrite(unsafe { &TEST_KEYBOARD_LAYOUT }, 0).unwrap(); + keyboard_layout_buffer.resize(buffer_size, 0); + unsafe { + if keyboard_layout_length.read() < buffer_size as u16 { + keyboard_layout_length.write(buffer_size as u16); + return efi::Status::BUFFER_TOO_SMALL; + } else { + if keyboard_layout_ptr.is_null() { + panic!("bad keyboard pointer)"); + } + keyboard_layout_length.write(buffer_size as u16); + let slice = from_raw_parts_mut(keyboard_layout_ptr as *mut u8, buffer_size); + slice.copy_from_slice(&keyboard_layout_buffer); + return efi::Status::SUCCESS; + } + } + } + + boot_services.expect_locate_protocol().returning(|protocol, _, interface| { + unsafe { + match *protocol { + protocols::hii_database::PROTOCOL_GUID => { + let hii_database = MaybeUninit::::zeroed(); + let mut hii_database = hii_database.assume_init(); + hii_database.get_keyboard_layout = get_keyboard_layout; + interface.write(Box::into_raw(Box::new(hii_database)) as *mut c_void); + } + unexpected_protocol => panic!("unexpected locate protocol request for {:?}", unexpected_protocol), + } + } + efi::Status::SUCCESS + }); + + let context = LayoutChangeContext { boot_services: boot_services, keyboard_handler: unsafe { HANDLER } }; + on_layout_update( + 3 as efi::Event, + &context as *const LayoutChangeContext as *mut LayoutChangeContext as *mut c_void, + ); + efi::Status::SUCCESS + } + + boot_services.expect_locate_protocol().returning(|protocol, _, interface| { + unsafe { + match *protocol { + protocols::hii_database::PROTOCOL_GUID => { + let hii_database = MaybeUninit::::zeroed(); + let mut hii_database = hii_database.assume_init(); + hii_database.new_package_list = new_package_list; + hii_database.set_keyboard_layout = set_keyboard_layout; + + interface.write(Box::into_raw(Box::new(hii_database)) as *mut c_void); + } + unexpected_protocol => panic!("unexpected locate protocol request for {:?}", unexpected_protocol), + } + } + efi::Status::SUCCESS + }); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + + unsafe { HANDLER = &mut keyboard_handler as *mut KeyboardHidHandler }; + + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + assert_eq!(keyboard_handler.initialize(2 as efi::Handle, &hid_io), Ok(())); + } + + #[test] + fn reset_should_reset_keyboard() { + let boot_services = create_fake_static_boot_service(); + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, _| efi::Status::SUCCESS); + boot_services.expect_signal_event().returning(|_| efi::Status::SUCCESS); + boot_services.expect_open_protocol().returning(|_, _, _, _, _, _| efi::Status::NOT_FOUND); + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + hid_io.expect_set_output_report().returning(|_, _| Ok(())); + + keyboard_handler.key_queue.set_layout(Some(hii_keyboard_layout::get_default_keyboard_layout())); + + assert_eq!(keyboard_handler.initialize(2 as efi::Handle, &hid_io), Ok(())); + + //buffer CapsLock + a, b, c + let report: &[u8] = &[0x00, 0x00, 0x39, 0x04, 0x05, 0x06, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + assert!(keyboard_handler.key_queue.peek_key().is_some()); + assert!(!keyboard_handler.led_state.is_empty()); + let prev_led_state = keyboard_handler.led_state.clone(); + assert!(!keyboard_handler.last_keys.is_empty()); + + let hid_io = MockHidIo::new(); + + keyboard_handler.reset(&hid_io, false).unwrap(); + assert!(keyboard_handler.key_queue.peek_key().is_none()); + assert!(keyboard_handler.last_keys.is_empty()); + assert_eq!(keyboard_handler.led_state, prev_led_state); + + let mut hid_io = MockHidIo::new(); + hid_io.expect_set_output_report().returning(|_, report| { + assert_eq!(report, &[0]); + Ok(()) + }); + + keyboard_handler.reset(&hid_io, true).unwrap(); + assert!(keyboard_handler.led_state.is_empty()); + } + + #[test] + fn misc_functions_test() { + let boot_services = create_fake_static_boot_service(); + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, _| efi::Status::SUCCESS); + boot_services.expect_signal_event().returning(|_| efi::Status::SUCCESS); + boot_services.expect_open_protocol().returning(|_, _, _, _, _, _| efi::Status::NOT_FOUND); + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + keyboard_handler.key_queue.set_layout(Some(hii_keyboard_layout::get_default_keyboard_layout())); + keyboard_handler.initialize(2 as efi::Handle, &hid_io).unwrap(); + + // press the 'a' key. + let report: &[u8] = &[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + assert_eq!( + OrdKeyData(keyboard_handler.peek_key().unwrap()), + OrdKeyData(keyboard_handler.key_queue.peek_key().unwrap()) + ); + assert_eq!( + OrdKeyData(keyboard_handler.key_queue.peek_key().unwrap()), + OrdKeyData(keyboard_handler.pop_key().unwrap()), + ); + + let mut hid_io = MockHidIo::new(); + hid_io.expect_set_output_report().returning(|_, _| Ok(())); + + //press capslock and release 'a' key + let report: &[u8] = &[0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + assert_eq!( + keyboard_handler.get_key_state().key_shift_state, + keyboard_handler.key_queue.init_key_state().key_shift_state + ); + assert_eq!( + keyboard_handler.get_key_state().key_toggle_state, + keyboard_handler.key_queue.init_key_state().key_toggle_state + ); + + keyboard_handler.set_key_toggle_state( + protocols::simple_text_input_ex::KEY_STATE_EXPOSED | protocols::simple_text_input_ex::CAPS_LOCK_ACTIVE, + ); + assert_eq!( + keyboard_handler.get_key_state().key_toggle_state, + protocols::simple_text_input_ex::KEY_STATE_EXPOSED + | protocols::simple_text_input_ex::TOGGLE_STATE_VALID + | protocols::simple_text_input_ex::CAPS_LOCK_ACTIVE + ); + + assert_eq!(keyboard_handler.get_controller(), keyboard_handler.controller); + assert_eq!(keyboard_handler.get_agent(), keyboard_handler.agent); + } + + #[test] + fn insert_and_remove_key_notifies_should_update_key_notify_structures() { + let boot_services = create_fake_static_boot_service(); + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, _| efi::Status::SUCCESS); + boot_services.expect_signal_event().returning(|_| efi::Status::SUCCESS); + boot_services.expect_open_protocol().returning(|_, _, _, _, _, _| efi::Status::NOT_FOUND); + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + keyboard_handler.key_queue.set_layout(Some(hii_keyboard_layout::get_default_keyboard_layout())); + keyboard_handler.initialize(2 as efi::Handle, &hid_io).unwrap(); + + extern "efiapi" fn mock_key_notify_callback( + _key_data: *mut protocols::simple_text_input_ex::KeyData, + ) -> efi::Status { + efi::Status::SUCCESS + } + + extern "efiapi" fn mock_key_notify_callback2( + _key_data: *mut protocols::simple_text_input_ex::KeyData, + ) -> efi::Status { + efi::Status::SUCCESS + } + + let mut key_data: protocols::simple_text_input_ex::KeyData = Default::default(); + + key_data.key.unicode_char = 'a' as u16; + let handle = keyboard_handler.insert_key_notify_callback(key_data.clone(), mock_key_notify_callback); + assert_eq!(handle, 1); + + key_data.key.unicode_char = 'b' as u16; + let handle = keyboard_handler.insert_key_notify_callback(key_data.clone(), mock_key_notify_callback); + assert_eq!(handle, 2); + + key_data.key.unicode_char = 'c' as u16; + let handle = keyboard_handler.insert_key_notify_callback(key_data.clone(), mock_key_notify_callback); + assert_eq!(handle, 3); + //insert a second callback function tied to same key + let handle = keyboard_handler.insert_key_notify_callback(key_data.clone(), mock_key_notify_callback2); + assert_eq!(handle, 4); + + //insert a key_data/callback pair that is already present. + key_data.key.unicode_char = 'a' as u16; + let handle = keyboard_handler.insert_key_notify_callback(key_data.clone(), mock_key_notify_callback); + assert_eq!(handle, 1); + + //check state after adding callbacks. + assert_eq!(keyboard_handler.next_notify_handle, 4); + assert_eq!(keyboard_handler.notification_callbacks.len(), 4); + key_data.key.unicode_char = 'a' as u16; + assert_eq!(keyboard_handler.notification_callbacks.get(&1).unwrap().0, OrdKeyData(key_data)); + assert!(keyboard_handler.notification_callbacks.get(&1).unwrap().1 == mock_key_notify_callback); + key_data.key.unicode_char = 'b' as u16; + assert_eq!(keyboard_handler.notification_callbacks.get(&2).unwrap().0, OrdKeyData(key_data)); + assert!(keyboard_handler.notification_callbacks.get(&2).unwrap().1 == mock_key_notify_callback); + key_data.key.unicode_char = 'c' as u16; + assert_eq!(keyboard_handler.notification_callbacks.get(&3).unwrap().0, OrdKeyData(key_data)); + assert!(keyboard_handler.notification_callbacks.get(&3).unwrap().1 == mock_key_notify_callback); + assert_eq!(keyboard_handler.notification_callbacks.get(&4).unwrap().0, OrdKeyData(key_data)); + assert!(keyboard_handler.notification_callbacks.get(&4).unwrap().1 == mock_key_notify_callback2); + + //press and release 'c' key + let report: &[u8] = &[0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + key_data.key_state.key_shift_state = protocols::simple_text_input_ex::SHIFT_STATE_VALID; + key_data.key_state.key_toggle_state = protocols::simple_text_input_ex::TOGGLE_STATE_VALID; + let (callback_key_data, callbacks) = keyboard_handler.get_pending_callbacks(); + assert_eq!(OrdKeyData(key_data), OrdKeyData(callback_key_data.unwrap())); + assert!(callbacks.contains( + &(mock_key_notify_callback as extern "efiapi" fn(*mut protocols::simple_text_input_ex::KeyData) -> efi::Status) + )); + assert!(callbacks.contains( + &(mock_key_notify_callback2 as extern "efiapi" fn(*mut protocols::simple_text_input_ex::KeyData) -> efi::Status) + )); + + let (callback_key_data, callbacks) = keyboard_handler.get_pending_callbacks(); + assert!(callback_key_data.is_none()); + assert!(callbacks.is_empty()); + + //remove one of the 'c' callbacks and make sure the other still works. + keyboard_handler.remove_key_notify_callback(4).unwrap(); + + //press and release 'a' key and 'c' key + let report: &[u8] = &[0x00, 0x00, 0x04, 0x06, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + loop { + let (callback_key_data, callbacks) = keyboard_handler.get_pending_callbacks(); + if let Some(callback_key_data) = callback_key_data { + match callback_key_data.key.unicode_char { + char if char == 'a' as u16 || char == 'c' as u16 => { + assert!(callbacks.contains( + &(mock_key_notify_callback + as extern "efiapi" fn(*mut protocols::simple_text_input_ex::KeyData) -> efi::Status) + )); + } + _ => panic!("unexpected pending callback key"), + } + } else { + break; + } + } + + //remove all the callbacks. + keyboard_handler.remove_key_notify_callback(1).unwrap(); + keyboard_handler.remove_key_notify_callback(2).unwrap(); + keyboard_handler.remove_key_notify_callback(3).unwrap(); + + //press and release 'a' key 'b' key, and 'c' key + let report: &[u8] = &[0x00, 0x00, 0x04, 0x05, 0x06, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + let (callback_key_data, callbacks) = keyboard_handler.get_pending_callbacks(); + assert!(callback_key_data.is_none()); + assert!(callbacks.is_empty()); + } +} diff --git a/HidPkg/UefiHidDxeV2/src/keyboard/simple_text_in.rs b/HidPkg/UefiHidDxeV2/src/keyboard/simple_text_in.rs new file mode 100644 index 0000000000..eacae9aeb5 --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/keyboard/simple_text_in.rs @@ -0,0 +1,639 @@ +//! Simple Text In Protocol FFI Support. +//! +//! This module manages the Simple Text In FFI. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. All rights reserved. +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! + +use alloc::boxed::Box; +use core::ffi::c_void; + +use r_efi::{efi, protocols}; +use rust_advanced_logger_dxe::{debugln, DEBUG_ERROR}; + +use crate::{ + boot_services::UefiBootServices, + hid_io::{HidIoFactory, UefiHidIoFactory}, + keyboard::KeyboardHidHandler, +}; + +/// FFI context +/// # Safety +/// A pointer to KeyboardHidHandler is included in the contexts so that it can be reclaimed in the simple_text_in API +/// implementations. Care must be taken to ensure that rust invariants are respected when accessing the +/// KeyboardHidHandler. In particular, the design must ensure mutual exclusion on the KeyboardHidHandler between +/// callbacks running at different TPL; this is accomplished by ensuring all access to the structure is at TPL_NOTIFY +/// once initialization is complete - for this reason the context structure includes a direct reference to boot_services +/// so that TPL can be enforced without access to the *mut KeyboardHidHandler. +/// +/// In addition, the simple_text_in protocol element needs to be the first element in the structure so that the full +/// structure can be recovered by simple casting for simple_text_in_ex FFI interfaces that only receive a pointer to the +/// simple_text_in protocol structure. +#[repr(C)] +pub struct SimpleTextInFfi { + simple_text_in: protocols::simple_text_input::Protocol, + boot_services: &'static dyn UefiBootServices, + keyboard_handler: *mut KeyboardHidHandler, +} + +impl SimpleTextInFfi { + /// Installs the simple text in protocol + pub fn install( + boot_services: &'static dyn UefiBootServices, + controller: efi::Handle, + keyboard_handler: &mut KeyboardHidHandler, + ) -> Result<(), efi::Status> { + //Create simple_text_in context + let simple_text_in_ctx = SimpleTextInFfi { + simple_text_in: protocols::simple_text_input::Protocol { + reset: Self::simple_text_in_reset, + read_key_stroke: Self::simple_text_in_read_key_stroke, + wait_for_key: core::ptr::null_mut(), + }, + boot_services, + keyboard_handler: keyboard_handler as *mut KeyboardHidHandler, + }; + + let simple_text_in_ptr = Box::into_raw(Box::new(simple_text_in_ctx)); + + //create event for wait_for_key + let mut wait_for_key_event: efi::Event = core::ptr::null_mut(); + let status = boot_services.create_event( + efi::EVT_NOTIFY_WAIT, + efi::TPL_NOTIFY, + Some(Self::simple_text_in_wait_for_key), + simple_text_in_ptr as *mut c_void, + core::ptr::addr_of_mut!(wait_for_key_event), + ); + if status.is_error() { + drop(unsafe { Box::from_raw(simple_text_in_ptr) }); + return Err(status); + } + + unsafe { (*simple_text_in_ptr).simple_text_in.wait_for_key = wait_for_key_event }; + + //install the simple_text_in protocol + let mut controller = controller; + let status = boot_services.install_protocol_interface( + core::ptr::addr_of_mut!(controller), + &protocols::simple_text_input::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + efi::NATIVE_INTERFACE, + simple_text_in_ptr as *mut c_void, + ); + + if status.is_error() { + let _ = boot_services.close_event(wait_for_key_event); + drop(unsafe { Box::from_raw(simple_text_in_ptr) }); + return Err(status); + } + + Ok(()) + } + + /// Uninstalls the simple text in protocol + pub fn uninstall( + boot_services: &'static dyn UefiBootServices, + agent: efi::Handle, + controller: efi::Handle, + ) -> Result<(), efi::Status> { + //Controller is set - that means initialize() was called, and there is potential state exposed thru FFI that needs + //to be cleaned up. + let mut simple_text_in_ptr: *mut SimpleTextInFfi = core::ptr::null_mut(); + let status = boot_services.open_protocol( + controller, + &protocols::simple_text_input::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + core::ptr::addr_of_mut!(simple_text_in_ptr) as *mut *mut c_void, + agent, + controller, + efi::OPEN_PROTOCOL_GET_PROTOCOL, + ); + if status.is_error() { + //No protocol is actually installed on this controller, so nothing to clean up. + return Ok(()); + } + + //Attempt to uninstall the simple_text_in interface - this should disconnect any drivers using it and release + //the interface. + let status = boot_services.uninstall_protocol_interface( + controller, + &protocols::simple_text_input::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + simple_text_in_ptr as *mut c_void, + ); + if status.is_error() { + //An error here means some other driver might be holding on to the simple_text_in_ptr. + //Mark the instance invalid by setting the keyboard_handler raw pointer to null, but leak the PointerContext + //instance. Leaking context allows calls through the pointers on absolute_pointer_ptr to continue to resolve + //and return error based on observing keyboard_handler is null. + debugln!(DEBUG_ERROR, "Failed to uninstall simple_text_in interface, status: {:x?}", status); + + unsafe { + (*simple_text_in_ptr).keyboard_handler = core::ptr::null_mut(); + } + //return without tearing down the context. + return Err(status); + } + + let wait_for_key_event: efi::Handle = unsafe { (*simple_text_in_ptr).simple_text_in.wait_for_key }; + let status = boot_services.close_event(wait_for_key_event); + if status.is_error() { + //An error here means the event was not closed, so in theory the notification_callback on it could still be + //fired. + //Mark the instance invalid by setting the keyboard_handler raw pointer to null, but leak the PointerContext + //instance. Leaking context allows calls through the pointers on simple_text_in_ptr to continue to resolve + //and return error based on observing keyboard_handler is null. + debugln!(DEBUG_ERROR, "Failed to close simple_text_in_ptr.wait_for_key event, status: {:x?}", status); + unsafe { + (*simple_text_in_ptr).keyboard_handler = core::ptr::null_mut(); + } + return Err(status); + } + // None of the parts of simple_text_in_ptr are in use, so it is safe to drop it. + drop(unsafe { Box::from_raw(simple_text_in_ptr) }); + Ok(()) + } + + // resets the keyboard state - part of the simple_text_in protocol interface. + extern "efiapi" fn simple_text_in_reset( + this: *mut protocols::simple_text_input::Protocol, + extended_verification: efi::Boolean, + ) -> efi::Status { + if this.is_null() { + return efi::Status::INVALID_PARAMETER; + } + let context = unsafe { (this as *mut SimpleTextInFfi).as_mut() }.expect("bad pointer"); + let old_tpl = context.boot_services.raise_tpl(efi::TPL_NOTIFY); + let mut status = efi::Status::DEVICE_ERROR; + '_reset_processing: { + let keyboard_handler = unsafe { context.keyboard_handler.as_mut() }; + if let Some(keyboard_handler) = keyboard_handler { + //reset requires an instance of hid_io to allow for LED updating, so use UefiHidIoFactory to build one. + if let Some(controller) = keyboard_handler.get_controller() { + let hid_io = + UefiHidIoFactory::new(context.boot_services, keyboard_handler.get_agent()).new_hid_io(controller, false); + if let Ok(hid_io) = hid_io { + if let Err(err) = keyboard_handler.reset(hid_io.as_ref(), extended_verification.into()) { + status = err; + } else { + status = efi::Status::SUCCESS; + } + } + } + } + } + context.boot_services.restore_tpl(old_tpl); + status + } + + // reads a key stroke - part of the simple_text_in protocol interface. + extern "efiapi" fn simple_text_in_read_key_stroke( + this: *mut protocols::simple_text_input::Protocol, + key: *mut protocols::simple_text_input::InputKey, + ) -> efi::Status { + if this.is_null() || key.is_null() { + return efi::Status::INVALID_PARAMETER; + } + let context = unsafe { (this as *mut SimpleTextInFfi).as_mut() }.expect("bad pointer"); + let status; + let old_tpl = context.boot_services.raise_tpl(efi::TPL_NOTIFY); + 'read_key_stroke: { + let keyboard_handler = unsafe { context.keyboard_handler.as_mut() }; + if let Some(keyboard_handler) = keyboard_handler { + loop { + if let Some(mut key_data) = keyboard_handler.pop_key() { + // skip partials + if key_data.key.unicode_char == 0 && key_data.key.scan_code == 0 { + continue; + } + //translate ctrl-alpha to corresponding control value. ctrl-a = 0x0001, ctrl-z = 0x001A + const CONTROL_PRESSED: u32 = protocols::simple_text_input_ex::RIGHT_CONTROL_PRESSED + | protocols::simple_text_input_ex::LEFT_CONTROL_PRESSED; + if (key_data.key_state.key_shift_state & CONTROL_PRESSED) != 0 { + if key_data.key.unicode_char >= 0x0061 && key_data.key.unicode_char <= 0x007a { + //'a' to 'z' + key_data.key.unicode_char = (key_data.key.unicode_char - 0x0061) + 1; + } + if key_data.key.unicode_char >= 0x0041 && key_data.key.unicode_char <= 0x005a { + //'A' to 'Z' + key_data.key.unicode_char = (key_data.key.unicode_char - 0x0041) + 1; + } + } + unsafe { key.write(key_data.key) } + status = efi::Status::SUCCESS; + } else { + status = efi::Status::NOT_READY; + } + break 'read_key_stroke; + } + } else { + status = efi::Status::DEVICE_ERROR; + break 'read_key_stroke; + } + } + context.boot_services.restore_tpl(old_tpl); + status + } + + // Event handler function for the wait_for_key_event + extern "efiapi" fn simple_text_in_wait_for_key(event: efi::Event, context: *mut c_void) { + if context.is_null() { + debugln!(DEBUG_ERROR, "simple_text_in_wait_for_key invoked with invalid context"); + return; + } + let context = unsafe { (context as *mut SimpleTextInFfi).as_mut() }.expect("bad pointer"); + let old_tpl = context.boot_services.raise_tpl(efi::TPL_NOTIFY); + { + if let Some(keyboard_handler) = unsafe { context.keyboard_handler.as_mut() } { + while let Some(key_data) = keyboard_handler.peek_key() { + if key_data.key.unicode_char == 0 && key_data.key.scan_code == 0 { + // consume (and ignore) the partial stroke. + let _ = keyboard_handler.pop_key(); + continue; + } else { + // valid keystroke + context.boot_services.signal_event(event); + break; + } + } + } + } + context.boot_services.restore_tpl(old_tpl); + } +} + +#[cfg(test)] +mod test { + use core::{ + ffi::c_void, + mem::MaybeUninit, + sync::atomic::{AtomicBool, AtomicPtr}, + }; + + use crate::{ + boot_services::MockUefiBootServices, + hid_io::{HidReportReceiver, MockHidIo}, + keyboard::KeyboardHidHandler, + }; + use r_efi::{efi, protocols}; + + use super::SimpleTextInFfi; + + static BOOT_KEYBOARD_REPORT_DESCRIPTOR: &[u8] = &[ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x06, // USAGE (Keyboard) + 0xa1, 0x01, // COLLECTION (Application) + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, 0x08, // REPORT_COUNT (8) + 0x05, 0x07, // USAGE_PAGE (Key Codes) + 0x19, 0xE0, // USAGE_MINIMUM (224) + 0x29, 0xE7, // USAGE_MAXIMUM (231) + 0x15, 0x00, // LOGICAL_MAXIMUM (0) + 0x25, 0x01, // LOGICAL_MINIMUM (1) + 0x81, 0x02, // INPUT (Data, Var, Abs) (Modifier Byte) + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x08, // REPORT_SIZE (8) + 0x81, 0x03, // INPUT (Const) (Reserved Byte) + 0x95, 0x05, // REPORT_COUNT (5) + 0x75, 0x01, // REPORT_SIZE (1) + 0x05, 0x08, // USAGE_PAGE (LEDs) + 0x19, 0x01, // USAGE_MINIMUM (1) + 0x29, 0x05, // USAGE_MAXIMUM (5) + 0x91, 0x02, // OUTPUT (Data, Var, Abs) (LED report) + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x03, // REPORT_SIZE (3) + 0x91, 0x02, // OUTPUT (Constant) (LED report padding) + 0x95, 0x06, // REPORT_COUNT (6) + 0x75, 0x08, // REPORT_SIZE (8) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x26, 0xff, 00, // LOGICAL_MAXIMUM (255) + 0x05, 0x07, // USAGE_PAGE (Key Codes) + 0x19, 0x00, // USAGE_MINIMUM (0) + 0x2a, 0xff, 00, // USAGE_MAXIMUM (255) + 0x81, 0x00, // INPUT (Data, Array) + 0xc0, // END_COLLECTION + ]; + + // In this module, the usage model for boot_services is global static, and so &'static dyn UefiBootServices is used + // throughout the API. For testing, each test will have a different set of expectations on the UefiBootServices mock + // object, and the mock object itself expects to be "mut", which makes it hard to handle as a single global static. + // Instead, raw pointers are used to simulate a MockUefiBootServices instance with 'static lifetime. + // This object needs to outlive anything that uses it - once created, it will live until the end of the program. + fn create_fake_static_boot_service() -> &'static mut MockUefiBootServices { + unsafe { Box::into_raw(Box::new(MockUefiBootServices::new())).as_mut().unwrap() } + } + + #[test] + fn install_should_install_simple_text_in_interface() { + let boot_services = create_fake_static_boot_service(); + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, protocol, _, _| { + assert_eq!(unsafe { protocol.read() }, protocols::simple_text_input::PROTOCOL_GUID); + efi::Status::SUCCESS + }); + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + + SimpleTextInFfi::install(boot_services, 2 as efi::Handle, &mut keyboard_handler).unwrap(); + } + + #[test] + fn uninstall_should_uninstall_simple_text_in_interface() { + static CONTEXT_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + let boot_services = create_fake_static_boot_service(); + + // used in install + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + CONTEXT_PTR.store(interface, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + }); + + // used in uninstall + boot_services.expect_open_protocol().returning(|_, protocol, interface, _, _, _| { + unsafe { + assert_eq!(protocol.read(), protocols::simple_text_input::PROTOCOL_GUID); + interface.write(CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst)); + } + efi::Status::SUCCESS + }); + boot_services.expect_uninstall_protocol_interface().returning(|_, protocol, interface| { + unsafe { + assert_eq!(protocol.read(), protocols::simple_text_input::PROTOCOL_GUID); + assert_eq!(interface, CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst)); + } + efi::Status::SUCCESS + }); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + + SimpleTextInFfi::install(boot_services, 2 as efi::Handle, &mut keyboard_handler).unwrap(); + assert_ne!(CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), core::ptr::null_mut()); + + SimpleTextInFfi::uninstall(boot_services, 1 as efi::Handle, 2 as efi::Handle).unwrap(); + } + + #[test] + fn reset_should_invoke_reset() { + static CONTEXT_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + let boot_services = create_fake_static_boot_service(); + + // used in install + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + CONTEXT_PTR.store(interface, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + }); + + // used in reset + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + extern "efiapi" fn mock_set_report( + _this: *const hid_io::protocol::Protocol, + _report_id: u8, + _report_type: hid_io::protocol::HidReportType, + _report_buffer_size: usize, + _report_buffer: *mut c_void, + ) -> efi::Status { + efi::Status::SUCCESS + } + + // used in reset and uninstall + boot_services.expect_open_protocol().returning(|_, protocol, interface, _, _, attributes| { + unsafe { + match protocol.read() { + hid_io::protocol::GUID => { + assert_eq!(attributes, efi::OPEN_PROTOCOL_GET_PROTOCOL); + let hid_io = MaybeUninit::::zeroed(); + let mut hid_io = hid_io.assume_init(); + hid_io.set_report = mock_set_report; + // note: this will leak a hid_io instance + interface.write(Box::into_raw(Box::new(hid_io)) as *mut c_void); + } + unrecognized_guid => panic!("Unexpected protocol: {:?}", unrecognized_guid), + } + } + efi::Status::SUCCESS + }); + + // used in uninstall. + boot_services.expect_uninstall_protocol_interface().returning(|_, protocol, interface| { + unsafe { + assert_eq!(protocol.read(), protocols::simple_text_input::PROTOCOL_GUID); + assert_eq!(interface, CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst)); + } + efi::Status::SUCCESS + }); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + keyboard_handler.set_controller(Some(2 as efi::Handle)); + + SimpleTextInFfi::install(boot_services, 2 as efi::Handle, &mut keyboard_handler).unwrap(); + assert_ne!(CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), core::ptr::null_mut()); + + let this = CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst) as *mut protocols::simple_text_input::Protocol; + SimpleTextInFfi::simple_text_in_reset(this, efi::Boolean::FALSE); + + // avoid keyboard drop uninstall flows + keyboard_handler.set_controller(None); + } + + #[test] + fn read_key_stroke_should_read_keystrokes() { + static CONTEXT_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + let boot_services = create_fake_static_boot_service(); + + // used in install + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + CONTEXT_PTR.store(interface, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + }); + + // used in reset + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + // used in keyboard init and uninstall + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_signal_event().returning(|_| efi::Status::SUCCESS); + boot_services.expect_open_protocol().returning(|_, _, _, _, _, _| efi::Status::NOT_FOUND); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + keyboard_handler.set_layout(Some(hii_keyboard_layout::get_default_keyboard_layout())); + keyboard_handler.initialize(2 as efi::Handle, &hid_io).unwrap(); + + SimpleTextInFfi::install(boot_services, 2 as efi::Handle, &mut keyboard_handler).unwrap(); + assert_ne!(CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), core::ptr::null_mut()); + + let report: &[u8] = &[0x00, 0x00, 0x04, 0x05, 0x06, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + //release keys and push ctrl + let report: &[u8] = &[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + //read with simple_text_in + let this = CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst) as *mut protocols::simple_text_input::Protocol; + let mut input_key: protocols::simple_text_input::InputKey = Default::default(); + + let status = SimpleTextInFfi::simple_text_in_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input::InputKey, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.unicode_char, 'c' as u16); + assert_eq!(input_key.scan_code, 0); + + let status = SimpleTextInFfi::simple_text_in_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input::InputKey, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.unicode_char, 'b' as u16); + assert_eq!(input_key.scan_code, 0); + + let status = SimpleTextInFfi::simple_text_in_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input::InputKey, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.unicode_char, 'a' as u16); + assert_eq!(input_key.scan_code, 0); + + let status = SimpleTextInFfi::simple_text_in_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input::InputKey, + ); + assert_eq!(status, efi::Status::NOT_READY); + + //send ctrl-a - expect it to be switched to control-character 0x01 + let report: &[u8] = &[0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + //release keys + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + let status = SimpleTextInFfi::simple_text_in_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input::InputKey, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.unicode_char, 0x1); + assert_eq!(input_key.scan_code, 0); + + //send ctrl-shift-Z - expect it to be switched to control-character 0x1A + let report: &[u8] = &[0x03, 0x00, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + //release keys + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + let status = SimpleTextInFfi::simple_text_in_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input::InputKey, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.unicode_char, 0x1a); + assert_eq!(input_key.scan_code, 0); + + keyboard_handler.set_key_toggle_state(protocols::simple_text_input_ex::KEY_STATE_EXPOSED); + + // press the right logo key + let report: &[u8] = &[0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + //release keys + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + // press the a key + let report: &[u8] = &[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + //release keys + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + // should get only the 'a', partial keystroke should be dropped. + let status = SimpleTextInFfi::simple_text_in_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input::InputKey, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.unicode_char, 'a' as u16); + assert_eq!(input_key.scan_code, 0); + + let status = SimpleTextInFfi::simple_text_in_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input::InputKey, + ); + assert_eq!(status, efi::Status::NOT_READY); + } + + #[test] + fn wait_for_key_should_wait_for_key() { + static CONTEXT_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + static RECEIVED_EVENT: AtomicBool = AtomicBool::new(false); + + let boot_services = create_fake_static_boot_service(); + + // used in install + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, protocol, _, interface| { + if unsafe { *protocol } == protocols::simple_text_input::PROTOCOL_GUID { + CONTEXT_PTR.store(interface, core::sync::atomic::Ordering::SeqCst); + } + efi::Status::SUCCESS + }); + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + boot_services.expect_signal_event().returning(|_| { + RECEIVED_EVENT.store(true, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + }); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + keyboard_handler.set_layout(Some(hii_keyboard_layout::get_default_keyboard_layout())); + keyboard_handler.set_controller(Some(2 as efi::Handle)); + keyboard_handler.initialize(2 as efi::Handle, &hid_io).unwrap(); + + RECEIVED_EVENT.store(false, core::sync::atomic::Ordering::SeqCst); + SimpleTextInFfi::simple_text_in_wait_for_key( + 3 as efi::Event, + CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), + ); + assert!(!RECEIVED_EVENT.load(core::sync::atomic::Ordering::SeqCst)); + + // press the a key + let report: &[u8] = &[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + //release keys + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + SimpleTextInFfi::simple_text_in_wait_for_key( + 3 as efi::Event, + CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), + ); + assert!(RECEIVED_EVENT.load(core::sync::atomic::Ordering::SeqCst)); + + // avoid keyboard drop uninstall flows + keyboard_handler.set_controller(None); + } +} diff --git a/HidPkg/UefiHidDxeV2/src/keyboard/simple_text_in_ex.rs b/HidPkg/UefiHidDxeV2/src/keyboard/simple_text_in_ex.rs new file mode 100644 index 0000000000..771b46a4c5 --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/keyboard/simple_text_in_ex.rs @@ -0,0 +1,1014 @@ +//! Simple Text In Ex Protocol FFI Support. +//! +//! This module manages the Simple Text In Ex FFI. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. All rights reserved. +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! +use alloc::{boxed::Box, vec::Vec}; +use core::ffi::c_void; + +use r_efi::{efi, protocols}; +use rust_advanced_logger_dxe::{debugln, DEBUG_ERROR}; + +use crate::{ + boot_services::UefiBootServices, + hid_io::{HidIoFactory, UefiHidIoFactory}, + keyboard::KeyboardHidHandler, +}; + +/// FFI context +/// # Safety +/// A pointer to KeyboardHidHandler is included in the contexts so that it can be reclaimed in the simple_text_in API +/// implementations. Care must be taken to ensure that rust invariants are respected when accessing the +/// KeyboardHidHandler. In particular, the design must ensure mutual exclusion on the KeyboardHidHandler between +/// callbacks running at different TPL; this is accomplished by ensuring all access to the structure is at TPL_NOTIFY +/// once initialization is complete - for this reason the context structure includes a direct reference to boot_services +/// so that TPL can be enforced without access to the *mut KeyboardHidHandler. +/// +/// In addition, the simple_text_in_ex protocol element needs to be the first element in the structure so that the full +/// structure can be recovered by simple casting for simple_text_in FFI interfaces that only receive a pointer to the +/// simple_text_in_ex protocol structure. +#[repr(C)] +pub struct SimpleTextInExFfi { + simple_text_in_ex: protocols::simple_text_input_ex::Protocol, + boot_services: &'static dyn UefiBootServices, + key_notify_event: efi::Event, + keyboard_handler: *mut KeyboardHidHandler, +} + +impl SimpleTextInExFfi { + /// Installs the simple text in ex protocol + pub fn install( + boot_services: &'static dyn UefiBootServices, + controller: efi::Handle, + keyboard_handler: &mut KeyboardHidHandler, + ) -> Result<(), efi::Status> { + //Create simple_text_in context + let simple_text_in_ex_ctx = SimpleTextInExFfi { + simple_text_in_ex: protocols::simple_text_input_ex::Protocol { + reset: Self::simple_text_in_ex_reset, + read_key_stroke_ex: Self::simple_text_in_ex_read_key_stroke, + wait_for_key_ex: core::ptr::null_mut(), + set_state: Self::simple_text_in_ex_set_state, + register_key_notify: Self::simple_text_in_ex_register_key_notify, + unregister_key_notify: Self::simple_text_in_ex_unregister_key_notify, + }, + boot_services, + key_notify_event: core::ptr::null_mut(), + keyboard_handler: keyboard_handler as *mut KeyboardHidHandler, + }; + + let simple_text_in_ex_ptr = Box::into_raw(Box::new(simple_text_in_ex_ctx)); + + //create event for wait_for_key + let mut wait_for_key_event: efi::Event = core::ptr::null_mut(); + let status = boot_services.create_event( + efi::EVT_NOTIFY_WAIT, + efi::TPL_NOTIFY, + Some(Self::simple_text_in_ex_wait_for_key), + simple_text_in_ex_ptr as *mut c_void, + core::ptr::addr_of_mut!(wait_for_key_event), + ); + if status.is_error() { + drop(unsafe { Box::from_raw(simple_text_in_ex_ptr) }); + return Err(status); + } + + unsafe { (*simple_text_in_ex_ptr).simple_text_in_ex.wait_for_key_ex = wait_for_key_event }; + + //Key notifies are required to dispatch at TPL_CALLBACK per UEFI spec 2.10 section 12.2.5. The keyboard handler + //interfaces run at TPL_NOTIFY and issue a boot_services.signal_event() on this event to pend key notifies to be + //serviced at TPL_CALLBACK. + let mut key_notify_event: efi::Event = core::ptr::null_mut(); + let status = boot_services.create_event( + efi::EVT_NOTIFY_SIGNAL, + efi::TPL_CALLBACK, + Some(Self::process_key_notifies), + simple_text_in_ex_ptr as *mut c_void, + core::ptr::addr_of_mut!(key_notify_event), + ); + if status.is_error() { + let _ = boot_services.close_event(wait_for_key_event); + drop(unsafe { Box::from_raw(simple_text_in_ex_ptr) }); + } + + unsafe { (*simple_text_in_ex_ptr).key_notify_event = key_notify_event }; + + //install the simple_text_in_ex protocol + let mut controller = controller; + let status = boot_services.install_protocol_interface( + core::ptr::addr_of_mut!(controller), + &protocols::simple_text_input_ex::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + efi::NATIVE_INTERFACE, + simple_text_in_ex_ptr as *mut c_void, + ); + + if status.is_error() { + let _ = boot_services.close_event(wait_for_key_event); + let _ = boot_services.close_event(key_notify_event); + drop(unsafe { Box::from_raw(simple_text_in_ex_ptr) }); + return Err(status); + } + + Ok(()) + } + + /// Uninstalls the simple text in ex protocol + pub fn uninstall( + boot_services: &'static dyn UefiBootServices, + agent: efi::Handle, + controller: efi::Handle, + ) -> Result<(), efi::Status> { + //Controller is set - that means initialize() was called, and there is potential state exposed thru FFI that needs + //to be cleaned up. + let mut simple_text_in_ex_ptr: *mut SimpleTextInExFfi = core::ptr::null_mut(); + let status = boot_services.open_protocol( + controller, + &protocols::simple_text_input_ex::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + core::ptr::addr_of_mut!(simple_text_in_ex_ptr) as *mut *mut c_void, + agent, + controller, + efi::OPEN_PROTOCOL_GET_PROTOCOL, + ); + if status.is_error() { + //No protocol is actually installed on this controller, so nothing to clean up. + return Ok(()); + } + + //Attempt to uninstall the simple_text_in interface - this should disconnect any drivers using it and release + //the interface. + let status = boot_services.uninstall_protocol_interface( + controller, + &protocols::simple_text_input_ex::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + simple_text_in_ex_ptr as *mut c_void, + ); + if status.is_error() { + //An error here means some other driver might be holding on to the simple_text_in_ptr. + //Mark the instance invalid by setting the keyboard_handler raw pointer to null, but leak the PointerContext + //instance. Leaking context allows calls through the pointers on absolute_pointer_ptr to continue to resolve + //and return error based on observing keyboard_handler is null. + debugln!(DEBUG_ERROR, "Failed to uninstall simple_text_in interface, status: {:x?}", status); + + unsafe { + (*simple_text_in_ex_ptr).keyboard_handler = core::ptr::null_mut(); + } + //return without tearing down the context. + return Err(status); + } + + let wait_for_key_event: efi::Handle = unsafe { (*simple_text_in_ex_ptr).simple_text_in_ex.wait_for_key_ex }; + let status = boot_services.close_event(wait_for_key_event); + if status.is_error() { + //An error here means the event was not closed, so in theory the notification_callback on it could still be + //fired. + //Mark the instance invalid by setting the keyboard_handler raw pointer to null, but leak the PointerContext + //instance. Leaking context allows calls through the pointers on simple_text_in_ptr to continue to resolve + //and return error based on observing keyboard_handler is null. + debugln!(DEBUG_ERROR, "Failed to close simple_text_in_ptr.wait_for_key event, status: {:x?}", status); + unsafe { + (*simple_text_in_ex_ptr).keyboard_handler = core::ptr::null_mut(); + } + return Err(status); + } + + let key_notify_event: efi::Handle = unsafe { (*simple_text_in_ex_ptr).key_notify_event }; + let status = boot_services.close_event(key_notify_event); + if status.is_error() { + //An error here means the event was not closed, so in theory the notification_callback on it could still be + //fired. + //Mark the instance invalid by setting the keyboard_handler raw pointer to null, but leak the PointerContext + //instance. Leaking context allows calls through the pointers on simple_text_in_ptr to continue to resolve + //and return error based on observing keyboard_handler is null. + debugln!(DEBUG_ERROR, "Failed to close key_notify_event event, status: {:x?}", status); + unsafe { + (*simple_text_in_ex_ptr).keyboard_handler = core::ptr::null_mut(); + } + return Err(status); + } + + // None of the parts of simple_text_in_ptr simple_text_in_ptr are in use, so it is safe to reclaim it. + drop(unsafe { Box::from_raw(simple_text_in_ex_ptr) }); + Ok(()) + } + + // resets the keyboard state - part of the simple_text_in_ex protocol interface. + extern "efiapi" fn simple_text_in_ex_reset( + this: *mut protocols::simple_text_input_ex::Protocol, + extended_verification: efi::Boolean, + ) -> efi::Status { + if this.is_null() { + return efi::Status::INVALID_PARAMETER; + } + let context = unsafe { (this as *mut SimpleTextInExFfi).as_mut() }.expect("bad pointer"); + let old_tpl = context.boot_services.raise_tpl(efi::TPL_NOTIFY); + let mut status = efi::Status::DEVICE_ERROR; + '_reset_processing: { + let keyboard_handler = unsafe { context.keyboard_handler.as_mut() }; + if let Some(keyboard_handler) = keyboard_handler { + //reset requires an instance of hid_io to allow for LED updating, so use UefiHidIoFactory to build one. + if let Some(controller) = keyboard_handler.get_controller() { + let hid_io = + UefiHidIoFactory::new(context.boot_services, keyboard_handler.get_agent()).new_hid_io(controller, false); + if let Ok(hid_io) = hid_io { + if let Err(err) = keyboard_handler.reset(hid_io.as_ref(), extended_verification.into()) { + status = err; + } else { + status = efi::Status::SUCCESS; + } + } + } + } + } + context.boot_services.restore_tpl(old_tpl); + status + } + + // reads a key stroke - part of the simple_text_in_ex protocol interface. + extern "efiapi" fn simple_text_in_ex_read_key_stroke( + this: *mut protocols::simple_text_input_ex::Protocol, + key_data: *mut protocols::simple_text_input_ex::KeyData, + ) -> efi::Status { + if this.is_null() || key_data.is_null() { + return efi::Status::INVALID_PARAMETER; + } + let context = unsafe { (this as *mut SimpleTextInExFfi).as_mut() }.expect("bad pointer"); + let mut status = efi::Status::SUCCESS; + let old_tpl = context.boot_services.raise_tpl(efi::TPL_NOTIFY); + 'read_key_stroke: { + let keyboard_handler = unsafe { context.keyboard_handler.as_mut() }; + if let Some(keyboard_handler) = keyboard_handler { + if let Some(key) = keyboard_handler.pop_key() { + unsafe { key_data.write(key) } + } else { + let key = protocols::simple_text_input_ex::KeyData { + key_state: keyboard_handler.get_key_state(), + ..Default::default() + }; + unsafe { key_data.write(key) }; + status = efi::Status::NOT_READY + } + } else { + status = efi::Status::DEVICE_ERROR; + break 'read_key_stroke; + } + } + context.boot_services.restore_tpl(old_tpl); + status + } + + // sets the keyboard state - part of the simple_text_in_ex protocol interface. + extern "efiapi" fn simple_text_in_ex_set_state( + this: *mut protocols::simple_text_input_ex::Protocol, + key_toggle_state: *mut protocols::simple_text_input_ex::KeyToggleState, + ) -> efi::Status { + if this.is_null() || key_toggle_state.is_null() { + return efi::Status::INVALID_PARAMETER; + } + let context = unsafe { (this as *mut SimpleTextInExFfi).as_mut() }.expect("bad pointer"); + let mut status = efi::Status::DEVICE_ERROR; + let old_tpl = context.boot_services.raise_tpl(efi::TPL_NOTIFY); + '_set_state_processing: { + if let Some(keyboard_handler) = unsafe { context.keyboard_handler.as_mut() } { + if let Some(controller) = keyboard_handler.get_controller() { + let hid_io = + UefiHidIoFactory::new(context.boot_services, keyboard_handler.get_agent()).new_hid_io(controller, false); + if let Ok(hid_io) = hid_io { + status = efi::Status::SUCCESS; + keyboard_handler.set_key_toggle_state(unsafe { key_toggle_state.read() }); + let result = keyboard_handler.update_leds(hid_io.as_ref()); + if let Err(result) = result { + status = result; + } + } + } + } + } + context.boot_services.restore_tpl(old_tpl); + status + } + + // registers a key notification callback function - part of the simple_text_in_ex protocol interface. + extern "efiapi" fn simple_text_in_ex_register_key_notify( + this: *mut protocols::simple_text_input_ex::Protocol, + key_data_ptr: *mut protocols::simple_text_input_ex::KeyData, + key_notification_function: protocols::simple_text_input_ex::KeyNotifyFunction, + notify_handle: *mut *mut c_void, + ) -> efi::Status { + if this.is_null() || key_data_ptr.is_null() || notify_handle.is_null() || key_notification_function as usize == 0 { + return efi::Status::INVALID_PARAMETER; + } + + let context = unsafe { (this as *mut SimpleTextInExFfi).as_mut() }.expect("bad pointer"); + let old_tpl = context.boot_services.raise_tpl(efi::TPL_NOTIFY); + let status; + { + if let Some(keyboard_handler) = unsafe { context.keyboard_handler.as_mut() } { + let key_data = unsafe { key_data_ptr.read() }; + let handle = keyboard_handler.insert_key_notify_callback(key_data, key_notification_function); + unsafe { notify_handle.write(handle as *mut c_void) }; + status = efi::Status::SUCCESS; + } else { + status = efi::Status::DEVICE_ERROR; + } + } + context.boot_services.restore_tpl(old_tpl); + status + } + + // Unregisters a key notification callback function - part of the simple_text_in_ex protocol interface. + extern "efiapi" fn simple_text_in_ex_unregister_key_notify( + this: *mut protocols::simple_text_input_ex::Protocol, + notification_handle: *mut c_void, + ) -> efi::Status { + if this.is_null() { + return efi::Status::INVALID_PARAMETER; + } + let status; + let context = unsafe { (this as *mut SimpleTextInExFfi).as_mut() }.expect("bad pointer"); + let old_tpl = context.boot_services.raise_tpl(efi::TPL_NOTIFY); + if let Some(keyboard_handler) = unsafe { context.keyboard_handler.as_mut() } { + if let Err(err) = keyboard_handler.remove_key_notify_callback(notification_handle as usize) { + status = err; + } else { + status = efi::Status::SUCCESS; + } + } else { + status = efi::Status::DEVICE_ERROR; + } + context.boot_services.restore_tpl(old_tpl); + status + } + + // Event handler function for the wait_for_key_event + extern "efiapi" fn simple_text_in_ex_wait_for_key(event: efi::Event, context: *mut c_void) { + if context.is_null() { + debugln!(DEBUG_ERROR, "simple_text_in_ex_wait_for_key invoked with invalid context"); + return; + } + let context = unsafe { (context as *mut SimpleTextInExFfi).as_mut() }.expect("bad pointer"); + + let old_tpl = context.boot_services.raise_tpl(efi::TPL_NOTIFY); + { + if let Some(keyboard_handler) = unsafe { context.keyboard_handler.as_mut() } { + while let Some(key_data) = keyboard_handler.peek_key() { + if key_data.key.unicode_char == 0 && key_data.key.scan_code == 0 { + // consume (and ignore) the partial stroke. + let _ = keyboard_handler.pop_key(); + continue; + } else { + // valid keystroke + context.boot_services.signal_event(event); + break; + } + } + } + } + context.boot_services.restore_tpl(old_tpl); + } + + // Event callback function for handling registered key notifications. Iterates over the queue of keys to be notified, + // and invokes the registered callback function for each of those keys. + extern "efiapi" fn process_key_notifies(_event: efi::Event, context: *mut c_void) { + if let Some(context) = unsafe { (context as *mut SimpleTextInExFfi).as_mut() } { + loop { + let mut pending_key = None; + let mut pending_callbacks = Vec::new(); + + let old_tpl = context.boot_services.raise_tpl(efi::TPL_NOTIFY); + if let Some(keyboard_handler) = unsafe { context.keyboard_handler.as_mut() } { + (pending_key, pending_callbacks) = keyboard_handler.get_pending_callbacks(); + } else { + debugln!(DEBUG_ERROR, "process_key_notifies event called without a valid keyboard_handler"); + } + context.boot_services.restore_tpl(old_tpl); + + //dispatch notifies (if any) at the TPL this event callback was invoked at. + if let Some(mut pending_key) = pending_key { + let key_ptr = &mut pending_key as *mut protocols::simple_text_input_ex::KeyData; + for callback in pending_callbacks { + let _ = callback(key_ptr); + } + } else { + // no pending notifies to process + break; + } + } + } + } +} + +#[cfg(test)] +mod test { + use core::{ + ffi::c_void, + mem::MaybeUninit, + sync::atomic::{AtomicBool, AtomicPtr}, + }; + + use r_efi::{efi, protocols}; + + use crate::{ + boot_services::MockUefiBootServices, + hid_io::{HidReportReceiver, MockHidIo}, + keyboard::KeyboardHidHandler, + }; + + use super::SimpleTextInExFfi; + + static BOOT_KEYBOARD_REPORT_DESCRIPTOR: &[u8] = &[ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x06, // USAGE (Keyboard) + 0xa1, 0x01, // COLLECTION (Application) + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, 0x08, // REPORT_COUNT (8) + 0x05, 0x07, // USAGE_PAGE (Key Codes) + 0x19, 0xE0, // USAGE_MINIMUM (224) + 0x29, 0xE7, // USAGE_MAXIMUM (231) + 0x15, 0x00, // LOGICAL_MAXIMUM (0) + 0x25, 0x01, // LOGICAL_MINIMUM (1) + 0x81, 0x02, // INPUT (Data, Var, Abs) (Modifier Byte) + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x08, // REPORT_SIZE (8) + 0x81, 0x03, // INPUT (Const) (Reserved Byte) + 0x95, 0x05, // REPORT_COUNT (5) + 0x75, 0x01, // REPORT_SIZE (1) + 0x05, 0x08, // USAGE_PAGE (LEDs) + 0x19, 0x01, // USAGE_MINIMUM (1) + 0x29, 0x05, // USAGE_MAXIMUM (5) + 0x91, 0x02, // OUTPUT (Data, Var, Abs) (LED report) + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x03, // REPORT_SIZE (3) + 0x91, 0x02, // OUTPUT (Constant) (LED report padding) + 0x95, 0x06, // REPORT_COUNT (6) + 0x75, 0x08, // REPORT_SIZE (8) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x26, 0xff, 00, // LOGICAL_MAXIMUM (255) + 0x05, 0x07, // USAGE_PAGE (Key Codes) + 0x19, 0x00, // USAGE_MINIMUM (0) + 0x2a, 0xff, 00, // USAGE_MAXIMUM (255) + 0x81, 0x00, // INPUT (Data, Array) + 0xc0, // END_COLLECTION + ]; + + // In this module, the usage model for boot_services is global static, and so &'static dyn UefiBootServices is used + // throughout the API. For testing, each test will have a different set of expectations on the UefiBootServices mock + // object, and the mock object itself expects to be "mut", which makes it hard to handle as a single global static. + // Instead, raw pointers are used to simulate a MockUefiBootServices instance with 'static lifetime. + // This object needs to outlive anything that uses it - once created, it will live until the end of the program. + fn create_fake_static_boot_service() -> &'static mut MockUefiBootServices { + unsafe { Box::into_raw(Box::new(MockUefiBootServices::new())).as_mut().unwrap() } + } + + #[test] + fn install_should_install_simple_text_in_ex_interface() { + let boot_services = create_fake_static_boot_service(); + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, protocol, _, _| { + assert_eq!(unsafe { protocol.read() }, protocols::simple_text_input_ex::PROTOCOL_GUID); + efi::Status::SUCCESS + }); + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + + SimpleTextInExFfi::install(boot_services, 2 as efi::Handle, &mut keyboard_handler).unwrap(); + } + + #[test] + fn uninstall_should_uninstall_simple_text_in_interface() { + static CONTEXT_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + let boot_services = create_fake_static_boot_service(); + + // used in install + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + CONTEXT_PTR.store(interface, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + }); + + // used in uninstall + boot_services.expect_open_protocol().returning(|_, protocol, interface, _, _, _| { + unsafe { + assert_eq!(protocol.read(), protocols::simple_text_input_ex::PROTOCOL_GUID); + interface.write(CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst)); + } + efi::Status::SUCCESS + }); + boot_services.expect_uninstall_protocol_interface().returning(|_, protocol, interface| { + unsafe { + assert_eq!(protocol.read(), protocols::simple_text_input_ex::PROTOCOL_GUID); + assert_eq!(interface, CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst)); + } + efi::Status::SUCCESS + }); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + + SimpleTextInExFfi::install(boot_services, 2 as efi::Handle, &mut keyboard_handler).unwrap(); + assert_ne!(CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), core::ptr::null_mut()); + + SimpleTextInExFfi::uninstall(boot_services, 1 as efi::Handle, 2 as efi::Handle).unwrap(); + } + + #[test] + fn reset_should_invoke_reset() { + static CONTEXT_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + let boot_services = create_fake_static_boot_service(); + + // used in install + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + CONTEXT_PTR.store(interface, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + }); + + // used in reset + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + extern "efiapi" fn mock_set_report( + _this: *const hid_io::protocol::Protocol, + _report_id: u8, + _report_type: hid_io::protocol::HidReportType, + _report_buffer_size: usize, + _report_buffer: *mut c_void, + ) -> efi::Status { + efi::Status::SUCCESS + } + + // used in reset and uninstall + boot_services.expect_open_protocol().returning(|_, protocol, interface, _, _, attributes| { + unsafe { + match protocol.read() { + hid_io::protocol::GUID => { + assert_eq!(attributes, efi::OPEN_PROTOCOL_GET_PROTOCOL); + let hid_io = MaybeUninit::::zeroed(); + let mut hid_io = hid_io.assume_init(); + hid_io.set_report = mock_set_report; + // note: this will leak a hid_io instance + interface.write(Box::into_raw(Box::new(hid_io)) as *mut c_void); + } + unrecognized_guid => panic!("Unexpected protocol: {:?}", unrecognized_guid), + } + } + efi::Status::SUCCESS + }); + + // used in uninstall. + boot_services.expect_uninstall_protocol_interface().returning(|_, protocol, interface| { + unsafe { + assert_eq!(protocol.read(), protocols::simple_text_input_ex::PROTOCOL_GUID); + assert_eq!(interface, CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst)); + } + efi::Status::SUCCESS + }); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + keyboard_handler.set_controller(Some(2 as efi::Handle)); + + SimpleTextInExFfi::install(boot_services, 2 as efi::Handle, &mut keyboard_handler).unwrap(); + assert_ne!(CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), core::ptr::null_mut()); + + let this = CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst) as *mut protocols::simple_text_input_ex::Protocol; + SimpleTextInExFfi::simple_text_in_ex_reset(this, efi::Boolean::FALSE); + + // avoid keyboard drop uninstall flows + keyboard_handler.set_controller(None); + } + + #[test] + fn read_key_stroke_should_read_keystrokes() { + static CONTEXT_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + let boot_services = create_fake_static_boot_service(); + + // used in install + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + CONTEXT_PTR.store(interface, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + }); + + // used in read key stroke + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + // used in keyboard init and uninstall + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_signal_event().returning(|_| efi::Status::SUCCESS); + boot_services.expect_open_protocol().returning(|_, _, _, _, _, _| efi::Status::NOT_FOUND); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + keyboard_handler.set_layout(Some(hii_keyboard_layout::get_default_keyboard_layout())); + keyboard_handler.initialize(2 as efi::Handle, &hid_io).unwrap(); + + SimpleTextInExFfi::install(boot_services, 2 as efi::Handle, &mut keyboard_handler).unwrap(); + assert_ne!(CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), core::ptr::null_mut()); + + let report: &[u8] = &[0x00, 0x00, 0x04, 0x05, 0x06, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + //release keys and push ctrl + let report: &[u8] = &[0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + //read with simple_text_in + let this = CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst) as *mut protocols::simple_text_input_ex::Protocol; + let mut input_key: protocols::simple_text_input_ex::KeyData = Default::default(); + + let status = SimpleTextInExFfi::simple_text_in_ex_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input_ex::KeyData, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.key.unicode_char, 'c' as u16); + assert_eq!(input_key.key.scan_code, 0); + + let status = SimpleTextInExFfi::simple_text_in_ex_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input_ex::KeyData, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.key.unicode_char, 'b' as u16); + assert_eq!(input_key.key.scan_code, 0); + + let status = SimpleTextInExFfi::simple_text_in_ex_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input_ex::KeyData, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.key.unicode_char, 'a' as u16); + assert_eq!(input_key.key.scan_code, 0); + + let status = SimpleTextInExFfi::simple_text_in_ex_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input_ex::KeyData, + ); + assert_eq!(status, efi::Status::NOT_READY); + + //send ctrl-a - expect it not to be switched to control-character 0x01 + let report: &[u8] = &[0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + //release keys + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + let status = SimpleTextInExFfi::simple_text_in_ex_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input_ex::KeyData, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.key.unicode_char, 'a' as u16); + assert_eq!(input_key.key.scan_code, 0); + assert_eq!( + input_key.key_state.key_shift_state, + protocols::simple_text_input_ex::SHIFT_STATE_VALID | protocols::simple_text_input_ex::LEFT_CONTROL_PRESSED + ); + + //send ctrl-shift-Z - expect it not to be switched to control-character 0x1A + let report: &[u8] = &[0x03, 0x00, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + //release keys + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + let status = SimpleTextInExFfi::simple_text_in_ex_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input_ex::KeyData, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.key.unicode_char, 'Z' as u16); + assert_eq!(input_key.key.scan_code, 0); + assert_eq!( + input_key.key_state.key_shift_state, + protocols::simple_text_input_ex::SHIFT_STATE_VALID | protocols::simple_text_input_ex::LEFT_CONTROL_PRESSED + ); + + keyboard_handler.set_key_toggle_state(protocols::simple_text_input_ex::KEY_STATE_EXPOSED); + + // press the right logo key + let report: &[u8] = &[0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + //release keys + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + // press the a key + let report: &[u8] = &[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + //release keys + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + // should get partial keystroke. + let status = SimpleTextInExFfi::simple_text_in_ex_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input_ex::KeyData, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.key.unicode_char, 0); + assert_eq!(input_key.key.scan_code, 0); + assert_eq!( + input_key.key_state.key_shift_state, + protocols::simple_text_input_ex::SHIFT_STATE_VALID | protocols::simple_text_input_ex::RIGHT_LOGO_PRESSED + ); + + // followed by 'a' key. + let status = SimpleTextInExFfi::simple_text_in_ex_read_key_stroke( + this, + &mut input_key as *mut protocols::simple_text_input_ex::KeyData, + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(input_key.key.unicode_char, 'a' as u16); + assert_eq!(input_key.key.scan_code, 0); + assert_eq!(input_key.key_state.key_shift_state, protocols::simple_text_input_ex::SHIFT_STATE_VALID); + } + + #[test] + fn set_state_should_set_state() { + static CONTEXT_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + let boot_services = create_fake_static_boot_service(); + + // used in install + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + CONTEXT_PTR.store(interface, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + }); + + // used in set state + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + // used in keyboard init and uninstall + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_signal_event().returning(|_| efi::Status::SUCCESS); + extern "efiapi" fn mock_set_report( + _this: *const hid_io::protocol::Protocol, + _report_id: u8, + _report_type: hid_io::protocol::HidReportType, + _report_buffer_size: usize, + _report_buffer: *mut c_void, + ) -> efi::Status { + efi::Status::SUCCESS + } + + // used in set_state and uninstall + boot_services.expect_open_protocol().returning(|_, protocol, interface, _, _, attributes| { + unsafe { + match protocol.read() { + hid_io::protocol::GUID => { + assert_eq!(attributes, efi::OPEN_PROTOCOL_GET_PROTOCOL); + let hid_io = MaybeUninit::::zeroed(); + let mut hid_io = hid_io.assume_init(); + hid_io.set_report = mock_set_report; + // note: this will leak a hid_io instance + interface.write(Box::into_raw(Box::new(hid_io)) as *mut c_void); + } + _unrecognized_guid => return efi::Status::NOT_FOUND, + } + } + efi::Status::SUCCESS + }); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + keyboard_handler.set_layout(Some(hii_keyboard_layout::get_default_keyboard_layout())); + keyboard_handler.initialize(2 as efi::Handle, &hid_io).unwrap(); + + SimpleTextInExFfi::install(boot_services, 2 as efi::Handle, &mut keyboard_handler).unwrap(); + assert_ne!(CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), core::ptr::null_mut()); + + let this = CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst) as *mut protocols::simple_text_input_ex::Protocol; + let mut key_toggle_state: protocols::simple_text_input_ex::KeyToggleState = + protocols::simple_text_input_ex::KEY_STATE_EXPOSED; + + let status = SimpleTextInExFfi::simple_text_in_ex_set_state(this, core::ptr::addr_of_mut!(key_toggle_state)); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!( + keyboard_handler.get_key_state().key_toggle_state, + key_toggle_state | protocols::simple_text_input_ex::TOGGLE_STATE_VALID + ); + + key_toggle_state = + protocols::simple_text_input_ex::CAPS_LOCK_ACTIVE | protocols::simple_text_input_ex::NUM_LOCK_ACTIVE; + let status = SimpleTextInExFfi::simple_text_in_ex_set_state(this, core::ptr::addr_of_mut!(key_toggle_state)); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!( + keyboard_handler.get_key_state().key_toggle_state, + key_toggle_state | protocols::simple_text_input_ex::TOGGLE_STATE_VALID + ); + } + + #[test] + fn register_key_notify_should_register_key() { + const NOTIFY_EVENT: efi::Event = 1 as efi::Event; + static CONTEXT_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + static KEY_NOTIFIED: AtomicBool = AtomicBool::new(false); + static KEY2_NOTIFIED: AtomicBool = AtomicBool::new(false); + + let boot_services = create_fake_static_boot_service(); + + // used in install + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + CONTEXT_PTR.store(interface, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + }); + + // used in read key stroke + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + // used in keyboard init and uninstall + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_signal_event().returning(|event| { + if event == NOTIFY_EVENT { + SimpleTextInExFfi::process_key_notifies(event, CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst)); + } + efi::Status::SUCCESS + }); + boot_services.expect_open_protocol().returning(|_, _, _, _, _, _| efi::Status::NOT_FOUND); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + keyboard_handler.set_layout(Some(hii_keyboard_layout::get_default_keyboard_layout())); + keyboard_handler.initialize(2 as efi::Handle, &hid_io).unwrap(); + keyboard_handler.set_notify_event(NOTIFY_EVENT); + + SimpleTextInExFfi::install(boot_services, 2 as efi::Handle, &mut keyboard_handler).unwrap(); + assert_ne!(CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), core::ptr::null_mut()); + + extern "efiapi" fn key_notify_callback_a(key_data: *mut protocols::simple_text_input_ex::KeyData) -> efi::Status { + let key = unsafe { key_data.read() }; + assert_eq!(key.key.unicode_char, 'a' as u16); + KEY_NOTIFIED.store(true, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + } + + extern "efiapi" fn key_notify_callback_a_and_b( + key_data: *mut protocols::simple_text_input_ex::KeyData, + ) -> efi::Status { + let key = unsafe { key_data.read() }; + assert!((key.key.unicode_char == 'a' as u16) || (key.key.unicode_char == 'b' as u16)); + KEY2_NOTIFIED.store(true, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + } + + let this = CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst) as *mut protocols::simple_text_input_ex::Protocol; + + let mut key_data: protocols::simple_text_input_ex::KeyData = Default::default(); + key_data.key.unicode_char = 'a' as u16; + let mut notify_handle = core::ptr::null_mut(); + + let status = SimpleTextInExFfi::simple_text_in_ex_register_key_notify( + this, + core::ptr::addr_of_mut!(key_data), + key_notify_callback_a, + core::ptr::addr_of_mut!(notify_handle), + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(notify_handle as usize, 1); + + notify_handle = core::ptr::null_mut(); + let status = SimpleTextInExFfi::simple_text_in_ex_register_key_notify( + this, + core::ptr::addr_of_mut!(key_data), + key_notify_callback_a_and_b, + core::ptr::addr_of_mut!(notify_handle), + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(notify_handle as usize, 2); + + notify_handle = core::ptr::null_mut(); + key_data.key.unicode_char = 'b' as u16; + let status = SimpleTextInExFfi::simple_text_in_ex_register_key_notify( + this, + core::ptr::addr_of_mut!(key_data), + key_notify_callback_a_and_b, + core::ptr::addr_of_mut!(notify_handle), + ); + assert_eq!(status, efi::Status::SUCCESS); + assert_eq!(notify_handle as usize, 3); + + //send 'b' + let report: &[u8] = &[0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + //release + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + assert!(!KEY_NOTIFIED.load(core::sync::atomic::Ordering::SeqCst)); + assert!(KEY2_NOTIFIED.load(core::sync::atomic::Ordering::SeqCst)); + + KEY_NOTIFIED.store(false, core::sync::atomic::Ordering::SeqCst); + KEY2_NOTIFIED.store(false, core::sync::atomic::Ordering::SeqCst); + + //send 'a' + let report: &[u8] = &[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + //release + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + assert!(KEY_NOTIFIED.load(core::sync::atomic::Ordering::SeqCst)); + assert!(KEY2_NOTIFIED.load(core::sync::atomic::Ordering::SeqCst)); + + KEY_NOTIFIED.store(false, core::sync::atomic::Ordering::SeqCst); + KEY2_NOTIFIED.store(false, core::sync::atomic::Ordering::SeqCst); + + //remove the 'a'-only callback + let status = SimpleTextInExFfi::simple_text_in_ex_unregister_key_notify(this, 1 as *mut c_void); + assert_eq!(status, efi::Status::SUCCESS); + + //send 'a' + let report: &[u8] = &[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + //release + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + assert!(!KEY_NOTIFIED.load(core::sync::atomic::Ordering::SeqCst)); + assert!(KEY2_NOTIFIED.load(core::sync::atomic::Ordering::SeqCst)); + } + + #[test] + fn wait_for_key_should_wait_for_key() { + static CONTEXT_PTR: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + static RECEIVED_EVENT: AtomicBool = AtomicBool::new(false); + + let boot_services = create_fake_static_boot_service(); + + // used in install + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, protocol, _, interface| { + if unsafe { *protocol } == protocols::simple_text_input_ex::PROTOCOL_GUID { + CONTEXT_PTR.store(interface, core::sync::atomic::Ordering::SeqCst); + } + efi::Status::SUCCESS + }); + boot_services.expect_create_event_ex().returning(|_, _, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_raise_tpl().returning(|_| efi::TPL_APPLICATION); + boot_services.expect_restore_tpl().returning(|_| ()); + + boot_services.expect_signal_event().returning(|_| { + RECEIVED_EVENT.store(true, core::sync::atomic::Ordering::SeqCst); + efi::Status::SUCCESS + }); + + let mut keyboard_handler = KeyboardHidHandler::new(boot_services, 1 as efi::Handle); + + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + keyboard_handler.set_layout(Some(hii_keyboard_layout::get_default_keyboard_layout())); + keyboard_handler.set_controller(Some(2 as efi::Handle)); + keyboard_handler.initialize(2 as efi::Handle, &hid_io).unwrap(); + + RECEIVED_EVENT.store(false, core::sync::atomic::Ordering::SeqCst); + SimpleTextInExFfi::simple_text_in_ex_wait_for_key( + 3 as efi::Event, + CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), + ); + assert!(!RECEIVED_EVENT.load(core::sync::atomic::Ordering::SeqCst)); + + // press the a key + let report: &[u8] = &[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + //release keys + let report: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + keyboard_handler.receive_report(report, &hid_io); + + SimpleTextInExFfi::simple_text_in_ex_wait_for_key( + 3 as efi::Event, + CONTEXT_PTR.load(core::sync::atomic::Ordering::SeqCst), + ); + assert!(RECEIVED_EVENT.load(core::sync::atomic::Ordering::SeqCst)); + + // avoid keyboard drop uninstall flows + keyboard_handler.set_controller(None); + } +} diff --git a/HidPkg/UefiHidDxeV2/src/lib.rs b/HidPkg/UefiHidDxeV2/src/lib.rs new file mode 100644 index 0000000000..5dca990737 --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/lib.rs @@ -0,0 +1,41 @@ +//! UefiHidDxe - Human Interface Device support. +//! +//! This crate provides a UEFI driver to support HID devices. At present, it has +//! support for pointer and keyboard devices. Devices are supported in Report +//! mode (as opposed to Boot mode) and the report descriptor is used to +//! inform the parsing of arbitrary input reports from the device. +//! +//! ## Usage +//! +//! To use this crate, a device must expose an instance of the HidIo protocol: +//! +//! +//! This driver will use that interface to query device report descriptors and +//! instantiate handling for keyboard, pointer, or both as appropriate. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. All rights reserved. +//! +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! +#![cfg_attr(target_os = "uefi", no_std)] + +extern crate alloc; + +pub mod boot_services; +pub mod driver_binding; +pub mod hid; +pub mod hid_io; +pub mod keyboard; +pub mod pointer; + +use boot_services::StandardUefiBootServices; +use core::sync::atomic::AtomicPtr; +use r_efi::efi; + +/// Global instance of UEFI Boot Services. +pub static BOOT_SERVICES: StandardUefiBootServices = StandardUefiBootServices::new(); + +/// Global instance of UEFI Runtime Services. +pub static RUNTIME_SERVICES: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); diff --git a/HidPkg/UefiHidDxeV2/src/main.rs b/HidPkg/UefiHidDxeV2/src/main.rs new file mode 100644 index 0000000000..140c8fb772 --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/main.rs @@ -0,0 +1,90 @@ +//! HID input driver for UEFI +//! +//! This crate provides input handlers for HID 1.1 compliant keyboards and pointers. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. All rights reserved. +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! + +#![cfg_attr(target_os = "uefi", no_std)] +#![cfg_attr(target_os = "uefi", no_main)] +#![allow(non_snake_case)] + +#[cfg(target_os = "uefi")] +mod uefi_entry { + extern crate alloc; + use core::{panic::PanicInfo, sync::atomic::Ordering}; + + use alloc::{boxed::Box, vec::Vec}; + + use r_efi::{efi, system}; + + use rust_advanced_logger_dxe::{debugln, init_debug, DEBUG_ERROR}; + use rust_boot_services_allocator_dxe::GLOBAL_ALLOCATOR; + use uefi_hid_dxe_v2::{ + boot_services::UefiBootServices, + driver_binding::UefiDriverBinding, + hid::{HidFactory, HidReceiverFactory}, + hid_io::{HidReportReceiver, UefiHidIoFactory}, + keyboard::KeyboardHidHandler, + pointer::PointerHidHandler, + BOOT_SERVICES, RUNTIME_SERVICES, + }; + + struct UefiReceivers { + boot_services: &'static dyn UefiBootServices, + agent: efi::Handle, + } + impl HidReceiverFactory for UefiReceivers { + fn new_hid_receiver_list(&self, _controller: efi::Handle) -> Result>, efi::Status> { + let mut receivers: Vec> = Vec::new(); + receivers.push(Box::new(PointerHidHandler::new(self.boot_services, self.agent))); + receivers.push(Box::new(KeyboardHidHandler::new(self.boot_services, self.agent))); + Ok(receivers) + } + } + + #[no_mangle] + pub extern "efiapi" fn efi_main(image_handle: efi::Handle, system_table: *const system::SystemTable) -> efi::Status { + // Safety: This block is unsafe because it assumes that system_table and (*system_table).boot_services are correct, + // and because it mutates/accesses the global BOOT_SERVICES static. + unsafe { + BOOT_SERVICES.initialize((*system_table).boot_services); + RUNTIME_SERVICES.store((*system_table).runtime_services, Ordering::SeqCst); + GLOBAL_ALLOCATOR.init((*system_table).boot_services); + init_debug((*system_table).boot_services); + } + + let hid_io_factory = Box::new(UefiHidIoFactory::new(&BOOT_SERVICES, image_handle)); + let receiver_factory = Box::new(UefiReceivers { boot_services: &BOOT_SERVICES, agent: image_handle }); + let hid_factory = Box::new(HidFactory::new(hid_io_factory, receiver_factory, image_handle)); + + let hid_binding = UefiDriverBinding::new(&BOOT_SERVICES, hid_factory, image_handle); + hid_binding.install().expect("failed to install HID driver binding"); + + efi::Status::SUCCESS + } + + #[panic_handler] + fn panic(info: &PanicInfo) -> ! { + debugln!(DEBUG_ERROR, "Panic: {:?}", info); + loop {} + } +} + +#[cfg(not(target_os = "uefi"))] +fn main() { + //do nothing. +} + +#[cfg(test)] +mod test { + use crate::main; + + #[test] + fn main_should_do_nothing() { + main(); + } +} diff --git a/HidPkg/UefiHidDxeV2/src/pointer/absolute_pointer.rs b/HidPkg/UefiHidDxeV2/src/pointer/absolute_pointer.rs new file mode 100644 index 0000000000..15e65fc773 --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/pointer/absolute_pointer.rs @@ -0,0 +1,597 @@ +//! Absolute Pointer Protocol FFI Support. +//! +//! This module manages the Absolute Pointer Protocol FFI. +//! +//! ## License +//! +//! Copyright (c) Microsoft Corporation. All rights reserved. +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! +use alloc::boxed::Box; +use core::ffi::c_void; + +use hidparser::report_data_types::Usage; +use r_efi::{efi, protocols}; +use rust_advanced_logger_dxe::{debugln, DEBUG_ERROR, DEBUG_INFO, DEBUG_WARN}; + +use crate::boot_services::UefiBootServices; + +use super::PointerHidHandler; + +// FFI context +// Safety: a pointer to PointerHidHandler is included in the context so that it can be reclaimed in the absolute_pointer +// API implementation. Care must be taken to ensure that rust invariants are respected when accessing the +// PointerHidHandler. In particular, the design must ensure mutual exclusion on the PointerHidHandler between callbacks +// running at different TPL; this is accomplished by ensuring all access to the structure is at TPL_NOTIFY once +// initialization is complete - for this reason the context structure includes a direct reference to boot_services so +// that TPL can be enforced without access to the *mut PointerHidHandle. +// +// In addition, the absolute_pointer element needs to be the first element in the structure so that the full structure +// can be recovered by simple casting for absolute_pointer FFI interfaces that only receive a pointer to the +// absolute_pointer structure. +#[repr(C)] +pub struct PointerContext { + absolute_pointer: protocols::absolute_pointer::Protocol, + boot_services: &'static dyn UefiBootServices, + pointer_handler: *mut PointerHidHandler, +} + +impl Drop for PointerContext { + fn drop(&mut self) { + if !self.absolute_pointer.mode.is_null() { + drop(unsafe { Box::from_raw(self.absolute_pointer.mode) }); + } + } +} + +impl PointerContext { + /// Installs the absolute pointer protocol + pub fn install( + boot_services: &'static dyn UefiBootServices, + controller: efi::Handle, + pointer_handler: &mut PointerHidHandler, + ) -> Result<(), efi::Status> { + // Create pointer context. + let pointer_ctx = PointerContext { + absolute_pointer: protocols::absolute_pointer::Protocol { + reset: Self::absolute_pointer_reset, + get_state: Self::absolute_pointer_get_state, + mode: Box::into_raw(Box::new(Self::initialize_mode(pointer_handler))), + wait_for_input: core::ptr::null_mut(), + }, + boot_services, + pointer_handler: pointer_handler as *mut PointerHidHandler, + }; + + let absolute_pointer_ptr = Box::into_raw(Box::new(pointer_ctx)); + + // create event for wait_for_input. + let mut wait_for_pointer_input_event: efi::Event = core::ptr::null_mut(); + let status = boot_services.create_event( + efi::EVT_NOTIFY_WAIT, + efi::TPL_NOTIFY, + Some(Self::wait_for_pointer), + absolute_pointer_ptr as *mut c_void, + core::ptr::addr_of_mut!(wait_for_pointer_input_event), + ); + if status.is_error() { + drop(unsafe { Box::from_raw(absolute_pointer_ptr) }); + return Err(status); + } + + unsafe { (*absolute_pointer_ptr).absolute_pointer.wait_for_input = wait_for_pointer_input_event }; + + // install the absolute_pointer protocol. + let mut controller = controller; + let status = boot_services.install_protocol_interface( + core::ptr::addr_of_mut!(controller), + &protocols::absolute_pointer::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + efi::NATIVE_INTERFACE, + absolute_pointer_ptr as *mut c_void, + ); + + if status.is_error() { + let _ = boot_services.close_event(wait_for_pointer_input_event); + drop(unsafe { Box::from_raw(absolute_pointer_ptr) }); + return Err(status); + } + + Ok(()) + } + + // Initializes the absolute_pointer mode structure. + fn initialize_mode(pointer_handler: &PointerHidHandler) -> protocols::absolute_pointer::Mode { + let mut mode: protocols::absolute_pointer::Mode = Default::default(); + + if pointer_handler.supported_usages.contains(&Usage::from(super::GENERIC_DESKTOP_X)) { + mode.absolute_max_x = super::AXIS_RESOLUTION; + mode.absolute_min_x = 0; + } else { + debugln!(DEBUG_WARN, "No x-axis usages found in the report descriptor."); + } + + if pointer_handler.supported_usages.contains(&Usage::from(super::GENERIC_DESKTOP_Y)) { + mode.absolute_max_y = super::AXIS_RESOLUTION; + mode.absolute_min_y = 0; + } else { + debugln!(DEBUG_WARN, "No y-axis usages found in the report descriptor."); + } + + if (pointer_handler.supported_usages.contains(&Usage::from(super::GENERIC_DESKTOP_Z))) + || (pointer_handler.supported_usages.contains(&Usage::from(super::GENERIC_DESKTOP_WHEEL))) + { + mode.absolute_max_z = super::AXIS_RESOLUTION; + mode.absolute_min_z = 0; + //TODO: Z-axis is interpreted as pressure data. This is for compat with reference implementation in C, but + //could consider e.g. looking for actual digitizer tip pressure usages or something. + mode.attributes |= 0x02; + } else { + debugln!(DEBUG_INFO, "No z-axis usages found in the report descriptor."); + } + + let button_count = pointer_handler.supported_usages.iter().filter(|x| x.page() == super::BUTTON_PAGE).count(); + + if button_count > 1 { + mode.attributes |= 0x01; // alternate button exists. + } + + mode + } + + /// Uninstalls the absolute pointer protocol + pub fn uninstall( + boot_services: &'static dyn UefiBootServices, + agent: efi::Handle, + controller: efi::Handle, + ) -> Result<(), efi::Status> { + let mut absolute_pointer_ptr: *mut PointerContext = core::ptr::null_mut(); + let status = boot_services.open_protocol( + controller, + &protocols::absolute_pointer::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + core::ptr::addr_of_mut!(absolute_pointer_ptr) as *mut *mut c_void, + agent, + controller, + efi::OPEN_PROTOCOL_GET_PROTOCOL, + ); + if status.is_error() { + //No protocol is actually installed on this controller, so nothing to clean up. + return Ok(()); + } + + //Attempt to uninstall the absolute_pointer interface - this should disconnect any drivers using it and release + //the interface. + let status = boot_services.uninstall_protocol_interface( + controller, + &protocols::absolute_pointer::PROTOCOL_GUID as *const efi::Guid as *mut efi::Guid, + absolute_pointer_ptr as *mut c_void, + ); + if status.is_error() { + //An error here means some other driver might be holding on to the absolute_pointer_ptr. + //Mark the instance invalid by setting the pointer_handler raw pointer to null, but leak the PointerContext + //instance. Leaking context allows calls through the pointers on absolute_pointer_ptr to continue to resolve + //and return error based on observing pointer_handler is null. + debugln!(DEBUG_ERROR, "Failed to uninstall absolute_pointer interface, status: {:x?}", status); + + unsafe { + (*absolute_pointer_ptr).pointer_handler = core::ptr::null_mut(); + } + //return without tearing down the context. + return Err(status); + } + + let wait_for_input_event: efi::Handle = unsafe { (*absolute_pointer_ptr).absolute_pointer.wait_for_input }; + let status = boot_services.close_event(wait_for_input_event); + if status.is_error() { + //An error here means the event was not uninstalled, so in theory the notification_callback on it could still be + //fired. + //Mark the instance invalid by setting the pointer_handler raw pointer to null, but leak the PointerContext + //instance. Leaking context allows calls through the pointers on absolute_pointer_ptr to continue to resolve + //and return error based on observing pointer_handler is null. + debugln!(DEBUG_ERROR, "Failed to close absolute_pointer.wait_for_input event, status: {:x?}", status); + unsafe { + (*absolute_pointer_ptr).pointer_handler = core::ptr::null_mut(); + } + return Err(status); + } + + // None of the parts of absolute pointer are in use, so it is safe to reclaim it. + drop(unsafe { Box::from_raw(absolute_pointer_ptr) }); + Ok(()) + } + + // event handler for wait_for_pointer event that is part of the absolute pointer interface. + extern "efiapi" fn wait_for_pointer(event: efi::Event, context: *mut c_void) { + let pointer_ctx = unsafe { (context as *mut PointerContext).as_mut().expect("bad context") }; + // raise to notify to protect access to pointer_handler, and check if event should be signalled. + let old_tpl = pointer_ctx.boot_services.raise_tpl(efi::TPL_NOTIFY); + { + let pointer_handler = unsafe { pointer_ctx.pointer_handler.as_mut() }; + if let Some(pointer_handler) = pointer_handler { + if pointer_handler.state_changed { + pointer_ctx.boot_services.signal_event(event); + } + } else { + // implies that this API was invoked after pointer handler was dropped. + debugln!(DEBUG_ERROR, "absolute_pointer_reset invoked after pointer dropped."); + } + } + pointer_ctx.boot_services.restore_tpl(old_tpl); + } + + // resets the pointer state - part of the absolute pointer interface. + extern "efiapi" fn absolute_pointer_reset( + this: *mut protocols::absolute_pointer::Protocol, + _extended_verification: bool, + ) -> efi::Status { + if this.is_null() { + return efi::Status::INVALID_PARAMETER; + } + let pointer_ctx = unsafe { (this as *mut PointerContext).as_mut().expect("bad context") }; + let mut status = efi::Status::SUCCESS; + { + // raise to notify to protect access to pointer_handler and reset pointer handler state + let old_tpl = pointer_ctx.boot_services.raise_tpl(efi::TPL_NOTIFY); + + let pointer_handler = unsafe { pointer_ctx.pointer_handler.as_mut() }; + if let Some(pointer_handler) = pointer_handler { + pointer_handler.reset_state(); + } else { + // implies that this API was invoked after pointer handler was dropped. + debugln!(DEBUG_ERROR, "absolute_pointer_reset invoked after pointer dropped."); + status = efi::Status::DEVICE_ERROR; + } + + pointer_ctx.boot_services.restore_tpl(old_tpl); + } + status + } + + // returns the current pointer state in the `state` buffer provided by the caller - part of the absolute pointer + // interface. + extern "efiapi" fn absolute_pointer_get_state( + this: *mut protocols::absolute_pointer::Protocol, + state: *mut protocols::absolute_pointer::State, + ) -> efi::Status { + if state.is_null() || state.is_null() { + return efi::Status::INVALID_PARAMETER; + } + + let pointer_ctx = unsafe { (this as *mut PointerContext).as_mut().expect("bad context") }; + let mut status = efi::Status::SUCCESS; + { + // raise to notify to protect access to pointer_handler, and retrieve pointer handler state. + let old_tpl = pointer_ctx.boot_services.raise_tpl(efi::TPL_NOTIFY); + + let pointer_handler = unsafe { pointer_ctx.pointer_handler.as_mut() }; + if let Some(pointer_handler) = pointer_handler { + if pointer_handler.state_changed { + unsafe { + state.write(pointer_handler.current_state); + } + pointer_handler.state_changed = false; + } else { + status = efi::Status::NOT_READY; + } + } else { + // implies that this API was invoked after pointer handler was dropped. + debugln!(DEBUG_ERROR, "absolute_pointer_get_state invoked after pointer dropped."); + status = efi::Status::DEVICE_ERROR; + } + + pointer_ctx.boot_services.restore_tpl(old_tpl); + } + status + } +} + +#[cfg(test)] +mod test { + + use core::ffi::c_void; + + use r_efi::{efi, protocols}; + + use super::*; + + use crate::{ + boot_services::MockUefiBootServices, + hid_io::{HidReportReceiver, MockHidIo}, + pointer::CENTER, + }; + + static MOUSE_REPORT_DESCRIPTOR: &[u8] = &[ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x02, // USAGE (Mouse) + 0xa1, 0x01, // COLLECTION (Application) + 0x09, 0x01, // USAGE(Pointer) + 0xa1, 0x00, // COLLECTION (Physical) + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM(1) + 0x29, 0x05, // USAGE_MAXIMUM(5) + 0x15, 0x00, // LOGICAL_MINIMUM(0) + 0x25, 0x01, // LOGICAL_MAXIMUM(1) + 0x95, 0x05, // REPORT_COUNT(5) + 0x75, 0x01, // REPORT_SIZE(1) + 0x81, 0x02, // INPUT(Data, Variable, Absolute) + 0x95, 0x01, // REPORT_COUNT(1) + 0x75, 0x03, // REPORT_SIZE(3) + 0x81, 0x01, // INPUT(Constant, Array, Absolute) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x09, 0x38, // USAGE (Wheel) + 0x15, 0x81, // LOGICAL_MINIMUM (-127) + 0x25, 0x7f, // LOGICAL_MAXIMUM (127) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x03, // REPORT_COUNT (3) + 0x81, 0x06, // INPUT(Data, Variable, Relative) + 0xc0, // END_COLLECTION + 0xc0, // END_COLLECTION + ]; + + // In this module, the usage model for boot_services is global static, and so &'static dyn UefiBootServices is used + // throughout the API. For testing, each test will have a different set of expectations on the UefiBootServices mock + // object, and the mock object itself expects to be "mut", which makes it hard to handle as a single global static. + // Instead, raw pointers are used to simulate a MockUefiBootServices instance with 'static lifetime. + // This object needs to outlive anything that uses it - once created, it will live until the end of the program. + fn create_fake_static_boot_service() -> &'static mut MockUefiBootServices { + unsafe { Box::into_raw(Box::new(MockUefiBootServices::new())).as_mut().unwrap() } + } + + #[test] + fn wait_for_event_should_wait_for_event() { + let boot_services = create_fake_static_boot_service(); + const AGENT_HANDLE: efi::Handle = 0x01 as efi::Handle; + const CONTROLLER_HANDLE: efi::Handle = 0x02 as efi::Handle; + const POINTER_EVENT: efi::Event = 0x03 as efi::Event; + + static mut ABS_PTR_INTERFACE: *mut c_void = core::ptr::null_mut(); + static mut EVENT_CONTEXT: *mut c_void = core::ptr::null_mut(); + static mut EVENT_SIGNALED: bool = false; + + // expected on PointerHidHandler::initialize(). + boot_services.expect_create_event().returning(|_, _, wait_for_ptr, context, event_ptr| { + assert!(wait_for_ptr == Some(PointerContext::wait_for_pointer)); + assert_ne!(context, core::ptr::null_mut()); + unsafe { + EVENT_CONTEXT = context; + event_ptr.write(POINTER_EVENT); + } + efi::Status::SUCCESS + }); + + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + unsafe { ABS_PTR_INTERFACE = interface }; + efi::Status::SUCCESS + }); + + // expected on PointerHidHandler::drop(). + boot_services.expect_open_protocol().returning(|_, _, interface, _, _, _| { + unsafe { *interface = ABS_PTR_INTERFACE }; + efi::Status::SUCCESS + }); + boot_services.expect_uninstall_protocol_interface().returning(|_, _, _| efi::Status::SUCCESS); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + // expected on PointerHidHandler::receive_report + boot_services.expect_raise_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_NOTIFY); + efi::TPL_APPLICATION + }); + + boot_services.expect_restore_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_APPLICATION); + () + }); + + boot_services.expect_signal_event().returning(|event| { + assert_eq!(event, POINTER_EVENT); + unsafe { EVENT_SIGNALED = true }; + efi::Status::SUCCESS + }); + + let agent = AGENT_HANDLE; + let mut pointer_handler = PointerHidHandler::new(boot_services, agent); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&MOUSE_REPORT_DESCRIPTOR).unwrap())); + + let controller = CONTROLLER_HANDLE; + assert_eq!(pointer_handler.initialize(controller, &hid_io), Ok(())); + + let absolute_pointer = unsafe { (ABS_PTR_INTERFACE as *mut PointerContext).as_mut() }.unwrap(); + assert_eq!(absolute_pointer.absolute_pointer.wait_for_input, POINTER_EVENT); + + // no pointer state change - should not signal event. + PointerContext::wait_for_pointer(POINTER_EVENT, unsafe { EVENT_CONTEXT }); + + assert_eq!(unsafe { EVENT_SIGNALED }, false); + + //click two buttons and move the cursor (+32,+32) + let report: &[u8] = &[0x05, 0x20, 0x20, 0]; + pointer_handler.receive_report(report, &hid_io); + + // pointer state change in place - should signal event. + PointerContext::wait_for_pointer(POINTER_EVENT, unsafe { EVENT_CONTEXT }); + assert_eq!(unsafe { EVENT_SIGNALED }, true); + } + + #[test] + fn absolute_pointer_reset_should_reset_pointer_state() { + let boot_services = create_fake_static_boot_service(); + const AGENT_HANDLE: efi::Handle = 0x01 as efi::Handle; + const CONTROLLER_HANDLE: efi::Handle = 0x02 as efi::Handle; + const EVENT_HANDLE: efi::Handle = 0x03 as efi::Handle; + + static mut ABS_PTR_INTERFACE: *mut c_void = core::ptr::null_mut(); + static mut EVENT_CONTEXT: *mut c_void = core::ptr::null_mut(); + + // expected on PointerHidHandler::initialize(). + boot_services.expect_create_event().returning(|_, _, wait_for_ptr, context, event_ptr| { + assert!(wait_for_ptr == Some(PointerContext::wait_for_pointer)); + assert_ne!(context, core::ptr::null_mut()); + unsafe { + EVENT_CONTEXT = context; + event_ptr.write(EVENT_HANDLE); + } + efi::Status::SUCCESS + }); + + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + unsafe { ABS_PTR_INTERFACE = interface }; + efi::Status::SUCCESS + }); + + // expected on PointerHidHandler::drop(). + boot_services.expect_open_protocol().returning(|_, _, interface, _, _, _| { + unsafe { *interface = ABS_PTR_INTERFACE }; + efi::Status::SUCCESS + }); + boot_services.expect_uninstall_protocol_interface().returning(|_, _, _| efi::Status::SUCCESS); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + // expected on PointerHidHandler::receive_report + boot_services.expect_raise_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_NOTIFY); + efi::TPL_APPLICATION + }); + + boot_services.expect_restore_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_APPLICATION); + () + }); + + let agent = AGENT_HANDLE; + let mut pointer_handler = PointerHidHandler::new(boot_services, agent); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&MOUSE_REPORT_DESCRIPTOR).unwrap())); + + let controller = CONTROLLER_HANDLE; + assert_eq!(pointer_handler.initialize(controller, &hid_io), Ok(())); + + assert_eq!(pointer_handler.current_state.active_buttons, 0); + assert_eq!(pointer_handler.current_state.current_x, CENTER); + assert_eq!(pointer_handler.current_state.current_y, CENTER); + assert_eq!(pointer_handler.current_state.current_z, 0); + assert_eq!(pointer_handler.state_changed, false); + + //click two buttons and move the cursor (+32,+32,+32) + let report: &[u8] = &[0x05, 0x20, 0x20, 0x20]; + pointer_handler.receive_report(report, &hid_io); + + assert_eq!(pointer_handler.current_state.active_buttons, 0x5); + assert_eq!(pointer_handler.current_state.current_x, CENTER + 0x20); + assert_eq!(pointer_handler.current_state.current_y, CENTER + 0x20); + assert_eq!(pointer_handler.current_state.current_z, 0x20); + assert_eq!(pointer_handler.state_changed, true); + + //reset state + let status = PointerContext::absolute_pointer_reset( + unsafe { ABS_PTR_INTERFACE as *mut protocols::absolute_pointer::Protocol }, + false, + ); + assert_eq!(status, efi::Status::SUCCESS); + + assert_eq!(pointer_handler.current_state.active_buttons, 0); + assert_eq!(pointer_handler.current_state.current_x, CENTER); + assert_eq!(pointer_handler.current_state.current_y, CENTER); + assert_eq!(pointer_handler.current_state.current_z, 0); + assert_eq!(pointer_handler.state_changed, false); + } + + #[test] + fn absolute_pointer_get_state_should_return_current_state_and_clear_changed_flag() { + let boot_services = create_fake_static_boot_service(); + const AGENT_HANDLE: efi::Handle = 0x01 as efi::Handle; + const CONTROLLER_HANDLE: efi::Handle = 0x02 as efi::Handle; + const EVENT_HANDLE: efi::Handle = 0x03 as efi::Handle; + + static mut ABS_PTR_INTERFACE: *mut c_void = core::ptr::null_mut(); + static mut EVENT_CONTEXT: *mut c_void = core::ptr::null_mut(); + + // expected on PointerHidHandler::initialize(). + boot_services.expect_create_event().returning(|_, _, wait_for_ptr, context, event_ptr| { + assert!(wait_for_ptr == Some(PointerContext::wait_for_pointer)); + assert_ne!(context, core::ptr::null_mut()); + unsafe { + EVENT_CONTEXT = context; + event_ptr.write(EVENT_HANDLE); + } + efi::Status::SUCCESS + }); + + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + unsafe { ABS_PTR_INTERFACE = interface }; + efi::Status::SUCCESS + }); + + // expected on PointerHidHandler::drop(). + boot_services.expect_open_protocol().returning(|_, _, interface, _, _, _| { + unsafe { *interface = ABS_PTR_INTERFACE }; + efi::Status::SUCCESS + }); + boot_services.expect_uninstall_protocol_interface().returning(|_, _, _| efi::Status::SUCCESS); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + // expected on PointerHidHandler::receive_report + boot_services.expect_raise_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_NOTIFY); + efi::TPL_APPLICATION + }); + + boot_services.expect_restore_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_APPLICATION); + () + }); + + let agent = AGENT_HANDLE; + let mut pointer_handler = PointerHidHandler::new(boot_services, agent); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&MOUSE_REPORT_DESCRIPTOR).unwrap())); + + let controller = CONTROLLER_HANDLE; + assert_eq!(pointer_handler.initialize(controller, &hid_io), Ok(())); + + assert_eq!(pointer_handler.current_state.active_buttons, 0); + assert_eq!(pointer_handler.current_state.current_x, CENTER); + assert_eq!(pointer_handler.current_state.current_y, CENTER); + assert_eq!(pointer_handler.current_state.current_z, 0); + assert_eq!(pointer_handler.state_changed, false); + + //click two buttons and move the cursor (+32,+32,+32) + let report: &[u8] = &[0x05, 0x20, 0x20, 0x20]; + pointer_handler.receive_report(report, &hid_io); + + assert_eq!(pointer_handler.current_state.active_buttons, 0x5); + assert_eq!(pointer_handler.current_state.current_x, CENTER + 0x20); + assert_eq!(pointer_handler.current_state.current_y, CENTER + 0x20); + assert_eq!(pointer_handler.current_state.current_z, 0x20); + assert_eq!(pointer_handler.state_changed, true); + + let mut absolute_pointer_state: protocols::absolute_pointer::State = Default::default(); + let status = PointerContext::absolute_pointer_get_state( + unsafe { ABS_PTR_INTERFACE as *mut protocols::absolute_pointer::Protocol }, + &mut absolute_pointer_state as *mut protocols::absolute_pointer::State, + ); + assert_eq!(status, efi::Status::SUCCESS); + + assert_eq!(absolute_pointer_state.current_x, pointer_handler.current_state.current_x); + assert_eq!(absolute_pointer_state.current_y, pointer_handler.current_state.current_y); + assert_eq!(absolute_pointer_state.current_z, pointer_handler.current_state.current_z); + assert_eq!(absolute_pointer_state.active_buttons, pointer_handler.current_state.active_buttons); + assert_eq!(pointer_handler.state_changed, false); + + //if get_state is attempted when there are no changes to state, it should return NOT_READY. + let mut absolute_pointer_state: protocols::absolute_pointer::State = Default::default(); + let status = PointerContext::absolute_pointer_get_state( + unsafe { ABS_PTR_INTERFACE as *mut protocols::absolute_pointer::Protocol }, + &mut absolute_pointer_state as *mut protocols::absolute_pointer::State, + ); + assert_eq!(status, efi::Status::NOT_READY); + } +} diff --git a/HidPkg/UefiHidDxeV2/src/pointer/mod.rs b/HidPkg/UefiHidDxeV2/src/pointer/mod.rs new file mode 100644 index 0000000000..b8ca2309da --- /dev/null +++ b/HidPkg/UefiHidDxeV2/src/pointer/mod.rs @@ -0,0 +1,668 @@ +//! Provides Pointer HID support. +//! +//! This module handles the core logic for processing pointer input from HID +//! devices. +//! +//! ## License +//! +//! Copyright (C) Microsoft Corporation. All rights reserved. +//! +//! SPDX-License-Identifier: BSD-2-Clause-Patent +//! +mod absolute_pointer; + +use alloc::{ + collections::{BTreeMap, BTreeSet}, + vec::Vec, +}; +use hidparser::{ + report_data_types::{ReportId, Usage}, + ReportDescriptor, ReportField, VariableField, +}; +use r_efi::{efi, protocols}; + +use rust_advanced_logger_dxe::{debugln, function, DEBUG_ERROR}; + +use crate::{ + boot_services::UefiBootServices, + hid_io::{HidIo, HidReportReceiver}, +}; + +use self::absolute_pointer::PointerContext; + +// Usages supported by this module. +const GENERIC_DESKTOP_X: u32 = 0x00010030; +const GENERIC_DESKTOP_Y: u32 = 0x00010031; +const GENERIC_DESKTOP_Z: u32 = 0x00010032; +const GENERIC_DESKTOP_WHEEL: u32 = 0x00010038; +const BUTTON_PAGE: u16 = 0x0009; +const BUTTON_MIN: u32 = 0x00090001; +const BUTTON_MAX: u32 = 0x00090020; //Per spec, the Absolute Pointer protocol supports a 32-bit button state field. + +// number of points on the X/Y axis for this implementation. +const AXIS_RESOLUTION: u64 = 1024; +const CENTER: u64 = AXIS_RESOLUTION / 2; + +// Maps a given field to a routine that handles input from it. +#[derive(Debug, Clone)] +struct ReportFieldWithHandler { + field: VariableField, + report_handler: fn(&mut PointerHidHandler, field: VariableField, report: &[u8]), +} + +// Defines a report and the fields of interest within it. +#[derive(Debug, Default, Clone)] +struct PointerReportData { + report_id: Option, + report_size: usize, + relevant_fields: Vec, +} + +/// Pointer HID Handler +pub struct PointerHidHandler { + boot_services: &'static dyn UefiBootServices, + agent: efi::Handle, + controller: Option, + input_reports: BTreeMap, PointerReportData>, + supported_usages: BTreeSet, + report_id_present: bool, + state_changed: bool, + current_state: protocols::absolute_pointer::State, +} + +impl PointerHidHandler { + /// Instantiates a new Pointer HID handler. `agent` is the handle that owns the handler (typically image_handle). + pub fn new(boot_services: &'static dyn UefiBootServices, agent: efi::Handle) -> Self { + let mut handler = Self { + boot_services, + agent, + controller: None, + input_reports: BTreeMap::new(), + supported_usages: BTreeSet::new(), + report_id_present: false, + state_changed: false, + current_state: Default::default(), + }; + handler.reset_state(); + handler + } + + // Processes the report descriptor to determine whether this is a supported device, and if so, extract the information + // required to process reports. + fn process_descriptor(&mut self, descriptor: ReportDescriptor) -> Result<(), efi::Status> { + let multiple_reports = descriptor.input_reports.len() > 1; + + for report in &descriptor.input_reports { + let mut report_data = PointerReportData { report_id: report.report_id, ..Default::default() }; + + self.report_id_present = report.report_id.is_some(); + + if multiple_reports && !self.report_id_present { + //invalid to have None ReportId if multiple reports present. + Err(efi::Status::DEVICE_ERROR)?; + } + + report_data.report_size = report.size_in_bits.div_ceil(8); + + for field in &report.fields { + if let ReportField::Variable(field) = field { + match field.usage.into() { + GENERIC_DESKTOP_X => { + let field_handler = ReportFieldWithHandler { field: field.clone(), report_handler: Self::x_axis_handler }; + report_data.relevant_fields.push(field_handler); + self.supported_usages.insert(field.usage); + } + GENERIC_DESKTOP_Y => { + let field_handler = ReportFieldWithHandler { field: field.clone(), report_handler: Self::y_axis_handler }; + report_data.relevant_fields.push(field_handler); + self.supported_usages.insert(field.usage); + } + GENERIC_DESKTOP_Z | GENERIC_DESKTOP_WHEEL => { + let field_handler = ReportFieldWithHandler { field: field.clone(), report_handler: Self::z_axis_handler }; + report_data.relevant_fields.push(field_handler); + self.supported_usages.insert(field.usage); + } + BUTTON_MIN..=BUTTON_MAX => { + let field_handler = ReportFieldWithHandler { field: field.clone(), report_handler: Self::button_handler }; + report_data.relevant_fields.push(field_handler); + self.supported_usages.insert(field.usage); + } + _ => (), //other usages irrelevant + } + } + } + + if !report_data.relevant_fields.is_empty() { + self.input_reports.insert(report_data.report_id, report_data); + } + } + + if !self.input_reports.is_empty() { + Ok(()) + } else { + Err(efi::Status::UNSUPPORTED) + } + } + + // Helper routine that handles projecting relative and absolute axis reports onto the fixed + // absolute report axis that this driver produces. + fn resolve_axis(current_value: u64, field: VariableField, report: &[u8]) -> Option { + if field.attributes.relative { + //for relative, just update and clamp the current state. + let new_value = current_value as i64 + field.field_value(report)?; + Some(new_value.clamp(0, AXIS_RESOLUTION as i64) as u64) + } else { + //for absolute, project onto 0..AXIS_RESOLUTION + let mut new_value = field.field_value(report)?; + + //translate to zero. + new_value = new_value.checked_sub(i32::from(field.logical_minimum) as i64)?; + + //scale to AXIS_RESOLUTION + new_value = (new_value * AXIS_RESOLUTION as i64 * 1000) / (field.field_range()? as i64 * 1000); + + Some(new_value.clamp(0, AXIS_RESOLUTION as i64) as u64) + } + } + + // handles x_axis inputs + fn x_axis_handler(&mut self, field: VariableField, report: &[u8]) { + if let Some(x_value) = Self::resolve_axis(self.current_state.current_x, field, report) { + if self.current_state.current_x != x_value { + self.current_state.current_x = x_value; + self.state_changed = true; + } + } + } + + // handles y_axis inputs + fn y_axis_handler(&mut self, field: VariableField, report: &[u8]) { + if let Some(y_value) = Self::resolve_axis(self.current_state.current_y, field, report) { + if self.current_state.current_y != y_value { + self.current_state.current_y = y_value; + self.state_changed = true; + } + } + } + + // handles z_axis inputs + fn z_axis_handler(&mut self, field: VariableField, report: &[u8]) { + if let Some(z_value) = Self::resolve_axis(self.current_state.current_z, field, report) { + if self.current_state.current_z != z_value { + self.current_state.current_z = z_value; + self.state_changed = true; + } + } + } + + // handles button inputs + fn button_handler(&mut self, field: VariableField, report: &[u8]) { + let shift: u32 = field.usage.into(); + if !(BUTTON_MIN..=BUTTON_MAX).contains(&shift) { + return; + } + + if let Some(button_value) = field.field_value(report) { + let button_value = button_value as u32; + + let shift = shift - BUTTON_MIN; + if shift > u32::BITS { + return; + } + let button_value = button_value << shift; + + let new_buttons = self.current_state.active_buttons + & !(1 << shift) // zero the relevant bit in the button state field. + | button_value; // or in the current button state into that bit position. + + if new_buttons != self.current_state.active_buttons { + self.current_state.active_buttons = new_buttons; + self.state_changed = true; + } + } + } + + fn reset_state(&mut self) { + self.current_state = Default::default(); + // initialize pointer to center of screen + self.current_state.current_x = CENTER; + self.current_state.current_y = CENTER; + self.state_changed = false; + } +} + +impl HidReportReceiver for PointerHidHandler { + fn initialize(&mut self, controller: efi::Handle, hid_io: &dyn HidIo) -> Result<(), efi::Status> { + let descriptor = hid_io.get_report_descriptor()?; + self.process_descriptor(descriptor)?; + + PointerContext::install(self.boot_services, controller, self)?; + + self.controller = Some(controller); + + Ok(()) + } + fn receive_report(&mut self, report: &[u8], _hid_io: &dyn HidIo) { + let old_tpl = self.boot_services.raise_tpl(efi::TPL_NOTIFY); + + 'report_processing: { + if report.is_empty() { + break 'report_processing; + } + + // determine whether report includes report id byte and adjust the buffer as needed. + let (report_id, report) = match self.report_id_present { + true => (Some(ReportId::from(&report[0..1])), &report[1..]), + false => (None, &report[0..]), + }; + + if report.is_empty() { + break 'report_processing; + } + + if let Some(report_data) = self.input_reports.get(&report_id).cloned() { + if report.len() != report_data.report_size { + break 'report_processing; + } + + // hand the report data to the handler for each relevant field for field-specific processing. + for field in report_data.relevant_fields { + (field.report_handler)(self, field.field, report); + } + } + } + + self.boot_services.restore_tpl(old_tpl); + } +} + +impl Drop for PointerHidHandler { + fn drop(&mut self) { + if let Some(controller) = self.controller { + let status = PointerContext::uninstall(self.boot_services, self.agent, controller); + if status.is_err() { + debugln!(DEBUG_ERROR, "{:?}: Failed to uninstall absolute_pointer: {:?}", function!(), status); + } + } + } +} + +#[cfg(test)] +mod test { + use core::{cmp::min, ffi::c_void}; + + use crate::{ + boot_services::MockUefiBootServices, + hid_io::{HidReportReceiver, MockHidIo}, + pointer::{AXIS_RESOLUTION, CENTER}, + }; + use r_efi::efi; + + use super::PointerHidHandler; + + static MINIMAL_BOOT_KEYBOARD_REPORT_DESCRIPTOR: &[u8] = &[ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x06, // USAGE (Keyboard) + 0xa1, 0x01, // COLLECTION (Application) + 0x75, 0x01, // REPORT_SIZE (1) + 0x95, 0x08, // REPORT_COUNT (8) + 0x05, 0x07, // USAGE_PAGE (Key Codes) + 0x19, 0xE0, // USAGE_MINIMUM (224) + 0x29, 0xE7, // USAGE_MAXIMUM (231) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x81, 0x02, // INPUT (Data, Var, Abs) (Modifier Byte) + 0xc0, // END_COLLECTION + ]; + + static MOUSE_REPORT_DESCRIPTOR: &[u8] = &[ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x02, // USAGE (Mouse) + 0xa1, 0x01, // COLLECTION (Application) + 0x09, 0x01, // USAGE(Pointer) + 0xa1, 0x00, // COLLECTION (Physical) + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM(1) + 0x29, 0x05, // USAGE_MAXIMUM(5) + 0x15, 0x00, // LOGICAL_MINIMUM(0) + 0x25, 0x01, // LOGICAL_MAXIMUM(1) + 0x95, 0x05, // REPORT_COUNT(5) + 0x75, 0x01, // REPORT_SIZE(1) + 0x81, 0x02, // INPUT(Data, Variable, Absolute) + 0x95, 0x01, // REPORT_COUNT(1) + 0x75, 0x03, // REPORT_SIZE(3) + 0x81, 0x01, // INPUT(Constant, Array, Absolute) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x09, 0x38, // USAGE (Wheel) + 0x15, 0x81, // LOGICAL_MINIMUM (-127) + 0x25, 0x7f, // LOGICAL_MAXIMUM (127) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, 0x03, // REPORT_COUNT (3) + 0x81, 0x06, // INPUT(Data, Variable, Relative) + 0xc0, // END_COLLECTION + 0xc0, // END_COLLECTION + ]; + + static ABS_POINTER_REPORT_DESCRIPTOR: &[u8] = &[ + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x02, // USAGE (Mouse) + 0xa1, 0x01, // COLLECTION (Application) + 0x09, 0x01, // USAGE(Pointer) + 0xa1, 0x00, // COLLECTION (Physical) + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM(1) + 0x29, 0x05, // USAGE_MAXIMUM(5) + 0x15, 0x00, // LOGICAL_MINIMUM(0) + 0x25, 0x01, // LOGICAL_MAXIMUM(1) + 0x95, 0x05, // REPORT_COUNT(5) + 0x75, 0x01, // REPORT_SIZE(1) + 0x81, 0x02, // INPUT(Data, Variable, Absolute) + 0x95, 0x01, // REPORT_COUNT(1) + 0x75, 0x03, // REPORT_SIZE(3) + 0x81, 0x01, // INPUT(Constant, Array, Absolute) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x09, 0x38, // USAGE (Wheel) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x26, 0xff, 0x0f, // LOGICAL_MAXIMUM (4095) + 0x75, 0x10, // REPORT_SIZE (16) + 0x95, 0x03, // REPORT_COUNT (3) + 0x81, 0x02, // INPUT(Data, Variable, Absolute) + 0xc0, // END_COLLECTION + 0xc0, // END_COLLECTION + ]; + + // In this module, the usage model for boot_services is global static, and so &'static dyn UefiBootServices is used + // throughout the API. For testing, each test will have a different set of expectations on the UefiBootServices mock + // object, and the mock object itself expects to be "mut", which makes it hard to handle as a single global static. + // Instead, raw pointers are used to simulate a MockUefiBootServices instance with 'static lifetime. + // This object needs to outlive anything that uses it - once created, it will live until the end of the program. + fn create_fake_static_boot_service() -> &'static mut MockUefiBootServices { + unsafe { Box::into_raw(Box::new(MockUefiBootServices::new())).as_mut().unwrap() } + } + + #[test] + fn pointer_initialize_should_fail_if_report_descriptor_not_supported() { + let boot_services = create_fake_static_boot_service(); + + let agent = 0x1 as efi::Handle; + let mut pointer_handler = PointerHidHandler::new(boot_services, agent); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&MINIMAL_BOOT_KEYBOARD_REPORT_DESCRIPTOR).unwrap())); + + let controller = 0x2 as efi::Handle; + assert_eq!(pointer_handler.initialize(controller, &hid_io), Err(efi::Status::UNSUPPORTED)); + } + + #[test] + fn successful_pointer_initialize_should_install_protocol_and_drop_should_tear_it_down() { + let boot_services = create_fake_static_boot_service(); + static mut ABS_PTR_INTERFACE: *mut c_void = core::ptr::null_mut(); + + // expected on PointerHidHandler::initialize(). + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + unsafe { ABS_PTR_INTERFACE = interface }; + efi::Status::SUCCESS + }); + + // expected on PointerHidHandler::drop(). + boot_services.expect_open_protocol().returning(|_, _, interface, _, _, _| { + unsafe { *interface = ABS_PTR_INTERFACE }; + efi::Status::SUCCESS + }); + boot_services.expect_uninstall_protocol_interface().returning(|_, _, _| efi::Status::SUCCESS); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + let agent = 0x1 as efi::Handle; + let mut pointer_handler = PointerHidHandler::new(boot_services, agent); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&MOUSE_REPORT_DESCRIPTOR).unwrap())); + + let controller = 0x2 as efi::Handle; + assert_eq!(pointer_handler.initialize(controller, &hid_io), Ok(())); + + assert_ne!(unsafe { ABS_PTR_INTERFACE }, core::ptr::null_mut()); + } + + #[test] + fn receive_report_should_process_relative_reports() { + let boot_services = create_fake_static_boot_service(); + + static mut ABS_PTR_INTERFACE: *mut c_void = core::ptr::null_mut(); + + // expected on PointerHidHandler::initialize(). + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + unsafe { ABS_PTR_INTERFACE = interface }; + efi::Status::SUCCESS + }); + + // expected on PointerHidHandler::drop(). + boot_services.expect_open_protocol().returning(|_, _, interface, _, _, _| { + unsafe { *interface = ABS_PTR_INTERFACE }; + efi::Status::SUCCESS + }); + boot_services.expect_uninstall_protocol_interface().returning(|_, _, _| efi::Status::SUCCESS); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + // expected on PointerHidHandler::receive_report + boot_services.expect_raise_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_NOTIFY); + efi::TPL_APPLICATION + }); + + boot_services.expect_restore_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_APPLICATION); + () + }); + + let agent = 0x1 as efi::Handle; + let mut pointer_handler = PointerHidHandler::new(boot_services, agent); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&MOUSE_REPORT_DESCRIPTOR).unwrap())); + + let controller = 0x2 as efi::Handle; + assert_eq!(pointer_handler.initialize(controller, &hid_io), Ok(())); + + assert_eq!(pointer_handler.current_state.active_buttons, 0); + assert_eq!(pointer_handler.current_state.current_x, CENTER); + assert_eq!(pointer_handler.current_state.current_y, CENTER); + assert_eq!(pointer_handler.current_state.current_z, 0); + assert_eq!(pointer_handler.state_changed, false); + + //click two buttons and move the cursor (+32,+32) + let report: &[u8] = &[0x05, 0x20, 0x20, 0]; + pointer_handler.receive_report(report, &hid_io); + + assert_eq!(pointer_handler.current_state.active_buttons, 0x05); + assert_eq!(pointer_handler.current_state.current_x, CENTER + 32); + assert_eq!(pointer_handler.current_state.current_y, CENTER + 32); + assert_eq!(pointer_handler.current_state.current_z, 0); + assert_eq!(pointer_handler.state_changed, true); + + //un-click and move the cursor (+32,-16) and wheel(+32). + let report: &[u8] = &[0x00, 0x20, 0xF0, 0x20]; //0xF0 = -16. + pointer_handler.receive_report(report, &hid_io); + + assert_eq!(pointer_handler.current_state.active_buttons, 0); + assert_eq!(pointer_handler.current_state.current_x, CENTER + 64); + assert_eq!(pointer_handler.current_state.current_y, CENTER + 16); + assert_eq!(pointer_handler.current_state.current_z, 32); + + //un-click and move the cursor (+32,-32) and wheel(+32). + let report: &[u8] = &[0x00, 0x20, 0xE0, 0x20]; //0xE0 = -32. + pointer_handler.receive_report(report, &hid_io); + + assert_eq!(pointer_handler.current_state.active_buttons, 0); + assert_eq!(pointer_handler.current_state.current_x, CENTER + 96); + assert_eq!(pointer_handler.current_state.current_y, CENTER - 16); + assert_eq!(pointer_handler.current_state.current_z, 64); + + //move the cursor (0,127) until is past saturation, and check the value each time + let report: &[u8] = &[0x00, 0x00, 0x7F, 0x0]; //0x7F = +127. + let starting_y = pointer_handler.current_state.current_y; + for i in 0..AXIS_RESOLUTION { + //starts near the center, so moving it well past saturation point + pointer_handler.receive_report(report, &hid_io); + let expected_y = min(AXIS_RESOLUTION, starting_y.saturating_add((i + 1) * 127)); + assert_eq!(pointer_handler.current_state.current_y, expected_y); + } + + //move the cursor(0,-127) until it saturates at zero, and check the value each time. + let report: &[u8] = &[0x00, 0x00, 0x81, 0x0]; //0x80 = -127. + let starting_y = pointer_handler.current_state.current_y; + for i in 0..AXIS_RESOLUTION { + // starts at max, but moving 127 each time, so well past saturation point. + pointer_handler.receive_report(report, &hid_io); + let expected_y = starting_y.saturating_sub((i + 1) * 127); + assert_eq!(pointer_handler.current_state.current_y, expected_y); + } + } + + #[test] + fn receive_report_should_process_absolute_reports() { + let boot_services = create_fake_static_boot_service(); + static mut ABS_PTR_INTERFACE: *mut c_void = core::ptr::null_mut(); + + // expected on PointerHidHandler::initialize(). + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + unsafe { ABS_PTR_INTERFACE = interface }; + efi::Status::SUCCESS + }); + + // expected on PointerHidHandler::drop(). + boot_services.expect_open_protocol().returning(|_, _, interface, _, _, _| { + unsafe { *interface = ABS_PTR_INTERFACE }; + efi::Status::SUCCESS + }); + boot_services.expect_uninstall_protocol_interface().returning(|_, _, _| efi::Status::SUCCESS); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + // expected on PointerHidHandler::receive_report + boot_services.expect_raise_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_NOTIFY); + efi::TPL_APPLICATION + }); + + boot_services.expect_restore_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_APPLICATION); + () + }); + + let agent = 0x1 as efi::Handle; + let mut pointer_handler = PointerHidHandler::new(boot_services, agent); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&ABS_POINTER_REPORT_DESCRIPTOR).unwrap())); + + let controller = 0x2 as efi::Handle; + assert_eq!(pointer_handler.initialize(controller, &hid_io), Ok(())); + + assert_eq!(pointer_handler.current_state.active_buttons, 0); + assert_eq!(pointer_handler.current_state.current_x, CENTER); + assert_eq!(pointer_handler.current_state.current_y, CENTER); + assert_eq!(pointer_handler.current_state.current_z, 0); + assert_eq!(pointer_handler.state_changed, false); + + //click two buttons and move the cursor (1024, 1024). + let report: &[u8] = &[0x05, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00]; + pointer_handler.receive_report(report, &hid_io); + + assert_eq!(pointer_handler.current_state.active_buttons, 0x05); + // input x from range 0-4095 projected on to 0-1024 axis is (x/4095) * 1024. For x=1024, result is 256. + assert_eq!(pointer_handler.current_state.current_x, 256); + assert_eq!(pointer_handler.current_state.current_y, 256); + assert_eq!(pointer_handler.current_state.current_z, 0); + assert_eq!(pointer_handler.state_changed, true); + } + + #[test] + fn bad_reports_should_be_ignored() { + let boot_services = create_fake_static_boot_service(); + static mut ABS_PTR_INTERFACE: *mut c_void = core::ptr::null_mut(); + + // expected on PointerHidHandler::initialize(). + boot_services.expect_create_event().returning(|_, _, _, _, _| efi::Status::SUCCESS); + boot_services.expect_install_protocol_interface().returning(|_, _, _, interface| { + unsafe { ABS_PTR_INTERFACE = interface }; + efi::Status::SUCCESS + }); + + // expected on PointerHidHandler::drop(). + boot_services.expect_open_protocol().returning(|_, _, interface, _, _, _| { + unsafe { *interface = ABS_PTR_INTERFACE }; + efi::Status::SUCCESS + }); + boot_services.expect_uninstall_protocol_interface().returning(|_, _, _| efi::Status::SUCCESS); + boot_services.expect_close_event().returning(|_| efi::Status::SUCCESS); + + // expected on PointerHidHandler::receive_report + boot_services.expect_raise_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_NOTIFY); + efi::TPL_APPLICATION + }); + + boot_services.expect_restore_tpl().returning(|new_tpl| { + assert_eq!(new_tpl, efi::TPL_APPLICATION); + () + }); + + let agent = 0x1 as efi::Handle; + let mut pointer_handler = PointerHidHandler::new(boot_services, agent); + let mut hid_io = MockHidIo::new(); + hid_io + .expect_get_report_descriptor() + .returning(|| Ok(hidparser::parse_report_descriptor(&ABS_POINTER_REPORT_DESCRIPTOR).unwrap())); + + let controller = 0x2 as efi::Handle; + assert_eq!(pointer_handler.initialize(controller, &hid_io), Ok(())); + + assert_eq!(pointer_handler.current_state.active_buttons, 0); + assert_eq!(pointer_handler.current_state.current_x, CENTER); + assert_eq!(pointer_handler.current_state.current_y, CENTER); + assert_eq!(pointer_handler.current_state.current_z, 0); + assert_eq!(pointer_handler.state_changed, false); + + //move the cursor (4096, 4096, 0) - changed fields are out of range + let report: &[u8] = &[0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00]; + pointer_handler.receive_report(report, &hid_io); + + assert_eq!(pointer_handler.current_state.active_buttons, 0); + assert_eq!(pointer_handler.current_state.current_x, CENTER); + assert_eq!(pointer_handler.current_state.current_y, CENTER); + assert_eq!(pointer_handler.current_state.current_z, 0); + assert_eq!(pointer_handler.state_changed, false); + + //report too long + let report: &[u8] = &[0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10]; + pointer_handler.receive_report(report, &hid_io); + + assert_eq!(pointer_handler.current_state.active_buttons, 0); + assert_eq!(pointer_handler.current_state.current_x, CENTER); + assert_eq!(pointer_handler.current_state.current_y, CENTER); + assert_eq!(pointer_handler.current_state.current_z, 0); + assert_eq!(pointer_handler.state_changed, false); + + //report too short + let report: &[u8] = &[0x00, 0x10, 0x00, 0x10, 0x00, 0x10]; + pointer_handler.receive_report(report, &hid_io); + + assert_eq!(pointer_handler.current_state.active_buttons, 0); + assert_eq!(pointer_handler.current_state.current_x, CENTER); + assert_eq!(pointer_handler.current_state.current_y, CENTER); + assert_eq!(pointer_handler.current_state.current_z, 0); + assert_eq!(pointer_handler.state_changed, false); + } +}