From 802c67134d291c830830583c13d880704a4aafbc Mon Sep 17 00:00:00 2001 From: Sathisha S Date: Mon, 1 Jun 2026 16:45:41 +0000 Subject: [PATCH] Add UefiDump recipe and integrate EBBR profile dumping/validation - Add uefi-dump BitBake recipe to build UefiDump.efi from the local bbr-acs source tree. - Include UefiDump.efi in the woden-image and install via the uefi-apps recipe. - Invoke UefiDump.efi from debug_dump.nsh only for Devicetree images. - Enhance capsule_ondisk_reporting_vars_check.py to parse the EbbrProfileTable and set the test level to MANDATORY when (EBBR >= 2.2 or no profile) and the OsIndicationsSupported on-disk bit is set; otherwise mark RECOMMENDED. - Update SystemReady-devicetree-band/README.md. Signed-off-by: Sathisha S Change-Id: I1f14e5387ee6a0578197f8cf90c59822ba8846ac --- SystemReady-devicetree-band/README.md | 5 +- .../recipes-acs/uefi-dump/uefi-dump.bb | 28 +++ .../recipes-images/images/woden-image.bb | 6 + .../capsule_ondisk_reporting_vars_check.py | 216 +++++++++++++++--- common/uefi_scripts/debug_dump.nsh | 7 + 5 files changed, 229 insertions(+), 33 deletions(-) create mode 100644 SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/uefi-dump/uefi-dump.bb diff --git a/SystemReady-devicetree-band/README.md b/SystemReady-devicetree-band/README.md index cf1ffb73..f2c072ee 100644 --- a/SystemReady-devicetree-band/README.md +++ b/SystemReady-devicetree-band/README.md @@ -129,7 +129,8 @@ This image comprises of 2 FAT file system partition recognized by UEFI:
│   │   ├── CapsuleApp.efi │   │   ├── https_boot.nsh │   │   ├── ledge.efi -│   │   └── UpdateVars.efi +│   │   ├── UpdateVars.efi +│   │   └── UefiDump.efi │   ├── bbr │   │   ├── SCT │   │   ├── ScrtStartup.nsh @@ -174,7 +175,7 @@ This image comprises of 2 FAT file system partition recognized by UEFI:
- grub.cfg - grub config file - startup.nsh - uefi automation run startup file - acs_tests contains executable files and configs related for test suites - - app directory contains CapsuleApp.efi + - app directory contains CapsuleApp.efi and UefiDump.efi - app/capsule_update.nsh is uefi script for capsule update - bbr directory contains SCT related binaries and sequence files - bbsr-keys contains cryptographic keys for secure boot and testing secure firmware updates diff --git a/SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/uefi-dump/uefi-dump.bb b/SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/uefi-dump/uefi-dump.bb new file mode 100644 index 00000000..3f332ca5 --- /dev/null +++ b/SystemReady-devicetree-band/Yocto/meta-woden/recipes-acs/uefi-dump/uefi-dump.bb @@ -0,0 +1,28 @@ +SUMMARY = "UEFI Dump Application" +DESCRIPTION = "Fetch bbr-acs from GitHub and build UefiDump.efi from the fetched source" + +require recipes-acs/edk2-firmware/edk2-firmware-rev.bb + +PROVIDES:remove = "virtual/uefi-firmware" +PROVIDES:remove = "virtual/bootloader" + +LICENSE = "CLOSED" +COMPATIBLE_MACHINE:genericarm64 = "genericarm64" +COMPATIBLE_HOST = "aarch64.*-linux" + +EDK2_ARCH = "AARCH64" +EDK2_PLATFORM = "MdeModule" +EDK2_PLATFORM_DSC = "${WORKDIR}/bbr-acs/ebbr/uefi_app/UefiDump.dsc" + +SRC_URI += "git://github.com/ARM-software/bbr-acs;destsuffix=bbr-acs;protocol=https;branch=main;name=bbr-acs" +SRCREV_bbr-acs = "${AUTOREV}" + +BBR_ACS_DIR = "${WORKDIR}/bbr-acs" +PACKAGES_PATH .= ":${BBR_ACS_DIR}" + +EDK2_EXTRA_BUILD = "" + +do_install() { + install -d ${D}/firmware + install ${B}/Build/${EDK2_PLATFORM}/${EDK2_BUILD_MODE}_${EDK_COMPILER}/${EDK2_ARCH}/UefiDump.efi ${D}/firmware/ +} diff --git a/SystemReady-devicetree-band/Yocto/meta-woden/recipes-images/images/woden-image.bb b/SystemReady-devicetree-band/Yocto/meta-woden/recipes-images/images/woden-image.bb index 9f51ada7..f0fae05b 100644 --- a/SystemReady-devicetree-band/Yocto/meta-woden/recipes-images/images/woden-image.bb +++ b/SystemReady-devicetree-band/Yocto/meta-woden/recipes-images/images/woden-image.bb @@ -24,6 +24,7 @@ EXTRA_IMAGEDEPENDS += "bsa-acs \ uefi-apps \ ledge-efi \ update-vars \ + uefi-dump \ ebbr-sct \ bootfs-files \ pfdi-acs \ @@ -46,6 +47,7 @@ IMAGE_EFI_BOOT_FILES += "Bsa.efi;acs_tests/bsa/Bsa.efi \ bbsr_SctStartup.nsh;acs_tests/bbr/bbsr_SctStartup.nsh \ CapsuleApp.efi;acs_tests/app/CapsuleApp.efi \ UpdateVars.efi;acs_tests/app/UpdateVars.efi \ + UefiDump.efi;acs_tests/app/UefiDump.efi \ Shell.efi;EFI/BOOT/Shell.efi \ " SYSTEMREADY_COMMIT_LOG ?= "${TOPDIR}/../recipes-acs/bootfs-files/files/systemready-commit.log" @@ -78,6 +80,10 @@ do_sign_images() { echo "WARNING: ledge.efi not found for signing (skipping)" fi + if [ -f DO_SIGN/acs_tests/app/UefiDump.efi ]; then + sbsign --key $TEST_DB1_KEY --cert $TEST_DB1_CRT DO_SIGN/acs_tests/app/UefiDump.efi --output DO_SIGN/acs_tests/app/UefiDump.efi + fi + echo "Signing images complete." wic cp DO_SIGN/EFI ${DEPLOY_DIR_IMAGE}/${IMAGE_LINK_NAME}.wic:1/ wic cp DO_SIGN/Image ${DEPLOY_DIR_IMAGE}/${IMAGE_LINK_NAME}.wic:1/ diff --git a/common/linux_scripts/capsule_ondisk_reporting_vars_check.py b/common/linux_scripts/capsule_ondisk_reporting_vars_check.py index 5fe5069e..1f7204f4 100755 --- a/common/linux_scripts/capsule_ondisk_reporting_vars_check.py +++ b/common/linux_scripts/capsule_ondisk_reporting_vars_check.py @@ -77,6 +77,9 @@ # Regular expression to match valid capsule variable names (CapsuleNNNN where NNNN is 4 hex digits) CAPSULE_NAME_RE = re.compile(r"^Capsule[0-9A-Fa-f]{4}$") +# Path where UEFI app writes the EBBR profile table hexdump (on host this is mounted under /mnt) +EBBR_PROFILE_TABLE_DUMP = "/mnt/acs_results_template/acs_results/uefi_dump/ebbr_profile_table.log" + def _ensure_log_dir(): """Ensure the log directory exists, creating it if necessary.""" try: @@ -101,17 +104,31 @@ def log(msg=""): except (OSError, IOError): pass -def status(ok: bool): +def debug(msg=""): + """ + Emit a debug message to both stdout and the log file. + + Args: + msg (str): Debug message to emit. + """ + print(msg) + log(msg) + +def result_status(ok: bool, test_level: str = "RECOMMENDED"): """ Convert a boolean test result to a status string. Args: ok (bool): True if test passed, False if failed + test_level (str): "MANDATORY" or "RECOMMENDED" Returns: - str: "PASSED" if ok is True, "WARNING" otherwise + str: "PASSED" if ok is True, "FAILED" for mandatory failures, + and "WARNING" for recommended failures. """ - return "PASSED" if ok else "WARNING" + if ok: + return "PASSED" + return "FAILED" if test_level == "MANDATORY" else "WARNING" def read_efi_var(var_name, guid=CapsuleReportGuid): """ @@ -178,7 +195,89 @@ def os_indications_supports_ondisk(): indications = struct.unpack(", Status - WARNING") + f"actual attributes Value:, Status - {result_status(False, test_level)}") return False ok = (actual == expected) log(f"RESULTS: {var_name} Variable Attribute Test: expected attributed value:0x{expected:X}, " - f"actual attributes Value:0x{actual:X}, Status - {status(ok)}") + f"actual attributes Value:0x{actual:X}, Status - {result_status(ok, test_level)}") return ok -def check_capsulemax(): +def check_capsulemax(test_level: str): """ Validate the CapsuleMax EFI variable. @@ -226,8 +325,8 @@ def check_capsulemax(): attrs, value = read_efi_var("CapsuleMax") if attrs is None: log("INFO: CapsuleMax Variable Test: CapsuleMax Variable - Not Found or Not Accessible") - log("RESULTS: CapsuleMax Variable Test: WARNING") - log_attr_test("CapsuleMax", EXPECTED_ATTR_CAPSULE_MAX, None) + log(f"RESULTS: CapsuleMax Variable Test: {result_status(False, test_level)}") + log_attr_test("CapsuleMax", EXPECTED_ATTR_CAPSULE_MAX, None, test_level) return False val = decode_char16_11_no_nul(value) @@ -236,12 +335,12 @@ def check_capsulemax(): ok_len = (len(value) >= 22) ok_fmt = bool(CAPSULE_NAME_RE.fullmatch(val)) ok_val = ok_len and ok_fmt - log(f"RESULTS: CapsuleMax Variable Test: {status(ok_val)}") + log(f"RESULTS: CapsuleMax Variable Test: {result_status(ok_val, test_level)}") - ok_attr = log_attr_test("CapsuleMax", EXPECTED_ATTR_CAPSULE_MAX, attrs) + ok_attr = log_attr_test("CapsuleMax", EXPECTED_ATTR_CAPSULE_MAX, attrs, test_level) return ok_val and ok_attr -def check_capsulelast(): +def check_capsulelast(test_level: str): """ Validate the CapsuleLast EFI variable. @@ -261,8 +360,8 @@ def check_capsulelast(): attrs, value = read_efi_var("CapsuleLast") if attrs is None: log("INFO: CapsuleLast Variable Test: CapsuleLast Variable - Not Found or Not Accessible") - log("RESULTS: CapsuleLast Variable Test: WARNING") - log_attr_test("CapsuleLast", EXPECTED_ATTR_CAPSULE_LAST, None) + log(f"RESULTS: CapsuleLast Variable Test: {result_status(False, test_level)}") + log_attr_test("CapsuleLast", EXPECTED_ATTR_CAPSULE_LAST, None, test_level) return False val = decode_char16_11_no_nul(value) @@ -271,12 +370,12 @@ def check_capsulelast(): ok_len = (len(value) >= 22) ok_fmt = bool(CAPSULE_NAME_RE.fullmatch(val)) ok_val = ok_len and ok_fmt - log(f"RESULTS: CapsuleLast Variable Test: {status(ok_val)}") + log(f"RESULTS: CapsuleLast Variable Test: {result_status(ok_val, test_level)}") - ok_attr = log_attr_test("CapsuleLast", EXPECTED_ATTR_CAPSULE_LAST, attrs) + ok_attr = log_attr_test("CapsuleLast", EXPECTED_ATTR_CAPSULE_LAST, attrs, test_level) return ok_val and ok_attr -def check_capsule_nnnn(): +def check_capsule_nnnn(test_level: str): """ Validate all CapsuleNNNN EFI variables in the Capsule Report GUID namespace. @@ -292,19 +391,19 @@ def check_capsule_nnnn(): The check iterates through all variables in the Capsule Report GUID namespace, filters for those matching the CapsuleNNNN pattern, and validates each one. - If no CapsuleNNNN variables are found at all, the check passes (firmware may - not have processed any capsules yet). + If no CapsuleNNNN variables are found at all, the check fails so the overall + result reflects the missing reporting variables. Returns: - bool: True if all found CapsuleNNNN variables are valid, or if none are - found. False if any CapsuleNNNN variables fail validation. + bool: True if all found CapsuleNNNN variables are valid. + False if none are found or if any CapsuleNNNN variables fail validation. """ suffix = "-" + CapsuleReportGuid try: entries = os.listdir(EFIVAR_PATH) except OSError: - log("RESULTS: CapsuleNNNN Variable Test: efivarfs not accessible - WARNING") + log(f"RESULTS: CapsuleNNNN Variable Test: efivarfs not accessible - {result_status(False, test_level)}") return False any_failed = False @@ -326,20 +425,20 @@ def check_capsule_nnnn(): attrs, _value = read_efi_var(var) if attrs is None: log(f"INFO: CapsuleNNNN Variable Test: {var} - Not Accessible") - log(f"RESULTS: CapsuleNNNN Variable Test: {var} Variable Test: WARNING") - if not log_attr_test(var, EXPECTED_ATTR_CAPSULE_NNNN, None): + log(f"RESULTS: CapsuleNNNN Variable Test: {var} Variable Test: {result_status(False, test_level)}") + if not log_attr_test(var, EXPECTED_ATTR_CAPSULE_NNNN, None, test_level): any_failed = True continue log(f"INFO: CapsuleNNNN Variable Test: {var} - Found") log(f"RESULTS: CapsuleNNNN Variable Test: {var} Variable Test: PASSED") - if not log_attr_test(var, EXPECTED_ATTR_CAPSULE_NNNN, attrs): + if not log_attr_test(var, EXPECTED_ATTR_CAPSULE_NNNN, attrs, test_level): any_failed = True if not found_any: - log("RESULTS: CapsuleNNNN Variable Test: No CapsuleNNNN reporting variables found - WARNING") - return True + log(f"RESULTS: CapsuleNNNN Variable Test: No CapsuleNNNN reporting variables found - {result_status(False, test_level)}") + return False return not any_failed @@ -384,25 +483,80 @@ def main(): log("RESULTS: Overall Capsule On-Disk Update Reporting Variables Result: SKIPPED") return 2 - log("INFO: OsIndicationsSupported indicates capsule on-disk support; running recommended checks") + # Determine whether the test should be treated as MANDATORY or RECOMMENDED. + # Decision rule: + # - MANDATORY when the parsed table contains EBBR >= 2.2 + # - MANDATORY when a valid parsed table contains zero profiles + # - RECOMMENDED when the table is missing, invalid, or contains only non-EBBR profiles + parse_status, version, number_of_profiles, guids = parse_ebbr_profile_table() + log(f"INFO: EBBR profile table path: {EBBR_PROFILE_TABLE_DUMP}") + if parse_status == "missing": + log("INFO: EBBR profile table dump file not found") + elif parse_status == "parse_failed": + log("INFO: EBBR profile table dump could not be parsed") + else: + log(f"INFO: EBBR profile table Version: {version}") + log(f"INFO: EBBR profile table NumberOfProfiles: {number_of_profiles}") + # Known EBBR GUIDs encoded as raw 16-byte EFI_GUID in-memory layout (little-endian for Data1/2/3) + EBBR_2_1 = bytes.fromhex('353ce3ccac748740bce78b29b02eeb27') + EBBR_2_2 = bytes.fromhex('d4ee73900de5ee11b8b08b68da62fc80') + EBBR_2_3 = bytes.fromhex('77fc217724a7ef118eaaf7c9b194ba75') + EBBR_VERSION_BY_GUID = { + EBBR_2_1: (2, 1), + EBBR_2_2: (2, 2), + EBBR_2_3: (2, 3), + } + + log(f"INFO: Extracted {len(guids)} GUID(s) from EBBR profile table") + for index, guid in enumerate(guids): + log(f"INFO: EBBR profile table GUID[{index}]: {guid.hex()}") + version_tuple = EBBR_VERSION_BY_GUID.get(guid) + if version_tuple is not None: + log(f"INFO: EFI Conformance Profile Table indicates platform compliance with EBBR {version_tuple[0]}.{version_tuple[1]}") + + found_versions = [] + for guid in guids: + version_tuple = EBBR_VERSION_BY_GUID.get(guid) + if version_tuple is not None: + found_versions.append(version_tuple) + + has_ebbr = len(found_versions) > 0 + has_ge_2_2 = any(v >= (2, 2) for v in found_versions) + valid_zero_profiles = (parse_status == "ok" and number_of_profiles == 0) + has_only_non_ebbr_profiles = (parse_status == "ok" and number_of_profiles and not has_ebbr) + + if on_disk_supported and (has_ge_2_2 or valid_zero_profiles): + test_level = "MANDATORY" + else: + test_level = "RECOMMENDED" + + log(f"INFO: OsIndicationsSupported indicates capsule on-disk support; running {test_level} checks") + if parse_status == "missing": + log("INFO: Test level is RECOMMENDED because the EBBR profile table dump is missing") + elif parse_status == "parse_failed": + log("INFO: Test level is RECOMMENDED because the EBBR profile table dump is invalid") + elif has_only_non_ebbr_profiles: + log("INFO: Test level is RECOMMENDED because the EBBR profile table contains only non-EBBR profiles") failed = False - if not check_capsulemax(): + if not check_capsulemax(test_level): failed = True log() - if not check_capsulelast(): + if not check_capsulelast(test_level): failed = True log() - if not check_capsule_nnnn(): + if not check_capsule_nnnn(test_level): failed = True log() exit_code = 3 if failed else 0 if exit_code == 0: log("RESULTS: Overall Capsule On-Disk Update Reporting Variables Result: PASSED") + elif test_level == "MANDATORY": + log("RESULTS: Overall Capsule On-Disk Update Reporting Variables Result: FAILED") else: log("RESULTS: Overall Capsule On-Disk Update Reporting Variables Result: WARNING") return exit_code diff --git a/common/uefi_scripts/debug_dump.nsh b/common/uefi_scripts/debug_dump.nsh index 432e4abd..d87e3fd4 100644 --- a/common/uefi_scripts/debug_dump.nsh +++ b/common/uefi_scripts/debug_dump.nsh @@ -48,6 +48,13 @@ else bcfg boot dump -v > bcfg.log devtree > devtree.log ver > uefi_version.log + if exist FS%m:\yocto_image.flag then + if exist FS%m:\acs_tests\app\UefiDump.efi then + FS%m:\acs_tests\app\UefiDump.efi + else + echo "UefiDump.efi not present" + endif + endif dmem > dmem.log sermode > sermode.log mode > mode.log