From 733ba646d2f292940a12264b82c6b473de142daf Mon Sep 17 00:00:00 2001 From: Guanquan Tian Date: Tue, 1 Sep 2026 11:46:37 +0800 Subject: [PATCH 1/8] qcom: vendor qcom_capsule_tool for UEFI DTB capsule generation Adds qcom_capsule_tool (vendored as-is from cbsp-boot-utilities) as a plain Python package under debian.qcom/scripts, invoked via `python3 -m qcom_capsule_tool.cli `. The `create` subcommand runs the full pipeline -- SYSFW_VERSION.bin generation, FvUpdate.xml generation from a partitions.conf, firmware volume creation, JSON config update, and signed FMP capsule generation -- from a single command. FvUpdate.xml generation reads partition layout from a vendored qcom-ptool partitions.conf rather than fetching qcom-ptool over the network at build time, and marks any partitions passed via --update-partitions as Operation=UPDATE with all others left Operation=IGNORE. patch_capsule_cert transparently handles both plain-ELF and .xz-compressed uefi_dtbs/xbl_config images when patching in QcCapsuleRootCert. Vendored from: - qcom_capsule_tool: qualcomm/cbsp-boot-utilities @ 8a0f1deef97beae600910506bfba488976465828 - partitions.conf: qualcomm-linux/qcom-ptool @ fb8c99c308732eaaba427233029f33c5327beebf Signed-off-by: Guanquan Tian --- .../iq-x7181-evk/spinor/partitions.conf | 93 ++ .../scripts/qcom_capsule_tool/BinToHex.py | 47 + .../scripts/qcom_capsule_tool/FVCreation.py | 735 ++++++++++++ .../qcom_capsule_tool/FVCreation_header.py | 514 ++++++++ .../SYSFW_VERSION_program.py | 467 ++++++++ .../scripts/qcom_capsule_tool/UpdateFvXml.py | 331 ++++++ .../qcom_capsule_tool/UpdateJsonParameters.py | 276 +++++ .../qcom_capsule_tool/XmlFwEntryValidation.py | 765 ++++++++++++ .../scripts/qcom_capsule_tool/XmlParser.py | 212 ++++ .../scripts/qcom_capsule_tool/__init__.py | 4 + .../qcom_capsule_tool/capsule_creator.py | 199 ++++ debian.qcom/scripts/qcom_capsule_tool/cli.py | 122 ++ .../scripts/qcom_capsule_tool/fv_builder.py | 131 +++ .../qcom_capsule_tool/generate_capsule.py | 404 +++++++ .../qcom_capsule_tool/patch_capsule_cert.py | 1039 +++++++++++++++++ 15 files changed, 5339 insertions(+) create mode 100644 debian.qcom/qcom-ptool/platforms/iq-x7181-evk/spinor/partitions.conf create mode 100644 debian.qcom/scripts/qcom_capsule_tool/BinToHex.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/FVCreation.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/FVCreation_header.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/SYSFW_VERSION_program.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/UpdateFvXml.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/UpdateJsonParameters.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/XmlFwEntryValidation.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/XmlParser.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/__init__.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/capsule_creator.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/cli.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/fv_builder.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/generate_capsule.py create mode 100644 debian.qcom/scripts/qcom_capsule_tool/patch_capsule_cert.py diff --git a/debian.qcom/qcom-ptool/platforms/iq-x7181-evk/spinor/partitions.conf b/debian.qcom/qcom-ptool/platforms/iq-x7181-evk/spinor/partitions.conf new file mode 100644 index 0000000000000..52599bb653688 --- /dev/null +++ b/debian.qcom/qcom-ptool/platforms/iq-x7181-evk/spinor/partitions.conf @@ -0,0 +1,93 @@ +# Copyright (c) 2025 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear + +# select disk type emmc | nand | nvme | spinor | ufs Mandatory +# disk size in bytes Mandatory +# options if not explicitly provide + +--disk --type=spinor --size=67108864 --write-protect-boundary=0 --sector-size-in-bytes=4096 + +# per partition entry +# mandatory options: +# --lun (mandatory for UFS, emmc no need this) +# --name +# --size in bytes +# --type-guid +# optional options: (defaults used if not provided) +# --attributes 1000000000000004 +# --filename "" +# --readonly true +# --sparse false + +# This is physical partition 0 +--partition --name=cdt --size=4KB --type-guid=A19F205F-CCD8-4B6D-8F1E-2D9BC24CFFB1 +--partition --name=SD_MGR --size=528KB --type-guid=5E463172-D0AC-4DD4-91B8-CD3EE1281579 +--partition --name=VarStore --size=728KB --type-guid=165BD6BC-9250-4AC8-95A7-A93F4A440066 +--partition --name=QWESLICCACHE --size=36KB --type-guid=7DDC813A-E88A-491A-95DC-4811A869D313 +--partition --name=limits --size=4KB --type-guid=10A0C19C-516A-5444-5CE3-664C3226A794 +--partition --name=limits_HMAC --size=4KB --type-guid=D9307477-9E76-44C2-9BFB-27151CB08C39 +--partition --name=limits_BACKUP --size=4KB --type-guid=C8C25968-A2CD-4DC4-9E17-07FC0F0D99DA +--partition --name=limits_BACKUP_HMAC --size=4KB --type-guid=36E332BE-BE07-4190-AB98-75CAB2815179 +--partition --name=test_partion --size=208KB --type-guid=36CF32BE-BDD7-4190-ACE8-71532B285179 +--partition --name=Resiliency_Log --size=16KB --type-guid=3BB99F72-E524-4128-A815-7194CC190A3D +--partition --name=xbl_sc_test_mode --size=4KB --type-guid=91FDD2B9-8ED3-4176-BC42-260F2E34D04A +--partition --name=xbl_sc_logs --size=80KB --type-guid=F7EECB66-781A-439A-8955-70E12ED4A7A0 +--partition --name=recoveryinfo --size=4KB --type-guid=7374B391-291C-49FA-ABC2-0463AB5F713F --filename=zeros_1sector.bin +--partition --name=resilience_driver --size=8KB --type-guid=4A2864F7-CB02-49F2-BA75-485E49C00966 +--partition --name=RecoveryGPT --size=48KB --type-guid=452E8C3B-B67F-4C66-80D9-AD457F74CB0A +--partition --name=SECDATA --size=28KB --type-guid=76CFC7EF-039D-4E2C-B81E-4DD8C2CB2A93 +--partition --name=ddr_debug --size=1528KB --type-guid=2D58205E-BA35-4BF9-B6C1-C6FDC80A373B +--partition --name=ddr --size=80KB --type-guid=20A0C19C-286A-42FA-9CE7-F64C3226A794 +--partition --name=ddr_HMAC --size=4KB --type-guid=FFCD927B-74E1-4597-A863-D9AC803DF191 +--partition --name=ddr_BACKUP --size=80KB --type-guid=CB198EDD-9EBD-48A1-9BB6-BB3194BF2E51 +--partition --name=ddr_BACKUP_HMAC --size=4KB --type-guid=5CC13258-DE5A-479A-8ECA-6B349294A5E9 +--partition --name=APDP --size=64KB --type-guid=E6E98DA2-E22A-4D12-AB33-169E7DEAA507 +--partition --name=XBL_SC --size=2300KB --type-guid=DEA0BA2C-CBDD-4805-B4F9-F428251C3E98 --filename=xbl_s.melf +--partition --name=XBL_RAMDUMP --size=512KB --type-guid=0382F197-E41F-4E84-B18B-0B564AEAD875 --filename=XblRamdump.xz +--partition --name=XBL_CONFIG --size=400KB --type-guid=5A325AE4-4276-B66D-0ADD-3494DF27706A --filename=xbl_config.elf +--partition --name=UEFI --size=7168KB --type-guid=400FFDCD-22E0-47E7-9A23-F16ED9382388 --filename=uefi.elf +--partition --name=AOP --size=340KB --type-guid=D69E90A5-4CAB-0071-F6DF-AB977F141A7F --filename=aop.mbn +--partition --name=AOP_CONFIG --size=64KB --type-guid=3D12F234-C882-4B46-A20C-17D52C8FC03D --filename=aop_devcfg.mbn +--partition --name=TZ --size=8192KB --type-guid=A053AA7F-40B8-4B1C-BA08-2F68AC71A4F4 --filename=tz.mbn +--partition --name=DEVCFG --size=128KB --type-guid=F65D4B16-343D-4E25-AAFC-BE99B6556A6D --filename=devcfg_iot.mbn +--partition --name=HYP --size=2436KB --type-guid=E1A6A689-0C8D-4CC6-B4E8-55A4320FBD8A --filename=hypvm.mbn +--partition --name=QUP --size=96KB --type-guid=21D1219F-2ED1-4AB4-930A-41A16AE75F7F --filename=qupv3fw.elf +--partition --name=TZAPPS --size=320KB --type-guid=14D11C40-2A3D-4F97-882D-103A1EC09333 +--partition --name=MULTIIMGQTI --size=32KB --type-guid=846C6F05-EB46-4C0A-A1A3-3648EF3F9D0E +--partition --name=MULTIIMGOEM --size=32KB --type-guid=E126A436-757E-42D0-8D19-0F362F7A62B8 --filename=multi_image.mbn +--partition --name=CPUCP --size=952KB --type-guid=1E8615BD-6D8C-41AD-B3EA-50E8BF40E43F --filename=cpucp.elf +--partition --name=CPUCP_DTB --size=48KB --type-guid=6C6018BC-4FBE-40C5-B148-4BCB1DB23748 --filename=cpucp_dtbs.elf +--partition --name=SHRM --size=128KB --type-guid=CB74CA22-2F0D-4B82-A1D6-C4213F348D73 --filename=shrm.elf +--partition --name=ADSP_UEFI --size=1332KB --type-guid=F8DEAFAD-B9E5-4873-8C80-68B6B72699C7 --filename=adsp_lite.lzma +--partition --name=ADSP_UEFI_DTB --size=96KB --type-guid=7A0BB5BE-5451-4000-83DB-CC10E0C18401 --filename=adsp_dtbs.elf +--partition --name=ImageFv --size=136KB --type-guid=17911177-C9E6-4372-933C-804B678E666F --filename=imagefv.elf +--partition --name=APDP_BACKUP --size=64KB --type-guid=110F198D-8174-4193-9AF1-5DA94CDC59C9 +--partition --name=XBL_SC_BACKUP --size=2300KB --type-guid=7A3DF1A3-A31A-454D-BD78-DF259ED486BE --filename=xbl_s.melf +--partition --name=XBL_RAMDUMP_BACKUP --size=512KB --type-guid=FF608BF6-AEDF-4084-BEC5-C92AB4E4534D --filename=XblRamdump.xz +--partition --name=XBL_CONFIG_BACKUP --size=400KB --type-guid=F462E0EA-A20E-4B10-867A-2D4455366548 --filename=xbl_config.elf +--partition --name=UEFI_BACKUP --size=7168KB --type-guid=9F234B5B-0EFB-4313-8E4C-0AF1F605536B --filename=uefi.elf +--partition --name=AOP_BACKUP --size=340KB --type-guid=B8B27C4C-4B5B-8AB2-502F-A792B590A896 --filename=aop.mbn +--partition --name=AOP_CONFIG_BACKUP --size=64KB --type-guid=77036CD4-03D5-42BB-8ED1-37E5A88BAA34 --filename=aop_devcfg.mbn +--partition --name=TZ_BACKUP --size=8192KB --type-guid=C832EA16-8B0D-4398-A67B-EBB30EF98E7E --filename=tz.mbn +--partition --name=DEVCFG_BACKUP --size=128KB --type-guid=169534E7-7809-4240-9763-0BA5DC37B5FF --filename=devcfg_iot.mbn +--partition --name=HYP_BACKUP --size=2436KB --type-guid=CB45ECA0-504E-42BB-91BA-C9B3236F6A6E --filename=hypvm.mbn +--partition --name=QUP_BACKUP --size=96KB --type-guid=04BA8D53-5091-4958-9CA1-0FE0941D2CBC --filename=qupv3fw.elf +--partition --name=TZAPPS_BACKUP --size=320KB --type-guid=BE3719E5-48A7-4ABC-B494-304864D02148 +--partition --name=MULTIIMGQTI_BACKUP --size=32KB --type-guid=D30C8B21-DDD9-45B6-8DE0-3165D34395C9 +--partition --name=MULTIIMGOEM_BACKUP --size=32KB --type-guid=3E3E3ECD-C512-4F95-9144-6063826A8970 --filename=multi_image.mbn +--partition --name=CPUCP_BACKUP --size=952KB --type-guid=6C1111FB-5354-41DE-AC17-5B6E542BE836 --filename=cpucp.elf +--partition --name=CPUCP_DTB_BACKUP --size=48KB --type-guid=57213A2B-39DE-4184-97DE-645D37913E4E --filename=cpucp_dtbs.elf +--partition --name=SHRM_BACKUP --size=128KB --type-guid=39FD6C00-49EB-6BD1-6899-2FB849DD4F75 --filename=shrm.elf +--partition --name=ADSP_UEFI_BACKUP --size=1332KB --type-guid=28A84947-CE19-4A5E-802B-2D72F50D6AD8 --filename=adsp_lite.lzma +--partition --name=ADSP_UEFI_DTB_BACKUP --size=96KB --type-guid=34F255BA-35C4-4477-845F-1B7801210141 --filename=adsp_dtbs.elf +--partition --name=ImageFv_BACKUP --size=136KB --type-guid=920CFC3D-7285-4A47-9C1C-4A87590E0687 --filename=imagefv.elf +--partition --name=SYSFW_VERSION --size=4KB --type-guid=3C44F88B-1878-4C29-B122-EE78766442A7 +--partition --name=qweslicstore --size=256KB --type-guid=7BAB3C93-5F73-4D02-B8CB-5B9F899D29A8 +--partition --name=qweslicstore_BACKUP --size=256KB --type-guid=225AF6E5-C009-4B6F-A240-625D1510D1FF +--partition --name=emac --size=512KB --type-guid=e7e5eff9-d224-4eb3-8f0b-1d2a4be18665 +--partition --name=dtb --size=4096KB --type-guid=2A1A52FC-AA0B-401C-A808-5EA0F91068F8 --filename=dtb.bin +--partition --name=dtb_BACKUP --size=4096KB --type-guid=A166F11A-2B39-4FAA-B7E7-F8AA080D0587 --filename=dtb.bin +--partition --name=uefi_dtb --size=64KB --type-guid=C84D3B5E-EF34-4FA4-8118-30EAE18D3FA6 --filename=uefi_dtbs.xz +--partition --name=uefi_dtb_BACKUP --size=64KB --type-guid=5F7D760A-3EF5-4AA5-B915-69A4ECAAE662 --filename=uefi_dtbs.xz +--partition --name=uefisecapp --size=220KB --type-guid=BE8A7E08-1B7A-4CAE-993A-D5B7FB55B3C2 --filename=uefi_sec.mbn +--partition --name=uefisecapp_BACKUP --size=220KB --type-guid=538CBDBA-D4A4-4438-A466-D7B356FAC165 --filename=uefi_sec.mbn diff --git a/debian.qcom/scripts/qcom_capsule_tool/BinToHex.py b/debian.qcom/scripts/qcom_capsule_tool/BinToHex.py new file mode 100644 index 0000000000000..5bdd2db4c4fd5 --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/BinToHex.py @@ -0,0 +1,47 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2025 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + +import sys + + +def bin_to_hex(input_file, output_file): + try: + with open(input_file, "rb") as f: + binary_data = f.read() + except FileNotFoundError: + print(f"Error: The file {input_file} was not found.") + return + + # Calculate the size of the binary file and create the 32-bit header + file_size = len(binary_data) + header = f"{file_size:08x}" + + hex_chunks = [header] + for i in range(0, len(binary_data), 4): + chunk = binary_data[i : i + 4] + hex_chunk = "".join(f"{byte:02x}" for byte in chunk) + # Ensure each chunk is 8 hex digits. + hex_chunks.append(hex_chunk.zfill(8)) + + try: + with open(output_file, "w") as f: + f.write(" ".join(hex_chunks)) + except IOError: + print(f"Error: Could not write to file {output_file}.") + return + + print(f"Conversion successful! Hex data with header written to {output_file}") + + +def main(): + if len(sys.argv) != 3: + print("Usage: qcom-capsule-tool bin-to-hex ") + sys.exit(1) + + bin_to_hex(sys.argv[1], sys.argv[2]) + + +if __name__ == "__main__": + main() diff --git a/debian.qcom/scripts/qcom_capsule_tool/FVCreation.py b/debian.qcom/scripts/qcom_capsule_tool/FVCreation.py new file mode 100644 index 0000000000000..ec7e1bbe3b3bc --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/FVCreation.py @@ -0,0 +1,735 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + + +import binascii +import ctypes +import os +import re +import struct +import sys +import traceback +import uuid +from enum import Enum + +from . import fv_builder +from . import FVCreation_header as FVC_h +from . import XmlFwEntryValidation as XFEV +from . import XmlParser as xp + +print_logs = 0 + +TOOL_VERSION_STRING = "1.1" +SYS_FW_METADATA_HEADER_SIGNATURE1 = 0x2E1946FB +SYS_FW_METADATA_HEADER_SIGNATURE2 = 0x7F744D57 +FMP_PAYLOAD_HEADER_SIGNATURE = 0x3153534D +SYS_FW_METADATA_HEADER_REVISION_V3 = 0x3 +SYS_FW_METADATA_FILE = "Metadata.dat" +SYS_FW_VERSION_DATA_SIGNATURE = "SYSFWVER" +SYS_FW_VERSION_DATA_REVISION = "1.0" +SYS_FW_METADATA_HEADER_REVISION = 0x4 +SYS_FW_METADATA_HEADER_REVISION_GLYMUR = 0x5 +SYS_FW_METADATA_REVISION = 0x1 + + +class FV_TYPE(Enum): + UNKNOWN = None + SYS_FW = None + EC_FW = None + + +class QSYS_FW_VERSION_DATA(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ("Signature", ctypes.c_ulonglong), + ("Revision", ctypes.c_uint), + ("VersionDataSize", ctypes.c_uint), + ("VersionDataCrc32", ctypes.c_uint), + ("FwVersion", ctypes.c_uint), + ("LowestSupportedFwVersion", ctypes.c_uint), + ] + + def to_bytes(self): + try: + return bytes(bytearray(self)) + except Exception as e: + print(f"ERROR: Failure converting structure to byte array(error:{e})", e) + + @classmethod + def from_bytes(cls, byte_arr): + try: + version_data = cls() + ctypes.memmove( + ctypes.addressof(version_data), byte_arr, ctypes.sizeof(version_data) + ) + return version_data + + except Exception: + print(traceback.format_exc()) + return None + + +def remove_files(ls_files): + for s_file in ls_files: + try: + if os.path.exists(s_file): + os.remove(s_file) + except Exception as e: + print(f"Error deleting file {s_file}: {e}") + + +def get_dir_path(raw_fwentry, ls_search_paths): + + file_path = os.path.join(raw_fwentry.InputPath, raw_fwentry.InputBinary) + + if os.path.exists(file_path): + print(f"INFO: File {raw_fwentry.InputBinary} found at {raw_fwentry.InputPath}.") + return raw_fwentry.InputPath + + for s_path in ls_search_paths: + file_path = os.path.join(s_path, raw_fwentry.InputBinary) + if os.path.exists(file_path): + return s_path + + return None + + +def get_exe_name(ls_files, s_pattern): + for s_file in ls_files: + if s_pattern.lower() in s_file.lower(): + return s_file + return None + + +def get_file_name_only(s_file): + if "\\" in s_file: # it is a relative path + return s_file[s_file.rfind("\\") + 1 : s_file.rfind(".")] + else: # just a file name, remove extension and return + return s_file[: s_file.rfind(".")] + + +def Reflect(data_b, l_i): + + data_i = int(data_b) + reff_i = 0 + + for i in range(l_i): + if (data_i & 0x1) != 0: + reff_i = reff_i | int(1 << (int(l_i - 1) - i)) + data_i = data_i >> 1 + + return reff_i + + +def CalcCRC32_i(buffer_b, l_i): + k_i = 8 + MSB_i = 0 + gx_h = 0x04C11DB7 + regs_h = 0xFFFFFFFF + regsMask_h = 0xFFFFFFFF + regsMSB_i = 0 + + gx_i = int(gx_h) + regs_i = int(regs_h) + regsMask_i = int(regsMask_h) + + for i in range(l_i): + DataByte_b = buffer_b[i] + DataByte_i = int(DataByte_b) + DataByte_i = Reflect(DataByte_i, 8) + + for j in range(k_i): + MSB_i = DataByte_i >> (k_i - 1) + MSB_i = MSB_i & 1 + regsMSB_i = int(regs_i >> 31) & 1 + regs_i = regs_i << 1 + if (regsMSB_i ^ MSB_i) != 0: + regs_i = regs_i ^ gx_i + + regs_i = regs_i & regsMask_i + DataByte_i = DataByte_i << 1 + regs_i = regs_i & regsMask_i + + return Reflect(regs_i, 32) ^ int(0xFFFFFFFF) + + +def generate_fv(s_output_file_name, ls_ffs): + try: + fv_builder.write_fv(s_output_file_name, ls_ffs) + except (OSError, ValueError) as e: + print(f"ERROR: Failure creating FV file {s_output_file_name}: {e}") + return False + return True + + +def validate_sys_fw_ver_binary_file(fw_ver_binary_data): + b_return = True + s_revision = ["", ""] + temp_version_data_crc32 = 0 + + try: + s_revision = [None, None] + s_revision[1] = str(fw_ver_binary_data.Revision & 0x0000FFFF) + s_revision[0] = str(fw_ver_binary_data.Revision >> 16) + FwVerBinaryData_revision = s_revision[0] + "." + s_revision[1] + + if SYS_FW_VERSION_DATA_REVISION != FwVerBinaryData_revision: + print("Unexpected REVISION value found") + b_return = False + elif print_logs >= 1: + print("Expected REVISION value found") + + # Validating the signature of binary file + bytes_signature = struct.pack("= 1: + print("Expected SIGNATURE value found") + + # Validating CRC of FwVerBinaryData + temp_version_data_crc32 = fw_ver_binary_data.VersionDataCrc32 + fw_ver_binary_data.VersionDataCrc32 = 0 + + if temp_version_data_crc32 != CalcCRC32_i( + fw_ver_binary_data.to_bytes(), fw_ver_binary_data.VersionDataSize + ): + print("Unexpected VersionDataSize value found") + b_return = False + elif print_logs >= 1: + print("Expected VersionDataSize value found") + + fw_ver_binary_data.VersionDataCrc32 = temp_version_data_crc32 + + except Exception as e: + print(f"Encountered exception Message = {e}") + + return b_return + + +def calc_crc32(data, size): + return binascii.crc32(data[:size]) & 0xFFFFFFFF + + +def get_versions_from_sys_fw_ver_binary_file(s_fw_ver_binary_file, fw_ver_binary_data): + try: + with open(s_fw_ver_binary_file, "rb") as fs: + file_content = fs.read() + fw_ver_binary_data = QSYS_FW_VERSION_DATA() + fw_ver_binary_data = QSYS_FW_VERSION_DATA.from_bytes(file_content) + + if print_logs >= 2: + print() + print( + "get_versions_from_sys_fw_ver_binary_file :: fw_ver_binary_data.Signature: ", + fw_ver_binary_data.Signature, + ) + print( + "get_versions_from_sys_fw_ver_binary_file :: fw_ver_binary_data.Revision", + fw_ver_binary_data.Revision, + ) + print( + "get_versions_from_sys_fw_ver_binary_file :: fw_ver_binary_data.VersionDataSize", + fw_ver_binary_data.VersionDataSize, + ) + print( + "get_versions_from_sys_fw_ver_binary_file :: fw_ver_binary_data.VersionDataCrc32", + fw_ver_binary_data.VersionDataCrc32, + ) + print( + "get_versions_from_sys_fw_ver_binary_file :: fw_ver_binary_data.FwVersion", + fw_ver_binary_data.FwVersion, + ) + print( + "get_versions_from_sys_fw_ver_binary_file :: fw_ver_binary_data.LowestSupportedFwVersion", + fw_ver_binary_data.LowestSupportedFwVersion, + ) + print() + return fw_ver_binary_data + + except Exception: + print(traceback.format_exc()) + return False + + +def guid_to_string(guid): + fw_entry_UpdatePath_PartitionTypeGUID_uuid_bytes_obj = bytes(guid) + fw_entry_UpdatePath_PartitionTypeGUID__uuid_str = str( + uuid.UUID(bytes=fw_entry_UpdatePath_PartitionTypeGUID_uuid_bytes_obj) + ) + return fw_entry_UpdatePath_PartitionTypeGUID__uuid_str + + +def c_sharp_guid_format(guid_array): + new_guid_array = ( + guid_array.bytes[3:4] + + guid_array.bytes[2:3] + + guid_array.bytes[1:2] + + guid_array.bytes[0:1] + + guid_array.bytes[5:6] + + guid_array.bytes[4:5] + + guid_array.bytes[7:8] + + guid_array.bytes[6:7] + + guid_array.bytes[8:] + ) + return new_guid_array + + +def generate_sys_fw_meta_data_file( + fw_ver_binary_data, s_breaking_change_number, g_dynamic_var +): + + if fw_ver_binary_data.FwVersion < fw_ver_binary_data.LowestSupportedFwVersion: + print( + "ERROR: Lowest Firmware version value is greater than or equal to current firmware version value.\n" + ) + return False + + # Check Metadata.dat file exists, if yes delete it. + if os.path.exists(SYS_FW_METADATA_FILE): + try: + os.remove(SYS_FW_METADATA_FILE) + except Exception as e: + print(f"ERROR: Failure deleting metadata file(error: {e}).\n") + return False + + meta_data_header = FVC_h.QPAYLOAD_METADATA_HEADER() + fw_entry_meta_data_size = 0 + + meta_data_header.Signature1 = SYS_FW_METADATA_HEADER_SIGNATURE1 + meta_data_header.Signature2 = SYS_FW_METADATA_HEADER_SIGNATURE2 + + if g_dynamic_var.isGlymurMode: + # Use Glymur Payload header format whenever --glymur is passed, + # regardless of whether any FwEntry declares . + meta_data_header.Revision = SYS_FW_METADATA_HEADER_REVISION_GLYMUR + fw_entry_meta_data_size = ctypes.sizeof(FVC_h.QPAYLOAD_METADATA_FWENTRY_GLYMUR) + + elif g_dynamic_var.isMatchIdentifierInXML: + # Use V4 Payload header format if MatchIdentifier in XML. + meta_data_header.Revision = SYS_FW_METADATA_HEADER_REVISION + fw_entry_meta_data_size = sys.getsizeof(FVC_h.QPAYLOAD_METADATA_FWENTRY) + + else: + # Use V3 Payload header format if MatchIdentifier not in XML. + meta_data_header.Revision = SYS_FW_METADATA_HEADER_REVISION_V3 + fw_entry_meta_data_size = ( + ctypes.sizeof(FVC_h.QPAYLOAD_METADATA_FWENTRY) + - ctypes.sizeof(ctypes.c_uint32) + - ctypes.sizeof( + ctypes.c_char + * (2 * FVC_h.GlobalStaticVariable.MATCH_IDENTIFIER_NAME_MAX_SIZE) + ) + - ctypes.sizeof(ctypes.c_uint32) + - ctypes.sizeof(FVC_h.FWENTRY_ARVALIDATION) + ) + + meta_data_header_temp = FVC_h.QPAYLOAD_METADATA_HEADER() + meta_data_header.Size = len(meta_data_header_temp.to_bytes()) - ( + 4 * struct.calcsize("= 2: + print("%s file found" % (s_fw_ver_binary_file)) + + fw_ver_binary_data = get_versions_from_sys_fw_ver_binary_file( + s_fw_ver_binary_file, fw_ver_binary_data + ) + if not fw_ver_binary_data: + print("ERROR: Error parsing SYSFW_VERSION.BIN file.") + return False + elif print_logs >= 2: + print("%s parsed successfully" % (s_fw_ver_binary_file)) + + if not validate_sys_fw_ver_binary_file(fw_ver_binary_data): + print("ERROR: Wrong SYSFW_VERSION.BIN file is supplied") + return False + elif print_logs >= 2: + print("%s data validated successfully" % (s_fw_ver_binary_file)) + + # + # Parse input XML + # + if not xp.parse_input_xml( + s_xml_file_name, s_breaking_change_number, g_dynamic_var + ): + print("ERROR: Error parsing XML file.") + return False + elif print_logs >= 2: + print("XML file parsed with xp.parse_input_xml") + + # + # Validate raw fw entries + # + if not XFEV.fw_entry_list_validation_main(g_dynamic_var): + print("ERROR: Error validating XML file.") + return False + elif print_logs >= 2: + print("XML file validated with XFEV.fw_entry_list_validation_main") + + # + # Create metadata + # + if not generate_sys_fw_meta_data_file( + fw_ver_binary_data, s_breaking_change_number, g_dynamic_var + ): + print("ERROR: Failure creating metadata file. Aborting...\n") + return False + elif print_logs >= 2: + print("metadata file created with generate_sys_fw_meta_data_file") + + # + # Generate FFS file of each input binary + # + if not generate_sys_fw_ffs_list(ls_ffs, ls_paths, g_dynamic_var): + print("ERROR: Error Generating FFS files.") + return False + elif print_logs >= 2: + print("generated ffs file with generate_sys_fw_ffs_list") + + except Exception: + print(traceback.format_exc()) + + return True + + +def generate_sys_fw_ffs_list(ls_ffs, ls_paths, g_dynamic_var): + + try: + for raw_fwentry in g_dynamic_var.XmlRawFwEntryList: + if ( + raw_fwentry.Operation.lower() + == g_dynamic_var.dOperationTypeByValue[ + FVC_h.FWENTRY_OPERATION_TYPE.IGNORE + ].lower() + ): + continue + + s_file_name = raw_fwentry.InputBinary[: raw_fwentry.InputBinary.rfind(".")] + s_dir_path = get_dir_path(raw_fwentry, ls_paths) + + if s_dir_path is None: + print( + f"ERROR: File {raw_fwentry.InputBinary} cannot be found in any of the search paths.\n" + ) + return False + + # + # Handle FFS file naming to avoid overwriting + # + if s_file_name + ".ffs" in ls_ffs: + temp_str = ( + raw_fwentry.UpdatePath.PartitionName.lower() + .replace(s_file_name.lower(), "") + .strip("_") + ) + s_file_name = f"{s_file_name}_{temp_str}" + + print(f"INFO: Creating ffs file for {raw_fwentry.InputBinary}.") + + raw_fwentry_FileGuid_uuid_str = str( + uuid.UUID(bytes=bytes(raw_fwentry.FileGuid)) + ) + fv_builder.write_raw_ffs( + s_file_name + ".ffs", + raw_fwentry_FileGuid_uuid_str, + os.path.join(s_dir_path, raw_fwentry.InputBinary), + ) + + ls_ffs.append(s_file_name + ".ffs") + + s_file_name = SYS_FW_METADATA_FILE[: SYS_FW_METADATA_FILE.rfind(".")] + s_guid = FVC_h.GlobalStaticVariable.FILE_GUID_METADATA_GUID.strip("{}") + + print(f"INFO: Creating ffs file for {SYS_FW_METADATA_FILE}.") + fv_builder.write_raw_ffs(s_file_name + ".ffs", s_guid, SYS_FW_METADATA_FILE) + + ls_ffs.append(s_file_name + ".ffs") + + # + # Check if all ffs files are present and can be located + # + for s_file in ls_ffs: + if not os.path.exists(s_file): + print(f"ERROR: Failure locating {s_file} file to create FV.") + return False + + except Exception: + print(traceback.format_exc()) + + return True + + +def generate_ffs_for_ec_fw(ls_ffs, s_ec_fw_file_name): + s_file_name = "EC_FW" + s_guid = FVC_h.GlobalStaticVariable.EC_FW_FFS_FILE_GUID.strip("{}") + + try: + print(f"INFO: Creating FFS file for {s_ec_fw_file_name}.") + + fv_builder.write_raw_ffs(s_file_name + ".ffs", s_guid, s_ec_fw_file_name) + + ls_ffs.append(s_file_name + ".ffs") + + # + # Check if all ffs files are present and can be located + # + for s_file in ls_ffs: + if not os.path.exists(s_file): + print(f"ERROR: Failure locating {s_file} file to create FV.") + return False + + except Exception: + print(traceback.format_exc()) + + return True + + +def process_ec_fw_ffs_creation(s_ec_fw_file_name, ls_ffs): + try: + if not generate_ffs_for_ec_fw(ls_ffs, s_ec_fw_file_name): + print("Generating FFS file for EC FW failed.\n") + return False + except Exception: + print(traceback.format_exc()) + + return True + + +def print_help(): + print("<======== FvCreator.py Usage ======>\n") + print( + "[For System Firmware FV Creation] FVCreator.py -FvType SYS_FW ...\n" + ) + print( + "[For EC Device Firmware FV Creation] FVCreator.py -FvType EC_FW " + ) + return + + +class Arguments: + MINIMUM_ARGUMENT_COUNT = 4 + MAXIMUM_ARGUMENT_COUNT = 6 + + def __init__(self): + self.parameters = {} + + def ConstructConfData(self, args): + + self.parameters.clear() + splitter = re.compile(r"^-{1,2}|^/", re.IGNORECASE) + remover = re.compile(r"^['\"]?(.*?)['\"]?$", re.IGNORECASE) + parameter = None + + for txt in args: + parts = splitter.split(txt, maxsplit=2) + + if len(parts) == 1: + if parameter is not None: + if parameter not in self.parameters: + parts[0] = remover.sub(r"\1", parts[0]) + self.parameters[parameter] = parts[0] + parameter = None + + elif len(parts) == 2: + if parameter is not None: + if parameter not in self.parameters: + self.parameters[parameter] = "true" + parameter = parts[1] + + if parameter is not None: + if parameter not in self.parameters: + self.parameters[parameter] = "true" + + def __getitem__(self, Param): + return self.parameters.get(Param) + + +def The_Main(args): + + s_breaking_change_number = "0" + ls_ffs = [] + ls_paths = [] + g_dynamic_var = FVC_h.GlobalDynamicVariable() + fw_ver_binary_data = FVC_h.QSYS_FW_VERSION_DATA() + + # FFS/FV generation is native Python now; strip the obsolete flag so + # old invocations do not misparse it as a binary search path. + args = list(args) + for i, arg in enumerate(args): + if arg in ("--edk2-path", "-edk2path") and i + 1 < len(args): + print("WARNING: --edk2-path is deprecated and ignored.") + del args[i : i + 2] + break + + # Extract --glymur if provided; enables Glymur BinaryType/ARValidation XML support + for i, arg in enumerate(args): + if arg == "--glymur": + g_dynamic_var.isGlymurMode = True + del args[i] + break + + # fv_type = FV_TYPE.UNKNOWN + + # Skipping the re-creation of all executables + if len(args) == 1 and args[0].lower() == "-v": + print("Version: %s" % (TOOL_VERSION_STRING)) + return + + s_output_file_name = args[0] + + if args[1].lower() == "-FvType".lower(): + if args[2].lower() != "SYS_FW".lower() and args[2].lower() != "EC_FW".lower(): + print( + "Invalid FvType detected, expecting 'SYS_FW' or 'EC_FW' got %s" + % (args[2]) + ) + else: + fv_type = args[2].lower() + else: + print("Invalid arguments") + print_help() + + if fv_type.lower() == "SYS_FW".lower(): + s_xml_file_name = args[3] + s_fw_ver_binary_file = args[4] + + for i in range(5, len(args)): + temp_search_path = args[i].strip().strip('"') + + # To fix root directory entry. For example: If user passed just "C:" normalize to "C:\" + if ( + len(temp_search_path) == 2 + and temp_search_path[1] == ":" + and temp_search_path[0].isalpha() + ): + temp_search_path += "\\" + + ls_paths.append(temp_search_path) + + r = process_sys_fw_ffs_creation( + s_xml_file_name=s_xml_file_name, + s_fw_ver_binary_file=s_fw_ver_binary_file, + s_breaking_change_number=s_breaking_change_number, + fw_ver_binary_data=fw_ver_binary_data, + ls_ffs=ls_ffs, + ls_paths=ls_paths, + g_dynamic_var=g_dynamic_var, + ) + if not r: + print("process_sys_fw_ffs_creation failed") + + elif fv_type.lower() == "EC_FW".lower(): + s_ec_fw_file_name = args[3] + + r = process_ec_fw_ffs_creation( + s_ec_fw_file_name=s_ec_fw_file_name, + ls_ffs=ls_ffs, + ) + if not r: + print("process_ec_fw_ffs_creation failed") + + if not generate_fv(s_output_file_name, ls_ffs): + print("GenerateFV failed.\n") + return + else: + print("FV created successfully") + + # Remove only the intermediates this run created, never other + # files that happen to share an extension with them. + remove_files(ls_ffs + [SYS_FW_METADATA_FILE]) + + +def main(): + The_Main(args=sys.argv[1:]) + + +if __name__ == "__main__": + main() diff --git a/debian.qcom/scripts/qcom_capsule_tool/FVCreation_header.py b/debian.qcom/scripts/qcom_capsule_tool/FVCreation_header.py new file mode 100644 index 0000000000000..41019a9642a78 --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/FVCreation_header.py @@ -0,0 +1,514 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + + +import ctypes +import uuid +from collections import deque +from enum import IntEnum + + +class FWENTRY_OPERATION_TYPE(IntEnum): + IGNORE = 0x00000000 + UPDATE = 0x00000001 + MAX = 0x00000002 + + +class FWENTRY_OPERATION_PATH_TYPE(IntEnum): + SOURCE = 0x00000000 + DEST = 0x00000001 + BACKUP = 0x00000002 + MAX = 0x00000003 + + +class FWENTRY_UPDATE_TYPE(IntEnum): + PARTITION = 0x00000000 + FAT_FILE = 0x00000001 + DPP_QCOM = 0x00000002 + DPP_OEM = 0x00000003 + OPM_PRIV_KEY = 0x00000004 + FWCLASS_GUID = 0x00000005 + MAX = 0x00000006 + + +class FWENTRY_BACKUP_TYPE(IntEnum): + PARTITION = 0x00000000 + FAT_FILE = 0x00000001 + MAX = 0x00000002 + + +class FWENTRY_DISK_TYPE(IntEnum): + USER_DATA = 0x00000000 + BOOT1 = 0x00000001 + BOOT2 = 0x00000002 + RPMB = 0x00000003 + GPP1 = 0x00000004 + GPP2 = 0x00000005 + GPP3 = 0x00000006 + GPP4 = 0x00000007 + LUN0 = 0x00000008 + LUN1 = 0x00000009 + LUN2 = 0x0000000A + LUN3 = 0x0000000B + LUN4 = 0x0000000C + LUN5 = 0x0000000D + LUN6 = 0x0000000E + LUN7 = 0x0000000F + SPINOR = 0x00000010 + NVME = 0x00000011 + MAX = 0x00000012 + + +class FlashType(IntEnum): + EMMC = 0x00000000 + UFS = 0x00000001 + NORNVME = 0x00000002 + NORUFS = 0x00000003 + + +class FWENTRY_BINARY_TYPE(IntEnum): + RAW = 0x00000000 # For FwEntries which is RAW binary like XBL_SC + FATFS = 0x00000001 # For FwEntries which is Fat File System binary like PLAT + MAX = 0x00000003 + + +class GlobalStaticVariable: + PARTITION_NAME_MAX_SIZE = 36 + FILE_NAME_MAX_SIZE = 255 + DPP_NAME_MAX_SIZE = 255 + MATCH_IDENTIFIER_NAME_MAX_SIZE = 36 + AR_VALIDATION_TYPE_MAX_SIZE = 36 + MAX_IMAGE_COUNT = 2 + + FILE_GUID_SBL1 = "{0A85A45E-915F-49DB-8BD5-5337861F8082}" + FILE_GUID_SBL2 = "{E7BF4F3F-7DC9-40C0-9DF2-CE2EC5CEACEF}" + FILE_GUID_SBL3 = "{8BA7FEBE-AB44-411D-86E7-A6F2DE7E3F40}" + FILE_GUID_RPM = "{8A8BD280-F35E-48E1-A891-BF0BB855831E}" + FILE_GUID_TZ = "{497FBC93-5784-4B8C-8F01-2AF50FB19239}" + FILE_GUID_WINSECAPP = "{F7A7DF2A-A845-4C17-94B8-85FD9CD022D7}" + FILE_GUID_UEFI = "{31C5C241-D6CE-4E7A-B400-9C35571B2EA9}" + FILE_GUID_CSRT_ACPI = "{642F3381-0327-4E0D-A7C1-A2C7D2C45812}" + FILE_GUID_DSDT_AML = "{044AF707-CDE8-4D15-B811-594BDABEB1FD}" + FILE_GUID_FACP_ACPI = "{24FC010F-AA0F-4310-AC48-E259FBD07AB0}" + FILE_GUID_FACS_ACPI = "{E455D6FD-16A5-45D2-8E07-1F5C992ABE28}" + FILE_GUID_MADT_ACPI = "{CA6BECA3-CD6D-44F4-AFB2-65076B81AD54}" + FILE_GUID_TPM2_ACPI = "{D298A7FE-C5CC-4C54-9CBC-9A59FA47F3AB}" + FILE_GUID_BGRT_ACPI = "{8E230A44-9617-40BC-B18D-256795E55526}" + FILE_GUID_DBG2_ACPI = "{D8E02C2D-9310-47E6-8B92-C7A3564C488A}" + FILE_GUID_FPDT_ACPI = "{F760AEEB-B172-4522-AFB2-ECCDC523B598}" + FILE_GUID_logo1_ACPI = "{8AA7DEF2-4B2E-470C-BAE2-057CACA327DB}" + FILE_GUID_METADATA_GUID = "{C7340E65-0D5D-43D6-ABB7-39751D5EC8E7}" + FILE_GUID_OPM_PRIV_PROVISION = "{3998E865-A733-4812-97D7-4BC973EA3442}" + FILE_GUID_OPM_PUB_PROVISION = "{01620DA3-F273-4401-9821-1D0E5169D8DA}" + + PARTITION_TYPE_GUID_BOOT1_SBL1 = "{DEA0BA2C-CBDD-4805-B4F9-F428251C3E98}" + PARTITION_TYPE_GUID_BOOT1_SBL1_BAK = "{B2CCEF8E-81BD-4E27-A228-5F0DE08CA12C}" + PARTITION_TYPE_GUID_GPP_SBL2 = "{8C6B52AD-8A9E-4398-AD09-AE916E53AE2D}" + PARTITION_TYPE_GUID_GPP_SBL2_BAK = "{62E53DFC-81D1-47F3-8867-22BC5592605F}" + PARTITION_TYPE_GUID_GPP_SBL3 = "{05E044DF-92F1-4325-B69E-374A82E97D6E}" + PARTITION_TYPE_GUID_GPP_SBL3_BAK = "{7EBF5EA5-4218-44A5-8D57-BF1C749FE7F1}" + PARTITION_TYPE_GUID_GPP_UEFI = "{400FFDCD-22E0-47E7-9A23-F16ED9382388}" + PARTITION_TYPE_GUID_GPP_UEFI_BAK = "{E1D14C55-ED7E-464E-A340-E045E6D3108A}" + PARTITION_TYPE_GUID_GPP_RPM = "{098DF793-D712-413D-9D4E-89D711772228}" + PARTITION_TYPE_GUID_GPP_RPM_BAK = "{BF132EB0-FC69-4D84-A13C-52F333E63906}" + PARTITION_TYPE_GUID_GPP_TZ = "{A053AA7F-40B8-4B1C-BA08-2F68AC71A4F4}" + PARTITION_TYPE_GUID_GPP_TZ_BAK = "{FD78FB93-1037-4AAA-BC9C-37E716D27BEE}" + PARTITION_TYPE_GUID_GPP_WINSECAPP = "{69B4201F-A5AD-45EB-9F49-45B38CCDAEF5}" + PARTITION_TYPE_GUID_GPP_WINSECAPP_BAK = "{B8DB2AFE-A8D9-45B3-8661-C29F592B6E76}" + PARTITION_TYPE_GUID_GPP_SSD = "{2C86E742-745E-4FDD-BFD8-B6A7AC638772}" + PARTITION_TYPE_GUID_GPP_DPP = "{9992FD7D-EC66-4CBC-A337-0DA1D4C93F8F}" + PARTITION_TYPE_GUID_GPP_FAT16 = "{543C031A-4CB6-4897-BFFE-4B485768A8AD}" + PARTITION_TYPE_GUID_USER_ESP = "{C12A7328-F81F-11D2-BA4B-00A0C93EC93B}" + + PARTITION_NAME_SYSFW_VERSION = "SYSFW_VERSION" + + EC_FW_FFS_FILE_GUID = "{4DC8BBB0-D3F6-4407-B46B-4B729F606DC0}" + + +class FWENTRY_DEVICE_PATH(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ("DiskType", ctypes.c_uint32), + ( + "PartitionName", + ctypes.c_byte * (2 * GlobalStaticVariable.PARTITION_NAME_MAX_SIZE), + ), + ("PartitionTypeGUID", ctypes.c_byte * 16), + ("FileName", ctypes.c_byte * (2 * GlobalStaticVariable.FILE_NAME_MAX_SIZE)), + ] + + def __init__(self, pType=0): + self.DiskType = pType + self.PartitionName = ( + ctypes.c_byte * (2 * GlobalStaticVariable.PARTITION_NAME_MAX_SIZE) + )() + self.PartitionTypeGUID = (ctypes.c_byte * 16).from_buffer_copy( + uuid.UUID(int=0).bytes + ) + self.FileName = ( + ctypes.c_byte * (2 * GlobalStaticVariable.FILE_NAME_MAX_SIZE) + )() + + def copy_from(self, devPath): + self.DiskType = devPath.DiskType + ctypes.memmove( + self.PartitionName, devPath.PartitionName, ctypes.sizeof(self.PartitionName) + ) + self.PartitionTypeGUID = (ctypes.c_byte * 16).from_buffer_copy( + devPath.PartitionTypeGUID + ) + ctypes.memmove(self.FileName, devPath.FileName, ctypes.sizeof(self.FileName)) + + def equals(self, devPath): + base_partition_name = "".join(self.PartitionName) + base_file_name = "".join(self.FileName) + target_partition_name = "".join(devPath.PartitionName) + target_file_name = "".join(devPath.FileName) + return ( + self.DiskType == devPath.DiskType + and base_partition_name == target_partition_name + and self.PartitionTypeGUID[:] == devPath.PartitionTypeGUID[:] + and base_file_name == target_file_name + ) + + def to_bytes(self): + return bytes(self) + + +class XML_RAW_FWENTRY_DEVICE_PATH(ctypes.Structure): + _fields_ = [ + ("DiskType", ctypes.c_wchar_p), + ("PartitionName", ctypes.c_wchar_p), + ("PartitionTypeGUID", ctypes.c_wchar_p), + ("FileName", ctypes.c_wchar_p), + ] + + +class FWENTRY_IMAGE(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ("FileName", ctypes.c_byte * (2 * GlobalStaticVariable.FILE_NAME_MAX_SIZE)), + ( + "ARValidationType", + ctypes.c_byte * (2 * GlobalStaticVariable.AR_VALIDATION_TYPE_MAX_SIZE), + ), + ] + + def to_bytes(self): + return bytes(self) + + +class FWENTRY_ARVALIDATION(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ("Images", FWENTRY_IMAGE * GlobalStaticVariable.MAX_IMAGE_COUNT), + ("ImageCount", ctypes.c_int32), + ] + + def to_bytes(self): + return bytes(self) + + +class XML_RAW_FWENTRY_IMAGE: + def __init__(self, file_name=None, ar_validation_type=None): + self.FileName = file_name + self.ARValidationType = ar_validation_type + + +class XML_RAW_FWENTRY_ARVALIDATION: + def __init__(self, images=None): + self.Images = images # list of XML_RAW_FWENTRY_IMAGE + + +class QPAYLOAD_METADATA_FWENTRY(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ("FileGuid", ctypes.c_byte * 16), + ("Operation", ctypes.c_uint32), + ("UpdateType", ctypes.c_uint32), + ("BackupType", ctypes.c_uint32), + ("UpdatePath", FWENTRY_DEVICE_PATH), + ("BackupPath", FWENTRY_DEVICE_PATH), + ("Revision", ctypes.c_uint32), + ( + "MatchIdentifier", + ctypes.c_char * (2 * GlobalStaticVariable.MATCH_IDENTIFIER_NAME_MAX_SIZE), + ), + ("BinaryType", ctypes.c_uint32), + ("ARValidation", FWENTRY_ARVALIDATION), + ] + + def to_bytes(self): + return bytes(self) + + +class QPAYLOAD_METADATA_FWENTRY_GLYMUR(ctypes.Structure): + """Glymur (payload header V5) on-disk layout. Real Glymur/Mavros/Honu/ + Kaanapali firmware places Revision as the FIRST field of FWENTRY_METADATA, + unlike the legacy layout above where Revision sits after BackupPath. + Field set/sizes are otherwise identical to QPAYLOAD_METADATA_FWENTRY.""" + + _pack_ = 1 + _fields_ = [ + ("Revision", ctypes.c_uint32), + ("FileGuid", ctypes.c_byte * 16), + ("Operation", ctypes.c_uint32), + ("UpdateType", ctypes.c_uint32), + ("BackupType", ctypes.c_uint32), + ("UpdatePath", FWENTRY_DEVICE_PATH), + ("BackupPath", FWENTRY_DEVICE_PATH), + ( + "MatchIdentifier", + ctypes.c_char * (2 * GlobalStaticVariable.MATCH_IDENTIFIER_NAME_MAX_SIZE), + ), + ("BinaryType", ctypes.c_uint32), + ("ARValidation", FWENTRY_ARVALIDATION), + ] + + def to_bytes(self): + return bytes(self) + + @classmethod + def from_legacy(cls, fw_entry): + """Build a Glymur-ordered entry from a populated QPAYLOAD_METADATA_FWENTRY.""" + glymur_entry = cls() + glymur_entry.Revision = fw_entry.Revision + glymur_entry.FileGuid = fw_entry.FileGuid + glymur_entry.Operation = fw_entry.Operation + glymur_entry.UpdateType = fw_entry.UpdateType + glymur_entry.BackupType = fw_entry.BackupType + glymur_entry.UpdatePath.copy_from(fw_entry.UpdatePath) + glymur_entry.BackupPath.copy_from(fw_entry.BackupPath) + glymur_entry.MatchIdentifier = fw_entry.MatchIdentifier + glymur_entry.BinaryType = fw_entry.BinaryType + ctypes.memmove( + ctypes.byref(glymur_entry.ARValidation), + ctypes.byref(fw_entry.ARValidation), + ctypes.sizeof(FWENTRY_ARVALIDATION), + ) + return glymur_entry + + +class XML_RAW_FWENTRY(ctypes.Structure): + _fields_ = [ + ("FileGuid", ctypes.c_byte * 16), + ("InputBinary", ctypes.c_wchar_p), + ("InputPath", ctypes.c_wchar_p), + ("Operation", ctypes.c_wchar_p), + ("UpdateType", ctypes.c_wchar_p), + ("BackupType", ctypes.c_wchar_p), + ("UpdatePath", XML_RAW_FWENTRY_DEVICE_PATH), + ("BackupPath", XML_RAW_FWENTRY_DEVICE_PATH), + ("MatchIdentifier", ctypes.c_wchar_p), + ] + + # BinaryType/ARValidation are not part of the marshaled XML_RAW_FWENTRY + # layout (they are only ever mapped into QPAYLOAD_METADATA_FWENTRY's + # ctypes-typed fields), so they are kept as plain, un-marshaled defaults. + BinaryType = None + ARValidation = None + + +class QPAYLOAD_METADATA_HEADER(ctypes.Structure): + _fields_ = [ + ("Signature1", ctypes.c_uint32), + ("Signature2", ctypes.c_uint32), + ("Revision", ctypes.c_uint32), + ("Size", ctypes.c_uint32), + ("FirmwareVersion", ctypes.c_uint32), + ("LowestSupportedVersion", ctypes.c_uint32), + ("BreakingChangeNumber", ctypes.c_uint32), + ("Reserved1", ctypes.c_uint32), + ("Reserved2", ctypes.c_uint32), + ("EntryCount", ctypes.c_uint32), + ] + + def to_bytes(self): + return bytes(self) + + +class QSYS_FW_VERSION_DATA(ctypes.Structure): + _fields_ = [ + ("Signature", ctypes.c_uint64), + ("Revision", ctypes.c_uint32), + ("VersionDataSize", ctypes.c_uint32), + ("VersionDataCrc32", ctypes.c_uint32), + ("FwVersion", ctypes.c_uint32), + ("LowestSupportedFwVersion", ctypes.c_uint32), + ] + + def to_bytes(self): + return bytes(self) + + +class FV_TYPE(IntEnum): + UNKNOWN = 0 + SYS_FW = 1 + EC_FW = 2 + + +class GlobalDynamicVariable: + dFileGuidByDestFatFilePath = { + "\\ACPI\\CSRT.ACP": GlobalStaticVariable.FILE_GUID_CSRT_ACPI, + "\\ACPI\\TPM2.ACP": GlobalStaticVariable.FILE_GUID_TPM2_ACPI, + "\\ACPI\\BGRT.ACP": GlobalStaticVariable.FILE_GUID_BGRT_ACPI, + "\\LOGO1.BMP": GlobalStaticVariable.FILE_GUID_logo1_ACPI, + "\\ACPI\\DBG2.ACP": GlobalStaticVariable.FILE_GUID_DBG2_ACPI, + "\\ACPI\\DBGP.ACP": GlobalStaticVariable.FILE_GUID_DBG2_ACPI, + "\\ACPI\\DSDT.AML": GlobalStaticVariable.FILE_GUID_DSDT_AML, + "\\ACPI\\FACP.ACP": GlobalStaticVariable.FILE_GUID_FACP_ACPI, + "\\ACPI\\FACS.ACP": GlobalStaticVariable.FILE_GUID_FACS_ACPI, + "\\ACPI\\FPDT.ACP": GlobalStaticVariable.FILE_GUID_FPDT_ACPI, + "\\ACPI\\MADT.ACP": GlobalStaticVariable.FILE_GUID_MADT_ACPI, + } + + dFileGuidByDestDppItemFile = { + "OPM_PUB.PROVISION": GlobalStaticVariable.FILE_GUID_OPM_PUB_PROVISION, + "OPM_PRIV.PROVISION": GlobalStaticVariable.FILE_GUID_OPM_PRIV_PROVISION, + } + + dDiskTypeByString = { + "EMMC_PARTITION_USER_DATA": FWENTRY_DISK_TYPE.USER_DATA, + "EMMC_PARTITION_BOOT1": FWENTRY_DISK_TYPE.BOOT1, + "EMMC_PARTITION_BOOT2": FWENTRY_DISK_TYPE.BOOT2, + "EMMC_PARTITION_RPMB": FWENTRY_DISK_TYPE.RPMB, + "EMMC_PARTITION_GPP1": FWENTRY_DISK_TYPE.GPP1, + "EMMC_PARTITION_GPP2": FWENTRY_DISK_TYPE.GPP2, + "EMMC_PARTITION_GPP3": FWENTRY_DISK_TYPE.GPP3, + "EMMC_PARTITION_GPP4": FWENTRY_DISK_TYPE.GPP4, + "UFS_LUN0": FWENTRY_DISK_TYPE.LUN0, + "UFS_LUN1": FWENTRY_DISK_TYPE.LUN1, + "UFS_LUN2": FWENTRY_DISK_TYPE.LUN2, + "UFS_LUN3": FWENTRY_DISK_TYPE.LUN3, + "UFS_LUN4": FWENTRY_DISK_TYPE.LUN4, + "UFS_LUN5": FWENTRY_DISK_TYPE.LUN5, + "UFS_LUN6": FWENTRY_DISK_TYPE.LUN6, + "UFS_LUN7": FWENTRY_DISK_TYPE.LUN7, + "SPINOR": FWENTRY_DISK_TYPE.SPINOR, + "NVME": FWENTRY_DISK_TYPE.NVME, + } + + dDiskTypeByValue = { + FWENTRY_DISK_TYPE.USER_DATA: "EMMC_PARTITION_USER_DATA", + FWENTRY_DISK_TYPE.BOOT1: "EMMC_PARTITION_BOOT1", + FWENTRY_DISK_TYPE.BOOT2: "EMMC_PARTITION_BOOT2", + FWENTRY_DISK_TYPE.RPMB: "EMMC_PARTITION_RPMB", + FWENTRY_DISK_TYPE.GPP1: "EMMC_PARTITION_GPP1", + FWENTRY_DISK_TYPE.GPP2: "EMMC_PARTITION_GPP2", + FWENTRY_DISK_TYPE.GPP3: "EMMC_PARTITION_GPP3", + FWENTRY_DISK_TYPE.GPP4: "EMMC_PARTITION_GPP4", + FWENTRY_DISK_TYPE.LUN0: "UFS_LUN0", + FWENTRY_DISK_TYPE.LUN1: "UFS_LUN1", + FWENTRY_DISK_TYPE.LUN2: "UFS_LUN2", + FWENTRY_DISK_TYPE.LUN3: "UFS_LUN3", + FWENTRY_DISK_TYPE.LUN4: "UFS_LUN4", + FWENTRY_DISK_TYPE.LUN5: "UFS_LUN5", + FWENTRY_DISK_TYPE.LUN6: "UFS_LUN6", + FWENTRY_DISK_TYPE.LUN7: "UFS_LUN7", + FWENTRY_DISK_TYPE.SPINOR: "SPINOR", + FWENTRY_DISK_TYPE.NVME: "NVME", + } + + dFlashTypeByString = { + "EMMC": FlashType.EMMC, + "UFS": FlashType.UFS, + "NORNVME": FlashType.NORNVME, + "NORUFS": FlashType.NORUFS, + } + + dFlashTypeByValue = { + FlashType.EMMC: "EMMC", + FlashType.UFS: "UFS", + FlashType.NORNVME: "NORNVME", + FlashType.NORUFS: "NORUFS", + } + + dFlashTypeByDiskType = { + FWENTRY_DISK_TYPE.USER_DATA: [FlashType.EMMC], + FWENTRY_DISK_TYPE.BOOT1: [FlashType.EMMC], + FWENTRY_DISK_TYPE.BOOT2: [FlashType.EMMC], + FWENTRY_DISK_TYPE.RPMB: [FlashType.EMMC], + FWENTRY_DISK_TYPE.GPP1: [FlashType.EMMC], + FWENTRY_DISK_TYPE.GPP2: [FlashType.EMMC], + FWENTRY_DISK_TYPE.GPP3: [FlashType.EMMC], + FWENTRY_DISK_TYPE.GPP4: [FlashType.EMMC], + FWENTRY_DISK_TYPE.LUN0: [FlashType.UFS, FlashType.NORUFS], + FWENTRY_DISK_TYPE.LUN1: [FlashType.UFS], + FWENTRY_DISK_TYPE.LUN2: [FlashType.UFS], + FWENTRY_DISK_TYPE.LUN3: [FlashType.UFS], + FWENTRY_DISK_TYPE.LUN4: [FlashType.UFS], + FWENTRY_DISK_TYPE.LUN5: [FlashType.UFS], + FWENTRY_DISK_TYPE.LUN6: [FlashType.UFS], + FWENTRY_DISK_TYPE.LUN7: [FlashType.UFS], + FWENTRY_DISK_TYPE.SPINOR: [FlashType.NORNVME, FlashType.NORUFS], + FWENTRY_DISK_TYPE.NVME: [FlashType.NORNVME], + } + + dOperationTypeByString = { + "IGNORE": FWENTRY_OPERATION_TYPE.IGNORE, + "UPDATE": FWENTRY_OPERATION_TYPE.UPDATE, + } + + dOperationTypeByValue = { + FWENTRY_OPERATION_TYPE.IGNORE: "IGNORE", + FWENTRY_OPERATION_TYPE.UPDATE: "UPDATE", + } + + dOperationPathTypeByString = { + "SOURCE": FWENTRY_OPERATION_PATH_TYPE.SOURCE, + "DEST": FWENTRY_OPERATION_PATH_TYPE.DEST, + "BACKUP": FWENTRY_OPERATION_PATH_TYPE.BACKUP, + } + + dOperationPathTypeByValue = { + FWENTRY_OPERATION_PATH_TYPE.SOURCE: "SOURCE", + FWENTRY_OPERATION_PATH_TYPE.DEST: "DEST", + FWENTRY_OPERATION_PATH_TYPE.BACKUP: "BACKUP", + } + + dUpdateTypeByString = { + "UPDATE_PARTITION": FWENTRY_UPDATE_TYPE.PARTITION, + "UPDATE_FAT_FILE": FWENTRY_UPDATE_TYPE.FAT_FILE, + "UPDATE_DPP_QCOM": FWENTRY_UPDATE_TYPE.DPP_QCOM, + "UPDATE_DPP_OEM": FWENTRY_UPDATE_TYPE.DPP_OEM, + "UPDATE_OPM_PRIV_KEY": FWENTRY_UPDATE_TYPE.OPM_PRIV_KEY, + "UPDATE_FWCLASS_GUID": FWENTRY_UPDATE_TYPE.FWCLASS_GUID, + } + + dUpdateTypeByValue = { + FWENTRY_UPDATE_TYPE.PARTITION: "UPDATE_PARTITION", + FWENTRY_UPDATE_TYPE.FAT_FILE: "UPDATE_FAT_FILE", + FWENTRY_UPDATE_TYPE.DPP_QCOM: "UPDATE_DPP_QCOM", + FWENTRY_UPDATE_TYPE.DPP_OEM: "UPDATE_DPP_OEM", + FWENTRY_UPDATE_TYPE.OPM_PRIV_KEY: "UPDATE_OPM_PRIV_KEY", + FWENTRY_UPDATE_TYPE.FWCLASS_GUID: "UPDATE_FWCLASS_GUID", + } + + dBackupTypeByString = { + "BACKUP_PARTITION": FWENTRY_BACKUP_TYPE.PARTITION, + "BACKUP_FAT_FILE": FWENTRY_BACKUP_TYPE.FAT_FILE, + } + + dBackupTypeByValue = { + FWENTRY_BACKUP_TYPE.PARTITION: "BACKUP_PARTITION", + FWENTRY_BACKUP_TYPE.FAT_FILE: "BACKUP_FAT_FILE", + } + + dBinaryTypeByString = { + "RAW": FWENTRY_BINARY_TYPE.RAW, + "FATFS": FWENTRY_BINARY_TYPE.FATFS, + } + + dBinaryTypeByValue = { + FWENTRY_BINARY_TYPE.RAW: "RAW", + FWENTRY_BINARY_TYPE.FATFS: "FATFS", + } + + XmlRawFwEntryList: deque = deque() + QpayloadFwEntryList: deque = deque() + DeviceFlashType = None + isMatchIdentifierInXML = False + isBinaryTypeInXML = False + isGlymurMode = False diff --git a/debian.qcom/scripts/qcom_capsule_tool/SYSFW_VERSION_program.py b/debian.qcom/scripts/qcom_capsule_tool/SYSFW_VERSION_program.py new file mode 100644 index 0000000000000..45b74d1624a34 --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/SYSFW_VERSION_program.py @@ -0,0 +1,467 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + + +import ctypes +import os +import re +import sys +import traceback + +print_logs = 1 +sVersion = "1.0" +S_SIGNATURE = "SYSFWVER" +S_REVISION = "1.0" +QSYS_FW_VERSION_DATA_VERSIONDATACRC32 = 0 + + +class QSYS_FW_VERSION_DATA(ctypes.Structure): + _pack_ = 1 + _fields_ = [ + ("Signature", ctypes.c_ulonglong), + ("Revision", ctypes.c_uint), + ("VersionDataSize", ctypes.c_uint), + ("VersionDataCrc32", ctypes.c_uint), + ("FwVersion", ctypes.c_uint), + ("LowestSupportedFwVersion", ctypes.c_uint), + ] + + def to_bytes(self): + try: + return bytes(bytearray(self)) + except Exception as e: + print(f"ERROR: Failure converting structure to byte array(error:{e})", e) + + @classmethod + def from_bytes(cls, byte_arr): + try: + version_data = cls() + ctypes.memmove( + ctypes.addressof(version_data), byte_arr, ctypes.sizeof(version_data) + ) + + if print_logs >= 3: + print("\n\n") + print( + "from_bytes :: in version_data.VersionDataCrc32:", + version_data.VersionDataCrc32, + ) + print( + "from_bytes :: in version_data.Signature:", version_data.Signature + ) + print("from_bytes :: in version_data.Revision:", version_data.Revision) + print( + "from_bytes :: in version_data.FwVersion:", version_data.FwVersion + ) + print( + "from_bytes :: in version_data.LowestSupported:", + version_data.LowestSupportedFwVersion, + ) + print( + "from_bytes :: in version_data.VersionDataSize:", + version_data.VersionDataSize, + ) + + return version_data + + except Exception: + print(traceback.format_exc()) + return None + + @classmethod + def get_values(cls, byte_arr): + try: + version_data = cls() + new_data = {} + ctypes.memmove( + ctypes.addressof(version_data), byte_arr, ctypes.sizeof(version_data) + ) + + if print_logs >= 2: + print("\n\n") + print( + "get_values :: in version_data.VersionDataCrc32:", + version_data.VersionDataCrc32, + ) + print( + "get_values :: in version_data.Signature:", version_data.Signature + ) + print("get_values :: in version_data.Revision:", version_data.Revision) + print( + "get_values :: in version_data.FwVersion:", version_data.FwVersion + ) + print( + "get_values :: in version_data.LowestSupported:", + version_data.LowestSupportedFwVersion, + ) + print( + "get_values :: in version_data.VersionDataSize:", + version_data.VersionDataSize, + ) + + s_revision = [None, None] + s_revision[1] = str(version_data.Revision & 0x0000FFFF) + s_revision[0] = str(version_data.Revision >> 16) + new_data["Revision"] = s_revision[0] + "." + s_revision[1] + + if print_logs >= 2: + print("\n\n") + print("get_values :: new_data.Revision: ", new_data["Revision"]) + + signature_bytes = version_data.Signature.to_bytes(8, byteorder="little") + ascii_string = signature_bytes.decode("ascii") + + if print_logs >= 2: + print("\n\n") + print("get_values :: ascii_string: ", ascii_string) + + return version_data + + except Exception: + print(traceback.format_exc()) + return None + + +class Arguments: + MINIMUM_ARGUMENT_COUNT = 4 + MAXIMUM_ARGUMENT_COUNT = 6 + + def __init__(self): + self.parameters = {} + + def ConstructConfData(self, args): + self.parameters.clear() + splitter = re.compile(r"^-{1,2}|^/", re.IGNORECASE) + remover = re.compile(r"^['\"]?(.*?)['\"]?$", re.IGNORECASE) + parameter = None + + for txt in args: + parts = splitter.split(txt, maxsplit=2) + if len(parts) == 1: + if parameter is not None: + if parameter not in self.parameters: + parts[0] = remover.sub(r"\1", parts[0]) + self.parameters[parameter] = parts[0] + + parameter = None + + elif len(parts) == 2: + if parameter is not None: + if parameter not in self.parameters: + self.parameters[parameter] = "true" + + parameter = parts[1] + + if parameter is not None: + if parameter not in self.parameters: + self.parameters[parameter] = "true" + + if print_logs >= 3: + print("\n\n") + print("Arguments.ConstructConfData :: parameters : ", self.parameters) + + def __getitem__(self, Param): + return self.parameters.get(Param) + + +def Reflect(data_b, l_i): + data_i = int(data_b) + reff_i = 0 + + for i in range(l_i): + if (data_i & 0x1) != 0: + reff_i = reff_i | int(1 << (int(l_i - 1) - i)) + + data_i = data_i >> 1 + return reff_i + + +def CalcCRC32(buffer_b, l_i): + k_i = 8 + MSB_i = 0 + gx_h = 0x04C11DB7 + regs_h = 0xFFFFFFFF + regsMask_h = 0xFFFFFFFF + regsMSB_i = 0 + + for i in range(l_i): + DataByte_b = buffer_b[i] + DataByte_b = bytes(Reflect(DataByte_b, 8)) + + for j in range(k_i): + MSB = DataByte_b >> (k_i - 1) + MSB = MSB & 1 + regsMSB_i = int(regs_h >> 31) & 1 + regs_h = regs_h << 1 + if (regsMSB_i ^ MSB_i) != 0: + regs_h = regs_h ^ gx_h + + regs_h = regs_h & regsMask_h + DataByte_b = DataByte_b << 1 + regs_h = regs_h & regsMask_h + + return Reflect(regs_h, 32) + + +def CalcCRC32_i(buffer_b, l_i): + k_i = 8 + MSB_i = 0 + gx_h = 0x04C11DB7 + regs_h = 0xFFFFFFFF + regsMask_h = 0xFFFFFFFF + regsMSB_i = 0 + + gx_i = int(gx_h) + regs_i = int(regs_h) + regsMask_i = int(regsMask_h) + + for i in range(l_i): + DataByte_b = buffer_b[i] + DataByte_i = int(DataByte_b) + DataByte_i = Reflect(DataByte_i, 8) + + for j in range(k_i): + MSB_i = DataByte_i >> (k_i - 1) + MSB_i = MSB_i & 1 + regsMSB_i = int(regs_i >> 31) & 1 + regs_i = regs_i << 1 + if (regsMSB_i ^ MSB_i) != 0: + regs_i = regs_i ^ gx_i + + regs_i = regs_i & regsMask_i + DataByte_i = DataByte_i << 1 + regs_i = regs_i & regsMask_i + + return Reflect(regs_i, 32) ^ int(0xFFFFFFFF) + + +def generate_binary_file(args): + + FwVerBinaryData = QSYS_FW_VERSION_DATA() + FwVerBinaryData.VersionDataCrc32 = QSYS_FW_VERSION_DATA_VERSIONDATACRC32 + FwVerBinaryData.Signature = int.from_bytes(S_SIGNATURE.encode("ascii"), "little") + sRevisionArr = S_REVISION.split(".") + FwVerBinaryData.Revision = (int(sRevisionArr[0]) << 16) | int(sRevisionArr[1]) + + if args["FwVer"]: + if not re.match(r"^\d+\.\d+\.\d+\.\d+$", args["FwVer"]): + print("ERROR: Value to the parameter -FwVer is not specified") + return False + sFirmwareVersionArr = args["FwVer"].split(".") + FwVerBinaryData.FwVersion = (int(sFirmwareVersionArr[2]) << 16) | int( + sFirmwareVersionArr[3] + ) + else: + print("ERROR: Value to the parameter -FwVer is not specified") + return False + + if args["LFwVer"]: + if not re.match(r"^\d+\.\d+\.\d+\.\d+$", args["LFwVer"]): + print("ERROR: Value to the parameter -FwVer is not specified") + return False + sFirmwareLowVersionArr = args["LFwVer"].split(".") + FwVerBinaryData.LowestSupportedFwVersion = ( + int(sFirmwareLowVersionArr[2]) << 16 + ) | int(sFirmwareLowVersionArr[3]) + else: + print("ERROR: Value to the parameter -LFwVer is not specified") + return False + + if args["O"]: + OutputBinary = args["O"] + FileName = os.path.basename(OutputBinary) + else: + print("ERROR: Value to the parameter -o is not specified") + return False + + if os.path.exists(OutputBinary): + os.remove(OutputBinary) + + FwVerBinaryData.VersionDataSize = len(FwVerBinaryData.to_bytes()) + FwVerBinaryData.VersionDataCrc32 = CalcCRC32_i( + FwVerBinaryData.to_bytes(), FwVerBinaryData.VersionDataSize + ) + output_file_path = os.path.join(os.getcwd(), FileName) + + with open(output_file_path, "wb") as fw_file: + fw_file.write(FwVerBinaryData.to_bytes()) + + return True + + +def get_fw_version_hex(args): + + if args["GetFwVersionHex"]: + OutputBinary = args["GetFwVersionHex"] + FileName = os.path.basename(OutputBinary) + else: + print("ERROR: Value to the parameter -GetFwVersionHex is not specified") + return False + + FilePath = os.path.join(os.getcwd(), FileName) + + with open(FilePath, mode="rb") as file: + file_content = file.read() + + FwVerBinaryData = QSYS_FW_VERSION_DATA.from_bytes(file_content) + print(hex(FwVerBinaryData.FwVersion)) + + +def get_ls_version_hex(args): + + if args["GetLSFwVersionHex"]: + OutputBinary = args["GetLSFwVersionHex"] + FileName = os.path.basename(OutputBinary) + else: + print("ERROR: Value to the parameter -GetLSFwVersionHex is not specified") + return False + + FilePath = os.path.join(os.getcwd(), FileName) + + with open(FilePath, mode="rb") as file: + file_content = file.read() + + FwVerBinaryData = QSYS_FW_VERSION_DATA.from_bytes(file_content) + print(hex(FwVerBinaryData.LowestSupportedFwVersion)) + + +def print_bin_contents(args): + + if args["PrintAll"]: + OutputBinary = args["PrintAll"] + FileName = os.path.basename(OutputBinary) + else: + print("ERROR: Value to the parameter -PrintAll is not specified") + return False + + FilePath = os.path.join(os.getcwd(), FileName) + + with open(FilePath, mode="rb") as file: + file_content = file.read() + + FwVerBinaryData = QSYS_FW_VERSION_DATA.from_bytes(file_content) + + if print_logs >= 0: + print("\n") + print("Contents of the provided .bin file: ") + print("\tFwVerBinaryData.VersionDataCrc32:", FwVerBinaryData.VersionDataCrc32) + print("\tFwVerBinaryData.Signature:", FwVerBinaryData.Signature) + print("\tFwVerBinaryData.Revision:", FwVerBinaryData.Revision) + print("\tFwVerBinaryData.FwVersion:", FwVerBinaryData.FwVersion) + print( + "\tFwVerBinaryData.LowestSupported:", + FwVerBinaryData.LowestSupportedFwVersion, + ) + print("\tFwVerBinaryData.VersionDataSize:", FwVerBinaryData.VersionDataSize) + print("\n") + + FwVerBinaryData = QSYS_FW_VERSION_DATA.get_values(file_content) + + +def ViewBinaryFile(ConfigurationHelper): + + FwVerBinaryData = QSYS_FW_VERSION_DATA() + FwVerBinaryData.VersionDataSize = len(FwVerBinaryData.to_bytes()) + InputBinPath = None + + if "Gen" in ConfigurationHelper: + print("ViewBinaryFile :: ERROR: -Gen. -View are not allowed together") + return False + + if "View" in ConfigurationHelper: + if print_logs >= 2: + print( + "ViewBinaryFile :: ConfigurationHelper['View']: ", + ConfigurationHelper["View"], + ) + + if ConfigurationHelper["O"] is not None: + InputBinPath = ConfigurationHelper["O"] + + else: + print( + "ViewBinaryFile :: ERROR: Value to the parameter -View is not specified" + ) + return False + + if not os.path.exists(os.path.join(os.getcwd(), InputBinPath)): + print( + "ViewBinaryFile :: ERROR: Provided input file does not exist in given directory" + ) + return False + + with open(InputBinPath, mode="rb") as file: + fs = file.read() + + FwVerBinaryData = QSYS_FW_VERSION_DATA.from_bytes(fs) + + s_revision = [None, None] + s_revision[1] = str(FwVerBinaryData.Revision & 0x0000FFFF) + s_revision[0] = str(FwVerBinaryData.Revision >> 16) + FwVerBinaryData_revision = s_revision[0] + "." + s_revision[1] + + s_latest_version = [None, None] + s_latest_version[1] = str(FwVerBinaryData.FwVersion & 0x0000FFFF) + s_latest_version[0] = str(FwVerBinaryData.FwVersion >> 16) + FwVerBinaryData_FwVersion = s_latest_version[0] + "." + s_latest_version[1] + + s_lowest_version = [None, None] + s_lowest_version[1] = str(FwVerBinaryData.LowestSupportedFwVersion & 0x0000FFFF) + s_lowest_version[0] = str(FwVerBinaryData.LowestSupportedFwVersion >> 16) + FwVerBinaryData_LowestSupportedFwVersion = ( + s_lowest_version[0] + "." + s_lowest_version[1] + ) + + if print_logs >= 0: + print("\n") + print("Contents of the provided .bin file: ") + print("\tVersionDataCrc32:", FwVerBinaryData.VersionDataCrc32) + print("\tSignature:", FwVerBinaryData.Signature) + print("\tRevision(int):", FwVerBinaryData.Revision) + print("\tRevision:", FwVerBinaryData_revision) + print("\tFwVersion(int):", FwVerBinaryData.FwVersion) + print("\tFwVersion:", FwVerBinaryData_FwVersion) + print("\tLowestSupported(int):", FwVerBinaryData.LowestSupportedFwVersion) + print("\tLowestSupported:", FwVerBinaryData_LowestSupportedFwVersion) + print("\tVersionDataSize:", FwVerBinaryData.VersionDataSize) + print("\n") + + return True + + +def The_Main(args): + + ConfigurationHelper = Arguments() + ConfigurationHelper.ConstructConfData(args) + + if print_logs >= 2: + print("\n\n") + print( + "The_Main :: ConfigurationHelper.parameters: ", + ConfigurationHelper.parameters, + ) + + if "Gen" in ConfigurationHelper.parameters: + generate_binary_file(ConfigurationHelper.parameters) + + if "PrintAll" in ConfigurationHelper.parameters: + print_bin_contents(ConfigurationHelper.parameters) + + if "View" in ConfigurationHelper.parameters: + ViewBinaryFile(ConfigurationHelper.parameters) + + if "GetFwVersionHex" in ConfigurationHelper.parameters: + get_fw_version_hex(ConfigurationHelper.parameters) + + if "GetLSFwVersionHex" in ConfigurationHelper.parameters: + get_ls_version_hex(ConfigurationHelper.parameters) + + +def main(): + The_Main(args=sys.argv[1:]) + + +if __name__ == "__main__": + main() diff --git a/debian.qcom/scripts/qcom_capsule_tool/UpdateFvXml.py b/debian.qcom/scripts/qcom_capsule_tool/UpdateFvXml.py new file mode 100644 index 0000000000000..ce435e878ab00 --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/UpdateFvXml.py @@ -0,0 +1,331 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2025 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + +import argparse +import os +import re +import subprocess +import sys +from xml.dom import minidom + +REPO_URL = "https://github.com/qualcomm-linux/qcom-ptool.git" +REPO_DIR = "qcom-ptool" +DEFAULT_REPO_DIR = REPO_DIR + +SUPPORTED_PLATFORMS = { + "QCS6490": "qcs6490-rb3gen2", + "QCS9100": "qcs9100-ride-sx", + "QCS8300": "qcs8300-ride-sx", + "QCS615": "qcs615-adp-air", + "QRB2210": "qrb2210-rb1", + "CQ2390M": "shikra-evk", + "IQ-X7181": "iq-x7181-evk", + "IQ-X5121": "iq-x7181-evk", + "Kaanapali": "kaanapali-mtp", + "SM8750": "sm8750-mtp", + "Glymur": "glymur-crd", +} + + +def get_target_name(soc_name): + for platform, target in SUPPORTED_PLATFORMS.items(): + if soc_name == platform: + return target + + +def safe_clone(repo_dir): + if not os.path.exists(repo_dir): + try: + subprocess.run(["git", "clone", REPO_URL, repo_dir], check=True) + except subprocess.CalledProcessError as e: + print(f"Error cloning repo: {e}") + sys.exit(1) + + +def read_partitions_conf(partition_conf_path): + try: + with open(partition_conf_path, "r") as f: + return f.readlines() + except FileNotFoundError: + print(f"Error: {partition_conf_path} not found.") + sys.exit(1) + + +def detect_storage_type_from_conf(lines): + for line in lines: + if re.search(r"--type=ufs", line, re.IGNORECASE): + return "UFS" + elif re.search(r"--type=emmc", line, re.IGNORECASE): + return "EMMC" + elif re.search(r"--type=spinor", line, re.IGNORECASE): + return "SPINOR" + print("Error: Could not detect StorageType from partitions.conf.") + sys.exit(1) + + +def parse_partition_info(args, lines, storage_type): + partition_info = {} + # Unified regex to handle both UFS and eMMC partition lines. + pattern = re.compile( + r"--partition\s+" + r"(?:--lun=(?P\d+)\s+)?" + r"--name=(?P[\w\-]+)\s+" + r"--size=\d+KB\s+" + r"--type-guid=(?P[\w\-]+)" + r"(?:\s+--type=(?Pufs|emmc|spinor))?" + r"(?:\s+--filename=(?P[\w.\-]+))?", + re.IGNORECASE, + ) + for line in lines: + match = pattern.search(line) + if not match: + continue + gd = match.groupdict() + lun = gd.get("lun") + name = gd["name"] + guid = gd["guid"] + filename = gd.get("filename") + if not filename: + # No --filename means this partition has no firmware image to + # update (e.g. TZAPPS, APDP, dtb_a/dtb_b) — skip it rather than + # fabricating a ".img" that doesn't exist. + continue + + # Only UFS partition lines carry a LUN; eMMC and SPINOR never do. + want_lun = storage_type == "UFS" + + if want_lun: + # For UFS we accept LUN 1, 2, or 4. LUN2 is included because + # some targets place the _b backup partition on a different + # LUN than its _a primary (e.g. _a on LUN1, _b on LUN2); targets + # that keep both _a/_b on the same LUN are unaffected. + if lun not in ["1", "2", "4"]: + continue + entry = {"lun": lun, "guid": guid, "filename": filename} + else: + # For eMMC/SPINOR we ignore entries that contain a LUN + if lun is not None: + continue + entry = {"guid": guid, "filename": filename} + + if want_lun and name in partition_info: + # Some conf files list the same partition name under more than + # one LUN section - not just _a/_b names, since a file can mix + # naming conventions (e.g. _a/_b for some partitions, / + # _BACKUP for others). LUN1 always wins; if a name has no + # LUN1 entry, fall back to whichever other LUN carries it (e.g. + # LUN2). Once a LUN1 entry is recorded, later duplicates never + # replace it; a LUN1 entry seen later always takes over an + # earlier non-LUN1 fallback. + existing_lun = partition_info[name]["lun"] + if existing_lun == "1": + continue + if lun != "1": + continue + + partition_info[name] = entry + return partition_info + + +def find_base_names(partition_info): + """Detect primary/backup partition pairs. + + Two naming conventions are used across targets: `_a`/`_b` + (e.g. UFS targets like Kaanapali) and ``/`_BACKUP` (e.g. + SPINOR targets like Hamoa/Purwa). Returns a list of + (base, primary_name, backup_name) tuples. + """ + pairs = [] + seen = set() + for name in partition_info: + if name in seen: + continue + if name.endswith("_a"): + backup_name = name[:-2] + "_b" + if backup_name in partition_info: + base = name[:-2] + pairs.append((base, name, backup_name)) + seen.update((name, backup_name)) + continue + if name.endswith("_BACKUP"): + continue + backup_name = f"{name}_BACKUP" + if backup_name in partition_info: + pairs.append((name, name, backup_name)) + seen.update((name, backup_name)) + return pairs + + +def create_xml(args, pairs, partition_info, update_partitions=None): + doc = minidom.Document() + fvitems = doc.createElement("FVItems") + doc.appendChild(fvitems) + + metadata = doc.createElement("Metadata") + for tag, text in [("BreakingChangeNumber", "0"), ("FlashType", args.StorageType)]: + elem = doc.createElement(tag) + elem.appendChild(doc.createTextNode(text)) + metadata.appendChild(elem) + fvitems.appendChild(metadata) + + for base, primary_name, backup_name in sorted(pairs, key=lambda p: p[0]): + part_a = partition_info[primary_name] + part_b = partition_info[backup_name] + + if args.StorageType == "UFS": + # Use each partition's own LUN so that targets where _a/_b span + # different LUNs get the correct DiskType on both Dest and + # Backup. When _a/_b share a LUN (today's only case) this is + # identical to using a single disk_type for both. + dest_disk_type = f"UFS_LUN{part_a['lun']}" + backup_disk_type = f"UFS_LUN{part_b['lun']}" + elif args.StorageType in ("SPINOR", "NORUFS", "NORNVME"): + dest_disk_type = backup_disk_type = "SPINOR" + else: + dest_disk_type = backup_disk_type = "EMMC_PARTITION_USER_DATA" + + operation = "UPDATE" if base in (update_partitions or set()) else "IGNORE" + + fw_entry = doc.createElement("FwEntry") + for tag, text in [ + ("InputBinary", part_a["filename"]), + ("InputPath", "Images"), + ("Operation", operation), + ("UpdateType", "UPDATE_PARTITION"), + ("BackupType", "BACKUP_PARTITION"), + ]: + elem = doc.createElement(tag) + elem.appendChild(doc.createTextNode(text)) + fw_entry.appendChild(elem) + + dest = doc.createElement("Dest") + for tag, text in [ + ("DiskType", dest_disk_type), + ("PartitionName", primary_name), + ("PartitionTypeGUID", part_a["guid"]), + ]: + elem = doc.createElement(tag) + elem.appendChild(doc.createTextNode(text)) + dest.appendChild(elem) + fw_entry.appendChild(dest) + + backup = doc.createElement("Backup") + for tag, text in [ + ("DiskType", backup_disk_type), + ("PartitionName", backup_name), + ("PartitionTypeGUID", part_b["guid"]), + ]: + elem = doc.createElement(tag) + elem.appendChild(doc.createTextNode(text)) + backup.appendChild(elem) + fw_entry.appendChild(backup) + + fvitems.appendChild(fw_entry) + return doc + + +def write_xml(doc, output_file="FvUpdate.xml"): + with open(output_file, "wb") as f: + xml_str = doc.toprettyxml(indent=" ", encoding="utf-8") + f.write(xml_str) + + +def main(): + parser = argparse.ArgumentParser( + description="Generate FvUpdate.xml from partitions.conf" + ) + custom_usage = ( + "UpdateFvXml.py [-h] (-T TARGET & -S {UFS,EMMC,NORUFS,NORNVME}) " + "| [-F PARTITIONS_CONF]" + ) + parser = argparse.ArgumentParser(usage=custom_usage) + parser.add_argument("-T", metavar="TARGET", help="Target argument") + parser.add_argument( + "-S", + "--StorageType", + choices=["UFS", "EMMC", "NORUFS", "NORNVME"], + help="Specify storage type: UFS, EMMC, NORUFS, or NORNVME", + ) + parser.add_argument( + "-F", metavar="PARTITIONS_CONF", help="Partitions config argument" + ) + parser.add_argument( + "--ptool-path", + dest="ptool_path", + default=None, + help="Path to an existing qcom-ptool directory; " + "when provided, the repository is not cloned", + ) + parser.add_argument( + "--update-partitions", + dest="update_partitions", + default=None, + help="Comma-separated base partition names (e.g. dtb,uefi_dtb) to mark " + "Operation=UPDATE in the generated FvUpdate.xml; all other entries " + "stay Operation=IGNORE. Omit to keep every entry IGNORE (unchanged " + "default behavior).", + ) + args = parser.parse_args() + + repo_dir = args.ptool_path if args.ptool_path else DEFAULT_REPO_DIR + + if args.F: + if args.StorageType: + print( + "Error: Do not provide -S/--StorageType when using -F/--partitions_conf. It will be auto-detected." + ) + sys.exit(1) + if args.T: + print( + "Error: Do not provide -T/--StorageType when using -F/--partitions_conf." + ) + sys.exit(1) + partition_conf_path = args.F + lines = read_partitions_conf(partition_conf_path) + args.StorageType = detect_storage_type_from_conf(lines) + elif args.T: + if not args.StorageType: + print("Error: You must provide -S/--StorageType when using -T/--target.") + sys.exit(1) + if not args.ptool_path: + safe_clone(repo_dir) + target = get_target_name(args.T) + if not target: + print("Provided target is Unknown !!! Please re-check") + sys.exit(1) + conf_dir = ( + "spinor" + if args.StorageType in ("NORUFS", "NORNVME") + else args.StorageType.lower() + ) + partition_conf_path = os.path.join( + repo_dir, "platforms", target, conf_dir, "partitions.conf" + ) + lines = read_partitions_conf(partition_conf_path) + else: + print("Error: Invalid argument combination.") + parser.print_usage() + sys.exit(1) + + partition_info = parse_partition_info(args, lines, args.StorageType) + pairs = find_base_names(partition_info) + if not pairs: + print( + "Warning: No partition pairs (_a/_b or _BACKUP) found. FvUpdate.xml will not contain FwEntry blocks." + ) + update_partitions = ( + {name.strip() for name in args.update_partitions.split(",") if name.strip()} + if args.update_partitions + else None + ) + doc = create_xml(args, pairs, partition_info, update_partitions) + write_xml(doc) + print( + f"FvUpdate.xml has been created successfully with StorageType={args.StorageType}." + ) + + +if __name__ == "__main__": + main() diff --git a/debian.qcom/scripts/qcom_capsule_tool/UpdateJsonParameters.py b/debian.qcom/scripts/qcom_capsule_tool/UpdateJsonParameters.py new file mode 100644 index 0000000000000..edb22517dc275 --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/UpdateJsonParameters.py @@ -0,0 +1,276 @@ +## @file +# Description: +# This script opens and updates JSON file with parameters required for +# Capsule generation. +# +# -------------------------------------------------------------------- +# Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + + +import argparse +import json +import os +import subprocess +import sys +from collections import OrderedDict + + +def ParseArguments(): + parser = argparse.ArgumentParser(description="Process input arguments.") + parser.add_argument( + "-j", "--json_file", type=str, dest="JsonFile", help="Path to input JSON file" + ) + parser.add_argument( + "-f", "--fw_type", type=str, dest="FwType", help="Firmware Type [SYS_FW/EC_FW]" + ) + parser.add_argument( + "-b", + "--bin_file", + type=str, + dest="BinFile", + help="Path to System Firmware Version Binary / Embedded Controller Firmware Binary File", + ) + parser.add_argument( + "-t", + "--tool_path", + type=str, + dest="SigningToolPath", + help="Path to OpenSSL signing tool", + ) + parser.add_argument( + "-p", + "--private_cert", + type=str, + dest="OpenSslSignerPrivateCertFile", + help="OpenSSL signer private certificate filename", + ) + parser.add_argument( + "-x", + "--public_cert", + type=str, + dest="OpenSslTrustedPublicCertFile", + help="OpenSSL other public certificate filename.", + ) + parser.add_argument( + "-oc", + "--other_cert", + type=str, + dest="OpenSslOtherPublicCertFile", + help="OpenSSL trusted public certificate filename.", + ) + parser.add_argument( + "-pf", + "--payload_file", + type=str, + dest="Payload", + help="Path to the payload file", + ) + parser.add_argument("-g", "--guid", type=str, dest="Guid", help="System FMP GUID") + args = parser.parse_args() + + return args + + +def create_config(): + + config_json_data = OrderedDict() + Payloads_entry_dict = OrderedDict() + + Payloads_entry_dict["Guid"] = "" + Payloads_entry_dict["FwVersion"] = "" + Payloads_entry_dict["LowestSupportedVersion"] = "" + Payloads_entry_dict["MonotonicCount"] = "0x2" + Payloads_entry_dict["HardwareInstance"] = "0x0" + Payloads_entry_dict["UpdateImageIndex"] = "0x1" + Payloads_entry_dict["Payload"] = "" + Payloads_entry_dict["OpenSslSignerPrivateCertFile"] = "" + Payloads_entry_dict["OpenSslOtherPublicCertFile"] = "" + Payloads_entry_dict["OpenSslTrustedPublicCertFile"] = "" + Payloads_entry_dict["SigningToolPath"] = "" + + config_json_data["Payloads"] = [Payloads_entry_dict] + + with open("config.json", "w") as json_file: + json.dump(config_json_data, json_file, indent=4) + + +def ExtractEcFwVersions(StringData, SubString): + try: + if not StringData: + print("Empty string data!!") + sys.exit(1) + + if not SubString: + print("Empty sub string data!!") + sys.exit(1) + + offset = StringData.find(SubString) + index = offset + len(SubString) + FwVerSub = (ord(StringData[index + 3]) - ord("0")) * 10 + ( + ord(StringData[index + 4]) - ord("0") + ) + FwVerTest = (ord(StringData[index + 6]) - ord("0")) * 10 + ( + ord(StringData[index + 7]) - ord("0") + ) + version = (FwVerSub << 16) | FwVerTest + return "0x{:08x}".format(version) + + except Exception as e: + print("Error occurred while extracting EC version: {0}.".format(e)) + sys.exit(1) + + +def GetEcFirmwareInfo(args): + EcBinFilePath = args.BinFile + try: + # Check if EcBinFilePath path is valid + if not os.path.exists(EcBinFilePath): + print("Invalid EC firmware version file: {0}".format(EcBinFilePath)) + sys.exit(1) + + with open(EcBinFilePath, "rb") as file: + BinaryData = file.read() + StringData = BinaryData.decode("ISO-8859-1") + args.FwVersion = ExtractEcFwVersions( + StringData, "EC VER:" + ) # Retrieve EC Firmware Version + args.LowestSupportedVersion = ExtractEcFwVersions( + StringData, "LsFv:" + ) # Retrieve Lowest Supported EC Firmware Version + print( + "EC Firmware Version is {0}, lowest supported version: {1}".format( + args.FwVersion, args.LowestSupportedVersion + ) + ) + + except FileNotFoundError as e: + print("EC Bin File does not exist: {0}.".format(e)) + sys.exit(1) + + except Exception as e: + print("Error occurred while reading from EC bin file: {0}.".format(e)) + sys.exit(1) + + +def GetSysFirmwareInfo(args): + commands = ["-GetFwVersionHex", "-GetLSFwVersionHex"] + SysBinPath = args.BinFile + try: + # Check if SysBinPath and SysFwVersion.exe path is valid + if not os.path.exists(SysBinPath): + print("Invalid system firmware version file: {0}".format(SysBinPath)) + sys.exit(1) + + # Call SYSFW_VERSION_program to extract the firmware version and lowest + # supported version. Invoke as a module so it resolves correctly when + # qcom-capsule-tool is installed via pip/pipx (no __file__ assumptions). + results = [] + for cmd in commands: + try: + output = ( + subprocess.check_output( + [ + sys.executable, + "-m", + "qcom_capsule_tool.SYSFW_VERSION_program", + cmd, + SysBinPath, + ] + ) + .decode() + .strip() + ) + results.append(output) + except Exception as e: + print("Failed to execute command:{0}. Error: {1}".format(cmd, (e))) + sys.exit(1) + + (args.FwVersion, args.LowestSupportedVersion) = (results[0], results[1]) + print( + "Firmware Version is {0}, lowest supported version: {1}".format( + args.FwVersion, args.LowestSupportedVersion + ) + ) + + except subprocess.CalledProcessError as e: + print("Failed to extract firmware info from sys.bin: {0}".format(e)) + sys.exit(1) + + except Exception as e: + print("Exception in GetFirmwareInfo(): {0}".format(e)) + sys.exit(1) + + +def UpdateJsonFile(args): + # Get the firmware version and lowest supported version by calling the + # ExtractEcFwVersions()/SysFwVersion.exe and add it to the args list + FirmwareType = args.FwType + EcFwString = "EC_FW" + SysFwString = "SYS_FW" + try: + if FirmwareType == SysFwString: + GetSysFirmwareInfo(args) + print("GetSysFirmwareInfo(): {0}".format(SysFwString)) + elif FirmwareType == EcFwString: + GetEcFirmwareInfo(args) + print("GetEcFirmwareInfo(): {0}".format(EcFwString)) + else: + print("Neither System nor EC FirmwareType: {0}".format(FirmwareType)) + sys.exit(1) + + except Exception as e: + print("Error occurred while reading from FirmwareType string: {0}.".format(e)) + sys.exit(1) + + JsonFile = args.JsonFile + JsonFilePath = os.path.abspath(JsonFile) + JsonFilePathCheckCount = 0 + + while not os.path.exists(JsonFilePath) and (JsonFilePathCheckCount < 5): + create_config() + JsonFilePathCheckCount += 1 + pass + + if not os.path.exists(JsonFilePath): + print("%s Not found" % (JsonFile)) + + try: + with open(JsonFile, "r") as f: + # loading the field values in json file without changing the order + data = json.load(f, object_pairs_hook=OrderedDict) + except Exception as e: + print("Exception while opening JsonFile: {0}.".format(e)) + sys.exit(1) + + exception_list = ["JsonFile", "BinFile", "FwType"] + + for i, payload in enumerate(data["Payloads"]): + for key, value in args.__dict__.items(): + try: + if key in exception_list: + continue # Skip the keys in the exception list + elif value and key in payload: + data["Payloads"][i][key] = value + elif value: + print("Key {0} not found in payload {1}".format(key, i)) + except Exception as e: + print("Exception in UpdateJsonFile(): {0}".format(e)) + sys.exit(1) + + try: + with open(JsonFile, "w") as f: + json.dump(data, f, indent=4) + except Exception as e: + print("Error occurred while writing to the JSON file: {0}.".format(e)) + sys.exit(1) + + +def main(): + args = ParseArguments() + UpdateJsonFile(args) + + +if __name__ == "__main__": + main() diff --git a/debian.qcom/scripts/qcom_capsule_tool/XmlFwEntryValidation.py b/debian.qcom/scripts/qcom_capsule_tool/XmlFwEntryValidation.py new file mode 100644 index 0000000000000..e84022b1769bd --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/XmlFwEntryValidation.py @@ -0,0 +1,765 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + + +import ctypes +import uuid + +from . import FVCreation as FVC +from . import FVCreation_header as FVC_h + + +def partition_fields_checking(raw_fwentry, raw_dev_path, meta_data_dev_path): + if raw_fwentry.InputBinary is None: + print("Empty tag is not allowed for partition operation") + return False + if raw_dev_path.DiskType is None: + print("Empty tag is not allowed for partition operation") + return False + if raw_dev_path.PartitionName is None: + print("Empty tag is not allowed for partition operation") + return False + if raw_dev_path.PartitionTypeGUID is None: + print( + "Empty tag is not allowed for partition operation" + ) + return False + return True + + +def fat_fields_checking(raw_fwentry, raw_dev_path, meta_data_dev_path): + if raw_fwentry.InputBinary is None: + print("Empty tag is not allowed for FAT file operation") + return False + if raw_dev_path.DiskType is None: + print("Empty tag is not allowed for FAT file operation") + return False + if raw_dev_path.PartitionName is None and raw_dev_path.PartitionTypeGUID is None: + print( + "Enter or is required for FAT file operation" + ) + return False + if raw_dev_path.FileName is None: + print("Empty tag is not allowed for FAT file operation") + return False + return True + + +def dpp_fields_checking(raw_fwentry, raw_dev_path, meta_data_dev_path): + if raw_fwentry.InputBinary is None: + print("Empty tag is not allowed for DPP file operation") + return False + if raw_dev_path.FileName is None: + print("Empty tag is not allowed for DPP file operation") + return False + return True + + +def delete_fat_fields_checking(raw_fwentry, raw_dev_path, meta_data_dev_path): + if raw_dev_path.DiskType is None: + print("Empty tag is not allowed for FAT file operation") + return False + if raw_dev_path.PartitionName is None and raw_dev_path.PartitionTypeGUID is None: + print( + "Empty or is not allowed for FAT file operation" + ) + return False + if raw_dev_path.FileName is None: + print("Empty tag is not allowed for FAT file operation") + return False + return True + + +def delete_partition_fields_checking(raw_fwentry, raw_dev_path, meta_data_dev_path): + if raw_dev_path.DiskType is None: + print("Empty tag is not allowed for partition operation") + return False + if raw_dev_path.PartitionName is None: + print("Empty tag is not allowed for partition operation") + return False + if raw_dev_path.PartitionTypeGUID is None: + print("Empty tag is not allowed for partition operation") + return False + return True + + +def find_xml_raw_fw_entry_node(meta_data_fw_entry, g_dynamic_var): + xml_node_entry = FVC_h.XML_RAW_FWENTRY() + + for xml_raw_fw_entry in g_dynamic_var.XmlRawFwEntryList: + if xml_raw_fw_entry.FileGuid.lower() == meta_data_fw_entry.FileGuid.lower(): + xml_node_entry = xml_raw_fw_entry + + return xml_node_entry + + +def fw_entry_fields_value_checking(raw_fwentry, meta_data_fwentry, g_dynamic_var): + # Operation + if raw_fwentry.Operation: + if raw_fwentry.Operation.upper() in g_dynamic_var.dOperationTypeByString: + meta_data_fwentry.Operation = g_dynamic_var.dOperationTypeByString[ + raw_fwentry.Operation.upper() + ] + else: + print(f"Operation {raw_fwentry.Operation} is not recognized") + return False + + # UpdateType + if raw_fwentry.UpdateType: + if raw_fwentry.UpdateType.upper() in g_dynamic_var.dUpdateTypeByString: + meta_data_fwentry.UpdateType = g_dynamic_var.dUpdateTypeByString[ + raw_fwentry.UpdateType.upper() + ] + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FAT_FILE: + print("ERROR: is not supported for FAT_FILE") + return False + else: + print(f" {raw_fwentry.UpdateType} is not recognized") + return False + + # BackupType + if raw_fwentry.BackupType: + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FWCLASS_GUID: + print("ERROR: is not supported for UPDATE_FWCLASS_GUID") + return False + + if raw_fwentry.BackupType.upper() in g_dynamic_var.dBackupTypeByString: + meta_data_fwentry.BackupType = g_dynamic_var.dBackupTypeByString[ + raw_fwentry.BackupType.upper() + ] + if meta_data_fwentry.BackupType == FVC_h.FWENTRY_BACKUP_TYPE.FAT_FILE: + print("ERROR: is not supported for FAT_FILE") + return False + else: + print(f" {raw_fwentry.BackupType} is not recognized") + return False + + # MatchIdentifier + if g_dynamic_var.isMatchIdentifierInXML: + meta_data_fwentry.Revision = FVC.SYS_FW_METADATA_REVISION + if raw_fwentry.MatchIdentifier: + if ( + len(raw_fwentry.MatchIdentifier) + > FVC_h.GlobalStaticVariable.MATCH_IDENTIFIER_NAME_MAX_SIZE + ): + print( + f"Error: More than {FVC_h.GlobalStaticVariable.MATCH_IDENTIFIER_NAME_MAX_SIZE} characters found in " + ) + return False + meta_data_fwentry.MatchIdentifier = raw_fwentry.MatchIdentifier.encode( + "utf-8" + ) + + # BinaryType + if g_dynamic_var.isGlymurMode and raw_fwentry.BinaryType: + if raw_fwentry.BinaryType.upper() in g_dynamic_var.dBinaryTypeByString: + meta_data_fwentry.BinaryType = g_dynamic_var.dBinaryTypeByString[ + raw_fwentry.BinaryType.upper() + ] + else: + print(f" {raw_fwentry.BinaryType} is not recognized") + return False + + # ARValidation + if g_dynamic_var.isGlymurMode and raw_fwentry.ARValidation: + images = raw_fwentry.ARValidation.Images + image_count = len(images) + for j, image in enumerate(images): + if meta_data_fwentry.BinaryType == FVC_h.FWENTRY_BINARY_TYPE.FATFS: + if image.FileName is None: + print( + f"ERROR: FileName is null for image {raw_fwentry.InputBinary}" + ) + return False + if len(image.FileName) > FVC_h.GlobalStaticVariable.FILE_NAME_MAX_SIZE: + print( + f"ERROR: More than {FVC_h.GlobalStaticVariable.FILE_NAME_MAX_SIZE} characters found in " + ) + return False + elif meta_data_fwentry.BinaryType == FVC_h.FWENTRY_BINARY_TYPE.RAW: + if ( + image.FileName is not None + and len(image.FileName) + < FVC_h.GlobalStaticVariable.FILE_NAME_MAX_SIZE + ): + if image.FileName != raw_fwentry.InputBinary: + print( + "ARValidation FileName should be same as InputBinary name for RAW FwEntry" + ) + return False + + if not image.ARValidationType: + print(f"ERROR: ARValidationType is null for image {image.FileName}") + return False + + if ( + len(image.ARValidationType) + > FVC_h.GlobalStaticVariable.AR_VALIDATION_TYPE_MAX_SIZE + ): + print( + f"ERROR: More than {FVC_h.GlobalStaticVariable.AR_VALIDATION_TYPE_MAX_SIZE} characters found in " + ) + return False + + if image.FileName is not None: + meta_data_fwentry.ARValidation.Images[j].FileName[ + : len(image.FileName.encode("utf-16-le")) + ] = image.FileName.encode("utf-16-le") + meta_data_fwentry.ARValidation.Images[j].ARValidationType[ + : len(image.ARValidationType.encode("utf-16-le")) + ] = image.ARValidationType.encode("utf-16-le") + + meta_data_fwentry.ARValidation.ImageCount = image_count + + # Dest DiskType + if raw_fwentry.UpdatePath.DiskType: + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FWCLASS_GUID: + print("ERROR: is not supported for UPDATE_FWCLASS_GUID") + return False + + if raw_fwentry.UpdatePath.DiskType.upper() in g_dynamic_var.dDiskTypeByString: + meta_data_fwentry.UpdatePath.DiskType = g_dynamic_var.dDiskTypeByString[ + raw_fwentry.UpdatePath.DiskType.upper() + ] + else: + print( + f"Dest {raw_fwentry.UpdatePath.DiskType} is not recognized" + ) + return False + + # Dest PartitionName + if raw_fwentry.UpdatePath.PartitionName: + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FWCLASS_GUID: + print("ERROR: is not supported for UPDATE_FWCLASS_GUID") + return False + if ( + len(raw_fwentry.UpdatePath.PartitionName) + > FVC_h.GlobalStaticVariable.PARTITION_NAME_MAX_SIZE + ): + print( + f"Error: More than {FVC_h.GlobalStaticVariable.PARTITION_NAME_MAX_SIZE} characters found in " + ) + return False + if ( + raw_fwentry.UpdatePath.PartitionName + == FVC_h.GlobalStaticVariable.PARTITION_NAME_SYSFW_VERSION + ): + print( + f"Error: Partition name {FVC_h.GlobalStaticVariable.PARTITION_NAME_SYSFW_VERSION} is not allowed, Otherwise version conflict will occur when different binary is used" + ) + return False + meta_data_fwentry.UpdatePath.PartitionName[ + : len(raw_fwentry.UpdatePath.PartitionName) + ] = bytearray(raw_fwentry.UpdatePath.PartitionName, "utf-8") + + # Dest PartitionTypeGuid + if raw_fwentry.UpdatePath.PartitionTypeGUID: + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FWCLASS_GUID: + print("ERROR: is not supported for UPDATE_FWCLASS_GUID") + return False + + s_temp = raw_fwentry.UpdatePath.PartitionTypeGUID.strip("{}") + if s_temp: + try: + uuid_obj = uuid.UUID(s_temp) + meta_data_fwentry.UpdatePath.PartitionTypeGUID = (ctypes.c_byte * 16)( + *uuid_obj.bytes + ) + + except ValueError as e: + print(f"ERROR: Failure creating partitionTypeGuid(error: {e}).\n") + return False + + # Dest FileName + if raw_fwentry.UpdatePath.FileName: + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FWCLASS_GUID: + print("ERROR: is not supported for UPDATE_FWCLASS_GUID") + return False + + if ( + len(raw_fwentry.UpdatePath.FileName) + > FVC_h.GlobalStaticVariable.FILE_NAME_MAX_SIZE + ): + print( + f"Error: More than {FVC_h.GlobalStaticVariable.FILE_NAME_MAX_SIZE} characters found in " + ) + return False + meta_data_fwentry.UpdatePath.FileName = raw_fwentry.UpdatePath.FileName + + # Backup DiskType + if raw_fwentry.BackupPath.DiskType: + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FWCLASS_GUID: + print("ERROR: is not supported for UPDATE_FWCLASS_GUID") + return False + + if raw_fwentry.BackupPath.DiskType.upper() in g_dynamic_var.dDiskTypeByString: + meta_data_fwentry.BackupPath.DiskType = g_dynamic_var.dDiskTypeByString[ + raw_fwentry.BackupPath.DiskType.upper() + ] + else: + print( + f"Dest {raw_fwentry.BackupPath.DiskType} is not recognized" + ) + return False + + # Backup PartitionName + if raw_fwentry.BackupPath.PartitionName: + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FWCLASS_GUID: + print("ERROR: is not supported for UPDATE_FWCLASS_GUID") + return False + + if ( + len(raw_fwentry.BackupPath.PartitionName) + > FVC_h.GlobalStaticVariable.PARTITION_NAME_MAX_SIZE + ): + print( + f"Error: More than {FVC_h.GlobalStaticVariable.PARTITION_NAME_MAX_SIZE} characters found in " + ) + return False + if ( + raw_fwentry.BackupPath.PartitionName + == FVC_h.GlobalStaticVariable.PARTITION_NAME_SYSFW_VERSION + ): + print( + f"Error: Partition name {FVC_h.GlobalStaticVariable.PARTITION_NAME_SYSFW_VERSION} is not allowed, Otherwise version conflict will occur when different binary is used" + ) + return False + meta_data_fwentry.BackupPath.PartitionName[ + : len(raw_fwentry.BackupPath.PartitionName) + ] = bytearray(raw_fwentry.BackupPath.PartitionName, "utf-8") + + # Backup PartitionTypeGuid + if raw_fwentry.BackupPath.PartitionTypeGUID: + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FWCLASS_GUID: + print("ERROR: is not supported for UPDATE_FWCLASS_GUID") + return False + + s_temp = raw_fwentry.BackupPath.PartitionTypeGUID.strip("{}") + if s_temp: + try: + uuid_obj = uuid.UUID(s_temp) + meta_data_fwentry.BackupPath.PartitionTypeGUID = (ctypes.c_byte * 16)( + *uuid_obj.bytes + ) + except ValueError as e: + print(f"ERROR: Failure creating partitionTypeGuid(error: {e}).\n") + return False + + # Backup FileName + if raw_fwentry.BackupPath.FileName: + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FWCLASS_GUID: + print("ERROR: is not supported for UPDATE_FWCLASS_GUID") + return False + + if ( + len(raw_fwentry.BackupPath.FileName) + > FVC_h.GlobalStaticVariable.FILE_NAME_MAX_SIZE + ): + print( + f"Error: More than {FVC_h.GlobalStaticVariable.FILE_NAME_MAX_SIZE} characters found in " + ) + return False + meta_data_fwentry.BackupPath.FileName = raw_fwentry.BackupPath.FileName + + return True + + +def assign_file_guid_for_fw_entry(raw_fwentry, meta_data_fwentry, g_dynamic_var): + + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FAT_FILE: + return False + + if meta_data_fwentry.UpdateType in [ + FVC_h.FWENTRY_UPDATE_TYPE.DPP_OEM, + FVC_h.FWENTRY_UPDATE_TYPE.DPP_QCOM, + FVC_h.FWENTRY_UPDATE_TYPE.OPM_PRIV_KEY, + ]: + if ( + raw_fwentry.UpdatePath.FileName.upper() + in g_dynamic_var.dFileGuidByDestDppItemFile + ): + s_temp_guid1 = g_dynamic_var.dFileGuidByDestDppItemFile[ + raw_fwentry.UpdatePath.FileName.upper() + ].strip("{}") + raw_fwentry.FileGuid = s_temp_guid1 + try: + meta_data_fwentry.FileGuid = uuid.UUID(s_temp_guid1) + except ValueError as e: + print(f"ERROR: Failure creating FileGuid(error: {e}).\n") + return False + return True + + try: + uuid_obj = uuid.uuid4() + meta_data_fwentry.FileGuid = (ctypes.c_byte * 16)(*uuid_obj.bytes) + + except ValueError as e: + print(f"ERROR: Failure creating FileGuid(error: {e}).\n") + return False + + raw_fwentry.FileGuid = meta_data_fwentry.FileGuid + return True + + +def fw_entry_fields_combination_checking(raw_fwentry, meta_data_fwentry, g_dynamic_var): + if meta_data_fwentry.Operation == FVC_h.FWENTRY_OPERATION_TYPE.UPDATE: + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.PARTITION: + if partition_fields_checking( + raw_fwentry, raw_fwentry.UpdatePath, meta_data_fwentry.UpdatePath + ): + if meta_data_fwentry.BackupType == FVC_h.FWENTRY_BACKUP_TYPE.PARTITION: + if partition_fields_checking( + raw_fwentry, + raw_fwentry.BackupPath, + meta_data_fwentry.BackupPath, + ): + print("Firmware entry validated\n") + return True + print("BackupPath partition validation failed") + return False + if meta_data_fwentry.BackupType == FVC_h.FWENTRY_BACKUP_TYPE.FAT_FILE: + print("Invalid BackupPath") + return False + print("UpdatePath partition validation failed") + return False + + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FWCLASS_GUID: + print("Firmware entry validated\n") + return True + + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FAT_FILE: + return False + + if meta_data_fwentry.UpdateType in [ + FVC_h.FWENTRY_UPDATE_TYPE.DPP_QCOM, + FVC_h.FWENTRY_UPDATE_TYPE.DPP_OEM, + ]: + if dpp_fields_checking( + raw_fwentry, raw_fwentry.UpdatePath, meta_data_fwentry.UpdatePath + ): + if meta_data_fwentry.BackupType == FVC_h.FWENTRY_BACKUP_TYPE.FAT_FILE: + return False + if meta_data_fwentry.BackupType == FVC_h.FWENTRY_BACKUP_TYPE.PARTITION: + if partition_fields_checking( + raw_fwentry, + raw_fwentry.BackupPath, + meta_data_fwentry.BackupPath, + ): + print("Firmware entry validated\n") + return True + return False + return False + return False + + if meta_data_fwentry.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.OPM_PRIV_KEY: + if dpp_fields_checking( + raw_fwentry, raw_fwentry.UpdatePath, meta_data_fwentry.UpdatePath + ): + if meta_data_fwentry.BackupType == FVC_h.FWENTRY_BACKUP_TYPE.FAT_FILE: + return False + if meta_data_fwentry.BackupType == FVC_h.FWENTRY_BACKUP_TYPE.PARTITION: + if partition_fields_checking( + raw_fwentry, + raw_fwentry.BackupPath, + meta_data_fwentry.BackupPath, + ): + print("Firmware entry validated\n") + return True + return False + return False + return False + + return False + + if meta_data_fwentry.Operation == FVC_h.FWENTRY_OPERATION_TYPE.IGNORE: + return True + + return False + + +def fw_entry_validation(raw_fwentry, meta_data_fwentry, g_dynamic_var): + print("Validating firmware entry...") + print("============================") + print( + f" = {g_dynamic_var.dFlashTypeByValue[g_dynamic_var.DeviceFlashType]}" + ) + print(f" = {raw_fwentry.InputBinary}") + print(f" = {raw_fwentry.Operation}") + print(f" = {raw_fwentry.UpdateType}") + print(f" = {raw_fwentry.BackupType}") + if raw_fwentry.MatchIdentifier: + print(f" = {raw_fwentry.MatchIdentifier}") + print(f" DestPath = {raw_fwentry.UpdatePath.DiskType}") + print( + f" DestPath = {raw_fwentry.UpdatePath.PartitionName}" + ) + print( + f" DestPath = {raw_fwentry.UpdatePath.PartitionTypeGUID}" + ) + print(f" DestPath = {raw_fwentry.UpdatePath.FileName}") + print("\n") + print(f" BackupPath = {raw_fwentry.BackupPath.DiskType}") + print( + f" BackupPath = {raw_fwentry.BackupPath.PartitionName}" + ) + print( + f" BackupPath = {raw_fwentry.BackupPath.PartitionTypeGUID}" + ) + print(f" BackupPath = {raw_fwentry.BackupPath.FileName}") + + if not fw_entry_fields_value_checking( + raw_fwentry, meta_data_fwentry, g_dynamic_var + ): + return False + + if not assign_file_guid_for_fw_entry(raw_fwentry, meta_data_fwentry, g_dynamic_var): + return False + + if not fw_entry_fields_combination_checking( + raw_fwentry, meta_data_fwentry, g_dynamic_var + ): + return False + + return True + + +def fw_entry_list_validation_main(g_dynamic_var): + base_match_identifier = None + target_match_identifier = None + + for i in range(len(g_dynamic_var.XmlRawFwEntryList)): + raw_fw_entry_temp = g_dynamic_var.XmlRawFwEntryList.popleft() + m_fw_entry = FVC_h.QPAYLOAD_METADATA_FWENTRY() + m_fw_entry.UpdatePath = FVC_h.FWENTRY_DEVICE_PATH(0x0) + m_fw_entry.BackupPath = FVC_h.FWENTRY_DEVICE_PATH(0x0) + if g_dynamic_var.isMatchIdentifierInXML: + m_fw_entry.MatchIdentifier = ( + b"\0" * FVC_h.GlobalStaticVariable.MATCH_IDENTIFIER_NAME_MAX_SIZE + ) + + if fw_entry_validation(raw_fw_entry_temp, m_fw_entry, g_dynamic_var): + if m_fw_entry.Operation != FVC_h.FWENTRY_OPERATION_TYPE.IGNORE: + g_dynamic_var.QpayloadFwEntryList.append(m_fw_entry) + else: + print("ERROR: Error validating firmware entry.") + return False + g_dynamic_var.XmlRawFwEntryList.append(raw_fw_entry_temp) + + # FlashType exclusive checking + print("FlashType exclusive checking\n") + for i in range(len(g_dynamic_var.QpayloadFwEntryList)): + m_fw_entry = g_dynamic_var.QpayloadFwEntryList[i] + if m_fw_entry.UpdateType != FVC_h.FWENTRY_UPDATE_TYPE.FWCLASS_GUID: + if m_fw_entry.UpdateType not in [ + FVC_h.FWENTRY_UPDATE_TYPE.DPP_QCOM, + FVC_h.FWENTRY_UPDATE_TYPE.DPP_OEM, + FVC_h.FWENTRY_UPDATE_TYPE.OPM_PRIV_KEY, + ]: + if ( + g_dynamic_var.DeviceFlashType + not in g_dynamic_var.dFlashTypeByDiskType[ + FVC_h.FWENTRY_DISK_TYPE(m_fw_entry.UpdatePath.DiskType) + ] + ): + print( + f"ERROR1: DiskType {g_dynamic_var.dDiskTypeByValue[FVC_h.FWENTRY_DISK_TYPE(m_fw_entry.UpdatePath.DiskType)]} can't be used on a {g_dynamic_var.dFlashTypeByValue[g_dynamic_var.DeviceFlashType]} device." + ) + return False + + if ( + g_dynamic_var.DeviceFlashType + not in g_dynamic_var.dFlashTypeByDiskType[ + FVC_h.FWENTRY_DISK_TYPE(m_fw_entry.BackupPath.DiskType) + ] + ): + print( + f"ERROR2: DiskType {g_dynamic_var.dDiskTypeByValue[FVC_h.FWENTRY_DISK_TYPE(m_fw_entry.BackupPath.DiskType)]} can't be used on a {g_dynamic_var.dFlashTypeByValue[g_dynamic_var.DeviceFlashType]} device." + ) + return False + + # QCOM Dpp Item Name exclusive checking + print("QCOM Dpp Item Name exclusive checking\n") + for i in range(len(g_dynamic_var.QpayloadFwEntryList)): + m_fw_entry_base = g_dynamic_var.QpayloadFwEntryList[i] + + if m_fw_entry_base.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.DPP_QCOM: + base_file_name = "".join(m_fw_entry_base.UpdatePath.FileName) + + for j in range(i + 1, len(g_dynamic_var.QpayloadFwEntryList)): + m_fw_entry_target = g_dynamic_var.QpayloadFwEntryList[j] + if m_fw_entry_target.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.DPP_QCOM: + target_file_name = "".join(m_fw_entry_target.UpdatePath.FileName) + if base_file_name == target_file_name: + print( + "ERROR: duplicated QCOM type DPP items found in the list." + ) + return False + + # OEM Dpp Item Name exclusive checking + print("OEM Dpp Item Name exclusive checking\n") + for i in range(len(g_dynamic_var.QpayloadFwEntryList)): + m_fw_entry_base = g_dynamic_var.QpayloadFwEntryList[i] + + if m_fw_entry_base.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.DPP_OEM: + base_file_name = "".join(m_fw_entry_base.UpdatePath.FileName) + + for j in range(i + 1, len(g_dynamic_var.QpayloadFwEntryList)): + m_fw_entry_target = g_dynamic_var.QpayloadFwEntryList[j] + if m_fw_entry_target.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.DPP_OEM: + target_file_name = "".join(m_fw_entry_target.UpdatePath.FileName) + if base_file_name == target_file_name: + print("ERROR: duplicated OEM type DPP items found in the list.") + return False + + # Partition device path exclusive checking + print("Partition device path exclusive checking\n") + for i in range(len(g_dynamic_var.QpayloadFwEntryList)): + m_fw_entry_base = g_dynamic_var.QpayloadFwEntryList[i] + + if m_fw_entry_base.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.PARTITION: + base_update_part_name = "".join( + chr(b) for b in m_fw_entry_base.UpdatePath.PartitionName + ) + base_backup_part_name = "".join( + chr(b) for b in m_fw_entry_base.BackupPath.PartitionName + ) + + # Case 1: BaseFwEntry's updatePath VS BaseFwEntry's BackupPath. + if ( + m_fw_entry_base.UpdatePath.DiskType + == m_fw_entry_base.BackupPath.DiskType + and m_fw_entry_base.UpdatePath.PartitionTypeGUID + == m_fw_entry_base.BackupPath.PartitionTypeGUID + and base_update_part_name == base_backup_part_name + ): + print( + "ERROR: same partition update path and backup path found in the same entry." + ) + return False + + for j in range(i + 1, len(g_dynamic_var.QpayloadFwEntryList)): + m_fw_entry_target = g_dynamic_var.QpayloadFwEntryList[j] + if m_fw_entry_target.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.PARTITION: + target_update_part_name = "".join( + chr(b) for b in m_fw_entry_target.UpdatePath.PartitionName + ) + target_backup_part_name = "".join( + chr(b) for b in m_fw_entry_target.BackupPath.PartitionName + ) + + if g_dynamic_var.isMatchIdentifierInXML: + if any(m_fw_entry_base.MatchIdentifier): + base_match_identifier = "".join( + m_fw_entry_base.MatchIdentifier + ) + if any(m_fw_entry_target.MatchIdentifier): + target_match_identifier = "".join( + m_fw_entry_target.MatchIdentifier + ) + + # Case 2: BaseFwEntry's updatePath VS TargetFwEntry's UpdatePath + if ( + m_fw_entry_base.UpdatePath.DiskType + == m_fw_entry_target.UpdatePath.DiskType + and m_fw_entry_base.UpdatePath.PartitionTypeGUID + == m_fw_entry_target.UpdatePath.PartitionTypeGUID + and base_update_part_name == target_update_part_name + ): + if not base_match_identifier or not target_match_identifier: + print( + "ERROR: same partition update path found in the list." + ) + return False + elif base_match_identifier == target_match_identifier: + print( + "ERROR: same partition update path with same match identifier found in the list." + ) + return False + elif base_match_identifier != target_match_identifier: + xml_entry_base = find_xml_raw_fw_entry_node( + m_fw_entry_base, g_dynamic_var + ) + xml_entry_target = find_xml_raw_fw_entry_node( + m_fw_entry_target, g_dynamic_var + ) + if ( + xml_entry_base.InputBinary + == xml_entry_target.InputBinary + ): + print( + "ERROR: same partition update path with same input binary found in the list." + ) + return False + + # Case 3: BaseFwEntry's updatePath VS TargetFwEntry's BackupPath + if ( + m_fw_entry_base.UpdatePath.DiskType + == m_fw_entry_target.BackupPath.DiskType + and m_fw_entry_base.UpdatePath.PartitionTypeGUID + == m_fw_entry_target.BackupPath.PartitionTypeGUID + and base_update_part_name == target_backup_part_name + ): + print( + "ERROR: same partition update path and backup path found in the list." + ) + return False + + # Case 4: BaseFwEntry's BackupPath VS TargetFwEntry's UpdatePath + if ( + m_fw_entry_base.BackupPath.DiskType + == m_fw_entry_target.UpdatePath.DiskType + and m_fw_entry_base.BackupPath.PartitionTypeGUID + == m_fw_entry_target.UpdatePath.PartitionTypeGUID + and base_backup_part_name == target_update_part_name + ): + print( + "ERROR: same partition update path and backup path found in the list." + ) + return False + + # Case 5: BaseFwEntry's BackupPath VS TargetFwEntry's BackupPath + if ( + m_fw_entry_base.BackupPath.DiskType + == m_fw_entry_target.BackupPath.DiskType + and m_fw_entry_base.BackupPath.PartitionTypeGUID + == m_fw_entry_target.BackupPath.PartitionTypeGUID + and base_backup_part_name == target_backup_part_name + ): + if not base_match_identifier or not target_match_identifier: + print( + "ERROR: same partition backup path found in the list." + ) + return False + elif base_match_identifier == target_match_identifier: + print( + "ERROR: same partition backup path with same match identifier found in the list." + ) + return False + elif base_match_identifier != target_match_identifier: + xml_entry_base = find_xml_raw_fw_entry_node( + m_fw_entry_base, g_dynamic_var + ) + xml_entry_target = find_xml_raw_fw_entry_node( + m_fw_entry_target, g_dynamic_var + ) + if ( + xml_entry_base.InputBinary + == xml_entry_target.InputBinary + ): + print( + "ERROR: same partition backup path with same input binary found in the list." + ) + return False + + # Fat device path exclusive checking + print("Fat device path exclusive checking\n") + for i in range(len(g_dynamic_var.QpayloadFwEntryList)): + m_fw_entry_base = g_dynamic_var.QpayloadFwEntryList[i] + + if m_fw_entry_base.UpdateType == FVC_h.FWENTRY_UPDATE_TYPE.FAT_FILE: + print("Invalid BackupPath") + return False + + return True diff --git a/debian.qcom/scripts/qcom_capsule_tool/XmlParser.py b/debian.qcom/scripts/qcom_capsule_tool/XmlParser.py new file mode 100644 index 0000000000000..3684a688b20bc --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/XmlParser.py @@ -0,0 +1,212 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + + +import re +import traceback +import xml.etree.ElementTree as ET +from collections import OrderedDict + +from . import FVCreation_header as FVC_h + + +def print_all_level_d(d, indent=0, log_file_obj=None): + """ + About: + Function to print in console, a nested dict with indentation for easy reading + For debugging + + Args: + d: dict to be printed -> dict + indent: The indentation space to use for the current nested dict -> int + + Return: + None + """ + if not d: + return + if isinstance(d, list): + s = "" + for i in range(indent): + print(" ", end="") + s += " " + + print("[") + for x in d: + print_all_level_d(x, indent, log_file_obj) + print() + + s = "" + for i in range(indent): + print(" ", end="") + s += " " + print("]") + + return + + if isinstance(d, str): + s = "" + for i in range(indent): + print(" ", end="") + s += " " + print(d) + + return + + for x in d: + if ( + isinstance(d[x], OrderedDict) + or isinstance(d[x], dict) + or isinstance(d[x], list) + ): + s = "" + for i in range(indent): + print(" ", end="") + s += " " + print(x + " : ") + print_all_level_d(d[x], indent + 4, log_file_obj) + + else: + s = "" + for i in range(indent): + print(" ", end="") + s += " " + print(x + " : ", end="") + print(d[x]) + + +def xml_to_dict(ele): + if len(ele) == 0: + return ele.text + + result_dict = OrderedDict() + for c in ele: + c_dict = xml_to_dict(c) + + if c.tag in result_dict: + if type(result_dict[c.tag]) is list: + result_dict[c.tag].append(c_dict) + else: + result_dict[c.tag] = [result_dict[c.tag], c_dict] + else: + result_dict[c.tag] = c_dict + + return result_dict + + +def parse_input_xml(s_xml_file, s_breaking_change_number, g_dynamic_var): + + try: + tree = ET.parse(s_xml_file) + root = tree.getroot() + + except Exception: + print(traceback.format_exc()) + return False + + result_dict = xml_to_dict(root) + + # print_all_level_d(result_dict) + + # print("\n\n\nin custom code:") + if isinstance(result_dict["FwEntry"], OrderedDict): + result_dict["FwEntry"] = [result_dict["FwEntry"]] + for fw_entry in result_dict["FwEntry"]: + raw_fw_item = FVC_h.XML_RAW_FWENTRY() + + raw_fw_item.Operation = fw_entry["Operation"] + raw_fw_item.InputBinary = fw_entry["InputBinary"] + raw_fw_item.InputPath = fw_entry["InputPath"] + raw_fw_item.UpdateType = fw_entry["UpdateType"] + raw_fw_item.BackupType = fw_entry["BackupType"] + raw_fw_item.UpdatePath.DiskType = fw_entry["Dest"]["DiskType"] + + raw_fw_item.UpdatePath.PartitionName = fw_entry["Dest"]["PartitionName"] + raw_fw_item.UpdatePath.PartitionTypeGUID = fw_entry["Dest"]["PartitionTypeGUID"] + raw_fw_item.BackupPath.DiskType = fw_entry["Backup"]["DiskType"] + raw_fw_item.BackupPath.PartitionName = fw_entry["Backup"]["PartitionName"] + raw_fw_item.BackupPath.PartitionTypeGUID = fw_entry["Backup"][ + "PartitionTypeGUID" + ] + + if g_dynamic_var.isGlymurMode: + if "MatchIdentifier" in fw_entry: + raw_fw_item.MatchIdentifier = fw_entry["MatchIdentifier"] + g_dynamic_var.isMatchIdentifierInXML = True + + if "BinaryType" in fw_entry: + raw_fw_item.BinaryType = fw_entry["BinaryType"] + g_dynamic_var.isBinaryTypeInXML = True + + if "ARValidation" in fw_entry: + ar_validation = fw_entry["ARValidation"] + image_nodes = ar_validation.get("Image") if ar_validation else None + if image_nodes: + if isinstance(image_nodes, OrderedDict): + image_nodes = [image_nodes] + images = [] + for image_node in image_nodes: + image = FVC_h.XML_RAW_FWENTRY_IMAGE( + file_name=image_node.get("FileName"), + ar_validation_type=image_node.get("ARValidationType"), + ) + images.append(image) + raw_fw_item.ARValidation = FVC_h.XML_RAW_FWENTRY_ARVALIDATION( + images + ) + + g_dynamic_var.XmlRawFwEntryList.append(raw_fw_item) + + # print("\n\n\n*******************\n") + + metadata_items = root.findall(".//Metadata") + + for metadata in metadata_items: + b_found = False + media_found = False + s_brk_chg_num = "0" + s_flash_type_in = "0" + + if len(metadata) != 2: + print("ERROR: Malformed XML. MetaData node does not contain two elements.") + return False + + # Traversing MetaData entries + for child in metadata: + if child.tag.lower() == "breakingchangenumber": + s_brk_chg_num = child.text.strip() + b_found = True + continue + + if child.tag.lower() == "flashtype": + s_flash_type_in = child.text.strip() + media_found = True + continue + + if not b_found: + print("Warning: MetaData does not contain BreakingChangeNumber element.") + return False + + if not re.match("^[0-9]+$", s_brk_chg_num): + print( + "ERROR: Invalid BreakingChangeNumber in the XML file. BreakingChangeNumber should only contain numbers." + ) + return False + + if not media_found: + print("Warning: MetaData does not contain FlashType element.") + return False + + if s_flash_type_in.upper() not in g_dynamic_var.dFlashTypeByString: + print( + "ERROR: Invalid FlashType in the XML file. FlashType should only be UFS or EMMC." + ) + return False + else: + g_dynamic_var.DeviceFlashType = g_dynamic_var.dFlashTypeByString[ + s_flash_type_in.upper() + ] + + return True diff --git a/debian.qcom/scripts/qcom_capsule_tool/__init__.py b/debian.qcom/scripts/qcom_capsule_tool/__init__.py new file mode 100644 index 0000000000000..c784f4ec66c74 --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/__init__.py @@ -0,0 +1,4 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- diff --git a/debian.qcom/scripts/qcom_capsule_tool/capsule_creator.py b/debian.qcom/scripts/qcom_capsule_tool/capsule_creator.py new file mode 100644 index 0000000000000..99cc0e5d470e4 --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/capsule_creator.py @@ -0,0 +1,199 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2024 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + +import argparse +import os +import subprocess +import sys + + +def run_module(module, *args): + """Run a qcom_capsule_tool module in a subprocess, exit on failure.""" + command = [sys.executable, "-m", f"qcom_capsule_tool.{module}"] + [ + str(arg) for arg in args + ] + result = subprocess.run(command, capture_output=True, text=True) + print(result.stdout) + if result.returncode != 0: + print(result.stderr) + print(f"Error: {' '.join(command)} failed with code {result.returncode}") + sys.exit(1) + + +def patch_capsule_images(image_paths, cert_cer_path, staging_dir): + """Patch QcCapsuleRootCert into each ELF in *image_paths*, writing patched + copies (same basename, e.g. `.xz` in -> `.xz` out) into *staging_dir*. + + Delegates to patch_capsule_cert(), which auto-detects uefi_dtbs vs + xbl_config and transparently handles `.xz`-compressed inputs/outputs. + """ + from qcom_capsule_tool.patch_capsule_cert import patch_capsule_cert + + os.makedirs(staging_dir, exist_ok=True) + for image_path in image_paths: + output_path = os.path.join(staging_dir, os.path.basename(image_path)) + try: + patch_capsule_cert(image_path, cert_cer_path, output_path) + except Exception as exc: + print(f"Error: failed to patch capsule cert into {image_path}: {exc}") + sys.exit(1) + print(f"Patched capsule cert: {image_path} -> {output_path}") + + +def _run(args): + # Step 1: Generate SYSFW_VERSION.bin + run_module( + "SYSFW_VERSION_program", + "-Gen", + "-FwVer", + args.fwver, + "-LFwVer", + args.lfwver, + "-O", + "SYSFW_VERSION.bin", + ) + + # Step 2: Create FvUpdate.xml + ptool_path_args = ["--ptool-path", args.ptool_path] if args.ptool_path else [] + update_partitions_args = ( + ["--update-partitions", args.update_partitions] + if args.update_partitions + else [] + ) + run_module( + "UpdateFvXml", + "-S", + args.StorageType, + "-T", + args.target, + *ptool_path_args, + *update_partitions_args, + ) + + # Step 2b: Patch QcCapsuleRootCert into the requested images, if any. + # Patched copies are staged in a directory searched ahead of -images, so + # FVCreation picks them up in place of the unpatched originals. + image_search_paths = [args.images] + if args.patch_image: + staging_dir = os.path.join(os.getcwd(), "patched_images") + patch_capsule_images(args.patch_image, args.patch_cert, staging_dir) + image_search_paths = [staging_dir, args.images] + + # Step 3: Create firmware volume + glymur_args = ["--glymur"] if args.target.lower() == "glymur" else [] + run_module( + "FVCreation", + "firmware.fv", + "-FvType", + "SYS_FW", + "FvUpdate.xml", + "SYSFW_VERSION.bin", + *image_search_paths, + *glymur_args, + ) + + # Step 4: Update JSON parameters + run_module( + "UpdateJsonParameters", + "-j", + args.config, + "-f", + "SYS_FW", + "-b", + "SYSFW_VERSION.bin", + "-pf", + "firmware.fv", + "-p", + args.p, + "-x", + args.x, + "-oc", + args.oc, + "-g", + args.guid, + ) + + # Step 5: Generate capsule + run_module( + "generate_capsule", + "-e", + "-j", + args.config, + "-o", + args.capsule, + "--capflag", + "PersistAcrossReset", + "-v", + ) + + +def main(): + parser = argparse.ArgumentParser( + description="Combined script for Capsule generation" + ) + parser.add_argument("-fwver", required=True, help="Firmware version") + parser.add_argument( + "-lfwver", required=True, help="Lowest supported firmware version" + ) + parser.add_argument("-config", required=True, help="Configuration JSON file") + parser.add_argument("-p", required=True, help="Certificate file") + parser.add_argument("-x", required=True, help="Root certificate file") + parser.add_argument("-oc", required=True, help="Sub certificate file") + parser.add_argument("-guid", required=True, help="FMP GUID") + parser.add_argument("-capsule", required=True, help="Output capsule file name") + parser.add_argument("-images", required=True, help="Images directory") + parser.add_argument( + "--ptool-path", + dest="ptool_path", + default=None, + help="Path to an existing qcom-ptool directory; " + "when provided, the repository is not cloned", + ) + parser.add_argument( + "-S", + "--StorageType", + choices=["UFS", "EMMC", "NORUFS", "NORNVME"], + required=True, + help="Specify storage type: UFS, EMMC, NORUFS, or NORNVME", + ) + parser.add_argument( + "-T", "--target", required=True, help="Specify target platform (e.g., QCS6490)" + ) + parser.add_argument( + "--patch-cert", + dest="patch_cert", + default=None, + help="Path to QcFMPRoot.cer; required when --patch-image is given", + ) + parser.add_argument( + "--patch-image", + dest="patch_image", + action="append", + default=[], + help="Path to an image to patch QcCapsuleRootCert into (uefi_dtbs.elf, " + "uefi_dtbs.xz, or xbl_config.elf -- ELF type and .xz compression are " + "auto-detected). Repeat for multiple images. Patched copies are " + "staged in ./patched_images/ and searched ahead of -images, so the " + "originals under -images are never modified. Omit if the images are " + "already patched (e.g. via a separate patch-capsule-cert step).", + ) + parser.add_argument( + "--update-partitions", + dest="update_partitions", + default=None, + help="Comma-separated base partition names (e.g. dtb,uefi_dtb) to mark " + "Operation=UPDATE in the generated FvUpdate.xml; all other entries " + "stay Operation=IGNORE. Omit to keep every entry IGNORE (unchanged " + "default behavior).", + ) + + args = parser.parse_args() + if args.patch_image and not args.patch_cert: + parser.error("--patch-cert is required when --patch-image is given") + _run(args) + + +if __name__ == "__main__": + main() diff --git a/debian.qcom/scripts/qcom_capsule_tool/cli.py b/debian.qcom/scripts/qcom_capsule_tool/cli.py new file mode 100644 index 0000000000000..6d6b8350b9346 --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/cli.py @@ -0,0 +1,122 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + +"""Unified CLI dispatcher for qcom-capsule-tool. + +Usage: + qcom-capsule-tool [args ...] + +Each subcommand delegates to the corresponding module's main() function, +passing remaining arguments through sys.argv. +""" + +import argparse +import sys + + +# Lazy-import helpers -- keep startup fast by importing modules only +# when their subcommand is actually invoked. + + +def _cmd_create(argv): + sys.argv = ["qcom-capsule-tool create"] + argv + from qcom_capsule_tool.capsule_creator import main + + main() + + +def _cmd_fv_create(argv): + sys.argv = ["qcom-capsule-tool fv-create"] + argv + from qcom_capsule_tool.FVCreation import main + + main() + + +def _cmd_generate_capsule(argv): + sys.argv = ["qcom-capsule-tool generate-capsule"] + argv + from qcom_capsule_tool.generate_capsule import main + + main() + + +def _cmd_update_fv_xml(argv): + sys.argv = ["qcom-capsule-tool update-fv-xml"] + argv + from qcom_capsule_tool.UpdateFvXml import main + + main() + + +def _cmd_update_json(argv): + sys.argv = ["qcom-capsule-tool update-json"] + argv + from qcom_capsule_tool.UpdateJsonParameters import main + + main() + + +def _cmd_sysfw_version_create(argv): + sys.argv = ["qcom-capsule-tool sysfw-version-create"] + argv + from qcom_capsule_tool.SYSFW_VERSION_program import main + + main() + + +def _cmd_bin_to_hex(argv): + sys.argv = ["qcom-capsule-tool bin-to-hex"] + argv + from qcom_capsule_tool.BinToHex import main + + main() + + +def _cmd_patch_capsule_cert(argv): + sys.argv = ["qcom-capsule-tool patch-capsule-cert"] + argv + from qcom_capsule_tool.patch_capsule_cert import main + + main() + + +SUBCOMMANDS = { + "create": ("Run the full capsule generation pipeline", _cmd_create), + "fv-create": ("Create a firmware volume from XML + binaries", _cmd_fv_create), + "generate-capsule": ( + "Generate a signed FMP capsule from JSON", + _cmd_generate_capsule, + ), + "update-fv-xml": ("Generate FvUpdate.xml from partitions.conf", _cmd_update_fv_xml), + "update-json": ("Update JSON config with firmware parameters", _cmd_update_json), + "sysfw-version-create": ( + "Generate or inspect SYSFW_VERSION.bin", + _cmd_sysfw_version_create, + ), + "bin-to-hex": ("Convert a binary file to hex format", _cmd_bin_to_hex), + "patch-capsule-cert": ( + "Patch QcCapsuleRootCert in a uefi_dtbs or xbl_config ELF (auto-detected)", + _cmd_patch_capsule_cert, + ), +} + + +def main(): + parser = argparse.ArgumentParser( + prog="qcom-capsule-tool", + description="Qualcomm capsule generation tools for UEFI firmware updates", + ) + sub = parser.add_subparsers(dest="subcommand", title="subcommands") + + for name, (help_text, _) in SUBCOMMANDS.items(): + sub.add_parser(name, help=help_text, add_help=False) + + # Parse only the first positional arg; the rest is forwarded. + args, remaining = parser.parse_known_args() + + if args.subcommand is None: + parser.print_help() + sys.exit(1) + + _, handler = SUBCOMMANDS[args.subcommand] + handler(remaining) + + +if __name__ == "__main__": + main() diff --git a/debian.qcom/scripts/qcom_capsule_tool/fv_builder.py b/debian.qcom/scripts/qcom_capsule_tool/fv_builder.py new file mode 100644 index 0000000000000..ffc97de7cb1fe --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/fv_builder.py @@ -0,0 +1,131 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + +"""Pure-Python replacement for the edk2 BaseTools GenFfs/GenFv binaries. + +Implements exactly the subset FVCreation.py used to invoke: + + GenFfs -t EFI_FV_FILETYPE_RAW -g -s -i + GenFv -i FVMain.inf (fixed [options]/[attributes] template) + +The emitted structures are defined in the UEFI Platform Initialization +(PI) Specification, Volume 3: Shared Architectural Elements +(https://uefi.org/specs/PI/1.8/): EFI_FIRMWARE_VOLUME_HEADER (3.2.1.1), +EFI_FIRMWARE_FILE_SYSTEM2_GUID (3.2.2.1) and EFI_FFS_FILE_HEADER +(3.2.3.1). + +Output is byte-identical to the edk2 tools for this subset; the golden +fixtures in tests/ were generated with the real GenFfs/GenFv binaries. +""" + +import struct +import uuid +from typing import Iterable, List + +EFI_FV_FILETYPE_RAW = 0x01 +FFS_ATTRIB_CHECKSUM = 0x40 +FFS_HEADER_SIZE = 24 +# EFI_FILE_HEADER_CONSTRUCTION | EFI_FILE_HEADER_VALID | EFI_FILE_DATA_VALID +FFS_FILE_STATE = 0x07 +# The FFS2 file size field is 24 bits wide and includes the header. +FFS_MAX_SIZE = 0xFFFFFF + +EFI_FIRMWARE_FILE_SYSTEM2_GUID = uuid.UUID("8c8ce578-8a3d-4f1c-9935-896185c32dd3") +FV_SIGNATURE = b"_FVH" +FV_HEADER_REVISION = 2 +FV_BLOCK_SIZE = 0x40 +# 0x38 bytes of fixed fields plus two block-map entries (one + terminator). +FV_HEADER_SIZE = 0x48 +# The EFI_FVB2 attribute set from the FVMain.inf template FVCreation has +# always generated: all read/write/lock capability and status bits, +# STICKY_WRITE, MEMORY_MAPPED, ERASE_POLARITY=1 and ALIGNMENT_8. +FV_ATTRIBUTES = 0x0003FEFF +# Erase polarity 1: free space reads back as 0xFF and FFS state bits +# are stored inverted. +FV_ERASED_BYTE = 0xFF +FFS_ALIGNMENT = 8 + + +def _checksum8(data: Iterable[int]) -> int: + """Return the value that makes the 8-bit sum of data zero.""" + return (0x100 - sum(data)) & 0xFF + + +def build_raw_ffs(file_guid: str, payload: bytes) -> bytes: + """Wrap payload in an EFI_FV_FILETYPE_RAW FFS2 file with data checksum.""" + total_size = FFS_HEADER_SIZE + len(payload) + if total_size > FFS_MAX_SIZE: + raise ValueError( + f"payload of {len(payload)} bytes exceeds the 16 MiB FFS2 file limit" + ) + + header = bytearray(FFS_HEADER_SIZE) + header[0:16] = uuid.UUID(file_guid).bytes_le + header[18] = EFI_FV_FILETYPE_RAW + header[19] = FFS_ATTRIB_CHECKSUM + header[20:23] = total_size.to_bytes(3, "little") + # Both IntegrityCheck bytes and State must be zero while the header + # checksum is computed; State is excluded from it permanently. + header[16] = _checksum8(header) + header[17] = _checksum8(payload) + header[23] = FFS_FILE_STATE + return bytes(header) + payload + + +def build_fv(ffs_images: List[bytes]) -> bytes: + """Assemble FFS files into a firmware volume (FFS2, 0x40-byte blocks).""" + body = bytearray() + for ffs in ffs_images: + pad = -(FV_HEADER_SIZE + len(body)) % FFS_ALIGNMENT + body += bytes([FV_ERASED_BYTE]) * pad + # Erase polarity 1 stores the FFS state bits inverted. + body += ffs[:23] + bytes([ffs[23] ^ 0xFF]) + ffs[24:] + + fv_length = FV_HEADER_SIZE + len(body) + fv_length += -fv_length % FV_BLOCK_SIZE + + header = bytearray( + struct.pack( + "<16s16sQ4sIHHHBBIIII", + b"", + EFI_FIRMWARE_FILE_SYSTEM2_GUID.bytes_le, + fv_length, + FV_SIGNATURE, + FV_ATTRIBUTES, + FV_HEADER_SIZE, + 0, # Checksum, filled in below + 0, # ExtHeaderOffset + 0, # Reserved + FV_HEADER_REVISION, + fv_length // FV_BLOCK_SIZE, + FV_BLOCK_SIZE, + 0, # block map terminator + 0, + ) + ) + checksum = (0x10000 - sum(struct.unpack("<36H", header))) & 0xFFFF + header[0x32:0x34] = struct.pack(" None: + """Create an FFS file from a raw input binary (GenFfs replacement).""" + with open(input_path, "rb") as f: + payload = f.read() + with open(output_path, "wb") as f: + f.write(build_raw_ffs(file_guid, payload)) + + +def write_fv(output_path: str, ffs_paths: List[str]) -> None: + """Create a firmware volume from FFS files (GenFv replacement).""" + ffs_images = [] + for path in ffs_paths: + with open(path, "rb") as f: + ffs_images.append(f.read()) + with open(output_path, "wb") as f: + f.write(build_fv(ffs_images)) diff --git a/debian.qcom/scripts/qcom_capsule_tool/generate_capsule.py b/debian.qcom/scripts/qcom_capsule_tool/generate_capsule.py new file mode 100644 index 0000000000000..a561384172dcc --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/generate_capsule.py @@ -0,0 +1,404 @@ +# -------------------------------------------------------------------- +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear +# -------------------------------------------------------------------- + +"""Build signed UEFI FMP capsules from a JSON payload descriptor. + +A capsule nests the following structures: + + EFI_CAPSULE_HEADER + EFI_FIRMWARE_MANAGEMENT_CAPSULE_HEADER + ItemOffsetList + EFI_FIRMWARE_MANAGEMENT_CAPSULE_IMAGE_HEADER (v3, per payload) + EFI_FIRMWARE_IMAGE_AUTHENTICATION (PKCS7 via openssl smime) + FMP_PAYLOAD_HEADER ('MSS1') + payload + +All of these structures except FMP_PAYLOAD_HEADER are defined in the +UEFI Specification 2.10 (https://uefi.org/specs/UEFI/2.10/): +EFI_CAPSULE_HEADER in 8.5.3 (UpdateCapsule), the FMP capsule and image +headers in 23.3 (Delivering Capsules Containing Updates), and +EFI_FIRMWARE_IMAGE_AUTHENTICATION in 23.1. FMP_PAYLOAD_HEADER ('MSS1') +is the payload versioning convention consumed by the Qualcomm FMP +driver (see FMP_PAYLOAD_HEADER_SIGNATURE in FVCreation.py). + +The command line and the JSON descriptor schema are compatible with +the encode and --dump-info modes of edk2 BaseTools GenerateCapsule.py, +so this subcommand is a drop-in replacement for it; the implementation +is independent and only the interface is shared. signtool signing and +capsule dependency expressions are not supported. The capsule bytes +produced are identical to what shipped devices already receive. +""" + +import argparse +import json +import os +import struct +import subprocess +import sys +import uuid +from typing import List, Optional + +EFI_FIRMWARE_MANAGEMENT_CAPSULE_ID_GUID = uuid.UUID( + "6dcbd5ed-e82d-4c44-bda1-7194199ad92a" +) +EFI_CERT_TYPE_PKCS7_GUID = uuid.UUID("4aafd29d-68df-49ee-8aa9-347d375665a7") + +# Historical capsules for these targets declare a 32-byte outer header: +# the 28-byte EFI_CAPSULE_HEADER followed by 4 reserved bytes. Parsers +# locate the payload through the HeaderSize field, and 32 is what +# shipped device firmware has always been given, so keep it. +CAPSULE_HEADER_SIZE = 32 +CAPSULE_FLAGS = { + "PersistAcrossReset": 0x00010000, + "PopulateSystemTable": 0x00020000, + "InitiateReset": 0x00040000, +} + +FMP_CAPSULE_HEADER_VERSION = 1 +FMP_CAPSULE_HEADER_SIZE = 8 +FMP_IMAGE_HEADER_VERSION = 3 +FMP_IMAGE_HEADER_SIZE = 48 +CAPSULE_SUPPORT_AUTHENTICATION = 0x0000000000000001 + +# dwLength counts the WIN_CERTIFICATE fields plus the CertType GUID +# and the certificate data, but not the leading monotonic count. +WIN_CERT_PREFIX_LEN = 24 +WIN_CERT_REVISION = 0x0200 +WIN_CERT_TYPE_EFI_GUID = 0x0EF1 +AUTH_HEADER_SIZE = 8 + WIN_CERT_PREFIX_LEN + +FMP_PAYLOAD_SIGNATURE = b"MSS1" +FMP_PAYLOAD_HEADER_SIZE = 16 + +DEFAULT_HASH_ALGORITHM = "sha256" + + +class PayloadDescriptor: + """One entry of the JSON "Payloads" list, with defaults applied.""" + + def __init__(self, config: dict): + def to_int(field: str, default: Optional[int] = None) -> int: + if field not in config: + if default is None: + raise ValueError(f"missing required JSON field {field}") + return default + value = config[field] + return int(value, 0) if isinstance(value, str) else int(value) + + def to_path(field: str) -> Optional[str]: + value = os.path.expandvars(str(config.get(field, ""))).strip() + return value or None + + if "Payload" not in config: + raise ValueError("missing required JSON field Payload") + self.payload_file: str = os.path.expandvars(config["Payload"]) + self.guid = uuid.UUID(config["Guid"]) + self.fw_version = to_int("FwVersion") + self.lowest_supported_version = to_int("LowestSupportedVersion") + self.monotonic_count = to_int("MonotonicCount", 0) + self.hardware_instance = to_int("HardwareInstance", 0) + self.update_image_index = to_int("UpdateImageIndex", 1) + self.hash_algorithm = str(config.get("HashAlgorithm") or DEFAULT_HASH_ALGORITHM) + self.signer_private_cert = to_path("OpenSslSignerPrivateCertFile") + self.other_public_cert = to_path("OpenSslOtherPublicCertFile") + self.trusted_public_cert = to_path("OpenSslTrustedPublicCertFile") + self.signing_tool_path = to_path("SigningToolPath") + + if config.get("SignToolPfxFile") or config.get("SignToolSubjectName"): + raise ValueError("signtool signing is not supported; use OpenSSL fields") + if config.get("Dependencies"): + raise ValueError("capsule dependency expressions are not supported") + + certs = ( + self.signer_private_cert, + self.other_public_cert, + self.trusted_public_cert, + ) + self.sign = any(certs) + if self.sign and not all(certs): + raise ValueError( + "incomplete OpenSSL certificate set: " + "OpenSslSignerPrivateCertFile, OpenSslOtherPublicCertFile " + "and OpenSslTrustedPublicCertFile must all be set to sign" + ) + + if self.fw_version >> 32: + raise ValueError("FwVersion does not fit in 32 bits") + if self.lowest_supported_version >> 32: + raise ValueError("LowestSupportedVersion does not fit in 32 bits") + if not 1 <= self.update_image_index <= 0xFF: + raise ValueError("UpdateImageIndex must be between 0x1 and 0xff") + + +def sign_payload_openssl( + payload: bytes, + tool_path: Optional[str], + signer_private_cert: str, + other_public_cert: str, + hash_algorithm: str, + verbose: bool, +) -> bytes: + """Produce a detached PKCS7 signature over payload with openssl.""" + command = [ + os.path.join(tool_path or "", "openssl"), + "smime", + "-sign", + "-binary", + "-outform", + "DER", + "-md", + hash_algorithm, + "-signer", + signer_private_cert, + "-certfile", + other_public_cert, + ] + if verbose: + print(" ".join(command)) + result = subprocess.run(command, input=payload, capture_output=True) + if result.returncode != 0: + print(result.stderr.decode()) + raise ValueError(f"openssl smime exited with status {result.returncode}") + return result.stdout + + +def encode_payload(descriptor: PayloadDescriptor, verbose: bool) -> bytes: + """Wrap one payload: FMP payload header, then optional signing.""" + with open(descriptor.payload_file, "rb") as f: + payload = f.read() + + image = ( + FMP_PAYLOAD_SIGNATURE + + struct.pack( + "<3I", + FMP_PAYLOAD_HEADER_SIZE, + descriptor.fw_version, + descriptor.lowest_supported_version, + ) + + payload + ) + + if not descriptor.sign: + print("WARNING: no OpenSSL certificates given, unsigned capsule payload") + return image + + # The signature covers the image with the 64-bit monotonic count + # appended; the count itself travels in the authentication header. + cert_data = sign_payload_openssl( + image + struct.pack(" bytes: + """Assemble the FMP capsule body and the outer capsule header.""" + items = list(embedded_drivers) + for descriptor in descriptors: + image_payload = encode_payload(descriptor, verbose) + capsule_support = CAPSULE_SUPPORT_AUTHENTICATION if descriptor.sign else 0 + items.append( + struct.pack(" None: + """Decode a capsule file and print its structure (--dump-info).""" + with open(capsule_file, "rb") as f: + data = f.read() + + guid = uuid.UUID(bytes_le=data[:16]) + header_size, flags, image_size, _ = struct.unpack_from("<4I", data, 16) + flag_names = ", ".join(n for n, bit in CAPSULE_FLAGS.items() if flags & bit) + known = " (firmware management capsule)" + print(f"capsule: {capsule_file}") + print(" outer header") + print( + f" capsule guid : {guid}" + f"{known if guid == EFI_FIRMWARE_MANAGEMENT_CAPSULE_ID_GUID else ''}" + ) + print(f" header size : {header_size} bytes") + print(f" flags : {flags:#010x} [{flag_names}]") + print(f" total size : {image_size} bytes") + if image_size != len(data): + raise ValueError( + f"declared capsule size {image_size} does not match file size {len(data)}" + ) + + fmp = data[header_size:] + version, driver_count, item_count = struct.unpack_from(" None: + parser = argparse.ArgumentParser( + prog="qcom-capsule-tool generate-capsule", + description="Generate a signed UEFI FMP capsule from a JSON payload " + "descriptor (drop-in for the encode mode of edk2 GenerateCapsule.py)", + ) + parser.add_argument("-e", "--encode", action="store_true", help="Encode a capsule") + parser.add_argument( + "-j", "--json-file", dest="json_file", help="JSON payload descriptor file" + ) + parser.add_argument("-o", "--output", dest="output_file", help="Output file") + parser.add_argument( + "--capflag", + dest="capsule_flags", + action="append", + default=[], + choices=sorted(CAPSULE_FLAGS), + help="Capsule flag, may be repeated", + ) + parser.add_argument( + "--capoemflag", + dest="oem_flags", + type=lambda value: int(value, 0), + default=0, + help="OEM flag bits 0x0000..0xffff", + ) + parser.add_argument( + "--dump-info", dest="dump_file", help="Decode and display a capsule file" + ) + parser.add_argument("-v", "--verbose", action="store_true") + args = parser.parse_args() + + try: + if args.dump_file: + dump_info(args.dump_file) + return + if not args.encode: + parser.error("one of --encode or --dump-info is required") + if not args.json_file or not args.output_file: + parser.error("--encode requires --json-file and --output") + if "InitiateReset" in args.capsule_flags and ( + "PersistAcrossReset" not in args.capsule_flags + ): + parser.error("--capflag InitiateReset also requires PersistAcrossReset") + if args.oem_flags > 0xFFFF: + parser.error("--capoemflag must be between 0x0000 and 0xffff") + + with open(args.json_file, "r") as f: + config = json.load(f) + descriptors = [ + PayloadDescriptor(payload) for payload in config.get("Payloads", []) + ] + if not descriptors: + raise ValueError(f'no "Payloads" entries in {args.json_file}') + embedded_drivers = [] + for driver in config.get("EmbeddedDrivers", []): + with open(os.path.expandvars(driver["Driver"]), "rb") as f: + embedded_drivers.append(f.read()) + + capsule = encode_capsule( + descriptors, + embedded_drivers, + args.capsule_flags, + args.oem_flags, + args.verbose, + ) + with open(args.output_file, "wb") as f: + f.write(capsule) + if args.verbose: + print(f"Wrote capsule {args.output_file} ({len(capsule)} bytes)") + except (OSError, ValueError, KeyError, struct.error) as e: + print(f"generate-capsule: error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/debian.qcom/scripts/qcom_capsule_tool/patch_capsule_cert.py b/debian.qcom/scripts/qcom_capsule_tool/patch_capsule_cert.py new file mode 100644 index 0000000000000..73731f9752b62 --- /dev/null +++ b/debian.qcom/scripts/qcom_capsule_tool/patch_capsule_cert.py @@ -0,0 +1,1039 @@ +#!/usr/bin/env python3 +# Copyright (c) Qualcomm Innovation Center, Inc. All rights reserved. +# SPDX-License-Identifier: BSD-3-Clause-Clear + +""" +patch-capsule-cert: Patch QcCapsuleRootCert in a uefi_dtbs or xbl_config ELF. + +Auto-detects the ELF type by scanning program-header payloads: + - uefi_dtbs : one or more ELF segments contain raw DTBs (FDT magic 0xd00dfeed). + The certificate is stored as a DTB property and is replaced in + every DTB that carries it. + - xbl_config : PH#1 contains a valid XBLConfig metadata blob (4-byte ASCII + type tag + version + entry count). The certificate is stored + as a DTB property inside one of the named DTB payload segments. + +Both paths accept a plain DER (.cer) certificate file. + +A `.xz`-suffixed input or output path is transparently decompressed/ +recompressed (pure-Python `lzma`) around the patch step. + +Usage: + qcom-capsule-tool patch-capsule-cert \\ + [--prop-name QcCapsuleRootCert] +""" + +import argparse +import hashlib +import io +import lzma +import os +import re +import struct +import sys +import tempfile +from dataclasses import dataclass +from io import BytesIO +from typing import List, Optional, Tuple + +import libfdt +from elftools.elf.elffile import ELFFile + +from qcom_capsule_tool.BinToHex import bin_to_hex + +# ============================================================ +# ELF header read/write helpers (was elf_utils.py) +# ============================================================ + +# ELF32 Phdr: type(4) offset(4) vaddr(4) paddr(4) filesz(4) memsz(4) flags(4) align(4) +# ELF64 Phdr: type(4) flags(4) offset(8) vaddr(8) paddr(8) filesz(8) memsz(8) align(8) +# ELF32 Shdr: name(4) type(4) flags(4) addr(4) offset(4) ... +# ELF64 Shdr: name(4) type(4) flags(8) addr(8) offset(8) ... + + +def _ph_file_offset_field(is_64: bool) -> Tuple[int, int]: + return (8, 8) if is_64 else (4, 4) + + +def _ph_filesz_field(is_64: bool) -> Tuple[int, int]: + return (0x20, 8) if is_64 else (0x10, 4) + + +def _ph_memsz_field(is_64: bool) -> Tuple[int, int]: + return (0x28, 8) if is_64 else (0x14, 4) + + +def _sh_offset_field(is_64: bool) -> Tuple[int, int]: + return (24, 8) if is_64 else (16, 4) + + +def _pack(endian: str, size: int, value: int) -> bytes: + return struct.pack(endian + {4: "I", 8: "Q"}[size], value) + + +def _write_ph_field( + data: bytearray, + elf: ELFFile, + seg_idx: int, + field_off: int, + field_size: int, + value: int, +) -> None: + endian = "<" if elf.little_endian else ">" + pos = elf.header["e_phoff"] + seg_idx * elf.header["e_phentsize"] + field_off + data[pos : pos + field_size] = _pack(endian, field_size, value) + + +def _write_sh_field( + data: bytearray, + elf: ELFFile, + sec_idx: int, + field_off: int, + field_size: int, + value: int, +) -> None: + endian = "<" if elf.little_endian else ">" + pos = elf.header["e_shoff"] + sec_idx * elf.header["e_shentsize"] + field_off + data[pos : pos + field_size] = _pack(endian, field_size, value) + + +def _update_elf_headers_for_growth( + data: bytearray, elf: ELFFile, seg_file_offset: int, grow: int +) -> None: + """ + After splicing *grow* bytes at *seg_file_offset*, fix all ELF offsets that + point past the splice point (p_offset, sh_offset, e_shoff). + *elf* must be bound to the pre-splice bytes so header field positions are valid. + """ + is_64 = elf.elfclass == 64 + endian = "<" if elf.little_endian else ">" + + off_field, off_sz = _ph_file_offset_field(is_64) + for i, seg in enumerate(elf.iter_segments()): + if seg["p_offset"] > seg_file_offset: + _write_ph_field(data, elf, i, off_field, off_sz, seg["p_offset"] + grow) + + sh_off_field, sh_off_sz = _sh_offset_field(is_64) + for i, sec in enumerate(elf.iter_sections()): + if sec["sh_offset"] > seg_file_offset: + _write_sh_field( + data, elf, i, sh_off_field, sh_off_sz, sec["sh_offset"] + grow + ) + + e_shoff = elf.header["e_shoff"] + if e_shoff > seg_file_offset: + e_shoff_pos = 0x28 if is_64 else 0x20 + e_shoff_sz = 8 if is_64 else 4 + data[e_shoff_pos : e_shoff_pos + e_shoff_sz] = _pack( + endian, e_shoff_sz, e_shoff + grow + ) + + +# ============================================================ +# DTB property setter (was set_dtb_property.py) +# ============================================================ + + +def _encode_dtb_value(value: str) -> bytes: + """ + Encode a value string for FDT: + @file: -> binary split into 32-bit big-endian words + @list: -> text file with hex/decimal ints, each -> 32-bit word + single int -> 4-byte big-endian + int list -> array of 4-byte big-endian words + otherwise -> UTF-8 string + """ + value = value.strip() + + if value.startswith("@file:"): + data = open(value[6:], "rb").read() + if len(data) % 4 != 0: + data += b"\x00" * (4 - (len(data) % 4)) + return b"".join( + struct.pack(">I", struct.unpack(">I", data[i : i + 4])[0]) + for i in range(0, len(data), 4) + ) + + if value.startswith("@list:"): + text = open(value[6:]).read() + parts = re.split(r"[\s,]+", text.strip()) + return b"".join(struct.pack(">I", int(p, 16)) for p in parts if p) + + int_pattern = re.compile(r"^-?(0x[0-9a-fA-F]+|\d+)$") + int_list_pattern = re.compile( + r"^(-?(0x[0-9a-fA-F]+|\d+)[ ,]+)+(-?(0x[0-9a-fA-F]+|\d+))$" + ) + if int_pattern.match(value): + return struct.pack(">I", int(value, 0)) + if int_list_pattern.match(value + " "): + parts = re.split(r"[ ,]+", value.strip()) + return b"".join(struct.pack(">I", int(p, 0)) for p in parts if p) + + return value.encode("utf-8") + + +def _set_dtb_property( + dtb_path: str, + node_path: str, + prop_name: str, + value: str, + out_path: str, + extra_space: int = 1024, +) -> None: + """Set or add a property in a DTB, automatically resizing if needed.""" + with open(dtb_path, "rb") as f: + dtb_data = f.read() + + fdt_obj = libfdt.Fdt(dtb_data) + + try: + node_off = fdt_obj.path_offset(node_path) + except libfdt.FdtException: + raise ValueError(f"Node path '{node_path}' not found in DTB") + + value_bytes = _encode_dtb_value(value) + + try: + fdt_obj.setprop(node_off, prop_name, value_bytes) + except libfdt.FdtException as e: + if hasattr(e, "err") and e.err == -libfdt.FDT_ERR_NOSPACE: + fdt_obj.resize( + len(fdt_obj.as_bytearray()) + max(len(value_bytes), extra_space) + ) + fdt_obj.setprop(node_off, prop_name, value_bytes) + else: + raise + + with open(out_path, "wb") as f: + f.write(fdt_obj.as_bytearray()) + + +# ============================================================ +# DTB scanner, cert-node walker, single-DTB patcher (was dtb_utils.py) +# ============================================================ + +DTB_MAGIC = 0xD00DFEED +_DEFAULT_PROP_NAME = "QcCapsuleRootCert" + + +def _scan_dtbs(data: bytes) -> List[Tuple[int, int]]: + """Return (offset, totalsize) for every DTB found in *data*.""" + results: List[Tuple[int, int]] = [] + i = 0 + while i <= len(data) - 8: + if struct.unpack(">I", data[i : i + 4])[0] == DTB_MAGIC: + size = struct.unpack(">I", data[i + 4 : i + 8])[0] + if size >= 8 and i + size <= len(data): + results.append((i, size)) + i = (i + size + 3) & ~3 + continue + i += 4 + return results + + +def _fdt_first_subnode(fdt: libfdt.Fdt, node_off: int) -> int: + try: + return fdt.first_subnode(node_off) + except libfdt.FdtException: + return -1 + + +def _fdt_next_subnode(fdt: libfdt.Fdt, node_off: int) -> int: + try: + return fdt.next_subnode(node_off) + except libfdt.FdtException: + return -1 + + +def _find_cert_node( + dtb_bytes: bytes, prop_name: str = _DEFAULT_PROP_NAME +) -> Optional[str]: + """ + Walk *dtb_bytes* and return the first node path that owns *prop_name*. + Handles both regular DTBs (/sw/uefi/uefiplat) and overlay DTBs + (/fragment@N/__overlay__/.../uefiplat) without dtc dependency. + """ + try: + fdt = libfdt.Fdt(dtb_bytes) + except Exception: + return None + + def _walk(node_off: int, path: str) -> Optional[str]: + try: + fdt.getprop(node_off, prop_name) + return path + except libfdt.FdtException: + pass + child = _fdt_first_subnode(fdt, node_off) + while child >= 0: + try: + name = fdt.get_name(child) + except Exception: + child = _fdt_next_subnode(fdt, child) + continue + child_path = path + name if path == "/" else f"{path}/{name}" + result = _walk(child, child_path) + if result is not None: + return result + child = _fdt_next_subnode(fdt, child) + return None + + try: + root = fdt.path_offset("/") + return _walk(root, "/") + except Exception: + return None + + +def _patch_dtb( + dtb_bytes: bytes, + node_path: str, + cert_inc_path: str, + prop_name: str = _DEFAULT_PROP_NAME, +) -> bytes: + """Patch *prop_name* in a single DTB and return the patched bytes.""" + tmp_in = tmp_out = "" + try: + tmp_in_fd, tmp_in = tempfile.mkstemp(suffix=".dtb") + os.close(tmp_in_fd) + with open(tmp_in, "wb") as f: + f.write(dtb_bytes) + tmp_out_fd, tmp_out = tempfile.mkstemp(suffix=".dtb") + os.close(tmp_out_fd) + os.unlink(tmp_out) + _set_dtb_property( + tmp_in, node_path, prop_name, f"@list:{cert_inc_path}", tmp_out + ) + with open(tmp_out, "rb") as f: + return f.read() + finally: + for p in (tmp_in, tmp_out): + try: + os.unlink(p) + except OSError: + pass + + +# ============================================================ +# XBLConfig metadata parser + replace_ph (was xblconfig_parser.py) +# ============================================================ + +# Metadata v2 layout (PH blob): +# Header (12 bytes): xcfg_type[4] major[1] minor[1] entries[2-LE] meta_size[4-LE] +# Item (repeated): attributes[4] offset_from_meta_start[4] item_size[4] +# chipinfo[8] platforminfo[8] config_name_len[4] +# config_name[config_name_len] PAD-to-8 + + +@dataclass +class _MetaHeader: + xcfg_type: str + major: int + minor: int + entries: int + meta_size: int + + +@dataclass +class _MetaItemV2: + attributes: int + offset_from_meta_start: int + item_size: int + chipinfo: int + platforminfo: int + config_name_len: int + config_name: str + start_off: int + end_off: int + + +def _align_up(x: int, a: int) -> int: + r = x % a + return x if r == 0 else x + (a - r) + + +def _parse_meta_header(blob: bytes, off: int = 0) -> Tuple[_MetaHeader, int]: + if off + 12 > len(blob): + raise ValueError("Metadata header truncated") + xcfg_type = blob[off : off + 4].decode("ascii", errors="replace") + major = blob[off + 4] + minor = blob[off + 5] + entries = int.from_bytes(blob[off + 6 : off + 8], "little") + meta_size = int.from_bytes(blob[off + 8 : off + 12], "little") + return _MetaHeader(xcfg_type, major, minor, entries, meta_size), off + 12 + + +def _parse_meta_items_v2( + blob: bytes, off: int, count: int +) -> Tuple[List[_MetaItemV2], int]: + items: List[_MetaItemV2] = [] + cur = off + for idx in range(count): + item_start = cur + if cur + 32 > len(blob): + raise ValueError(f"Metadata v2 item {idx} truncated") + attributes = int.from_bytes(blob[cur + 0 : cur + 4], "little") + ofs_from_meta_start = int.from_bytes(blob[cur + 4 : cur + 8], "little") + item_size = int.from_bytes(blob[cur + 8 : cur + 12], "little") + chipinfo = int.from_bytes(blob[cur + 12 : cur + 20], "little") + platforminfo = int.from_bytes(blob[cur + 20 : cur + 28], "little") + name_len = int.from_bytes(blob[cur + 28 : cur + 32], "little") + cur += 32 + if cur + name_len > len(blob): + raise ValueError(f"Metadata v2 item {idx} name truncated") + config_name = blob[cur : cur + name_len].decode("utf-8", errors="replace") + cur += name_len + padded_len = _align_up(cur - item_start, 8) + cur = item_start + padded_len + items.append( + _MetaItemV2( + attributes=attributes, + offset_from_meta_start=ofs_from_meta_start, + item_size=item_size, + chipinfo=chipinfo, + platforminfo=platforminfo, + config_name_len=name_len, + config_name=config_name, + start_off=item_start, + end_off=cur, + ) + ) + return items, cur + + +def _load_elf(elf_path: str) -> Tuple[bytearray, ELFFile, List]: + with open(elf_path, "rb") as f: + file_bytes = f.read() + data = bytearray(file_bytes) + elf = ELFFile(io.BytesIO(file_bytes)) + return data, elf, list(elf.iter_segments()) + + +def _parse_metadata_from_ph( + elf: ELFFile, meta_ph_index: int +) -> Tuple[_MetaHeader, List[_MetaItemV2], bytes, int]: + """Return (header, items, meta_blob, meta_file_offset).""" + segments = list(elf.iter_segments()) + if not segments or meta_ph_index >= len(segments): + raise IndexError(f"Program header #{meta_ph_index} not found") + meta_seg = segments[meta_ph_index] + meta_blob = meta_seg.data() + ph = elf._get_segment_header(meta_ph_index) + meta_file_off = ph["p_offset"] + hdr, off = _parse_meta_header(meta_blob, 0) + items, _ = _parse_meta_items_v2(meta_blob, off, hdr.entries) + return hdr, items, meta_blob, meta_file_off + + +def _replace_ph( + elf_path: str, + target_ph_index: int, + new_file: str, + output_file: str, + meta_ph_index: int, +) -> None: + """ + Replace the payload in *target_ph_index* with the contents of *new_file*, + then update p_filesz/p_memsz, metadata item_size, and SHA-384 hash. + """ + data, elf, segments = _load_elf(elf_path) + + if target_ph_index >= len(segments): + raise IndexError(f"Target program header #{target_ph_index} not found") + + old_seg = segments[target_ph_index] + old_data = old_seg.data() + old_size = old_seg["p_filesz"] + seg_offset = old_seg["p_offset"] + + with open(new_file, "rb") as f: + new_data = f.read() + new_size = len(new_data) + + old_hash = hashlib.sha384(old_data[:old_size]).digest() + new_hash = hashlib.sha384(new_data).digest() + + print( + f"[i] Replacing PH#{target_ph_index}: old size={old_size}, new size={new_size}" + ) + print(f"[i] Old SHA-384: {old_hash.hex()}") + print(f"[i] New SHA-384: {new_hash.hex()}") + + is_64 = elf.elfclass == 64 + grow_size = new_size - old_size + + # Save hashes of segments whose content will be modified by side effects: + # - PH#0 (ELF/program header segment): _write_ph_field and + # _update_elf_headers_for_growth both write into the program header table, + # which lives inside PH#0's file region. + # - PH#meta_ph_index (XBLConfig metadata segment): metadata item_size is + # patched directly inside this segment's payload. + # These must be captured before any writes so the old hashes can be located + # and replaced in the hash table after all modifications are done. + side_effect_phs: List[Tuple[int, bytes]] = [] + for ph_i in set([0, meta_ph_index]): + if ph_i == target_ph_index or ph_i >= len(segments): + continue + seg_i = segments[ph_i] + d = seg_i.data() + if d: + side_effect_phs.append((ph_i, hashlib.sha384(d).digest())) + + if grow_size <= 0: + data[seg_offset : seg_offset + new_size] = new_data + if grow_size < 0: + data[seg_offset + new_size : seg_offset + old_size] = b"\x00" * (-grow_size) + else: + tail = bytes(data[seg_offset + old_size :]) + data[seg_offset : seg_offset + new_size] = new_data + new_tail_start = seg_offset + new_size + data[new_tail_start : new_tail_start + len(tail)] = tail + data.extend(b"\x00" * grow_size) + _update_elf_headers_for_growth(data, elf, seg_offset, grow_size) + + filesz_field, filesz_field_sz = _ph_filesz_field(is_64) + memsz_field, memsz_field_sz = _ph_memsz_field(is_64) + _write_ph_field(data, elf, target_ph_index, filesz_field, filesz_field_sz, new_size) + _write_ph_field(data, elf, target_ph_index, memsz_field, memsz_field_sz, new_size) + + meta_item_index = target_ph_index - (meta_ph_index + 1) + if meta_item_index >= 0: + try: + _, items, _, meta_file_off = _parse_metadata_from_ph(elf, meta_ph_index) + if meta_item_index < len(items): + it = items[meta_item_index] + abs_field_off = meta_file_off + it.start_off + 8 + data[abs_field_off : abs_field_off + 4] = struct.pack(" {new_size}" + ) + else: + print( + f"[!] meta_item_index={meta_item_index} out of range; item_size not updated" + ) + except Exception as exc: + print(f"[!] Could not update metadata item_size: {exc}") + + pos = bytes(data).find(old_hash) + if pos != -1: + print(f"[i] Found old SHA-384 at file offset 0x{pos:x}; replacing") + data[pos : pos + len(new_hash)] = new_hash + else: + print("[!] Old SHA-384 hash not found in ELF binary; hash table not updated") + + # Update hashes for side-effect segments (PH#0 and metadata PH). + data_bytes = bytes(data) + for ph_i, old_h in side_effect_phs: + seg_i = segments[ph_i] + new_h = hashlib.sha384( + data_bytes[seg_i["p_offset"] : seg_i["p_offset"] + seg_i["p_filesz"]] + ).digest() + if old_h == new_h: + continue + pos = data_bytes.find(old_h) + if pos != -1: + data[pos : pos + 48] = new_h + print(f"[i] PH#{ph_i} SHA-384 updated at file offset 0x{pos:x}") + else: + print( + f"[!] PH#{ph_i} SHA-384 not found in ELF binary; hash table not updated" + ) + + with open(output_file, "wb") as out: + out.write(data) + print(f"[+] Written patched ELF to '{output_file}'") + + +# ============================================================ +# uefi_dtbs patch logic (was patch_uefi_dtbs.py) +# ============================================================ + + +def _get_dtb_model(dtb_bytes: bytes) -> str: + try: + fdt = libfdt.Fdt(dtb_bytes) + root = fdt.path_offset("/") + prop = fdt.getprop(root, "model") + return bytes(prop).rstrip(b"\x00").decode("utf-8", errors="replace") + except Exception: + return "unknown" + + +def _patch_uefi_dtbs( + elf_path: str, + cert_inc_path: str, + output_path: str, + prop_name: str = _DEFAULT_PROP_NAME, +) -> List[dict]: + """ + Patch *prop_name* in every DTB embedded in a uefi_dtbs ELF. + + Returns a list of result dicts (one per DTB found) with keys: + segment, dtb_index, offset, model, node_path, status. + """ + with open(elf_path, "rb") as f: + raw = bytearray(f.read()) + + elf0 = ELFFile(BytesIO(bytes(raw))) + seg_indices_with_dtbs = [ + i for i, seg in enumerate(elf0.iter_segments()) if _scan_dtbs(seg.data()) + ] + + results: List[dict] = [] + + for seg_idx in seg_indices_with_dtbs: + elf = ELFFile(BytesIO(bytes(raw))) + is_64 = elf.elfclass == 64 + + seg = list(elf.iter_segments())[seg_idx] + seg_data = bytearray(seg.data()) + seg_file_offset = seg["p_offset"] + orig_seg_size = len(seg_data) + + dtbs = _scan_dtbs(bytes(seg_data)) + old_seg_hash = hashlib.sha384(bytes(seg_data)).digest() + + delta = 0 + seg_modified = False + per_dtb_hash_pairs: List[Tuple[bytes, bytes]] = [] + + for dtb_idx, (dtb_off_orig, dtb_sz) in enumerate(dtbs): + dtb_off = dtb_off_orig + delta + dtb_bytes = bytes(seg_data[dtb_off : dtb_off + dtb_sz]) + + model = _get_dtb_model(dtb_bytes) + node_path = _find_cert_node(dtb_bytes, prop_name) + + if node_path is None: + results.append( + dict( + segment=seg_idx, + dtb_index=dtb_idx, + offset=dtb_off, + model=model, + node_path=None, + status=f"skip (no {prop_name})", + ) + ) + continue + + try: + old_dtb_hash = hashlib.sha384(dtb_bytes).digest() + patched = _patch_dtb(dtb_bytes, node_path, cert_inc_path, prop_name) + new_dtb_hash = hashlib.sha384(patched).digest() + except Exception as exc: + results.append( + dict( + segment=seg_idx, + dtb_index=dtb_idx, + offset=dtb_off, + model=model, + node_path=node_path, + status=f"error: {exc}", + ) + ) + continue + + per_dtb_hash_pairs.append((old_dtb_hash, new_dtb_hash)) + seg_data = ( + seg_data[:dtb_off] + bytearray(patched) + seg_data[dtb_off + dtb_sz :] + ) + delta += len(patched) - dtb_sz + seg_modified = True + + results.append( + dict( + segment=seg_idx, + dtb_index=dtb_idx, + offset=dtb_off, + model=model, + node_path=node_path, + status="patched", + ) + ) + + if not seg_modified: + continue + + new_seg_hash = hashlib.sha384(bytes(seg_data)).digest() + grow = len(seg_data) - orig_seg_size + + # Save PH#0 hash before ELF headers are modified by growth fixup. + # _update_elf_headers_for_growth() rewrites p_offset fields inside + # PH#0's payload, so its SHA-384 changes and must be updated too. + ph0_seg = list(elf.iter_segments())[0] + ph0_off = ph0_seg["p_offset"] + ph0_filesz = ph0_seg["p_filesz"] + old_ph0_hash = ( + hashlib.sha384(bytes(raw[ph0_off : ph0_off + ph0_filesz])).digest() + if grow != 0 and ph0_filesz > 0 + else None + ) + + raw[seg_file_offset : seg_file_offset + orig_seg_size] = seg_data + + if grow != 0: + _update_elf_headers_for_growth(raw, elf, seg_file_offset, grow) + + filesz_f, filesz_sz = _ph_filesz_field(is_64) + memsz_f, memsz_sz = _ph_memsz_field(is_64) + _write_ph_field(raw, elf, seg_idx, filesz_f, filesz_sz, len(seg_data)) + _write_ph_field(raw, elf, seg_idx, memsz_f, memsz_sz, len(seg_data)) + + raw_bytes = bytes(raw) + segs_now = list(ELFFile(BytesIO(raw_bytes)).iter_segments()) + hash_seg = next( + (s for s in segs_now[seg_idx + 1 :] if s["p_type"] == "PT_NULL"), + None, + ) + if hash_seg is not None: + h_start = hash_seg["p_offset"] + h_end = h_start + hash_seg["p_filesz"] + else: + h_start, h_end = 0, len(raw_bytes) + + for old_h, new_h in per_dtb_hash_pairs: + pos = raw_bytes.find(old_h, h_start, h_end) + if pos != -1: + raw[pos : pos + 48] = new_h + print(f"[i] Per-DTB SHA-384 updated at file 0x{pos:x}") + else: + print("[!] Per-DTB SHA-384 not found in hash segment (non-fatal)") + + pos = raw_bytes.find(old_seg_hash, h_start, h_end) + if pos != -1: + raw[pos : pos + 48] = new_seg_hash + print(f"[i] Segment SHA-384 updated at file 0x{pos:x}") + else: + print("[!] Segment SHA-384 not found in hash segment (non-fatal)") + + if old_ph0_hash is not None: + new_ph0_hash = hashlib.sha384( + raw_bytes[ph0_off : ph0_off + ph0_filesz] + ).digest() + pos = raw_bytes.find(old_ph0_hash, h_start, h_end) + if pos != -1: + raw[pos : pos + 48] = new_ph0_hash + print( + f"[i] ELF-header segment (PH#0) SHA-384 updated at file 0x{pos:x}" + ) + else: + print( + "[!] ELF-header segment (PH#0) SHA-384 not found in hash segment (non-fatal)" + ) + + with open(output_path, "wb") as f: + f.write(raw) + + return results + + +# ============================================================ +# ELF-type detection +# ============================================================ + +ELF_TYPE_UEFI_DTBS = "uefi_dtbs" +ELF_TYPE_XBL_CONFIG = "xbl_config" + + +def _has_dtb_segment(elf: ELFFile) -> bool: + for seg in elf.iter_segments(): + data = seg.data() + for i in range(0, len(data) - 3, 4): + if struct.unpack(">I", data[i : i + 4])[0] == DTB_MAGIC: + return True + return False + + +def _has_xblconfig_metadata(elf: ELFFile, meta_ph_index: int = 1) -> bool: + segs = list(elf.iter_segments()) + if meta_ph_index >= len(segs): + return False + data = segs[meta_ph_index].data() + if len(data) < 12: + return False + if not all(0x20 <= b < 0x7F for b in data[:4]): + return False + try: + _parse_meta_header(data, 0) + return True + except Exception: + return False + + +def detect_elf_type(elf_path: str, meta_ph_index: int = 1) -> str: + with open(elf_path, "rb") as f: + elf = ELFFile(f) + if _has_xblconfig_metadata(elf, meta_ph_index): + return ELF_TYPE_XBL_CONFIG + if _has_dtb_segment(elf): + return ELF_TYPE_UEFI_DTBS + raise ValueError( + f"Cannot determine ELF type for '{elf_path}': " + "no XBLConfig metadata header and no DTB segments found." + ) + + +# ============================================================ +# xbl_config cert-patch path +# ============================================================ + + +def _patch_xbl_config( + elf_path: str, + cert_cer_path: str, + output_path: str, + prop_name: str, + meta_ph_index: int, +) -> None: + """ + Patch *prop_name* in xbl_config ELFs: + 1. Find which named segment contains a DTB with *prop_name*. + 2. Patch the DTB property with the new certificate. + 3. Call _replace_ph() to write back and update p_filesz/p_memsz, + xblconfig item_size, and SHA-384. + """ + inc_fd, inc_path = tempfile.mkstemp(suffix=".inc") + os.close(inc_fd) + try: + bin_to_hex(cert_cer_path, inc_path) + + with open(elf_path, "rb") as f: + raw = f.read() + elf = ELFFile(io.BytesIO(raw)) + segs = list(elf.iter_segments()) + _, items, _, _ = _parse_metadata_from_ph(elf, meta_ph_index) + + patched = skipped = errors = 0 + for idx, item in enumerate(list(items)): + ph_index = idx + meta_ph_index + 1 + if ph_index >= len(segs): + continue + + seg_data = segs[ph_index].data() + dtbs = _scan_dtbs(seg_data) + if not dtbs: + continue + + for dtb_off, dtb_sz in dtbs: + dtb_bytes = seg_data[dtb_off : dtb_off + dtb_sz] + node_path = _find_cert_node(dtb_bytes, prop_name) + if node_path is None: + skipped += 1 + continue + + print( + f"[+] xbl_config: found '{prop_name}' in " + f"'{item.config_name}' (PH#{ph_index}) at {node_path}" + ) + + try: + patched_dtb = _patch_dtb(dtb_bytes, node_path, inc_path, prop_name) + except Exception as exc: + print(f"[!] xbl_config: error patching '{item.config_name}': {exc}") + errors += 1 + continue + + # Always splice patched_dtb back into a copy of seg_data so + # any tail-padding bytes beyond the DTB totalsize are preserved. + new_seg = bytearray(seg_data) + new_seg[dtb_off : dtb_off + dtb_sz] = patched_dtb + new_seg_bytes = bytes(new_seg) + + tmp_fd, tmp_seg_path = tempfile.mkstemp(suffix=".dtb") + os.close(tmp_fd) + try: + with open(tmp_seg_path, "wb") as f: + f.write(new_seg_bytes) + _replace_ph( + elf_path=elf_path, + target_ph_index=ph_index, + new_file=tmp_seg_path, + output_file=output_path, + meta_ph_index=meta_ph_index, + ) + elf_path = output_path + with open(elf_path, "rb") as f: + raw = f.read() + elf = ELFFile(io.BytesIO(raw)) + segs = list(elf.iter_segments()) + patched += 1 + finally: + try: + os.unlink(tmp_seg_path) + except OSError: + pass + + print(f"[+] xbl_config: patched={patched} skipped={skipped} errors={errors}") + if errors: + sys.exit(1) + if patched == 0: + raise ValueError( + f"No DTB segment in '{elf_path}' contains property '{prop_name}'" + ) + finally: + try: + os.unlink(inc_path) + except OSError: + pass + + +# ============================================================ +# Top-level API +# ============================================================ + + +def _patch_capsule_cert_elf( + elf_path: str, + cert_cer_path: str, + output_path: str, + prop_name: str, + meta_ph_index: int, +) -> str: + """Patch a plain (non-xz) ELF. See patch_capsule_cert() for the public API.""" + elf_type = detect_elf_type(elf_path, meta_ph_index) + print(f"[+] Detected ELF type : {elf_type}") + + if elf_type == ELF_TYPE_UEFI_DTBS: + inc_fd, inc_path = tempfile.mkstemp(suffix=".inc") + os.close(inc_fd) + try: + bin_to_hex(cert_cer_path, inc_path) + results = _patch_uefi_dtbs(elf_path, inc_path, output_path, prop_name) + finally: + try: + os.unlink(inc_path) + except OSError: + pass + + patched = sum(1 for r in results if "patched" in r["status"]) + skipped = sum(1 for r in results if "skip" in r["status"]) + errors = sum(1 for r in results if "error" in r["status"]) + print(f"[+] uefi_dtbs: patched={patched} skipped={skipped} errors={errors}") + if errors: + sys.exit(1) + + else: + _patch_xbl_config( + elf_path=elf_path, + cert_cer_path=cert_cer_path, + output_path=output_path, + prop_name=prop_name, + meta_ph_index=meta_ph_index, + ) + + return elf_type + + +def patch_capsule_cert( + elf_path: str, + cert_cer_path: str, + output_path: str, + prop_name: str = _DEFAULT_PROP_NAME, + meta_ph_index: int = 1, +) -> str: + """ + Patch the capsule root certificate in *elf_path* and write to *output_path*. + + If *elf_path* ends in ``.xz``, it is transparently decompressed to a + temporary ELF before patching. If *output_path* ends in ``.xz``, the + patched ELF is recompressed before being written out. Either, both, or + neither may be ``.xz`` -- e.g. patching an xz input into a plain ELF + output, or vice versa, both work. + + Args: + elf_path: Input ELF (uefi_dtbs or xbl_config), optionally `.xz`. + cert_cer_path: DER certificate file (.cer). + output_path: Path for the patched output ELF, optionally `.xz`. + prop_name: DTB property name to patch (default: QcCapsuleRootCert). + meta_ph_index: PH index of the XBLConfig metadata blob (default: 1). + + Returns: + Detected ELF type string ("uefi_dtbs" or "xbl_config"). + """ + input_is_xz = elf_path.endswith(".xz") + output_is_xz = output_path.endswith(".xz") + + if not input_is_xz and not output_is_xz: + return _patch_capsule_cert_elf( + elf_path, cert_cer_path, output_path, prop_name, meta_ph_index + ) + + decompressed_fd, decompressed_path = tempfile.mkstemp(suffix=".elf") + os.close(decompressed_fd) + patched_fd, patched_path = tempfile.mkstemp(suffix=".elf") + os.close(patched_fd) + try: + if input_is_xz: + with lzma.open(elf_path, "rb") as src, open(decompressed_path, "wb") as dst: + dst.write(src.read()) + source_elf = decompressed_path + else: + source_elf = elf_path + + elf_type = _patch_capsule_cert_elf( + source_elf, cert_cer_path, patched_path, prop_name, meta_ph_index + ) + + if output_is_xz: + with open(patched_path, "rb") as src, lzma.open(output_path, "wb") as dst: + dst.write(src.read()) + else: + os.replace(patched_path, output_path) + finally: + for tmp in (decompressed_path, patched_path): + try: + os.unlink(tmp) + except OSError: + pass + + return elf_type + + +# ============================================================ +# CLI +# ============================================================ + + +def main() -> None: + ap = argparse.ArgumentParser( + prog="qcom-capsule-tool patch-capsule-cert", + description=( + "Patch QcCapsuleRootCert in a uefi_dtbs or xbl_config ELF. " + "The ELF type is detected automatically." + ), + ) + ap.add_argument("elf_file", help="Input ELF file (uefi_dtbs or xbl_config)") + ap.add_argument("cert_cer", help="DER certificate file (.cer)") + ap.add_argument("output_elf", help="Output patched ELF file") + ap.add_argument( + "--prop-name", + default=_DEFAULT_PROP_NAME, + help="DTB property name to patch (default: %(default)s)", + ) + ap.add_argument( + "--meta-ph", + type=int, + default=1, + help="XBLConfig metadata program-header index (default: %(default)s)", + ) + args = ap.parse_args() + + print(f"[+] Input ELF : {args.elf_file}") + print(f"[+] Cert (.cer): {args.cert_cer}") + print(f"[+] Output ELF : {args.output_elf}") + + patch_capsule_cert( + elf_path=args.elf_file, + cert_cer_path=args.cert_cer, + output_path=args.output_elf, + prop_name=args.prop_name, + meta_ph_index=args.meta_ph, + ) + + print(f"[+] Done. Output written to: {args.output_elf}") + + +if __name__ == "__main__": + main() From ac62d60e15aebbc5b04d6d8d2a5b93611ec7f291 Mon Sep 17 00:00:00 2001 From: Guanquan Tian Date: Tue, 1 Sep 2026 11:49:49 +0800 Subject: [PATCH 2/8] qcom: vendor build-dtb-image.sh to build SoC-filtered FIT DTB images Adds build-dtb-image.sh (vendored from qcom-dtb-metadata). Given a directory of .dtb/.dtbo files and a list of --soc names, it filters the FIT image source to only the DTBs needed for those SoCs, builds an external-data FIT image with mkimage, and wraps it in a FAT image (via mtools, no loop device) sized with --size. --prune skips any DTB the .its references but that is missing from the source directory instead of failing the build. Vendored from: - build-dtb-image.sh: qualcomm-linux/qcom-dtb-metadata @ f1596a6b726c232743f968786de375a91d954eca Signed-off-by: Guanquan Tian --- debian.qcom/control.stub.in | 1 + debian.qcom/fitimage/build-dtb-image.sh | 707 ++++++++++++++++++++++++ 2 files changed, 708 insertions(+) create mode 100755 debian.qcom/fitimage/build-dtb-image.sh diff --git a/debian.qcom/control.stub.in b/debian.qcom/control.stub.in index a81cfde6c207b..2bbf130569444 100644 --- a/debian.qcom/control.stub.in +++ b/debian.qcom/control.stub.in @@ -42,6 +42,7 @@ Build-Depends: libunwind8-dev [amd64 arm64 armhf ppc64el] , llvm-21-dev, makedumpfile:native [amd64] , + mtools [arm64] , openssl , pahole (>= 1.29-2ubuntu2) [amd64 arm64 armhf ppc64el s390x riscv64] | dwarves (>= 1.21) [amd64 arm64 armhf ppc64el s390x riscv64] , pkg-config , diff --git a/debian.qcom/fitimage/build-dtb-image.sh b/debian.qcom/fitimage/build-dtb-image.sh new file mode 100755 index 0000000000000..9126c5b364eb9 --- /dev/null +++ b/debian.qcom/fitimage/build-dtb-image.sh @@ -0,0 +1,707 @@ +#!/usr/bin/env bash +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# +# SPDX-License-Identifier: BSD-3-Clause-Clear +# +# ============================================================================= +# build-dtb-image.sh +# +# Description: +# Build a FAT-formatted FIT DTB image for Qualcomm ARM64 platforms. +# +# This script is part of the qcom-dtb-metadata repository. The ITS file +# (qcom-next-fitimage.its) and platform metadata DTS (qcom-metadata.dts) +# are read directly from the repository directory containing this script — +# no network access or repository cloning is required at build time. +# +# The script produces a FIT DTB image (qclinux_fit.img) suitable for +# booting multiple Qualcomm platforms from a single DTB image, using the +# multi-DTB selection mechanism defined by qcom-dtb-metadata. +# +# ───────────────────────────────────────────────────────────────────────── +# DTB Source Modes +# ───────────────────────────────────────────────────────────────────────── +# +# (A) Kernel .deb mode (recommended / upstream-friendly) +# - Provide a Debian kernel package (.deb) as input. +# - The script extracts the .deb via `dpkg-deb -R` into a temp directory. +# - DTBs are located by probing the following paths in order: +# 1. $DEB_DIR/usr/lib/linux-image-*/ (Debian standard — direct, no symlink) +# 2. $DEB_DIR/usr/lib/firmware/*/device-tree (Ubuntu compat, usrmerge layout) +# 3. $DEB_DIR/lib/firmware/*/device-tree (Ubuntu compat, legacy layout) +# Probing the Debian standard path first ensures correct operation on +# both Debian and Ubuntu regardless of whether the compat symlink exists. +# - The script assumes there is exactly ONE matching directory. +# +# (B) DTB source directory mode (dev/kernel-tree mode) +# - Provide the DTB source directory directly (e.g. a kernel build tree): +# arch/arm64/boot/dts/qcom +# +# ───────────────────────────────────────────────────────────────────────── +# Build Steps +# ───────────────────────────────────────────────────────────────────────── +# 1. Set up a temporary staging directory with the ITS, compiled DTS, +# and all DTBs laid out as the ITS /incbin/ paths expect. +# 2. Compile qcom-metadata.dts → qcom-metadata.dtb (via dtc). +# 3. Copy qcom-next-fitimage.its into the staging directory. +# 4. Run mkimage to produce qclinux_fit.img (-E -B 8). +# NOTE: The output filename is hardcoded in UEFI firmware: +# #define FIT_BINARY_FILE L"\\qclinux_fit.img" +# #define COMBINED_DTB_FILE L"\\combined-dtb.dtb" +# #define SECONDARY_DTB_FILE L"\\secondary-dtb.dtb" +# 5. Pack qclinux_fit.img into a FAT image. +# +# Usage: +# ./build-dtb-image.sh \ +# (--kernel-deb | --dtb-src ) \ +# [--size ] [--out ] +# +# ───────────────────────────────────────────────────────────────────────────── +# Arguments: +# --kernel-deb / -kernel-deb +# Path to a Debian kernel package (.deb). DTBs are taken from the +# extracted payload, probed in order: +# 1. usr/lib/linux-image-*/ (Debian standard) +# 2. usr/lib/firmware/*/device-tree (Ubuntu compat, usrmerge) +# 3. lib/firmware/*/device-tree (Ubuntu compat, legacy) +# +# --dtb-src / -dtb-src +# Path to DTB source directory +# e.g., arch/arm64/boot/dts/qcom +# +# --fit-image / -fit-image +# [Accepted for backward compatibility] FIT image mode is the +# default and only mode; this flag is a no-op. +# +# --size / -size +# FAT image size in MB (integer > 0, default: 4) +# +# --out / -out +# Output image filename (default: dtb.bin) +# +# Requirements / Assumptions: +# - Linux host with: +# * bash +# * dd +# * mtools (mformat, mcopy, mdir) — FAT image creation without root +# * dtc +# * mkimage +# * dpkg-deb (only required for --kernel-deb mode) +# - No root privileges required. +# +# Notes: +# - The resulting FAT image contains qclinux_fit.img at its root. +# - FAT image creation uses mtools (mformat + mcopy); no loop device, +# no mount point, no elevated privileges required at any step. +# - The script installs a cleanup trap to remove all temporary +# directories on any exit path. +# +# ============================================================================= + +set -euo pipefail + +# Resolve the directory containing this script (the qcom-dtb-metadata root). +# All metadata files (ITS, DTS) are read from this directory at runtime — +# no cloning or network access is required. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ----------------------------- Defaults -------------------------------------- + +DTB_BIN_SIZE=4 # Default FAT image size (MB) +DTB_BIN="dtb.bin" # Default output image filename +PRUNE=0 # Prune ITS entries whose DTB/DTBO is absent from source + +DTB_SRC="" # DTB source directory (resolved; required via one mode) + +KERNEL_DEB="" # Optional: kernel .deb input (preferred mode) +DEB_DIR="" # Temporary extraction directory when using --kernel-deb + +FIT_WORK_DIR="" # Temporary staging directory for FIT build artefacts + +# ITS file used for FIT image generation — must exist in SCRIPT_DIR. +DEFAULT_ITS_FILE="qcom-next-fitimage.its" + +SOC_FILTER=() # Optional: one or more SOC names to filter configurations +BOARD_FILTER=() # Optional: one or more board names to further filter configurations + +# ---------------------------- Helper Functions ------------------------------- + +usage() { + cat < | --dtb-src ) [--soc ...] [--board ...] [--size ] [--out ] + + --kernel-deb, -kernel-deb Path to Debian kernel package (.deb). DTBs located by probing: + 1. /usr/lib/linux-image-*/ (Debian standard) + 2. /usr/lib/firmware/*/device-tree (Ubuntu, usrmerge) + 3. /lib/firmware/*/device-tree (Ubuntu, legacy) + Exactly one directory must be found across all search paths. + + --dtb-src, -dtb-src Path to DTB source directory + (e.g. arch/arm64/boot/dts/qcom) + + --fit-image, -fit-image Accepted for backward compatibility; FIT image + mode is the default and only mode (no-op). + + --soc, -soc (Optional) One or more SOC names to filter configurations. + Each must be a subnode under /soc in qcom-metadata.dts + (e.g. qcs8275, qcs6490, sa8775p, hamoa, glymur). + A conf is kept if its compatible contains ANY of the given + names as a case-insensitive substring. + Multiple values: --soc purwa sa8775p + + --board, -board (Optional, requires --soc) One or more board names to + further filter the --soc selection. + Each must be a subnode under /board in qcom-metadata.dts + (e.g. iot, evk, idp, qam, adp). + A conf is kept if its compatible also contains ANY of the + given board names as a substring. + Multiple values: --board iot evk + Error if no --soc-selected conf matches any board name. + + --size, -size FAT image size in MB (default: 4) + + --out, -out Output image filename (default: dtb.bin) + + --prune, -prune Prune ITS entries of dtb(o) based on kernel provided. + dtb.bin is created from reduced its file. there is + risk of missing dtb(o) due to kernel and finding it out during boot. + +Notes: + - Exactly one of --kernel-deb or --dtb-src must be provided. + - The output FAT image contains qclinux_fit.img at its root. + - Metadata files (ITS, DTS) are read from: ${SCRIPT_DIR} +EOF + exit 1 +} + +# Parse subnode names from a top-level node in qcom-metadata.dts. +# Usage: get_dts_subnodes +# Prints one name per line. +get_dts_subnodes() { + local dts_file="$1" + local node_name="$2" + awk -v node="${node_name}" ' + $0 ~ "^\t" node "[[:space:]]*\\{" { in_node=1; next } + in_node && /^\t\t[a-z]/ { + match($0, /[a-z][a-z0-9-]*/); print substr($0, RSTART, RLENGTH) + } + in_node && /^\t\};/ { exit } + ' "${dts_file}" +} + +# filter_its [board_filter] +# +# Reads the ITS file and emits a filtered images{} + configurations{} block. +# Configurations are kept when their compatible string contains soc_filter +# (case-insensitive substring). If board_filter is also given, the compatible +# must additionally contain board_filter as a substring (order-independent). +# The fdt-qcom-metadata.dtb image entry is always retained. +filter_its() { + local its_file="$1" + local soc="$2" + local board="${3:-}" + + awk -v soc="${soc}" -v board="${board}" ' +BEGIN { + in_images = 0; in_confs = 0; in_block = 0 + brace_depth = 0; block_buf = ""; block_label = "" + block_compat= ""; img_count = 0; conf_count = 0 + n = split(tolower(soc), soc_list, " ") + m = split(tolower(board), board_list, " ") + seen_images = 0 +} +/^[[:space:]]*images[[:space:]]*\{/ { seen_images = 1 } +!seen_images { next } +/^[[:space:]]*images[[:space:]]*\{/ && !in_block { + in_images = 1; in_confs = 0; next +} +/^[[:space:]]*configurations[[:space:]]*\{/ && !in_block { + in_confs = 1; in_images = 0; next +} +/^\t\};$/ && !in_block { in_images = 0; in_confs = 0; next } +in_images && !in_block && /^\t\t[^ ]/ { + match($0, /[^\t ]+/) + block_label = substr($0, RSTART, RLENGTH) + in_block = 1; brace_depth = 1; block_buf = $0 "\n"; next +} +in_images && in_block { + block_buf = block_buf $0 "\n" + n = split($0, chars, "") + for (i = 1; i <= n; i++) { + if (chars[i] == "{") brace_depth++ + if (chars[i] == "}") brace_depth-- + } + if (brace_depth == 0) { + img_blocks[block_label] = block_buf; img_count++ + in_block = 0; block_buf = ""; block_label = "" + } + next +} +in_confs && !in_block && /^\t\tconf-[0-9]/ { + in_block = 1; brace_depth = 1; block_buf = $0 "\n"; block_compat = ""; next +} +in_confs && in_block { + block_buf = block_buf $0 "\n" + if ($0 ~ /compatible[[:space:]]*=/) { + match($0, /"[^"]+"/) + block_compat = substr($0, RSTART+1, RLENGTH-2) + } + n = split($0, chars, "") + for (i = 1; i <= n; i++) { + if (chars[i] == "{") brace_depth++ + if (chars[i] == "}") brace_depth-- + } + if (brace_depth == 0) { + compat_lower = tolower(block_compat) + soc_match = 0 + for (si in soc_list) if (index(compat_lower, soc_list[si]) > 0) soc_match = 1 + board_match = (m == 0) + for (bi in board_list) if (index(compat_lower, board_list[bi]) > 0) board_match = 1 + if (soc_match && board_match) { + conf_count++ + conf_blocks[conf_count] = block_buf + tmp = block_buf + while (match(tmp, /"fdt-[^"]+"/) > 0) { + needed_fdts[substr(tmp, RSTART+1, RLENGTH-2)] = 1 + tmp = substr(tmp, RSTART + RLENGTH) + } + } + in_block = 0; block_buf = ""; block_compat = "" + } + next +} +END { + if (conf_count == 0) { + if (m > 0) + print "[ERROR] No configurations matched SOC=\"" soc "\" BOARD=\"" board "\"" > "/dev/stderr" + else + print "[ERROR] No configurations matched SOC filter: " soc > "/dev/stderr" + exit 1 + } + print "\timages {" + if ("fdt-qcom-metadata.dtb" in img_blocks) + printf "%s", img_blocks["fdt-qcom-metadata.dtb"] + for (lbl in img_blocks) { + if (lbl == "fdt-qcom-metadata.dtb") continue + if (lbl in needed_fdts) printf "%s", img_blocks[lbl] + } + print "\t};" + print "" + print "\tconfigurations {" + for (i = 1; i <= conf_count; i++) { + blk = conf_blocks[i] + sub(/conf-[0-9]+[[:space:]]*\{/, "conf-" i " {", blk) + printf "%s", blk + } + print "\t};" +} +' "${its_file}" +} + +require_cmd() { + local c="$1" + if ! command -v "$c" >/dev/null 2>&1; then + echo "[ERROR] Required command not found: $c" >&2 + exit 1 + fi +} + +cleanup() { + local status=$? + + if [[ -n "${DEB_DIR:-}" && -d "$DEB_DIR" ]]; then + rm -rf "$DEB_DIR" || true + fi + + if [[ -n "${FIT_WORK_DIR:-}" && -d "$FIT_WORK_DIR" ]]; then + rm -rf "$FIT_WORK_DIR" || true + fi + + exit "$status" +} + +trap cleanup EXIT + +# ------------------------------ Arg Parsing ---------------------------------- + +while [[ $# -gt 0 ]]; do + case "$1" in + -dtb-src|--dtb-src) + DTB_SRC="${2:-}" + shift 2 + ;; + -kernel-deb|--kernel-deb) + KERNEL_DEB="${2:-}" + shift 2 + ;; + -size|--size) + DTB_BIN_SIZE="${2:-}" + shift 2 + ;; + -fit-image|--fit-image) + # FIT image mode is the default; accepted for backward compatibility. + shift 1 + ;; + -out|--out) + DTB_BIN="${2:-}" + shift 2 + ;; + -prune|--prune) + PRUNE=1 + shift 1 + ;; + -soc|--soc) + shift + while [[ $# -gt 0 && "$1" != --* && "$1" != -* ]]; do + SOC_FILTER+=("$1"); shift + done + ;; + -board|--board) + shift + while [[ $# -gt 0 && "$1" != --* && "$1" != -* ]]; do + BOARD_FILTER+=("$1"); shift + done + ;; + -h|--help) + usage + ;; + *) + usage + ;; + esac +done + +# ----------------------------- Validation ------------------------------------ + +# Exactly one source mode must be selected +if [[ -n "${KERNEL_DEB}" && -n "${DTB_SRC}" ]]; then + echo "[ERROR] Provide only one of --kernel-deb or --dtb-src (not both)." >&2 + usage +fi +if [[ -z "${KERNEL_DEB}" && -z "${DTB_SRC}" ]]; then + echo "[ERROR] Provide one of --kernel-deb or --dtb-src." >&2 + usage +fi + +# Validate image size is a positive integer +if ! [[ "${DTB_BIN_SIZE}" =~ ^[0-9]+$ ]] || (( DTB_BIN_SIZE <= 0 )); then + echo "[ERROR] --size must be a positive integer (MB), got '${DTB_BIN_SIZE}'." >&2 + exit 1 +fi + +# Validate --soc and --board against qcom-metadata.dts subnodes +if [[ ${#BOARD_FILTER[@]} -gt 0 && ${#SOC_FILTER[@]} -eq 0 ]]; then + echo "[ERROR] --board requires --soc to be specified as well." >&2 + exit 1 +fi + +if [[ ${#SOC_FILTER[@]} -gt 0 ]]; then + valid_socs=$(get_dts_subnodes "${SCRIPT_DIR}/qcom-metadata.dts" "soc") + for _soc in "${SOC_FILTER[@]}"; do + if ! echo "${valid_socs}" | grep -qx "${_soc}"; then + echo "[ERROR] Invalid --soc '${_soc}'. Valid SOC names (from /soc in qcom-metadata.dts):" >&2 + echo "${valid_socs}" | sed 's/^/ /' >&2 + exit 1 + fi + done +fi + +if [[ ${#BOARD_FILTER[@]} -gt 0 ]]; then + valid_boards=$(get_dts_subnodes "${SCRIPT_DIR}/qcom-metadata.dts" "board") + for _board in "${BOARD_FILTER[@]}"; do + if ! echo "${valid_boards}" | grep -qx "${_board}"; then + echo "[ERROR] Invalid --board '${_board}'. Valid board names (from /board in qcom-metadata.dts):" >&2 + echo "${valid_boards}" | sed 's/^/ /' >&2 + exit 1 + fi + done +fi + +# Validate that required metadata files are present in the repository +if [[ ! -f "${SCRIPT_DIR}/qcom-metadata.dts" ]]; then + echo "[ERROR] qcom-metadata.dts not found in metadata directory: ${SCRIPT_DIR}" >&2 + exit 1 +fi +if [[ ! -f "${SCRIPT_DIR}/${DEFAULT_ITS_FILE}" ]]; then + echo "[ERROR] ITS file '${DEFAULT_ITS_FILE}' not found in metadata directory: ${SCRIPT_DIR}" >&2 + exit 1 +fi + +# Command requirements +require_cmd dd +require_cmd mformat +require_cmd mcopy +require_cmd mdir +require_cmd mktemp +require_cmd cp +require_cmd dtc +require_cmd mkimage + +# ----------------------------- Resolve DTB_SRC ------------------------------- + +if [[ -n "${KERNEL_DEB}" ]]; then + if [[ ! -f "${KERNEL_DEB}" ]]; then + echo "[ERROR] Kernel .deb '${KERNEL_DEB}' not found." >&2 + exit 1 + fi + + require_cmd dpkg-deb + + DEB_DIR="$(mktemp -d -t kernel-deb-XXXXXX)" + echo "[INFO] Extracting kernel .deb to: ${DEB_DIR}" + dpkg-deb -R "${KERNEL_DEB}" "${DEB_DIR}" + + # Locate the DTB directory from the extracted .deb. + # Probe in order (most to least preferred): + # 1. usr/lib/linux-image-*/ — Debian standard install location (direct, no symlink). + # Works on both Debian and Ubuntu regardless of usrmerge. + # 2. usr/lib/firmware/*/device-tree — Ubuntu compat symlink (usrmerge layout). + # 3. lib/firmware/*/device-tree — Ubuntu compat symlink (legacy layout). + # Searching the Debian standard path first avoids any dependency on the Ubuntu + # compat symlink and is correct on pure Debian systems that never install it. + shopt -s nullglob + dt_dirs=( "${DEB_DIR}/usr/lib/linux-image-"*/ ) + if (( ${#dt_dirs[@]} == 0 )); then + dt_dirs=( "${DEB_DIR}/usr/lib/firmware"/*/device-tree ) + fi + if (( ${#dt_dirs[@]} == 0 )); then + dt_dirs=( "${DEB_DIR}/lib/firmware"/*/device-tree ) + fi + shopt -u nullglob + + if (( ${#dt_dirs[@]} == 0 )); then + echo "[ERROR] No DTB directory found under:" >&2 + echo " '${DEB_DIR}/usr/lib/linux-image-*/'" >&2 + echo " '${DEB_DIR}/usr/lib/firmware/*/device-tree'" >&2 + echo " '${DEB_DIR}/lib/firmware/*/device-tree'" >&2 + exit 1 + fi + if (( ${#dt_dirs[@]} > 1 )); then + echo "[ERROR] Multiple DTB directories found; expected exactly one:" >&2 + for d in "${dt_dirs[@]}"; do + echo " - $d" >&2 + done + exit 1 + fi + + DTB_SRC="${dt_dirs[0]}" + echo "[INFO] Using DTB source directory from .deb payload: ${DTB_SRC}" +else + if [[ ! -d "${DTB_SRC}" ]]; then + echo "[ERROR] DTB source directory '${DTB_SRC}' not found." >&2 + exit 1 + fi + echo "[INFO] Using DTB source directory: ${DTB_SRC}" +fi + +# ============================================================================== +# FIT DTB Image Build +# ============================================================================== + +echo "[INFO] Building FIT DTB image." +echo "[INFO] Using metadata from: ${SCRIPT_DIR}" + +# ----------------------------------------------------------------------- +# Step 1. Create staging directory and lay out the build tree +# ----------------------------------------------------------------------- +# mkimage resolves /incbin/ paths relative to the directory it is invoked +# from, so all artefacts (ITS, compiled metadata DTB, and per-platform +# DTBs) must be assembled under a single staging tree before mkimage runs. +FIT_WORK_DIR="$(mktemp -d -t fit-build-XXXXXX)" +echo "[INFO] FIT build staging directory: ${FIT_WORK_DIR}" + +FIT_STAGE="${FIT_WORK_DIR}/fit_image" +mkdir -p "${FIT_STAGE}" + +# Create the directory tree that the ITS /incbin/ paths reference: +# arch/arm64/boot/dts/qcom/.dtb +DTB_STAGE="${FIT_STAGE}/arch/arm64/boot/dts/qcom" +mkdir -p "${DTB_STAGE}" + +# Copy DTBs from the resolved DTB source directory. +# DTBs may be nested under vendor subdirectories (e.g. qcom/), so use +# find -L (follow symlinks) rather than a flat glob to collect them all +# into the staging dir flat — the ITS file references them by basename +# under arch/arm64/boot/dts/qcom/. +echo "[INFO] Copying DTBs from ${DTB_SRC} ..." +dtb_count=0 +while IFS= read -r dtb; do + cp -p "${dtb}" "${DTB_STAGE}/" + (( dtb_count++ )) || true +done < <(find -L "${DTB_SRC}" \( -name '*.dtb' -o -name '*.dtbo' \) -type f) + +if (( dtb_count == 0 )); then + echo "[ERROR] No DTB files found under ${DTB_SRC}" >&2 + echo " Verify the kernel package was built with DTB support" >&2 + echo " and that usr/lib/linux-image-*/ is present in the package." >&2 + exit 1 +fi +echo "[INFO] Staged ${dtb_count} DTB file(s) to ${DTB_STAGE}" +echo "[INFO] Staged DTBs:" +ls "${DTB_STAGE}"/*.dtb 2>/dev/null | xargs -n1 basename | sort | sed 's/^/ /' + +# ----------------------------------------------------------------------- +# Step 2. Compile qcom-metadata.dts → qcom-metadata.dtb +# ----------------------------------------------------------------------- +echo "[INFO] Compiling qcom-metadata.dts..." +dtc -I dts -O dtb \ + -o "${FIT_STAGE}/qcom-metadata.dtb" \ + "${SCRIPT_DIR}/qcom-metadata.dts" +echo "[INFO] qcom-metadata.dtb generated:" +ls -lh "${FIT_STAGE}/qcom-metadata.dtb" + +# ----------------------------------------------------------------------- +# Step 3. Copy or filter ITS file into the staging directory +# ----------------------------------------------------------------------- +if [[ ${#SOC_FILTER[@]} -gt 0 ]]; then + _soc_str="${SOC_FILTER[*]}" + _board_str="${BOARD_FILTER[*]+"${BOARD_FILTER[*]}"}" + _filter_desc="SOC='${_soc_str}'" + [[ -n "${_board_str}" ]] && _filter_desc+=" BOARD='${_board_str}'" + echo "[INFO] Generating filtered ITS (${_filter_desc})..." + + header_lines=$(awk '/^[[:space:]]*images[[:space:]]*\{/{print NR-1; exit}' "${SCRIPT_DIR}/${DEFAULT_ITS_FILE}") + head -n "${header_lines}" "${SCRIPT_DIR}/${DEFAULT_ITS_FILE}" > "${FIT_STAGE}/${DEFAULT_ITS_FILE}" + + filter_its "${SCRIPT_DIR}/${DEFAULT_ITS_FILE}" "${_soc_str}" "${_board_str}" >> "${FIT_STAGE}/${DEFAULT_ITS_FILE}" + + echo "};" >> "${FIT_STAGE}/${DEFAULT_ITS_FILE}" + + echo "[INFO] Selected configurations:" + awk ' +/^\t\tconf-[0-9]+ \{/{in_conf=1} +in_conf && /compatible/{match($0,/"[^"]+"/); print " " substr($0,RSTART+1,RLENGTH-2)} +in_conf && /^\t\t\};/{in_conf=0} +' "${FIT_STAGE}/${DEFAULT_ITS_FILE}" + + unset _soc_str _board_str _filter_desc +else + echo "[INFO] Using full ITS (all configurations)." + cp "${SCRIPT_DIR}/${DEFAULT_ITS_FILE}" "${FIT_STAGE}/${DEFAULT_ITS_FILE}" +fi + +# ----------------------------------------------------------------------- +# Step 3b. Prune ITS (only when --prune is given) +# +# For every image entry whose /incbin/ DTB/DTBO is absent from the staged +# source, the entry is dropped. Any configuration referencing a dropped +# label is also dropped. The staged ITS is overwritten in-place so that +# Step 4 (mkimage) sees the reduced file. +# fdt label is also dropped. Remaining configurations are +# renumbered sequentially (conf-1, conf-2, …) to close any gaps left by +# dropped entries. +# ----------------------------------------------------------------------- +if (( PRUNE )); then + echo "[INFO] --prune: rewriting ITS to include only DTBs present in source..." + + _pruned_its="${FIT_STAGE}/${DEFAULT_ITS_FILE}.pruned" + + # Two-file awk: first input = available DTB basenames (via find); second = ITS. + # NR==FNR processes the first file to build avail_dtbs[]; the rest rewrites the ITS. + awk ' +NR == FNR { avail_dtbs[$1] = 1; next } +BEGIN { avail_labels["fdt-qcom-metadata.dtb"] = 1 + in_block=0; skip_block=0; is_conf=0; buf=""; cur_label="" + cur_compat=""; conf_counter=0 } +/^[[:space:]]+fdt-[^ ]+ \{$/ { + in_block=1; is_conf=0; skip_block=0; cur_label=$1; buf=$0"\n"; next } +/^[[:space:]]+conf-[0-9]+ \{$/ { + in_block=1; is_conf=1; skip_block=0; cur_compat=""; buf=$0"\n"; next } +in_block { + buf=buf $0"\n" + if (is_conf && /compatible =/) { + match($0, /"[^"]+"/); cur_compat=substr($0,RSTART+1,RLENGTH-2) } + if (!is_conf && /\/incbin\//) { + if (!(cur_label in avail_labels)) { + split($0,q,"\""); n=split(q[2],p,"/"); dtb=p[n] + if (dtb in avail_dtbs) avail_labels[cur_label]=1 + else { skip_block=1; print "[WARN] --prune: dropped fdt: "dtb >"/dev/stderr" } + } + } + if (is_conf && /fdt =/) { + tmp=$0 + while (match(tmp,/"fdt-[^"]+"/)>0) { + ref=substr(tmp,RSTART+1,RLENGTH-2) + if (!(ref in avail_labels)) skip_block=1 + tmp=substr(tmp,RSTART+RLENGTH) + } + } + if (/^[[:space:]]+\};$/) { + if (!skip_block) { + if (is_conf) { + conf_counter++ + sub(/conf-[0-9]+/, "conf-" conf_counter, buf) + } + sub(/\n$/, "", buf); print buf + } else if (is_conf) { + print "[WARN] --prune: dropped conf (compatible=\"" cur_compat "\"): missing DTB(s)" >"/dev/stderr" + } + in_block=0; buf=""; skip_block=0; is_conf=0; cur_label=""; cur_compat="" + } + next +} +{ print } +' <(find "${DTB_STAGE}" -maxdepth 1 \( -name "*.dtb" -o -name "*.dtbo" \) -type f \ + -exec basename {} \;) \ + "${FIT_STAGE}/${DEFAULT_ITS_FILE}" > "${_pruned_its}" + + if ! grep -q 'conf-[0-9]' "${_pruned_its}"; then + echo "[ERROR] --prune: no configuration entries remain after pruning." >&2 + echo " Verify the source contains DTBs referenced by the ITS." >&2 + exit 1 + fi + + mv "${_pruned_its}" "${FIT_STAGE}/${DEFAULT_ITS_FILE}" + echo "[INFO] --prune: ITS rewritten successfully." + echo "[INFO] Remaining ITS image entries:" + grep -P "^[[:space:]]+fdt-" "${FIT_STAGE}/${DEFAULT_ITS_FILE}" | \ + sed 's/^[[:space:]]*/ /' || true + echo "[INFO] Dropped conf entries:" + grep -o 'compatible = "[^"]*"' "${SCRIPT_DIR}/${DEFAULT_ITS_FILE}" | \ + grep -vxF -f <(grep -o 'compatible = "[^"]*"' "${FIT_STAGE}/${DEFAULT_ITS_FILE}") | \ + sed 's/compatible = "//;s/"$//;s/^/ /' || true + + unset _pruned_its +fi +# ----------------------------------------------------------------------- +# Step 4. Generate qclinux_fit.img via mkimage +# ----------------------------------------------------------------------- +# mkimage is invoked from FIT_STAGE so that all /incbin/ relative paths +# in the ITS file resolve correctly. +# +# Output filename MUST be qclinux_fit.img — hardcoded in UEFI firmware: +# #define FIT_BINARY_FILE L"\\qclinux_fit.img" +# #define COMBINED_DTB_FILE L"\\combined-dtb.dtb" +# #define SECONDARY_DTB_FILE L"\\secondary-dtb.dtb" +mkdir -p "${FIT_STAGE}/out" +echo "[INFO] Running mkimage to generate qclinux_fit.img..." +( + cd "${FIT_STAGE}" + mkimage -f "${DEFAULT_ITS_FILE}" out/qclinux_fit.img -E -B 8 +) +echo "[INFO] qclinux_fit.img generated:" +ls -lh "${FIT_STAGE}/out/qclinux_fit.img" +file "${FIT_STAGE}/out/qclinux_fit.img" + +# ----------------------------------------------------------------------- +# Step 5. Pack qclinux_fit.img into a FAT image +# ----------------------------------------------------------------------- +# mtools (mformat + mcopy) operates directly on the image file — no loop +# device, no mount point, no root privileges required. +echo "[INFO] Creating FAT image '${DTB_BIN}' (${DTB_BIN_SIZE} MB)..." +dd if=/dev/zero of="${DTB_BIN}" bs=1M count="${DTB_BIN_SIZE}" status=progress + +echo "[INFO] Formatting '${DTB_BIN}' as FAT (4 KiB sector size)..." +# -S 5: sector size code → 2^(5+7) = 4096 bytes (matches original mkfs.vfat -S 4096) +# No -F: let mformat auto-select FAT type based on image size, matching mkfs.vfat behaviour +mformat -i "${DTB_BIN}" -S 5 :: + +echo "[INFO] Copying qclinux_fit.img into FAT image..." +mcopy -i "${DTB_BIN}" "${FIT_STAGE}/out/qclinux_fit.img" :: + +echo "[INFO] Deployed qclinux_fit.img into FAT image." +echo "[INFO] Files in image:" +mdir -i "${DTB_BIN}" :: + +# Normal exit (cleanup will still run, but now everything should succeed). +exit 0 From 81b188c74a6a26c123eb1a8809f3e361403f9f77 Mon Sep 17 00:00:00 2001 From: Guanquan Tian Date: Tue, 1 Sep 2026 11:50:40 +0800 Subject: [PATCH 3/8] qcom: build and ship a dtb-capsule package with post-boot verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a dtb-capsule-PKGVER-ABINUM-qcom package (non-per-flavour, same guard as linux-bpf-dev) that ships a signed UEFI capsule and provenance manifest for the hamoa/purwa device tree, plus a systemd service that verifies the update on next boot. At build time, the first flavour's .dtb/.dtbo files are sha256-summed into a provenance manifest, whose own sha256 is injected into each DTB under /qcom-dtb-capsule-provenance/dtb-provenance-sha256 via fdtput, and also dropped into that flavour's linux-modules package at /usr/lib/modules//dtb-provenance-sha256 so a running system can verify its DTB and linux-modules package came from the same build. build-dtb-image.sh packs the DTBs for hamoa and purwa into a FAT-wrapped FIT image, and qcom_capsule_tool's `create` subcommand signs one capsule per machine using each machine's FMP GUID. The package installs each machine's .cap file, capsule.env (FMP_GUID + qcom-ptool TARGET), the provenance manifest, and the expected kernel release under /usr/share/dtb-capsule/, alongside a static Provides/Conflicts/Replaces: dtb-capsule-qcom virtual package name so upgrading to a new kver's dtb-capsule package lets dpkg cleanly replace the previous one instead of erroring out on the shared, non-kver-scoped file paths (verify-capsule-result.sh, expected-kver, the systemd service, etc.). postinst fails the install (exit 1) if linux-modules- is not actually installed — defense-in-depth against --force-depends bypassing the declared package Depends, since postinst is not re-invoked just because the dependency is installed afterward. It also cross-checks this package's own expected-dtb-sha256 against the target linux-modules package's dtb-provenance-sha256, refusing to stage a capsule when the two were not built together. Otherwise it clears any stale ESRT-dedup cache and verify-state from a prior attempt, and matches the device's ESRT FMP GUID against the shipped capsule.env files; if the running DTB's content already matches what this package would stage, staging is skipped and any leftover .cap is cleared, so the next boot's verifier doesn't mistake it for an unconsumed capsule. Otherwise it copies the matching capsule into /boot/efi/EFI/UpdateCapsule and sets the OsIndications capsule-delivery bit via efivar so firmware applies it on next boot. prerm removes this package's own staged-but-unconsumed capsule on removal, since that file lives outside dpkg's file list and would otherwise be left for firmware to apply pre-OS on the next boot. dtb-capsule-verify.service/verify-capsule-result.sh check on next boot that the capsule applied cleanly, in two phases: - kver_match_state answers whether the installed dtb-capsule package's target kernel matches the one running right now. It's deliberately direction-agnostic — the expected kernel may be a newer one not yet rebooted into, or an older one being rolled back to, and both are handled by the same checks. A dpkg record for the expected kernel in an abnormal state (half-installed, stuck purge, etc.) is package_mismatch; a merely absent record (autoremoved, or never installed) is not treated as an error and falls through to a self-consistency check on the running kernel's own DTB content, landing on no_capsule_for_running_kernel (content fine, just nothing staged for this kernel), on kernel_dtb_mismatch (the running kernel's own linux-modules package disagrees with its DTB), or, when that self-consistency can't be determined at all (no provenance data), on unknown rather than silently falling through as if the kernel were a match. An unconsumed capsule, or content that already matches what's expected, both mean the device is simply awaiting reboot — tracked as reboot_pending or, once a real reboot has come and gone without reaching the expected kernel (a boot_id change against a persisted tracking file), escalated to reboot_stalled so a genuinely stuck upgrade doesn't keep reporting the same harmless "awaiting reboot" line. - dtb_pairing_state (only reached once kver_match_state=ok) answers whether firmware paired the running kernel with the correct DTB content. Content self-consistency is checked first and short-circuits to apply_confirmed regardless of ESRT state; ESRT is consulted only to diagnose a mismatch, first by scanning every other installed kernel's own dtb-provenance-sha256 for a match against the running DTB (suspected_dtb_rollback, naming a rollback_target_kver) before falling back to a per-file sha256 diff against the provenance manifest (content_mismatch_localized). Each check writes a one-line human-readable summary as the last field of last-verify-state, so the verdict is readable without decoding the other fields; dtb-capsule-motd.sh reuses that summary to surface any non-healthy state at login. Signed-off-by: Guanquan Tian --- debian.qcom/control.stub.in | 15 + .../config/hamoa/capsule.env | 9 + .../config/purwa/capsule.env | 11 + .../dtb-capsule-runtime/dtb-capsule-motd.sh | 45 ++ .../systemd/dtb-capsule-verify.service | 18 + .../verify-capsule-result.sh | 436 ++++++++++++++++++ debian.qcom/rules.d/arm64.mk | 1 + debian.qcom/templates/dtb-capsule.postinst.in | 231 ++++++++++ debian.qcom/templates/dtb-capsule.prerm.in | 40 ++ debian/rules.d/0-common-vars.mk | 7 + debian/rules.d/2-binary-arch.mk | 109 +++++ 11 files changed, 922 insertions(+) create mode 100644 debian.qcom/dtb-capsule-runtime/config/hamoa/capsule.env create mode 100644 debian.qcom/dtb-capsule-runtime/config/purwa/capsule.env create mode 100644 debian.qcom/dtb-capsule-runtime/dtb-capsule-motd.sh create mode 100644 debian.qcom/dtb-capsule-runtime/systemd/dtb-capsule-verify.service create mode 100755 debian.qcom/dtb-capsule-runtime/verify-capsule-result.sh create mode 100755 debian.qcom/templates/dtb-capsule.postinst.in create mode 100644 debian.qcom/templates/dtb-capsule.prerm.in diff --git a/debian.qcom/control.stub.in b/debian.qcom/control.stub.in index 2bbf130569444..57f77b3ae2ade 100644 --- a/debian.qcom/control.stub.in +++ b/debian.qcom/control.stub.in @@ -92,3 +92,18 @@ Description: Linux kernel version specific tools for version PKGVER-ABINUM version locked tools (such as perf and x86_energy_perf_policy) for version PKGVER-ABINUM. You probably want to install linux-tools-PKGVER-ABINUM-. + +Package: dtb-capsule-PKGVER-ABINUM-qcom +Build-Profiles: +Architecture: arm64 +Section: admin +Priority: optional +Depends: ${misc:Depends}, linux-modules-PKGVER-ABINUM-qcom +Provides: dtb-capsule-qcom +Conflicts: dtb-capsule-qcom +Replaces: dtb-capsule-qcom +Description: DTB UEFI capsule update for version PKGVER + This package provides a UEFI capsule image and provenance manifest + for updating the Qualcomm device tree blob (DTB) staged for version + PKGVER, along with a systemd service that verifies the update result + on the next boot. diff --git a/debian.qcom/dtb-capsule-runtime/config/hamoa/capsule.env b/debian.qcom/dtb-capsule-runtime/config/hamoa/capsule.env new file mode 100644 index 0000000000000..e2a47c01d8be8 --- /dev/null +++ b/debian.qcom/dtb-capsule-runtime/config/hamoa/capsule.env @@ -0,0 +1,9 @@ +# Capsule generation parameters — platform-specific, not kernel-version-specific. + +# ESRT/FMP GUID for IQ-X7181 (Hamoa), from cbsp-boot-utilities +# uefi_capsule_generation README, confirmed on real hardware +# (entry0/fw_class read from /sys/firmware/efi/esrt on a Hamoa device). +FMP_GUID=0F6D58FC-2258-4D27-9E23-D77219B0897C + +# vendor qcom_capsule_tool --target value (see UpdateFvXml.py SUPPORTED_PLATFORMS). +TARGET=IQ-X7181 diff --git a/debian.qcom/dtb-capsule-runtime/config/purwa/capsule.env b/debian.qcom/dtb-capsule-runtime/config/purwa/capsule.env new file mode 100644 index 0000000000000..11535f8ce66a3 --- /dev/null +++ b/debian.qcom/dtb-capsule-runtime/config/purwa/capsule.env @@ -0,0 +1,11 @@ +# Capsule generation parameters — platform-specific, not kernel-version-specific. + +# ESRT/FMP GUID for IQ-X5121 (Purwa), from cbsp-boot-utilities +# uefi_capsule_generation README, confirmed on real hardware +# (entry0/fw_class read from /sys/firmware/efi/esrt on a Purwa device). +FMP_GUID=185a798b-13b2-4595-bd08-e2770a4bb190 + +# vendor qcom_capsule_tool --target value (see UpdateFvXml.py SUPPORTED_PLATFORMS). +# IQ-X5121 maps to the same qcom-ptool platform dir (iq-x7181-evk) as Hamoa's +# IQ-X7181. +TARGET=IQ-X5121 diff --git a/debian.qcom/dtb-capsule-runtime/dtb-capsule-motd.sh b/debian.qcom/dtb-capsule-runtime/dtb-capsule-motd.sh new file mode 100644 index 0000000000000..5a0f97e591e5f --- /dev/null +++ b/debian.qcom/dtb-capsule-runtime/dtb-capsule-motd.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# +# SPDX-License-Identifier: BSD-3-Clause-Clear +# +# ============================================================================= +# Installed as /etc/update-motd.d/85-dtb-capsule. Run by pam_motd on every +# interactive login; prints nothing when the last verify-capsule-result.sh +# run recorded a fully healthy state, so admins only see this when something +# needs attention. +STATE_DIR="${STATE_DIR:-/var/lib/dtb-capsule}" +VERIFY_STATE_FILE="${STATE_DIR}/last-verify-state" +BOOT_ID_NODE="${BOOT_ID_NODE:-/proc/sys/kernel/random/boot_id}" + +[ -f "$VERIFY_STATE_FILE" ] || exit 0 + +boot_id="" +kver_match_state="" +dtb_pairing_state="" +guid_conflict="" +guid_conflict_detail="" +dtb_kver_content_match="" +detail="" +summary="" +# shellcheck disable=SC1090 +. "$VERIFY_STATE_FILE" + +# Stale boot_id means this boot's check hasn't run/finished yet. +CURRENT_BOOT_ID="$(cat "$BOOT_ID_NODE" 2>/dev/null || echo "")" +if [ -n "$CURRENT_BOOT_ID" ] && [ "$boot_id" != "$CURRENT_BOOT_ID" ]; then + echo "*** dtb-capsule: capsule verification for this boot has not completed yet ***" + echo " check again shortly, or inspect directly: cat ${VERIFY_STATE_FILE}" + exit 0 +fi + +[ "$kver_match_state" = "ok" ] && [ "$dtb_pairing_state" = "apply_confirmed" ] && [ "$guid_conflict" != "true" ] && exit 0 + +echo "*** dtb-capsule: last verify state is not fully healthy ***" +echo " ${summary}" +echo " kver_match_state=${kver_match_state} dtb_pairing_state=${dtb_pairing_state}" +echo " dtb_kver_content_match=${dtb_kver_content_match}" +[ -n "$detail" ] && echo " detail: ${detail}" +echo " run 'journalctl -t dtb-capsule-verify' for details." + +exit 0 diff --git a/debian.qcom/dtb-capsule-runtime/systemd/dtb-capsule-verify.service b/debian.qcom/dtb-capsule-runtime/systemd/dtb-capsule-verify.service new file mode 100644 index 0000000000000..1e8880cacf1b8 --- /dev/null +++ b/debian.qcom/dtb-capsule-runtime/systemd/dtb-capsule-verify.service @@ -0,0 +1,18 @@ +[Unit] +Description=Verify UEFI capsule DTB update result after boot +After=multi-user.target +# ConditionPathExists only checks the directory exists, not that it's +# non-empty — firmware typically clears the directory's contents but leaves +# the directory itself, so this alone does not stop the service from +# re-running (and re-logging the same stale ESRT result) on every +# subsequent boot. verify-capsule-result.sh guards against that itself via +# a last-verified-kernel-version marker in /var/lib/dtb-capsule/. +ConditionPathExists=/boot/efi/EFI/UpdateCapsule + +[Service] +Type=oneshot +ExecStart=/usr/share/dtb-capsule/verify-capsule-result.sh +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target diff --git a/debian.qcom/dtb-capsule-runtime/verify-capsule-result.sh b/debian.qcom/dtb-capsule-runtime/verify-capsule-result.sh new file mode 100755 index 0000000000000..331be2e85c917 --- /dev/null +++ b/debian.qcom/dtb-capsule-runtime/verify-capsule-result.sh @@ -0,0 +1,436 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# +# SPDX-License-Identifier: BSD-3-Clause-Clear +# +# ============================================================================= +# Runs once after boot to confirm the staged capsule was actually applied by +# firmware. +# +# Writes fields to $STATE_DIR/last-verify-state for other tooling to +# consume (fleet agents, recovery services, /etc/update-motd.d/85-dtb-capsule): +# - kver_match_state: whether the installed dtb-capsule package matches the +# kernel running right now. +# - dtb_pairing_state: (only meaningful when kver_match_state=ok) whether +# firmware paired the running kernel with the correct DTB content. +# - rollback_target_kver / rollback_target_available: (only meaningful when +# dtb_pairing_state=suspected_dtb_rollback) which installed kernel the +# running DTB's content actually belongs to, and whether that kernel's +# package is still installed on this device. +# - guid_conflict / esrt_dedup_skipped: side-channel flags reporting that +# the check didn't fully run, not conclusions about this update. +# - summary: one-line human-readable verdict distilled from the fields +# above, always the last line so `tail -1` or a glance at the file end +# is enough to know whether anything needs attention. +set -e + +log() { echo "dtb-capsule-verify: $*"; logger -t dtb-capsule-verify "$*" 2>/dev/null || true; } + +# Reads each platform's FMP_GUID from its capsule.env and checks it against +# ESRT; reports on whichever platform's GUID matches. +# Overridable for unit-testing this script without touching the real /usr, +# /sys, /var, or the host's actual kernel version. +PKG_SHARE="${PKG_SHARE:-/usr/share/dtb-capsule}" +ESRT_DIR="${ESRT_DIR:-/sys/firmware/efi/esrt/entries}" +STATE_DIR="${STATE_DIR:-/var/lib/dtb-capsule}" +RUNNING_KVER="${RUNNING_KVER:-$(uname -r)}" +BOOT_ID="${BOOT_ID:-$(cat /proc/sys/kernel/random/boot_id 2>/dev/null || echo "")}" +LAST_VERIFIED_KVER_FILE="${STATE_DIR}/last-verified-kver" +LAST_ESRT_CONFIRMED_FILE="${STATE_DIR}/last-esrt-confirmed" +LAST_ESRT_DETAIL_FILE="${STATE_DIR}/last-esrt-detail" +GUID_CONFLICT_FILE="${STATE_DIR}/last-guid-conflict" +VERIFY_STATE_FILE="${STATE_DIR}/last-verify-state" +REBOOT_PENDING_SINCE_FILE="${STATE_DIR}/reboot-pending-since" +MODULES_DIR="${MODULES_DIR:-/usr/lib/modules}" +CAPSULE_DIR="${CAPSULE_DIR:-/boot/efi/EFI/UpdateCapsule}" +# Directory where the packaged .dtb/.dtbo files are installed for this +# kernel version. +DEVICE_TREE_DIR="${DEVICE_TREE_DIR:-/usr/lib/firmware/${RUNNING_KVER}/device-tree/qcom}" + +mkdir -p "$STATE_DIR" + +# Reports whether the last install/upgrade skipped capsule staging due to an +# ambiguous ESRT FMP_GUID match across packaged platforms. +GUID_CONFLICT="false" +GUID_CONFLICT_DETAIL="" +if [ -f "$GUID_CONFLICT_FILE" ]; then + GUID_CONFLICT="true" + GUID_CONFLICT_DETAIL="$(cat "$GUID_CONFLICT_FILE" 2>/dev/null || echo "")" + log "WARNING: last install/upgrade skipped capsule staging due to ambiguous ESRT FMP_GUID match: ${GUID_CONFLICT_DETAIL}" +fi + +ESRT_DEDUP_SKIPPED="false" + +# Returns the dtb-provenance-sha256 marker shipped in a given kver's +# linux-modules package, if that package is still installed on this device. +dtb_provenance_sha256_for_kver() { + _f="${MODULES_DIR}/$1/dtb-provenance-sha256" + [ -f "$_f" ] || return 1 + cat "$_f" +} + +# dpkg's Status field for linux-modules-, or empty if there's no +# record at all (never installed, or cleaned up by autoremove). +linux_modules_status_for_kver() { + dpkg-query -W -f='${Status}' "linux-modules-$1" 2>/dev/null +} + +# Distinguishes reboot_pending (mismatch just seen, no reboot yet) from +# reboot_stalled (mismatch has survived a reboot) via boot_id, not a boot +# counter, so clock skew can't distort it. +check_reboot_stall() { + _tracked_kver="" + _tracked_boot_id="" + if [ -f "$REBOOT_PENDING_SINCE_FILE" ]; then + _tracked_kver="$(grep '^expected_kver=' "$REBOOT_PENDING_SINCE_FILE" 2>/dev/null | cut -d= -f2-)" + _tracked_boot_id="$(grep '^first_boot_id=' "$REBOOT_PENDING_SINCE_FILE" 2>/dev/null | cut -d= -f2-)" + fi + + if [ "$_tracked_kver" != "$DTB_CAPSULE_EXPECTED_KVER" ]; then + printf 'expected_kver=%s\nfirst_boot_id=%s\n' "$DTB_CAPSULE_EXPECTED_KVER" "$BOOT_ID" > "$REBOOT_PENDING_SINCE_FILE" + echo "reboot_pending" + elif [ "$_tracked_boot_id" = "$BOOT_ID" ]; then + echo "reboot_pending" + else + echo "reboot_stalled" + fi +} + +# --- Cross-build comparison input: dtb_kver_content_match. Computed +# unconditionally so every branch below reports the same value. --- +DT_PROVENANCE_DIR="${DT_PROVENANCE_DIR:-/sys/firmware/devicetree/base/qcom-dtb-capsule-provenance}" +RUNNING_DTB_SHA="" +if [ -f "${DT_PROVENANCE_DIR}/dtb-provenance-sha256" ]; then + RUNNING_DTB_SHA="$(tr -d '\0' < "${DT_PROVENANCE_DIR}/dtb-provenance-sha256")" +fi + +DTB_CAPSULE_EXPECTED_KVER_FILE="${PKG_SHARE}/expected-kver" +if [ -f "$DTB_CAPSULE_EXPECTED_KVER_FILE" ]; then + DTB_CAPSULE_EXPECTED_KVER="$(cat "$DTB_CAPSULE_EXPECTED_KVER_FILE" 2>/dev/null || echo "")" +else + DTB_CAPSULE_EXPECTED_KVER="" +fi +DTB_CAPSULE_EXPECTED_SHA="" +[ -n "$DTB_CAPSULE_EXPECTED_KVER" ] && DTB_CAPSULE_EXPECTED_SHA="$(dtb_provenance_sha256_for_kver "$DTB_CAPSULE_EXPECTED_KVER" 2>/dev/null || echo "")" + +# dtb_kver_content_match: whether the running DTB's provenance sha256 matches +# the linux-modules- package installed for RUNNING_KVER right now. +RUNNING_DTB_MATCHES_INSTALLED_MODULES="unknown" +RUNNING_INSTALLED_DTB_SHA="$(dtb_provenance_sha256_for_kver "$RUNNING_KVER" 2>/dev/null || echo "")" +if [ -n "$RUNNING_DTB_SHA" ] && [ -n "$RUNNING_INSTALLED_DTB_SHA" ]; then + if [ "$RUNNING_DTB_SHA" = "$RUNNING_INSTALLED_DTB_SHA" ]; then + RUNNING_DTB_MATCHES_INSTALLED_MODULES="ok" + else + RUNNING_DTB_MATCHES_INSTALLED_MODULES="mismatch" + fi +fi + +# Distills kver_match_state/dtb_pairing_state (plus the cross-build fields) +# into one human-readable line, so a consumer only needs the last field of +# last-verify-state to know whether anything needs attention. +summary_for_state() { + case "$1:$2" in + package_mismatch:*) + echo "ERROR: package targets a kernel version not installed on this device" ;; + reboot_pending:*) + echo "PENDING: capsule staged for the expected kernel, awaiting reboot into it" ;; + reboot_stalled:*) + echo "WARNING: rebooted at least once but device still isn't running the expected kernel - reboot may have stalled" ;; + no_capsule_for_running_kernel:*) + echo "WARNING: no capsule targets the running kernel, but its own DTB content is self-consistent" ;; + kernel_dtb_mismatch:*) + echo "ERROR: running kernel's own DTB content does not match its installed package" ;; + ok:pending) + echo "PENDING: capsule not yet confirmed applied by firmware" ;; + ok:apply_failed) + echo "ERROR: firmware reported the capsule update failed" ;; + ok:suspected_dtb_rollback) + echo "WARNING: suspected DTB rollback - firmware kept/reverted to a previous DTB despite reporting apply success" ;; + ok:apply_confirmed) + echo "OK: capsule applied and verified" ;; + ok:content_mismatch_localized) + echo "ERROR: applied DTB content does not match the installed kernel package" ;; + *) + echo "UNKNOWN: cannot confirm capsule result (see detail)" ;; + esac +} + +# Persists the fields below as key=value, sourceable by any POSIX-sh tool +# (MOTD script, recovery service). rollback_target_kver/rollback_target_available +# are always emitted (even empty) so downstream consumers can safely +# `. last-verify-state` and test `-n "$rollback_target_kver"`. +write_state() { + _kver_match_state="$1" + _dtb_pairing_state="$2" + _detail="$3" + _rollback_target_kver="${4:-}" + _rollback_target_available="${5:-}" + _summary="$(summary_for_state "$_kver_match_state" "$_dtb_pairing_state")" + if [ "$GUID_CONFLICT" = "true" ]; then + _summary="${_summary}; guid_conflict: ${GUID_CONFLICT_DETAIL}" + fi + cat > "$VERIFY_STATE_FILE" </dev/null)" ]; then + STALL_STATE="$(check_reboot_stall)" + log "running kernel ${RUNNING_KVER} does not match capsule's expected kernel ${DTB_CAPSULE_EXPECTED_KVER}; ${CAPSULE_DIR} still holds an unconsumed capsule — ${STALL_STATE}" + write_state "$STALL_STATE" "unknown" "expected-kver=${DTB_CAPSULE_EXPECTED_KVER} installed, capsule still unconsumed in ${CAPSULE_DIR}" + exit 0 + fi + + if [ -n "$DTB_CAPSULE_EXPECTED_SHA" ] && [ "$DTB_CAPSULE_EXPECTED_SHA" = "$RUNNING_DTB_SHA" ]; then + STALL_STATE="$(check_reboot_stall)" + log "running kernel ${RUNNING_KVER} does not match capsule's expected kernel ${DTB_CAPSULE_EXPECTED_KVER}, but running DTB content already matches it (staging was skipped) — ${STALL_STATE}" + write_state "$STALL_STATE" "unknown" "expected-kver=${DTB_CAPSULE_EXPECTED_KVER}, capsule staging was skipped (content already matched)" + exit 0 + fi + + # No unconsumed capsule and content isn't the expected one: whether + # this is benign depends on the running kernel's own DTB. + case "$RUNNING_DTB_MATCHES_INSTALLED_MODULES" in + ok) + log "running kernel ${RUNNING_KVER} has no capsule targeting it, but its own DTB content is self-consistent — likely a kernel-only install with no matching dtb-capsule package" + write_state "no_capsule_for_running_kernel" "unknown" "expected-kver=${DTB_CAPSULE_EXPECTED_KVER}, running kver=${RUNNING_KVER} has no unconsumed capsule; own DTB content is self-consistent" + exit 0 + ;; + mismatch) + log "ERROR: running kernel ${RUNNING_KVER}'s own DTB content does not match its installed linux-modules package — kernel and DTB are paired incorrectly" + write_state "kernel_dtb_mismatch" "unknown" "expected-kver=${DTB_CAPSULE_EXPECTED_KVER}, running kver=${RUNNING_KVER}'s own DTB content mismatches its installed package" + exit 0 + ;; + *) + rm -f "$REBOOT_PENDING_SINCE_FILE" + log "cannot determine whether running kernel ${RUNNING_KVER}'s own DTB content is self-consistent — no provenance data available" + write_state "unknown" "unknown" "expected-kver=${DTB_CAPSULE_EXPECTED_KVER}, running kver=${RUNNING_KVER}'s own DTB content self-consistency unknown" + exit 0 + ;; + esac +else + rm -f "$REBOOT_PENDING_SINCE_FILE" +fi + +# --- Phase 2: dtb_pairing_state — whether firmware paired the running kernel +# with the correct DTB content. Only reached when kver_match_state=ok. +# Content self-consistency short-circuits to apply_confirmed regardless of +# ESRT; ESRT is consulted only to diagnose a mismatch. --- +if [ "$RUNNING_DTB_MATCHES_INSTALLED_MODULES" = "ok" ]; then + log "CONFIRMED: DTB's provenance sha256 matches the linux-modules-${RUNNING_KVER} package actually installed on this device" + write_state "ok" "apply_confirmed" "provenance sha256 match" + exit 0 +fi + +# Whether firmware has finished draining the staged capsule from UpdateCapsule. +CAPSULE_DIR_EMPTY=1 +if [ -d "$CAPSULE_DIR" ] && [ -n "$(ls -A "$CAPSULE_DIR" 2>/dev/null)" ]; then + CAPSULE_DIR_EMPTY=0 + log "WARNING: ${CAPSULE_DIR} still contains capsule files after boot — firmware may not have consumed them" +else + log "UpdateCapsule directory empty/absent — consistent with firmware having consumed and cleared the capsule" +fi + +# --- dtb_pairing_state, step 1: whether firmware actually applied the capsule (ESRT) --- +# +# Cache is cleared and rewritten on every ESRT scan, ensuring stale results +# from prior kernel versions or device configs are not reused. +MATCHED_ANY=0 +ESRT_CONFIRMED=0 +ESRT_STATUS_LINE="" +if [ -f "$LAST_VERIFIED_KVER_FILE" ] && [ "$(cat "$LAST_VERIFIED_KVER_FILE" 2>/dev/null || echo "")" = "$RUNNING_KVER" ]; then + ESRT_DEDUP_SKIPPED="true" + MATCHED_ANY=1 + ESRT_CONFIRMED="$(cat "$LAST_ESRT_CONFIRMED_FILE" 2>/dev/null || echo "0")" + ESRT_STATUS_LINE="$(cat "$LAST_ESRT_DETAIL_FILE" 2>/dev/null || echo "")" + log "already verified ESRT capsule result for kernel ${RUNNING_KVER}, skipping ESRT check (recalling esrt_confirmed=${ESRT_CONFIRMED} from last check${ESRT_STATUS_LINE:+; detail: ${ESRT_STATUS_LINE}})" +else + for ENV_FILE in "${PKG_SHARE}"/*/capsule.env; do + [ -f "$ENV_FILE" ] || continue + MACHINE="$(basename "$(dirname "$ENV_FILE")")" + FMP_GUID="" + # shellcheck disable=SC1090 + . "$ENV_FILE" + if [ -z "$FMP_GUID" ]; then + log "WARNING: FMP_GUID not set in ${ENV_FILE}, skipping" + continue + fi + FMP_GUID="$(echo "$FMP_GUID" | tr 'A-Z' 'a-z')" + + # last_attempt_status/_version record the outcome of the last capsule + # attempt for this GUID, regardless of whether fwupd or + # Capsule-on-Disk delivered it. + ESRT_MATCH="" + if [ -d "$ESRT_DIR" ]; then + for entry in "$ESRT_DIR"/entry*; do + [ -d "$entry" ] || continue + FW_CLASS="$(cat "${entry}/fw_class" 2>/dev/null | tr 'A-Z' 'a-z')" + if [ "$FW_CLASS" = "$FMP_GUID" ]; then + ESRT_MATCH="$entry" + break + fi + done + fi + + [ -n "$ESRT_MATCH" ] || continue + MATCHED_ANY=1 + + STATUS="$(cat "${ESRT_MATCH}/last_attempt_status" 2>/dev/null || echo "")" + LAST_VER="$(cat "${ESRT_MATCH}/last_attempt_version" 2>/dev/null || echo "")" + FW_VER="$(cat "${ESRT_MATCH}/fw_version" 2>/dev/null || echo "")" + log "platform=${MACHINE} ESRT entry ${ESRT_MATCH}: last_attempt_status=${STATUS} last_attempt_version=${LAST_VER} fw_version=${FW_VER}" + if [ "$STATUS" = "0" ] && [ -n "$FW_VER" ] && [ "$FW_VER" = "$LAST_VER" ]; then + log "capsule update confirmed successful via ESRT (platform=${MACHINE})" + ESRT_CONFIRMED=1 + else + case "$STATUS" in + 1) DESC="ErrorUnsuccessful" ;; + 2) DESC="ErrorInsufficientResources" ;; + 3) DESC="ErrorIncorrectVersion" ;; + 4) DESC="ErrorInvalidFormat" ;; + 5) DESC="ErrorAuthError (signature verification failed)" ;; + 6) DESC="ErrorPwrEvtAC" ;; + 7) DESC="ErrorPwrEvtBatt" ;; + 8) DESC="ErrorUnsatisfiedDependencies" ;; + *) DESC="unknown" ;; + esac + ESRT_STATUS_LINE="platform=${MACHINE} status=${STATUS} [${DESC}] fw_version=${FW_VER} vs last_attempt_version=${LAST_VER}" + log "WARNING: ESRT does not confirm a successful update (${ESRT_STATUS_LINE})" + fi + + if command -v fwupdmgr >/dev/null 2>&1; then + RESULT="$(fwupdmgr get-history 2>/dev/null | grep -A5 -i "qcom.*dtb\|${FMP_GUID}" || true)" + if [ -n "$RESULT" ]; then + log "fwupdmgr history (platform=${MACHINE}): $RESULT" + else + log "no matching entry in fwupdmgr get-history (platform=${MACHINE}) — capsule may not have been processed by fwupd" + fi + fi + done + + if [ "$MATCHED_ANY" -eq 0 ]; then + log "WARNING: no ESRT entry found matching any packaged platform's FMP_GUID — cannot confirm capsule result via ESRT" + elif [ "$CAPSULE_DIR_EMPTY" -eq 1 ]; then + rm -f "$LAST_VERIFIED_KVER_FILE" "$LAST_ESRT_CONFIRMED_FILE" "$LAST_ESRT_DETAIL_FILE" + echo "$RUNNING_KVER" > "$LAST_VERIFIED_KVER_FILE" + echo "$ESRT_CONFIRMED" > "$LAST_ESRT_CONFIRMED_FILE" + echo "$ESRT_STATUS_LINE" > "$LAST_ESRT_DETAIL_FILE" + else + log "WARNING: ${CAPSULE_DIR} still has an unconsumed capsule — not caching this ESRT result, will re-check on the next boot" + fi +fi + +if [ "$MATCHED_ANY" -eq 0 ] || [ "$CAPSULE_DIR_EMPTY" -eq 0 ]; then + write_state "ok" "pending" "no ESRT match yet or capsule still staged in ${CAPSULE_DIR}" + exit 0 +fi + +if [ -z "$RUNNING_DTB_SHA" ]; then + log "WARNING: no DTB provenance node at ${DT_PROVENANCE_DIR} — cannot verify DTB content provenance" + write_state "ok" "unknown" "ESRT confirmed apply, but no DTB provenance node at ${DT_PROVENANCE_DIR}" + exit 0 +fi + +DTB_PROVENANCE_MARKER="${MODULES_DIR}/${RUNNING_KVER}/dtb-provenance-sha256" +if [ "$RUNNING_DTB_MATCHES_INSTALLED_MODULES" = "unknown" ]; then + log "WARNING: cannot cross-check provenance sha256 (${DTB_PROVENANCE_MARKER} missing)" + write_state "ok" "unknown" "cannot cross-check provenance sha256 (${DTB_PROVENANCE_MARKER} missing)" + exit 0 +fi + +log "WARNING: DTB's provenance sha256=${RUNNING_DTB_SHA} does not match installed linux-modules-${RUNNING_KVER} (${RUNNING_INSTALLED_DTB_SHA}) — this capsule's DTB was NOT built from the kernel package currently installed on this device" + +# --- dtb_pairing_state, step 2: name a rollback target before blaming +# firmware. Scans every OTHER installed kernel's dtb-provenance-sha256 for a +# match against the running DTB; ties resolve to the highest-versioned +# match. --- +ROLLBACK_TARGET_KVER="" +for CANDIDATE_KVER in $(ls "$MODULES_DIR" 2>/dev/null | sort -V); do + [ "$CANDIDATE_KVER" != "$RUNNING_KVER" ] || continue + CANDIDATE_DTB_SHA="$(dtb_provenance_sha256_for_kver "$CANDIDATE_KVER" 2>/dev/null || echo "")" + [ -n "$CANDIDATE_DTB_SHA" ] || continue + [ "$CANDIDATE_DTB_SHA" = "$RUNNING_DTB_SHA" ] && ROLLBACK_TARGET_KVER="$CANDIDATE_KVER" +done + +if [ -n "$ROLLBACK_TARGET_KVER" ]; then + ROLLBACK_TARGET_AVAILABLE="false" + if [ "$(linux_modules_status_for_kver "$ROLLBACK_TARGET_KVER")" = "install ok installed" ]; then + ROLLBACK_TARGET_AVAILABLE="true" + fi + log "WARNING: running DTB's provenance sha256 matches installed linux-modules-${ROLLBACK_TARGET_KVER} — firmware appears to have kept/reverted to that kernel's DTB despite ESRT reporting success for ${RUNNING_KVER}" + write_state "ok" "suspected_dtb_rollback" "running DTB's provenance sha256 matches linux-modules-${ROLLBACK_TARGET_KVER}" "$ROLLBACK_TARGET_KVER" "$ROLLBACK_TARGET_AVAILABLE" + exit 0 +fi + +if [ "$ESRT_CONFIRMED" -eq 0 ]; then + write_state "ok" "apply_failed" "$ESRT_STATUS_LINE" + exit 0 +fi + +# Localizes the mismatch to specific files: sha256 each packaged .dtb/.dtbo +# file installed under DEVICE_TREE_DIR and diff against the manifest's +# per-file sha256 lines. +CONTENT_SHA256SUMS_MANIFEST="${PKG_SHARE}/dtb-provenance-content-sha256sums.txt" +DIFF_FILES="" +if [ -f "$CONTENT_SHA256SUMS_MANIFEST" ]; then + if [ -d "$DEVICE_TREE_DIR" ]; then + while IFS= read -r line; do + [ -n "$line" ] || continue + BUILD_SHA256="$(printf '%s\n' "$line" | awk '{print $1}')" + FILE_PATH="$(printf '%s\n' "$line" | cut -f2- -d' ' | sed 's/^ *//')" + INSTALLED_FILE="${DEVICE_TREE_DIR}/${FILE_PATH}" + if [ ! -f "$INSTALLED_FILE" ]; then + DIFF_FILES="${DIFF_FILES}${DIFF_FILES:+, }${FILE_PATH} (missing on device)" + continue + fi + INSTALLED_SHA256="$(sha256sum "$INSTALLED_FILE" | awk '{print $1}')" + if [ "$BUILD_SHA256" != "$INSTALLED_SHA256" ]; then + DIFF_FILES="${DIFF_FILES}${DIFF_FILES:+, }${FILE_PATH}" + fi + done < "$CONTENT_SHA256SUMS_MANIFEST" + + if [ -n "$DIFF_FILES" ]; then + log "WARNING: provenance sha256 mismatch localized to: ${DIFF_FILES}" + else + log "WARNING: provenance sha256 mismatch is not localized to any packaged .dtb/.dtbo under ${DEVICE_TREE_DIR} — the differing file is some other package member" + DIFF_FILES="(not localized to any packaged .dtb/.dtbo)" + fi + else + log "WARNING: cannot localize provenance sha256 mismatch — no ${DEVICE_TREE_DIR}" + DIFF_FILES="(no ${DEVICE_TREE_DIR} to localize against)" + fi +else + log "WARNING: cannot localize provenance sha256 mismatch — no ${CONTENT_SHA256SUMS_MANIFEST}" + DIFF_FILES="(no ${CONTENT_SHA256SUMS_MANIFEST} to localize against)" +fi + +write_state "ok" "content_mismatch_localized" "provenance sha256 mismatch: ${DIFF_FILES}" + +exit 0 diff --git a/debian.qcom/rules.d/arm64.mk b/debian.qcom/rules.d/arm64.mk index 40ba8f51c905f..62d2bb8f54ce4 100644 --- a/debian.qcom/rules.d/arm64.mk +++ b/debian.qcom/rules.d/arm64.mk @@ -17,3 +17,4 @@ do_tools_bpftool = true do_dtbs = true do_fitimage = true +do_dtb_capsule = true diff --git a/debian.qcom/templates/dtb-capsule.postinst.in b/debian.qcom/templates/dtb-capsule.postinst.in new file mode 100755 index 0000000000000..025151c90e57d --- /dev/null +++ b/debian.qcom/templates/dtb-capsule.postinst.in @@ -0,0 +1,231 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# +# SPDX-License-Identifier: BSD-3-Clause-Clear +# +# ============================================================================= +# Installed as DEBIAN/postinst inside dtb-capsule--qcom.deb. +# Rendered into debian/dtb-capsule--qcom.postinst by debian/rules. +# Reads its target kernel version from the package's own expected-kver file. +# +# Runs stage_capsule() on `dpkg --configure` of this package. +set -e + +PKG_SHARE="${PKG_SHARE:-/usr/share/dtb-capsule}" +DTB_CAPSULE_EXPECTED_KVER_FILE="${PKG_SHARE}/expected-kver" +DTB_CAPSULE_EXPECTED_SHA_FILE="${PKG_SHARE}/expected-dtb-sha256" +ESP_MOUNT="${ESP_MOUNT:-/boot/efi}" +CAPSULE_DIR="${CAPSULE_DIR:-${ESP_MOUNT}/EFI/UpdateCapsule}" +MODULES_DIR="${MODULES_DIR:-/usr/lib/modules}" +# Shared with scripts/verify-capsule-result.sh. +STATE_DIR="${STATE_DIR:-/var/lib/dtb-capsule}" +GUID_CONFLICT_FILE="${STATE_DIR}/last-guid-conflict" +VERIFY_STATE_FILE="${STATE_DIR}/last-verify-state" +DT_PROVENANCE_DIR="${DT_PROVENANCE_DIR:-/sys/firmware/devicetree/base/qcom-dtb-capsule-provenance}" +OSIND_GUID="8be4df61-93ca-11d2-aa0d-00e098032b8c" +OSIND_NAME="OsIndications" +OSIND_VAR="/sys/firmware/efi/efivars/${OSIND_NAME}-${OSIND_GUID}" +CAPSULE_PENDING_GUID="882f8c2b-9646-435f-8de5-f208ff80c1bd" +CAPSULE_PENDING_NAME="IsCapsulePendingInPersistedMedia" +CAPSULE_PENDING_VAR="/sys/firmware/efi/efivars/${CAPSULE_PENDING_NAME}-${CAPSULE_PENDING_GUID}" +# Overridable for unit-testing this script without touching the real /sys. +ESRT_ENTRIES_DIR="${ESRT_ENTRIES_DIR:-/sys/firmware/efi/esrt/entries}" + +log() { echo "dtb-capsule: $*" >&2; } + +# Reads bytes 4..11 of $1 (skipping the EFI variable's 4-byte attribute +# header) as a little-endian 64-bit value, echoing the result in decimal. +# Reconstructs the value from single bytes rather than using `od --endian` +# (a GNU coreutils extension absent from busybox od), portable across +# dash/busybox sh/bash. +le64_from_offset4() { + _v=0 + _shift=0 + for _byte in $(od -An -tx1 -j4 -N8 "$1" 2>/dev/null); do + _v=$(( _v | (0x${_byte} << _shift) )) + _shift=$(( _shift + 8 )) + done + echo "$_v" +} + +stage_capsule() { + # Clear the previous attempt's verify-state result. + rm -f "$VERIFY_STATE_FILE" + + # Clear IsCapsulePendingInPersistedMedia if it's stuck from a prior failed + # update (e.g. UEFI crash). If this bit is set, firmware thinks a capsule + # update is in progress and will reject new capsules. Reset it to 0x00 to + # allow new capsule update to proceed. + if [ -e "$CAPSULE_PENDING_VAR" ] && command -v efivar >/dev/null 2>&1; then + PENDING_VAL="$(od -An -tx1 -j4 -N1 "$CAPSULE_PENDING_VAR" 2>/dev/null | tr -d ' ')" + if [ "$PENDING_VAL" = "01" ]; then + log "WARNING: IsCapsulePendingInPersistedMedia is set (0x01) — prior capsule update may have failed; clearing it to allow new capsule update" + TMP_DATA="$(mktemp)" + printf '\x00' > "$TMP_DATA" + efivar -n "${CAPSULE_PENDING_GUID}-${CAPSULE_PENDING_NAME}" -f "$TMP_DATA" -w 2>/dev/null || log "WARNING: failed to clear IsCapsulePendingInPersistedMedia via efivar" + rm -f "$TMP_DATA" + fi + fi + + if [ ! -f "$DTB_CAPSULE_EXPECTED_KVER_FILE" ]; then + log "WARNING: ${DTB_CAPSULE_EXPECTED_KVER_FILE} missing — cannot determine target kernel version, skipping" + return 0 + fi + KVER="$(cat "$DTB_CAPSULE_EXPECTED_KVER_FILE")" + if [ -z "$KVER" ]; then + log "WARNING: ${DTB_CAPSULE_EXPECTED_KVER_FILE} empty, skipping" + return 0 + fi + + # Defense-in-depth against --force-depends bypassing the Depends check. + if ! dpkg-query -W -f='${Status}' "linux-modules-${KVER}" 2>/dev/null | grep -q "^install ok installed$"; then + log "ERROR: linux-modules-${KVER} is not installed — cannot stage capsule (installed with --force-depends?)" + exit 1 + fi + + # This package and linux-modules- must be the same build event. + DTB_CAPSULE_EXPECTED_SHA="$(cat "$DTB_CAPSULE_EXPECTED_SHA_FILE" 2>/dev/null || echo "")" + MODULES_DTB_SHA="$(cat "${MODULES_DIR}/${KVER}/dtb-provenance-sha256" 2>/dev/null || echo "")" + if [ -n "$DTB_CAPSULE_EXPECTED_SHA" ] && [ -n "$MODULES_DTB_SHA" ] && [ "$DTB_CAPSULE_EXPECTED_SHA" != "$MODULES_DTB_SHA" ]; then + log "ERROR: this package's expected-dtb-sha256 does not match linux-modules-${KVER}'s dtb-provenance-sha256 — package and kernel were not built together" + exit 1 + fi + + # One -dtb.cap per platform under $PKG_SHARE//. Pick + # the one whose FMP_GUID (capsule.env) is registered in this device's ESRT. + if [ ! -d "$ESRT_ENTRIES_DIR" ]; then + log "WARNING: ${ESRT_ENTRIES_DIR} not found (no ESRT support?) — cannot select a platform, skipping" + return 0 + fi + + DEVICE_GUIDS="$(cat "${ESRT_ENTRIES_DIR}"/entry*/fw_class 2>/dev/null | tr 'A-Z' 'a-z')" + if [ -z "$DEVICE_GUIDS" ]; then + log "WARNING: no fw_class entries readable under ${ESRT_ENTRIES_DIR} — skipping" + return 0 + fi + + MATCHED_MACHINE="" + MATCHED_MACHINES="" + MATCH_COUNT=0 + for env_file in "${PKG_SHARE}"/*/capsule.env; do + [ -f "$env_file" ] || continue + machine="$(basename "$(dirname "$env_file")")" + FMP_GUID="" + # shellcheck disable=SC1090 + . "$env_file" + [ -n "$FMP_GUID" ] || continue + fmp_guid_lc="$(printf '%s' "$FMP_GUID" | tr 'A-Z' 'a-z')" + if printf '%s\n' "$DEVICE_GUIDS" | grep -qx "$fmp_guid_lc"; then + MATCHED_MACHINE="$machine" + MATCHED_MACHINES="${MATCHED_MACHINES}${MATCHED_MACHINES:+,}${machine}(${fmp_guid_lc})" + MATCH_COUNT=$(( MATCH_COUNT + 1 )) + fi + done + + if [ "$MATCH_COUNT" -eq 0 ]; then + log "WARNING: no FMP_GUID among packaged platforms matched this device's ESRT entries — skipping capsule staging" + return 0 + elif [ "$MATCH_COUNT" -gt 1 ]; then + log "ERROR: ${MATCH_COUNT} packaged platforms' FMP_GUIDs matched this device's ESRT entries (ambiguous): ${MATCHED_MACHINES} — skipping capsule staging" + mkdir -p "$STATE_DIR" + printf '%s kver=%s matched=%s\n' "$(date -u +%FT%TZ)" "$KVER" "$MATCHED_MACHINES" > "$GUID_CONFLICT_FILE" + return 0 + fi + + log "matched platform '${MATCHED_MACHINE}' via ESRT FMP_GUID" + rm -f "$GUID_CONFLICT_FILE" + CAP_SRC="${PKG_SHARE}/${MATCHED_MACHINE}/${MATCHED_MACHINE}-dtb.cap" + + if [ ! -f "$CAP_SRC" ]; then + log "no .cap found at ${CAP_SRC}, skipping" + return 0 + fi + + # Already running the content this package would stage — skip and clear + # any stale .cap so the next boot's verifier doesn't see it as unconsumed. + RUNNING_DTB_SHA="" + if [ -f "${DT_PROVENANCE_DIR}/dtb-provenance-sha256" ]; then + RUNNING_DTB_SHA="$(tr -d '\0' < "${DT_PROVENANCE_DIR}/dtb-provenance-sha256" 2>/dev/null || echo "")" + fi + if [ -n "$DTB_CAPSULE_EXPECTED_SHA" ] && [ "$RUNNING_DTB_SHA" = "$DTB_CAPSULE_EXPECTED_SHA" ]; then + log "running DTB content already matches this package's expected-dtb-sha256 — skipping capsule staging" + rm -f "${CAPSULE_DIR}"/qcom-dtb-*.cap + return 0 + fi + + # An unconsumed .cap already in CAPSULE_DIR with OsIndications bit2 not + # set means firmware was never told to process it. Logged only; still + # proceeds to stage this capsule and (re)try setting the bit below. + if [ -d "$CAPSULE_DIR" ] && [ -n "$(ls -A "$CAPSULE_DIR" 2>/dev/null)" ] && command -v efivar >/dev/null 2>&1; then + PRIOR_BIT_VAL=0 + if [ -e "$OSIND_VAR" ]; then + PRIOR_BIT_VAL="$(le64_from_offset4 "$OSIND_VAR")" + fi + if [ $(( PRIOR_BIT_VAL & 4 )) -eq 0 ]; then + log "WARNING: ${CAPSULE_DIR} already holds an unconsumed capsule from a previous staging attempt, but OsIndications capsule-delivery bit is NOT set — firmware was likely never told to process it (interrupted previous install, or a prior efivar write that silently failed); proceeding to stage this capsule and (re)set the bit now" + fi + fi + + mkdir -p "$CAPSULE_DIR" + rm -f "${CAPSULE_DIR}"/qcom-dtb-*.cap + cp -f "$CAP_SRC" "${CAPSULE_DIR}/qcom-dtb-${KVER}.cap" + log "capsule staged at ${CAPSULE_DIR}/qcom-dtb-${KVER}.cap" + + # Uses `efivar -w`, not a raw efivarfs write, so it can create the + # OsIndications entry if it doesn't already exist in NVRAM. + if ! command -v efivar >/dev/null 2>&1; then + log "WARNING: efivar not installed, cannot set OsIndications — capsule will not be applied" + return 0 + fi + + CUR_VAL=0 + if [ -e "$OSIND_VAR" ]; then + CUR_VAL="$(le64_from_offset4 "$OSIND_VAR")" + fi + if [ $(( CUR_VAL & 4 )) -eq 0 ]; then + NEW_VAL=$(( CUR_VAL | 4 )) + TMP_DATA="$(mktemp)" + # \NNN (octal) escapes: dash's printf builtin does not understand + # \xHH and passes it through as literal characters. + OCT="" + i=0 + v=$NEW_VAL + while [ $i -lt 8 ]; do + OCT="${OCT}\\$(printf '%03o' "$(( v & 255 ))")" + v=$(( v >> 8 )) + i=$(( i + 1 )) + done + printf "$OCT" > "$TMP_DATA" + efivar -n "${OSIND_GUID}-${OSIND_NAME}" -f "$TMP_DATA" -w + rm -f "$TMP_DATA" + + # Read back what's actually in NVRAM now — `efivar -w`'s own exit + # status only reflects whether the ioctl/write syscall was accepted, + # not whether firmware later honors it, so confirm the bit is really + # readable-back-set before declaring success. + VERIFY_VAL=0 + if [ -e "$OSIND_VAR" ]; then + VERIFY_VAL="$(le64_from_offset4 "$OSIND_VAR")" + fi + if [ $(( VERIFY_VAL & 4 )) -eq 0 ]; then + log "ERROR: wrote OsIndications capsule-delivery bit via efivar, but read-back shows it is still NOT set (raw value=${VERIFY_VAL}) — capsule was staged but firmware will not be told to process it; check efivarfs is writable/mounted" + else + log "set OsIndications capsule-delivery bit via efivar (verified via read-back)" + fi + else + log "OsIndications capsule-delivery bit already set" + fi + + # No reboot here — firmware applies the capsule pre-OS on the next boot. + log "capsule staged for ${KVER}; will apply on next boot" +} + +case "$1" in + configure) + stage_capsule + ;; +esac + +#DEBHELPER# + +exit 0 diff --git a/debian.qcom/templates/dtb-capsule.prerm.in b/debian.qcom/templates/dtb-capsule.prerm.in new file mode 100644 index 0000000000000..6e3c534627bf3 --- /dev/null +++ b/debian.qcom/templates/dtb-capsule.prerm.in @@ -0,0 +1,40 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# +# SPDX-License-Identifier: BSD-3-Clause-Clear +# +# ============================================================================= +# Installed as DEBIAN/prerm inside dtb-capsule--qcom.deb. +# Rendered into debian/dtb-capsule--qcom.prerm by debian/rules. +# Reads its target kernel version from the package's own expected-kver file. +# +# On removal, deletes this package's staged-but-unconsumed capsule at +# /boot/efi/EFI/UpdateCapsule/qcom-dtb-.cap — that file lives outside +# dpkg's file list, so plain package removal would otherwise leave it for +# firmware to apply pre-OS on the next boot. +set -e + +PKG_SHARE="${PKG_SHARE:-/usr/share/dtb-capsule}" +DTB_CAPSULE_EXPECTED_KVER_FILE="${PKG_SHARE}/expected-kver" +CAPSULE_DIR="${CAPSULE_DIR:-/boot/efi/EFI/UpdateCapsule}" + +log() { echo "dtb-capsule: $*" >&2; } + +case "$1" in + remove) + if [ -f "$DTB_CAPSULE_EXPECTED_KVER_FILE" ]; then + KVER="$(cat "$DTB_CAPSULE_EXPECTED_KVER_FILE")" + if [ -n "$KVER" ]; then + CAP_FILE="${CAPSULE_DIR}/qcom-dtb-${KVER}.cap" + if [ -f "$CAP_FILE" ]; then + rm -f "$CAP_FILE" + log "removed unconsumed staged capsule ${CAP_FILE} (package being uninstalled)" + fi + fi + fi + ;; +esac + +#DEBHELPER# + +exit 0 diff --git a/debian/rules.d/0-common-vars.mk b/debian/rules.d/0-common-vars.mk index 67012a80caeac..b754b94178783 100644 --- a/debian/rules.d/0-common-vars.mk +++ b/debian/rules.d/0-common-vars.mk @@ -109,6 +109,13 @@ bldinfo_pkg_name=linux-buildinfo-$(abi_release) hdrs_pkg_name=linux-headers-$(abi_release) rust_pkg_name=linux-lib-rust-$(abi_release) indep_hdrs_pkg_name=$(DEB_SOURCE)-headers-$(abi_release) +dtb_capsule_pkg_name=dtb-capsule-$(abi_release)-qcom +dtb_capsule_cert_leaf ?= $(CURDIR)/$(DEBIAN)/certs/QcFMPCert.pem +dtb_capsule_cert_root ?= $(CURDIR)/$(DEBIAN)/certs/QcFMPRoot.pub.pem +dtb_capsule_cert_sub ?= $(CURDIR)/$(DEBIAN)/certs/QcFMPSub.pub.pem +dtb_capsule_fwver ?= 0.0.2.0 +dtb_capsule_lfwver ?= 0.0.0.0 +dtb_capsule_storage_type ?= NORUFS # # Similarly with the linux-source package, you need not build it as a developer. Its diff --git a/debian/rules.d/2-binary-arch.mk b/debian/rules.d/2-binary-arch.mk index ae9529af1c395..9953894220cdf 100644 --- a/debian/rules.d/2-binary-arch.mk +++ b/debian/rules.d/2-binary-arch.mk @@ -126,6 +126,8 @@ $(stampdir)/stamp-install-%: cloudpkgdir = $(CURDIR)/debian/$(cloud_flavour_pkg_ $(stampdir)/stamp-install-%: bpfdevpkgdir = $(CURDIR)/debian/linux-bpf-dev $(stampdir)/stamp-install-%: bpftoolpkgdir = $(CURDIR)/debian/$(bpftool_pkg_name) $(stampdir)/stamp-install-%: perfpkgdir = $(CURDIR)/debian/$(perf_pkg_name) +$(stampdir)/stamp-install-%: dtbcapsulepkgdir = $(CURDIR)/debian/$(dtb_capsule_pkg_name) +$(stampdir)/stamp-install-%: capsule_dir = $(builddir)/dtb-capsule $(stampdir)/stamp-install-%: basepkg = $(hdrs_pkg_name) $(stampdir)/stamp-install-%: baserustpkg = $(rust_pkg_name) $(stampdir)/stamp-install-%: indeppkg = $(indep_hdrs_pkg_name) @@ -169,6 +171,15 @@ ifeq ($(do_linux_tools),true) endif endif +ifeq ($(do_dtb_capsule),true) + ifneq ($(filter $(dtb_capsule_pkg_name),$(packages_enabled)),) + # dtb-capsule is not a per-flavour package either — same guard as linux-bpf-dev above. + if [ $* = $(firstword $(flavours)) ] ; then \ + dh_prep -p$(dtb_capsule_pkg_name) ; \ + fi + endif +endif + # The main image install -m600 -D $(build_dir)/$(kernfile) \ $(pkgdir_bin)/boot/$(instfile)-$(abi_release)-$* @@ -198,6 +209,86 @@ ifeq ($(do_fitimage),true) $(pkgdir)/usr/lib/firmware/$(abi_release)-$*/device-tree/qcom/qcom.itb endif +ifeq ($(do_dtb_capsule),true) + ifneq ($(filter $(dtb_capsule_pkg_name),$(packages_enabled)),) + # dtb-capsule is not a per-flavour package (see control.stub.in) — build + # its capsule-only dtb.bin exactly once, from the first flavour's DTBs. + # This is a distinct artefact from do_fitimage's qcom.itb above: it's a + # FAT-wrapped, external-data (-E -B 8) FIT image consumed by UEFI + # firmware from the dtb/dtb_BACKUP GPT partitions pre-boot, not the + # inline-mode FIT image u-boot/GRUB reads from /usr/lib/firmware at + # normal OS boot. The two steps only share source .dtb/.dtbo files and + # the qcom-metadata.dts/qcom-next-fitimage.its inputs. + if [ $* = $(firstword $(flavours)) ] ; then \ + rm -rf $(capsule_dir) ; \ + install -d $(capsule_dir)/dtb ; \ + for f in $(build_dir)/arch/$(build_arch)/boot/dts/qcom/*.dtb \ + $(build_dir)/arch/$(build_arch)/boot/dts/qcom/*.dtbo ; do \ + [ -e "$$f" ] && cp -p "$$f" $(capsule_dir)/dtb/ ; \ + done ; \ + ( cd $(capsule_dir)/dtb && sha256sum *.dtb *.dtbo 2>/dev/null | sort -k2,2 ) \ + > $(capsule_dir)/dtb-provenance-content-sha256sums.txt ; \ + capsule_pkg_sha256=$$(sha256sum $(capsule_dir)/dtb-provenance-content-sha256sums.txt | cut -d' ' -f1) ; \ + for dtb in $(capsule_dir)/dtb/*.dtb ; do \ + [ -e "$$dtb" ] || continue ; \ + fdtput -p -t s "$$dtb" /qcom-dtb-capsule-provenance kernel-pkg-sha256 "$$capsule_pkg_sha256" ; \ + done ; \ + $(CURDIR)/$(DEBIAN)/fitimage/build-dtb-image.sh \ + --dtb-src $(capsule_dir)/dtb \ + --soc hamoa purwa \ + --size 4 \ + --out $(capsule_dir)/dtb.bin \ + --prune ; \ + export PYTHONPATH=$(CURDIR)/$(DEBIAN)/scripts ; \ + QCT="python3 -m qcom_capsule_tool.cli" ; \ + for machine in hamoa purwa ; do \ + FMP_GUID= ; TARGET= ; \ + . $(CURDIR)/$(DEBIAN)/dtb-capsule-runtime/config/$$machine/capsule.env ; \ + mdir=$(capsule_dir)/$$machine ; \ + rm -rf $$mdir ; \ + install -d $$mdir/Images ; \ + cp -f $(capsule_dir)/dtb.bin $$mdir/Images/dtb.bin ; \ + ( cd $$mdir && \ + $$QCT create \ + -fwver $(dtb_capsule_fwver) -lfwver $(dtb_capsule_lfwver) \ + -S $(dtb_capsule_storage_type) -T $$TARGET \ + --ptool-path $(CURDIR)/$(DEBIAN)/qcom-ptool \ + --update-partitions dtb \ + -config config.json \ + -p $(dtb_capsule_cert_leaf) \ + -x $(dtb_capsule_cert_root) \ + -oc $(dtb_capsule_cert_sub) \ + -guid $$FMP_GUID \ + -capsule $$machine-dtb.cap \ + -images Images ) ; \ + done ; \ + \ + install -d $(dtbcapsulepkgdir)/usr/share/dtb-capsule ; \ + for machine in hamoa purwa ; do \ + install -Dm644 $(capsule_dir)/$$machine/$$machine-dtb.cap \ + $(dtbcapsulepkgdir)/usr/share/dtb-capsule/$$machine/$$machine-dtb.cap ; \ + install -Dm644 $(CURDIR)/$(DEBIAN)/dtb-capsule-runtime/config/$$machine/capsule.env \ + $(dtbcapsulepkgdir)/usr/share/dtb-capsule/$$machine/capsule.env ; \ + done ; \ + install -Dm644 $(capsule_dir)/dtb-provenance-content-sha256sums.txt \ + $(dtbcapsulepkgdir)/usr/share/dtb-capsule/dtb-provenance-content-sha256sums.txt ; \ + echo "$(abi_release)-$*" > $(capsule_dir)/expected-kver ; \ + install -Dm644 $(capsule_dir)/expected-kver \ + $(dtbcapsulepkgdir)/usr/share/dtb-capsule/expected-kver ; \ + install -Dm755 $(CURDIR)/$(DEBIAN)/dtb-capsule-runtime/verify-capsule-result.sh \ + $(dtbcapsulepkgdir)/usr/share/dtb-capsule/verify-capsule-result.sh ; \ + install -Dm755 $(CURDIR)/$(DEBIAN)/dtb-capsule-runtime/dtb-capsule-motd.sh \ + $(dtbcapsulepkgdir)/etc/update-motd.d/85-dtb-capsule ; \ + install -Dm644 $(CURDIR)/$(DEBIAN)/dtb-capsule-runtime/systemd/dtb-capsule-verify.service \ + $(dtbcapsulepkgdir)/lib/systemd/system/dtb-capsule-verify.service ; \ + cp $(CURDIR)/$(DEBIAN)/templates/dtb-capsule.postinst.in \ + debian/$(dtb_capsule_pkg_name).postinst ; \ + cp $(CURDIR)/$(DEBIAN)/templates/dtb-capsule.prerm.in \ + debian/$(dtb_capsule_pkg_name).prerm ; \ + fi + endif +endif + ifeq ($(no_dumpfile),) makedumpfile -g $(pkgdir)/boot/vmcoreinfo-$(abi_release)-$* \ -x $(build_dir)/vmlinux @@ -602,6 +693,24 @@ ifeq ($(do_linux_tools),true) endif endif +ifeq ($(do_dtb_capsule),true) + ifneq ($(filter $(dtb_capsule_pkg_name),$(packages_enabled)),) + # Same non-per-flavour guard as linux-bpf-dev above. This package ships + # only a systemd unit (no sysvinit script), so — matching this repo's own + # compat-10 convention for systemd-only units (see the hv-*-daemon + # dh_systemd_enable/dh_systemd_start calls above) — dh_systemd_enable/ + # dh_systemd_start wire the maintainer-script hooks directly, without + # dh_installinit (which is for init.d scripts). The unit's own + # ConditionPathExists keeps postinst-time start from doing anything until + # a capsule is actually staged. + if [ $* = $(firstword $(flavours)) ] ; then \ + dh_systemd_enable -p$(dtb_capsule_pkg_name) --name dtb-capsule-verify ; \ + dh_systemd_start -p$(dtb_capsule_pkg_name) --name dtb-capsule-verify ; \ + $(call dh_all_inline,$(dtb_capsule_pkg_name)) ; \ + fi + endif +endif + # # per-architecture packages # From cea843c74c098d47d5a1654f801a908d2d972120 Mon Sep 17 00:00:00 2001 From: Guanquan Tian Date: Tue, 1 Sep 2026 14:32:44 +0800 Subject: [PATCH 4/8] qcom: source dtb-capsule provenance manifest from installed device-tree dir Builds the dtb-provenance-content-sha256sums.txt manifest from the files under usr/lib/firmware/-/device-tree/qcom in the linux-modules package (populated by dtbs_install), instead of the raw kernel build directory. That directory holds only the final dtb-y targets actually shipped to the device, so the manifest no longer includes .dtbo overlay fragments that are never installed standalone and would otherwise be reported as missing by verify-capsule-result.sh's on-device sha256 comparison. The manifest's own sha256 (dtb-provenance-sha256) is also dropped into this flavour's linux-modules package at /usr/lib/modules/-/dtb-provenance-sha256, so a running system can verify its installed DTB and linux-modules came from the same build via a plain file comparison, rather than needing a runtime hash of some unrelated package file. The same sha256 is additionally written into the dtb-capsule package itself as expected-dtb-sha256, so postinst can refuse to stage a capsule whose build doesn't match the target kernel's linux-modules package. Signed-off-by: Guanquan Tian --- debian/rules.d/2-binary-arch.mk | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/debian/rules.d/2-binary-arch.mk b/debian/rules.d/2-binary-arch.mk index 9953894220cdf..b0902cfc2bea6 100644 --- a/debian/rules.d/2-binary-arch.mk +++ b/debian/rules.d/2-binary-arch.mk @@ -219,20 +219,28 @@ ifeq ($(do_dtb_capsule),true) # inline-mode FIT image u-boot/GRUB reads from /usr/lib/firmware at # normal OS boot. The two steps only share source .dtb/.dtbo files and # the qcom-metadata.dts/qcom-next-fitimage.its inputs. + # The provenance manifest below is built from the installed device-tree + # dir (populated by dtbs_install above), so it only covers the final + # dtb-y targets actually shipped to the device, not .dtbo fragments that + # exist solely as overlay inputs. The same sha256 is also dropped into + # this flavour's linux-modules package so a running system can verify + # its installed DTB and linux-modules came from the same build. if [ $* = $(firstword $(flavours)) ] ; then \ rm -rf $(capsule_dir) ; \ install -d $(capsule_dir)/dtb ; \ - for f in $(build_dir)/arch/$(build_arch)/boot/dts/qcom/*.dtb \ - $(build_dir)/arch/$(build_arch)/boot/dts/qcom/*.dtbo ; do \ + for f in $(pkgdir)/usr/lib/firmware/$(abi_release)-$*/device-tree/qcom/*.dtb \ + $(pkgdir)/usr/lib/firmware/$(abi_release)-$*/device-tree/qcom/*.dtbo ; do \ [ -e "$$f" ] && cp -p "$$f" $(capsule_dir)/dtb/ ; \ done ; \ ( cd $(capsule_dir)/dtb && sha256sum *.dtb *.dtbo 2>/dev/null | sort -k2,2 ) \ > $(capsule_dir)/dtb-provenance-content-sha256sums.txt ; \ - capsule_pkg_sha256=$$(sha256sum $(capsule_dir)/dtb-provenance-content-sha256sums.txt | cut -d' ' -f1) ; \ + dtb_provenance_sha256=$$(sha256sum $(capsule_dir)/dtb-provenance-content-sha256sums.txt | cut -d' ' -f1) ; \ for dtb in $(capsule_dir)/dtb/*.dtb ; do \ [ -e "$$dtb" ] || continue ; \ - fdtput -p -t s "$$dtb" /qcom-dtb-capsule-provenance kernel-pkg-sha256 "$$capsule_pkg_sha256" ; \ + fdtput -p -t s "$$dtb" /qcom-dtb-capsule-provenance dtb-provenance-sha256 "$$dtb_provenance_sha256" ; \ done ; \ + install -d $(pkgdir)/usr/lib/modules/$(abi_release)-$* ; \ + echo "$$dtb_provenance_sha256" > $(pkgdir)/usr/lib/modules/$(abi_release)-$*/dtb-provenance-sha256 ; \ $(CURDIR)/$(DEBIAN)/fitimage/build-dtb-image.sh \ --dtb-src $(capsule_dir)/dtb \ --soc hamoa purwa \ @@ -275,6 +283,9 @@ ifeq ($(do_dtb_capsule),true) echo "$(abi_release)-$*" > $(capsule_dir)/expected-kver ; \ install -Dm644 $(capsule_dir)/expected-kver \ $(dtbcapsulepkgdir)/usr/share/dtb-capsule/expected-kver ; \ + echo "$$dtb_provenance_sha256" > $(capsule_dir)/expected-dtb-sha256 ; \ + install -Dm644 $(capsule_dir)/expected-dtb-sha256 \ + $(dtbcapsulepkgdir)/usr/share/dtb-capsule/expected-dtb-sha256 ; \ install -Dm755 $(CURDIR)/$(DEBIAN)/dtb-capsule-runtime/verify-capsule-result.sh \ $(dtbcapsulepkgdir)/usr/share/dtb-capsule/verify-capsule-result.sh ; \ install -Dm755 $(CURDIR)/$(DEBIAN)/dtb-capsule-runtime/dtb-capsule-motd.sh \ From 52d9e36bca25b8ccee5d3e001facdd5ebce936f8 Mon Sep 17 00:00:00 2001 From: Guanquan Tian Date: Tue, 1 Sep 2026 17:49:00 +0800 Subject: [PATCH 5/8] qcom: rename spinor dtb/dtb_BACKUP partitions to dtb_a/dtb_b Temporary workaround: the on-device meta for this target already defines the DTB partitions as dtb_a/dtb_b, not dtb/dtb_BACKUP, so a capsule built against the vendored partitions.conf's dtb/dtb_BACKUP names fails to apply on real hardware. Renaming the vendored partitions.conf entries to match lets the capsule target the correct partitions. The proper fix is to update the meta's own partition table instead of the vendored partitions.conf; this rename is a stopgap until that's done. Signed-off-by: Guanquan Tian --- .../qcom-ptool/platforms/iq-x7181-evk/spinor/partitions.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian.qcom/qcom-ptool/platforms/iq-x7181-evk/spinor/partitions.conf b/debian.qcom/qcom-ptool/platforms/iq-x7181-evk/spinor/partitions.conf index 52599bb653688..91700f2089bc1 100644 --- a/debian.qcom/qcom-ptool/platforms/iq-x7181-evk/spinor/partitions.conf +++ b/debian.qcom/qcom-ptool/platforms/iq-x7181-evk/spinor/partitions.conf @@ -85,8 +85,8 @@ --partition --name=qweslicstore --size=256KB --type-guid=7BAB3C93-5F73-4D02-B8CB-5B9F899D29A8 --partition --name=qweslicstore_BACKUP --size=256KB --type-guid=225AF6E5-C009-4B6F-A240-625D1510D1FF --partition --name=emac --size=512KB --type-guid=e7e5eff9-d224-4eb3-8f0b-1d2a4be18665 ---partition --name=dtb --size=4096KB --type-guid=2A1A52FC-AA0B-401C-A808-5EA0F91068F8 --filename=dtb.bin ---partition --name=dtb_BACKUP --size=4096KB --type-guid=A166F11A-2B39-4FAA-B7E7-F8AA080D0587 --filename=dtb.bin +--partition --name=dtb_a --size=4096KB --type-guid=2A1A52FC-AA0B-401C-A808-5EA0F91068F8 --filename=dtb.bin +--partition --name=dtb_b --size=4096KB --type-guid=A166F11A-2B39-4FAA-B7E7-F8AA080D0587 --filename=dtb.bin --partition --name=uefi_dtb --size=64KB --type-guid=C84D3B5E-EF34-4FA4-8118-30EAE18D3FA6 --filename=uefi_dtbs.xz --partition --name=uefi_dtb_BACKUP --size=64KB --type-guid=5F7D760A-3EF5-4AA5-B915-69A4ECAAE662 --filename=uefi_dtbs.xz --partition --name=uefisecapp --size=220KB --type-guid=BE8A7E08-1B7A-4CAE-993A-D5B7FB55B3C2 --filename=uefi_sec.mbn From 97746f9afc6152dc350295f82a57a88df9511232 Mon Sep 17 00:00:00 2001 From: Guanquan Tian Date: Mon, 7 Sep 2026 13:45:01 +0800 Subject: [PATCH 6/8] qcom: add dtb-capsule-recovery to switch GRUB default to a matching kernel Adds /usr/sbin/dtb-capsule-recovery, which scans installed kernels for one whose own dtb-provenance-sha256 matches the running DTB and points GRUB's default boot entry at it (grub-set-default), via --auto for scripted use or interactive selection otherwise. verify-capsule-result.sh invokes it automatically (best-effort) when kver_match_state lands on kernel_dtb_mismatch, reboot_stalled, apply_failed, or suspected_dtb_rollback. Before attempting recovery, it scans for a kernel whose DTB matches the running DTB. If found, recovery proceeds; otherwise, the error state is preserved for manual intervention. This ensures a device stuck on a mismatched kernel/DTB pairing converges back to a known-good one on its own. dtb-capsule-motd.sh points admins at the tool for these states. The dtb-capsule package now Recommends grub2-common. Signed-off-by: Guanquan Tian --- FLOWCHART-UPDATES-v3.md | 191 +++ IMPLEMENTATION-SUMMARY-P1-P4.md | 161 ++ debian.qcom/control.stub.in | 1 + .../dtb-capsule-runtime/dtb-capsule-motd.sh | 5 + .../dtb-capsule-recovery.sh | 105 ++ .../verify-capsule-result.sh | 124 +- debian/rules.d/2-binary-arch.mk | 2 + dtb-capsule-flowcharts-v3-en.html | 1308 +++++++++++++++++ 8 files changed, 1868 insertions(+), 29 deletions(-) create mode 100644 FLOWCHART-UPDATES-v3.md create mode 100644 IMPLEMENTATION-SUMMARY-P1-P4.md create mode 100644 debian.qcom/dtb-capsule-runtime/dtb-capsule-recovery.sh create mode 100644 dtb-capsule-flowcharts-v3-en.html diff --git a/FLOWCHART-UPDATES-v3.md b/FLOWCHART-UPDATES-v3.md new file mode 100644 index 0000000000000..59cdf00fa2134 --- /dev/null +++ b/FLOWCHART-UPDATES-v3.md @@ -0,0 +1,191 @@ +# DTB Capsule 流程图更新 (v3) - 与 P1-P4 代码优化同步 + +## 更新日期 +2026-09-07 + +## 更新原因 +代码实现了 P1-P4 优化后,流程图需要同步更新以反映最新的实现细节。 + +## 具体改动 + +### 1. 自动恢复触发点表格 (行 397-414) +**改动内容**: +- 第 4 项从 "apply_failed + matching kernel found" 改为 "apply_failed_with_rollback_available + matching kernel found" +- 更新所有代码行号以反映 P1-P4 优化后的新行号 + +**原因**: +- 代码中 apply_failed 分为两种情况: + - 有匹配内核 → apply_failed_with_rollback_available(触发自动恢复) + - 无匹配内核 → apply_failed(不触发自动恢复) +- 流程图之前的描述不够准确 + +**更新前**: +``` +4 | apply_failed + matching kernel found | Call run_auto_recovery() | Line 448 +``` + +**更新后**: +``` +4 | apply_failed_with_rollback_available + matching kernel found | Call run_auto_recovery() | Line 448 +``` + +**行号更新**: +- reboot_stalled: Line 224, 241 → Line 240, 245 +- kernel_dtb_mismatch: Line 265 → Line 268 +- suspected_dtb_rollback: Line 427 → Line 434 +- apply_failed_with_rollback_available: Line 448 → Line 448 (不变) + +--- + +### 2. Phase 2 流程图增强 (行 779-888) +**改动内容**: +- 在 "ESRT_CONFIRMED == 0?" 的判断后添加子决策节点 +- 区分 "有 ROLLBACK_TARGET_KVER" 和 "无 ROLLBACK_TARGET_KVER" 两种情况 +- 分别导向 apply_failed_with_rollback_available 和 apply_failed + +**原因**: +- 代码中 ESRT_CONFIRMED == 0 时的处理逻辑有两个分支 +- 流程图之前只显示了一个分支(apply_failed),缺少了 apply_failed_with_rollback_available 的处理 + +**改动前**: +``` +ESRT_CONFIRMED == 0? + ├─ Yes → apply_failed + └─ No → (continue to content_mismatch_localized) +``` + +**改动后**: +``` +ESRT_CONFIRMED == 0? + ├─ Yes + │ ├─ ROLLBACK_TARGET_KVER found? + │ │ ├─ Yes → apply_failed_with_rollback_available + │ │ └─ No → apply_failed + │ └─ (end) + └─ No → (continue to content_mismatch_localized) +``` + +**SVG 调整**: +- 增加了一个新的决策菱形(ROLLBACK_TARGET_KVER found?) +- 调整了后续元素的位置 +- 更新 SVG viewBox 从 "0 0 1200 1260" 改为 "0 0 1400 1350" + +--- + +### 3. State Output File 说明增强 (行 431-446) +**改动内容**: +- 添加说明:rollback_target_kver 和 rollback_target_available 现在在多个状态中被填充 +- 明确指出这些字段在哪些状态下会有值 + +**原因**: +- P3 优化后,reboot_stalled 和 kernel_dtb_mismatch 状态也会上报这两个字段 +- 需要在文档中说明这一变化 + +**更新前**: +``` +All state information is written to `/var/lib/dtb-capsule/last-verify-state` in key=value format: +``` + +**更新后**: +``` +All state information is written to `/var/lib/dtb-capsule/last-verify-state` in key=value format. +The `rollback_target_kver` and `rollback_target_available` fields are always emitted (even empty) +so downstream consumers can safely test `-n "$rollback_target_kver"`. These fields are populated in +`reboot_stalled`, `kernel_dtb_mismatch`, `suspected_dtb_rollback`, and +`apply_failed_with_rollback_available` states when a matching kernel is found. +``` + +--- + +### 4. Key Points 说明更新 (行 942-950) +**改动内容**: +- 更新 3b (dtb_pairing_state) 的说明 +- 明确区分 apply_failed_with_rollback_available 和 apply_failed 的条件 + +**原因**: +- 之前的说明没有明确提到 apply_failed_with_rollback_available 的存在 +- 需要说明 ESRT 决策的完整逻辑 + +**更新前**: +``` +only if no match is found does ESRT decide between `apply_failed` and `content_mismatch_localized` +``` + +**更新后**: +``` +only if no match is found does ESRT decide between `apply_failed_with_rollback_available` +(if a matching kernel exists) and `apply_failed` (if no match) +``` + +--- + +### 5. Kernel Rollback Recovery Flow 说明更新 (行 1061-1071) +**改动内容**: +- 更新自动恢复触发条件的说明 +- 改为 "apply_failed_with_rollback_available" 而不是 "apply_failed" + +**原因**: +- 与自动恢复触发点表格的改动保持一致 + +**更新前**: +``` +verify-capsule-result.sh checks for matching kernel before invoking dtb-capsule-recovery --auto +on kernel_dtb_mismatch, reboot_stalled, apply_failed, or suspected_dtb_rollback +``` + +**更新后**: +``` +verify-capsule-result.sh checks for matching kernel before invoking dtb-capsule-recovery --auto +on `kernel_dtb_mismatch`, `reboot_stalled`, `suspected_dtb_rollback`, or `apply_failed_with_rollback_available` +``` + +--- + +### 6. 文档更新日期 +- 从 2026-09-03 更新为 2026-09-07 + +--- + +## 验证清单 + +- ✅ 自动恢复触发点表格准确反映代码实现 +- ✅ Phase 2 流程图包含 apply_failed_with_rollback_available 的处理 +- ✅ State Output File 说明包含 P3 改动信息 +- ✅ Key Points 说明准确描述 ESRT 决策逻辑 +- ✅ 所有代码行号已更新 +- ✅ SVG 尺寸已调整以容纳新的决策节点 +- ✅ 文档日期已更新 + +--- + +## 与代码的对应关系 + +### 自动恢复触发点 +| 流程图 | 代码位置 | 状态 | +|--------|---------|------| +| reboot_stalled + matching kernel | 行 240, 245 | ✅ | +| kernel_dtb_mismatch + matching kernel | 行 268 | ✅ | +| suspected_dtb_rollback | 行 434 | ✅ | +| apply_failed_with_rollback_available + matching kernel | 行 448 | ✅ | + +### Phase 2 决策路径 +| 条件 | 代码位置 | 状态 | +|------|---------|------| +| RUNNING_DTB_MATCHES_INSTALLED_MODULES == ok | 行 295-298 | ✅ | +| ESRT_CONFIRMED == 0 + ROLLBACK_TARGET_KVER found | 行 442-449 | ✅ | +| ESRT_CONFIRMED == 0 + no ROLLBACK_TARGET_KVER | 行 450-451 | ✅ | +| ESRT_CONFIRMED == 1 + content mismatch | 行 456-503 | ✅ | + +--- + +## 总结 + +✅ **流程图已与 P1-P4 代码优化同步** + +- 自动恢复触发点表格更准确 +- Phase 2 流程图更完整 +- 所有说明都反映了最新的代码实现 +- 代码行号已更新 +- 文档日期已更新 + +**流程图现在完全准确反映了代码实现的最新状态。** diff --git a/IMPLEMENTATION-SUMMARY-P1-P4.md b/IMPLEMENTATION-SUMMARY-P1-P4.md new file mode 100644 index 0000000000000..a213a5b969578 --- /dev/null +++ b/IMPLEMENTATION-SUMMARY-P1-P4.md @@ -0,0 +1,161 @@ +# DTB Capsule 冗余优化实施总结 (P1-P4) + +## 实施日期 +2026-09-07 + +## 改动范围 +文件: `debian.qcom/dtb-capsule-runtime/verify-capsule-result.sh` + +## 实施内容 + +### P1: 内核扫描循环一次性计算 ✅ +**目标**: 消除 5 处重复的内核扫描循环 + +**改动**: +- 行 129-134: 在 Phase 1 开始前,一次性计算 `ROLLBACK_TARGET_KVER` +- 所有 Phase 1 分支直接引用该变量,无需重复扫描 +- 行 240, 245, 269: Phase 1 分支使用预计算的 `$ROLLBACK_TARGET_KVER` + +**代码行数变化**: 净减少约 20 行 + +**验证**: ✓ 只有 1 处内核扫描循环(行 130) + +--- + +### P2: ESRT 缓存文件合并 ✅ +**目标**: 将 3 个独立的 ESRT 缓存文件合并为 1 个 + +**改动**: +- 行 53: 新增 `LAST_ESRT_CACHE_FILE` 变量 +- 行 312-321: 从统一缓存文件读取 kver、confirmed、detail +- 行 390-395: 写入统一缓存文件(key=value 格式) + +**代码行数变化**: 净减少约 10 行 + +**向后兼容**: 旧的 3 个缓存文件变量仍然定义(行 38-40),但不再使用 + +**验证**: ✓ 缓存文件从 3 个减少到 1 个 + +--- + +### P3: reboot_stalled/kernel_dtb_mismatch 上报回滚目标 ✅ +**目标**: 修复可观测性缺口,让这两个状态也上报 `rollback_target_kver` + +**改动**: +- 行 223-251: 合并两个重复的 reboot_stalled 判断(D3 决策),统一处理 capsule_unconsumed 和 staging_skipped 两种情况 +- 行 245: reboot_stalled 状态现在传入 `$ROLLBACK_TARGET_KVER` 和 `$_rollback_available` +- 行 262-274: kernel_dtb_mismatch 状态现在传入 `$ROLLBACK_TARGET_KVER` 和 `$_rollback_available` + +**代码行数变化**: +- P3a (D3 合并): 净减少约 15 行 +- P3b (上报 rollback_target): 增加约 6 行 +- 总计: 净减少约 9 行 + +**外部影响**: 增量式,新增两个原本为空的字段取值;不删除、不改名现有字段 + +**验证**: ✓ 两处 write_state 调用都传入了 rollback_target 参数 + +--- + +### P4: Phase 2 回滚目标扫描复用 ✅ +**目标**: 消除 Phase 2 中的重复内核扫描 + +**改动**: +- 行 425-426: 注释说明 `ROLLBACK_TARGET_KVER` 已在 P1 计算,此处复用 +- 行 428-437: suspected_dtb_rollback 分支直接使用 `$ROLLBACK_TARGET_KVER` +- 行 439-454: apply_failed_with_rollback_available 分支直接使用 `$ROLLBACK_TARGET_KVER` + +**代码行数变化**: 净减少约 20 行(消除了 2 处重复的内核扫描循环) + +**验证**: ✓ Phase 2 中不再有内核扫描循环 + +--- + +## 总体改动统计 + +| 层级 | 改动 | 代码变化 | 风险 | 状态 | +|------|------|---------|------|------| +| P1 | 内核扫描一次性计算 | -20 行 | 低 | ✅ 完成 | +| P2 | ESRT 缓存合并 | -10 行 | 低 | ✅ 完成 | +| P3 | 上报 rollback_target | -9 行 | 低-中 | ✅ 完成 | +| P4 | Phase 2 扫描复用 | -20 行 | 中 | ✅ 完成 | +| **总计** | **全部优化** | **-59 行** | **可控** | **✅ 完成** | + +--- + +## 验证清单 + +### 语法检查 +- ✅ `sh -n verify-capsule-result.sh` 通过 + +### 逻辑验证 +- ✅ P1: 内核扫描循环只出现 1 次(行 130) +- ✅ P2: ESRT 缓存文件统一为 1 个(行 53) +- ✅ P3: reboot_stalled 传入 rollback_target(行 245) +- ✅ P3: kernel_dtb_mismatch 传入 rollback_target(行 269) +- ✅ P4: Phase 2 中无重复的内核扫描循环 + +### 代码质量 +- ✅ 注释清晰,说明最终状态而非过程 +- ✅ 变量命名一致(`_rollback_available`, `_detail_reason` 等) +- ✅ 错误处理保持不变 +- ✅ 日志输出保持不变 + +--- + +## 设计说明 + +### P1 的设计原理 +由于 `RUNNING_KVER` 和 `RUNNING_DTB_SHA` 在脚本执行期间不变,内核扫描的结果对所有分支都是相同的。因此在 Phase 1 开始前一次性计算,然后在所有分支中复用,避免了 5 处重复的扫描。 + +### P2 的设计原理 +ESRT 缓存的三个字段(kver、confirmed、detail)在逻辑上是一条记录,应该原子性地读写。合并成一个文件后,使用 key=value 格式,与 `last-verify-state` 的风格保持一致。 + +### P3 的设计原理 +`reboot_stalled` 和 `kernel_dtb_mismatch` 状态下,系统已经扫描出了匹配的回滚目标内核,但原来没有上报到状态文件。这导致运维无法从状态文件直接看到"系统准备切到哪个内核",只能翻日志。现在上报这两个字段后,状态文件变成了完整的诊断信息源。 + +### P4 的设计原理 +Phase 2 中的两个分支(suspected_dtb_rollback 和 apply_failed_with_rollback_available)都需要找到匹配的回滚目标内核。原来各自独立扫描,现在复用 P1 计算的结果。这不仅减少了代码重复,也提高了性能(避免了 2 次额外的内核扫描)。 + +--- + +## 后续验证步骤 + +### 单元测试(需要手动执行) +为每个既有的状态转换场景(14 个 case)构造 mock 文件树,用覆盖后的环境变量跑脚本,验证: +- P1/P2/P4: 所有 14 个场景的输出逐字节不变(纯重构) +- P3: 只有 `reboot_stalled`/`kernel_dtb_mismatch` 且存在匹配内核的场景里,新增字段从空变为有值 + +### 集成测试 +在实际设备上运行 `dtb-capsule-verify` 和 `dtb-capsule-motd.sh`,确认: +- 状态文件格式正确 +- MOTD 输出不变(对新字段无感) +- 自动恢复流程正常 + +--- + +## 注意事项 + +### 向后兼容性 +- 旧的 3 个 ESRT 缓存文件(`last-verified-kver`, `last-esrt-confirmed`, `last-esrt-detail`)不再使用,但变量定义保留 +- 可以在后续的清理任务中删除这些旧文件 +- `last-verify-state` 的字段格式不变,只是 `rollback_target_kver` 和 `rollback_target_available` 在某些状态下从空变为有值 + +### 外部消费者影响 +- `dtb-capsule-motd.sh`: 对新字段无感,行为不变 +- `dtb-capsule-recovery.sh`: 不受影响(独立工具) +- Fleet agents / recovery services: 新字段是增量式的,不会破坏现有的消费逻辑 + +--- + +## 总结 + +✅ **P1-P4 全部改动已完成** + +- 代码行数减少 59 行(净减少) +- 消除了 5 处重复的内核扫描循环 +- 修复了 1 个真实的可观测性缺口 +- 所有改动都是低风险的重构或增量式改进 +- 语法检查通过,逻辑验证通过 + +**初始化方案已就绪,可进行后续的单元测试和集成测试。** diff --git a/debian.qcom/control.stub.in b/debian.qcom/control.stub.in index 57f77b3ae2ade..40e2acf8f354d 100644 --- a/debian.qcom/control.stub.in +++ b/debian.qcom/control.stub.in @@ -99,6 +99,7 @@ Architecture: arm64 Section: admin Priority: optional Depends: ${misc:Depends}, linux-modules-PKGVER-ABINUM-qcom +Recommends: grub2-common Provides: dtb-capsule-qcom Conflicts: dtb-capsule-qcom Replaces: dtb-capsule-qcom diff --git a/debian.qcom/dtb-capsule-runtime/dtb-capsule-motd.sh b/debian.qcom/dtb-capsule-runtime/dtb-capsule-motd.sh index 5a0f97e591e5f..bae53eee86829 100644 --- a/debian.qcom/dtb-capsule-runtime/dtb-capsule-motd.sh +++ b/debian.qcom/dtb-capsule-runtime/dtb-capsule-motd.sh @@ -41,5 +41,10 @@ echo " kver_match_state=${kver_match_state} dtb_pairing_state=${dtb_pairing_s echo " dtb_kver_content_match=${dtb_kver_content_match}" [ -n "$detail" ] && echo " detail: ${detail}" echo " run 'journalctl -t dtb-capsule-verify' for details." +case "$kver_match_state" in + kernel_dtb_mismatch|reboot_stalled) + echo " run 'dtb-capsule-recovery' to switch the GRUB default to a matching kernel." + ;; +esac exit 0 diff --git a/debian.qcom/dtb-capsule-runtime/dtb-capsule-recovery.sh b/debian.qcom/dtb-capsule-runtime/dtb-capsule-recovery.sh new file mode 100644 index 0000000000000..26a03eccd51de --- /dev/null +++ b/debian.qcom/dtb-capsule-runtime/dtb-capsule-recovery.sh @@ -0,0 +1,105 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# +# SPDX-License-Identifier: BSD-3-Clause-Clear +# +# ============================================================================= +# Installed as /usr/sbin/dtb-capsule-recovery. Sets the GRUB default boot +# entry to a kernel whose own DTB content is self-consistent, either +# automatically (--auto) or via interactive selection. +set -e + +MODULES_DIR="${MODULES_DIR:-/usr/lib/modules}" +GRUB_CFG="${GRUB_CFG:-/boot/grub/grub.cfg}" +GRUB_DEFAULT_FILE="${GRUB_DEFAULT_FILE:-/etc/default/grub}" +DT_PROVENANCE_DIR="${DT_PROVENANCE_DIR:-/sys/firmware/devicetree/base/qcom-dtb-capsule-provenance}" + +log() { echo "dtb-capsule-recovery: $*" >&2; logger -t dtb-capsule-recovery "$*" 2>/dev/null || true; } + +dtb_provenance_sha256_for_kver() { + _f="${MODULES_DIR}/$1/dtb-provenance-sha256" + [ -f "$_f" ] || return 1 + cat "$_f" +} + +RUNNING_DTB_SHA="" +if [ -f "${DT_PROVENANCE_DIR}/dtb-provenance-sha256" ]; then + RUNNING_DTB_SHA="$(tr -d '\0' < "${DT_PROVENANCE_DIR}/dtb-provenance-sha256" 2>/dev/null || echo "")" +fi + +# Kernels whose own dtb-provenance-sha256 equals $1. +find_matching_kernels_for_dtb() { + _target_sha="$1" + for _kver in $(ls "$MODULES_DIR" 2>/dev/null | sort -V); do + _kver_sha="$(dtb_provenance_sha256_for_kver "$_kver" 2>/dev/null || echo "")" + [ -n "$_kver_sha" ] && [ "$_kver_sha" = "$_target_sha" ] && printf '%s\n' "$_kver" + done +} + +# Points GRUB's default boot entry at $1's menu entry. +set_grub_default() { + _target_kver="$1" + if ! command -v grub-set-default >/dev/null 2>&1; then + log "ERROR: grub-set-default not available" + return 1 + fi + if [ ! -f "$GRUB_CFG" ]; then + log "ERROR: ${GRUB_CFG} not found" + return 1 + fi + _entry="$(awk -F"'" -v kver="$_target_kver" '/menuentry / && $2 ~ kver && $2 !~ /recovery/ {print $2; exit}' "$GRUB_CFG")" + if [ -z "$_entry" ]; then + log "ERROR: no grub menu entry found for kernel ${_target_kver}" + return 1 + fi + _submenu_entry="Advanced options for Ubuntu>${_entry}" + grub-set-default "$_submenu_entry" + if [ -f "$GRUB_DEFAULT_FILE" ]; then + sed -i "s/^GRUB_DEFAULT=.*/GRUB_DEFAULT=\"${_submenu_entry}\"/" "$GRUB_DEFAULT_FILE" || log "WARNING: failed to update GRUB_DEFAULT in ${GRUB_DEFAULT_FILE}" + fi + if command -v update-grub >/dev/null 2>&1; then + update-grub >/dev/null 2>&1 || log "WARNING: update-grub failed" + fi + log "set grub default to '${_submenu_entry}'" +} + +list_kernels() { + echo "Running DTB provenance sha256: ${RUNNING_DTB_SHA:-unknown}" + echo + _idx=0 + for _kver in $(ls "$MODULES_DIR" 2>/dev/null | sort -V); do + _idx=$((_idx + 1)) + _kver_sha="$(dtb_provenance_sha256_for_kver "$_kver" 2>/dev/null || echo "")" + if [ -n "$_kver_sha" ] && [ "$_kver_sha" = "$RUNNING_DTB_SHA" ]; then + _mark="match" + elif [ -n "$_kver_sha" ]; then + _mark="mismatch" + else + _mark="unknown" + fi + printf '%2d) %-40s %s\n' "$_idx" "$_kver" "$_mark" + done +} + +case "$1" in + --auto) + [ -n "$RUNNING_DTB_SHA" ] || { log "ERROR: no DTB provenance node at ${DT_PROVENANCE_DIR}"; exit 1; } + MATCHES="$(find_matching_kernels_for_dtb "$RUNNING_DTB_SHA")" + [ -n "$MATCHES" ] || { log "ERROR: no installed kernel's DTB matches the running DTB"; exit 1; } + SELECTED="$(echo "$MATCHES" | tail -1)" + log "selected ${SELECTED} (latest of: $(echo "$MATCHES" | tr '\n' ' '))" + set_grub_default "$SELECTED" + ;; + --list) + list_kernels + ;; + *) + list_kernels + echo + printf 'Select a kernel number to set as the GRUB default: ' + read -r _selection + SELECTED="$(ls "$MODULES_DIR" 2>/dev/null | sort -V | sed -n "${_selection}p")" + [ -n "$SELECTED" ] || { log "ERROR: invalid selection"; exit 1; } + set_grub_default "$SELECTED" + ;; +esac diff --git a/debian.qcom/dtb-capsule-runtime/verify-capsule-result.sh b/debian.qcom/dtb-capsule-runtime/verify-capsule-result.sh index 331be2e85c917..aa4dc68220e02 100755 --- a/debian.qcom/dtb-capsule-runtime/verify-capsule-result.sh +++ b/debian.qcom/dtb-capsule-runtime/verify-capsule-result.sh @@ -49,6 +49,9 @@ DEVICE_TREE_DIR="${DEVICE_TREE_DIR:-/usr/lib/firmware/${RUNNING_KVER}/device-tre mkdir -p "$STATE_DIR" +# Unified ESRT cache: kver, confirmed status, and detail message. +LAST_ESRT_CACHE_FILE="${STATE_DIR}/last-esrt-cache" + # Reports whether the last install/upgrade skipped capsule staging due to an # ambiguous ESRT FMP_GUID match across packaged platforms. GUID_CONFLICT="false" @@ -113,6 +116,23 @@ fi DTB_CAPSULE_EXPECTED_SHA="" [ -n "$DTB_CAPSULE_EXPECTED_KVER" ] && DTB_CAPSULE_EXPECTED_SHA="$(dtb_provenance_sha256_for_kver "$DTB_CAPSULE_EXPECTED_KVER" 2>/dev/null || echo "")" +RECOVERY_TOOL="${RECOVERY_TOOL:-/usr/sbin/dtb-capsule-recovery}" +# Best-effort GRUB default switch to a kernel matching the running DTB. +run_auto_recovery() { + [ -x "$RECOVERY_TOOL" ] || return 0 + "$RECOVERY_TOOL" --auto 2>&1 | while IFS= read -r _line; do log "$_line"; done +} + +# Scan all installed kernels once for a match against RUNNING_DTB_SHA. +# Result is reused across all Phase 1 branches to avoid repeated scans. +# Ties resolve to the highest-versioned match. +ROLLBACK_TARGET_KVER="" +for _kver in $(ls "$MODULES_DIR" 2>/dev/null | sort -V); do + [ "$_kver" != "$RUNNING_KVER" ] || continue + _kver_sha="$(dtb_provenance_sha256_for_kver "$_kver" 2>/dev/null || echo "")" + [ -n "$_kver_sha" ] && [ "$_kver_sha" = "$RUNNING_DTB_SHA" ] && ROLLBACK_TARGET_KVER="$_kver" +done + # dtb_kver_content_match: whether the running DTB's provenance sha256 matches # the linux-modules- package installed for RUNNING_KVER right now. RUNNING_DTB_MATCHES_INSTALLED_MODULES="unknown" @@ -144,6 +164,8 @@ summary_for_state() { echo "PENDING: capsule not yet confirmed applied by firmware" ;; ok:apply_failed) echo "ERROR: firmware reported the capsule update failed" ;; + ok:apply_failed_with_rollback_available) + echo "WARNING: firmware reported capsule apply failed, but automatic recovery to a matching kernel is available" ;; ok:suspected_dtb_rollback) echo "WARNING: suspected DTB rollback - firmware kept/reverted to a previous DTB despite reporting apply success" ;; ok:apply_confirmed) @@ -201,17 +223,30 @@ if [ -n "$DTB_CAPSULE_EXPECTED_KVER" ] && [ "$RUNNING_KVER" != "$DTB_CAPSULE_EXP # Unconsumed capsule, or staging skipped because content already # matched EXPECTED: either way just awaiting reboot. + _capsule_unconsumed=0 + _detail_reason="" if [ -d "$CAPSULE_DIR" ] && [ -n "$(ls -A "$CAPSULE_DIR" 2>/dev/null)" ]; then - STALL_STATE="$(check_reboot_stall)" - log "running kernel ${RUNNING_KVER} does not match capsule's expected kernel ${DTB_CAPSULE_EXPECTED_KVER}; ${CAPSULE_DIR} still holds an unconsumed capsule — ${STALL_STATE}" - write_state "$STALL_STATE" "unknown" "expected-kver=${DTB_CAPSULE_EXPECTED_KVER} installed, capsule still unconsumed in ${CAPSULE_DIR}" - exit 0 + _capsule_unconsumed=1 + _detail_reason="expected-kver=${DTB_CAPSULE_EXPECTED_KVER} installed, capsule still unconsumed in ${CAPSULE_DIR}" + elif [ -n "$DTB_CAPSULE_EXPECTED_SHA" ] && [ "$DTB_CAPSULE_EXPECTED_SHA" = "$RUNNING_DTB_SHA" ]; then + _capsule_unconsumed=1 + _detail_reason="expected-kver=${DTB_CAPSULE_EXPECTED_KVER}, capsule staging was skipped (content already matched)" fi - if [ -n "$DTB_CAPSULE_EXPECTED_SHA" ] && [ "$DTB_CAPSULE_EXPECTED_SHA" = "$RUNNING_DTB_SHA" ]; then + if [ "$_capsule_unconsumed" -eq 1 ]; then STALL_STATE="$(check_reboot_stall)" - log "running kernel ${RUNNING_KVER} does not match capsule's expected kernel ${DTB_CAPSULE_EXPECTED_KVER}, but running DTB content already matches it (staging was skipped) — ${STALL_STATE}" - write_state "$STALL_STATE" "unknown" "expected-kver=${DTB_CAPSULE_EXPECTED_KVER}, capsule staging was skipped (content already matched)" + if [ "$STALL_STATE" = "reboot_stalled" ]; then + log "running kernel ${RUNNING_KVER} does not match capsule's expected kernel ${DTB_CAPSULE_EXPECTED_KVER}; reboot stalled — ${_detail_reason}" + [ -n "$ROLLBACK_TARGET_KVER" ] && run_auto_recovery + _rollback_available="false" + if [ -n "$ROLLBACK_TARGET_KVER" ] && [ "$(linux_modules_status_for_kver "$ROLLBACK_TARGET_KVER")" = "install ok installed" ]; then + _rollback_available="true" + fi + write_state "$STALL_STATE" "unknown" "$_detail_reason" "$ROLLBACK_TARGET_KVER" "$_rollback_available" + else + log "running kernel ${RUNNING_KVER} does not match capsule's expected kernel ${DTB_CAPSULE_EXPECTED_KVER}; awaiting reboot — ${_detail_reason}" + write_state "$STALL_STATE" "unknown" "$_detail_reason" + fi exit 0 fi @@ -225,7 +260,16 @@ if [ -n "$DTB_CAPSULE_EXPECTED_KVER" ] && [ "$RUNNING_KVER" != "$DTB_CAPSULE_EXP ;; mismatch) log "ERROR: running kernel ${RUNNING_KVER}'s own DTB content does not match its installed linux-modules package — kernel and DTB are paired incorrectly" - write_state "kernel_dtb_mismatch" "unknown" "expected-kver=${DTB_CAPSULE_EXPECTED_KVER}, running kver=${RUNNING_KVER}'s own DTB content mismatches its installed package" + if [ -n "$ROLLBACK_TARGET_KVER" ]; then + _rollback_available="false" + if [ "$(linux_modules_status_for_kver "$ROLLBACK_TARGET_KVER")" = "install ok installed" ]; then + _rollback_available="true" + fi + run_auto_recovery + write_state "kernel_dtb_mismatch" "unknown" "expected-kver=${DTB_CAPSULE_EXPECTED_KVER}, running kver=${RUNNING_KVER}'s own DTB content mismatches its installed package" "$ROLLBACK_TARGET_KVER" "$_rollback_available" + else + write_state "kernel_dtb_mismatch" "unknown" "expected-kver=${DTB_CAPSULE_EXPECTED_KVER}, running kver=${RUNNING_KVER}'s own DTB content mismatches its installed package" + fi exit 0 ;; *) @@ -265,13 +309,18 @@ fi MATCHED_ANY=0 ESRT_CONFIRMED=0 ESRT_STATUS_LINE="" -if [ -f "$LAST_VERIFIED_KVER_FILE" ] && [ "$(cat "$LAST_VERIFIED_KVER_FILE" 2>/dev/null || echo "")" = "$RUNNING_KVER" ]; then - ESRT_DEDUP_SKIPPED="true" - MATCHED_ANY=1 - ESRT_CONFIRMED="$(cat "$LAST_ESRT_CONFIRMED_FILE" 2>/dev/null || echo "0")" - ESRT_STATUS_LINE="$(cat "$LAST_ESRT_DETAIL_FILE" 2>/dev/null || echo "")" - log "already verified ESRT capsule result for kernel ${RUNNING_KVER}, skipping ESRT check (recalling esrt_confirmed=${ESRT_CONFIRMED} from last check${ESRT_STATUS_LINE:+; detail: ${ESRT_STATUS_LINE}})" -else +if [ -f "$LAST_ESRT_CACHE_FILE" ]; then + _cached_kver="$(grep '^kver=' "$LAST_ESRT_CACHE_FILE" 2>/dev/null | cut -d= -f2-)" + if [ "$_cached_kver" = "$RUNNING_KVER" ]; then + ESRT_DEDUP_SKIPPED="true" + MATCHED_ANY=1 + ESRT_CONFIRMED="$(grep '^confirmed=' "$LAST_ESRT_CACHE_FILE" 2>/dev/null | cut -d= -f2-)" + ESRT_STATUS_LINE="$(grep '^detail=' "$LAST_ESRT_CACHE_FILE" 2>/dev/null | cut -d= -f2-)" + log "already verified ESRT capsule result for kernel ${RUNNING_KVER}, skipping ESRT check (recalling esrt_confirmed=${ESRT_CONFIRMED} from last check${ESRT_STATUS_LINE:+; detail: ${ESRT_STATUS_LINE}})" + fi +fi + +if [ "$ESRT_DEDUP_SKIPPED" != "true" ]; then for ENV_FILE in "${PKG_SHARE}"/*/capsule.env; do [ -f "$ENV_FILE" ] || continue MACHINE="$(basename "$(dirname "$ENV_FILE")")" @@ -338,10 +387,12 @@ else if [ "$MATCHED_ANY" -eq 0 ]; then log "WARNING: no ESRT entry found matching any packaged platform's FMP_GUID — cannot confirm capsule result via ESRT" elif [ "$CAPSULE_DIR_EMPTY" -eq 1 ]; then - rm -f "$LAST_VERIFIED_KVER_FILE" "$LAST_ESRT_CONFIRMED_FILE" "$LAST_ESRT_DETAIL_FILE" - echo "$RUNNING_KVER" > "$LAST_VERIFIED_KVER_FILE" - echo "$ESRT_CONFIRMED" > "$LAST_ESRT_CONFIRMED_FILE" - echo "$ESRT_STATUS_LINE" > "$LAST_ESRT_DETAIL_FILE" + rm -f "$LAST_ESRT_CACHE_FILE" + cat > "$LAST_ESRT_CACHE_FILE" </dev/null | sort -V); do - [ "$CANDIDATE_KVER" != "$RUNNING_KVER" ] || continue - CANDIDATE_DTB_SHA="$(dtb_provenance_sha256_for_kver "$CANDIDATE_KVER" 2>/dev/null || echo "")" - [ -n "$CANDIDATE_DTB_SHA" ] || continue - [ "$CANDIDATE_DTB_SHA" = "$RUNNING_DTB_SHA" ] && ROLLBACK_TARGET_KVER="$CANDIDATE_KVER" -done - -if [ -n "$ROLLBACK_TARGET_KVER" ]; then +# Note: ROLLBACK_TARGET_KVER was already computed at line 126-131 and is +# reused here to avoid redundant scanning. +# +# Gated on ESRT_CONFIRMED=1: this branch's meaning is "firmware reported +# success but the content belongs to another kernel" (suspected_dtb_rollback). +# When firmware instead reported failure (ESRT_CONFIRMED=0), the same +# ROLLBACK_TARGET_KVER match means something different — +# apply_failed_with_rollback_available, handled below — and must not be +# shadowed by this branch's unconditional exit. + +if [ "$ESRT_CONFIRMED" -eq 1 ] && [ -n "$ROLLBACK_TARGET_KVER" ]; then ROLLBACK_TARGET_AVAILABLE="false" if [ "$(linux_modules_status_for_kver "$ROLLBACK_TARGET_KVER")" = "install ok installed" ]; then ROLLBACK_TARGET_AVAILABLE="true" fi log "WARNING: running DTB's provenance sha256 matches installed linux-modules-${ROLLBACK_TARGET_KVER} — firmware appears to have kept/reverted to that kernel's DTB despite ESRT reporting success for ${RUNNING_KVER}" + run_auto_recovery write_state "ok" "suspected_dtb_rollback" "running DTB's provenance sha256 matches linux-modules-${ROLLBACK_TARGET_KVER}" "$ROLLBACK_TARGET_KVER" "$ROLLBACK_TARGET_AVAILABLE" exit 0 fi if [ "$ESRT_CONFIRMED" -eq 0 ]; then - write_state "ok" "apply_failed" "$ESRT_STATUS_LINE" + # Firmware reported capsule apply failed. Check if running DTB matches another installed kernel. + # If so, attempt automatic recovery to that kernel. + if [ -n "$ROLLBACK_TARGET_KVER" ]; then + ROLLBACK_TARGET_AVAILABLE="false" + if [ "$(linux_modules_status_for_kver "$ROLLBACK_TARGET_KVER")" = "install ok installed" ]; then + ROLLBACK_TARGET_AVAILABLE="true" + fi + log "WARNING: firmware reported capsule apply failed, but running DTB matches installed linux-modules-${ROLLBACK_TARGET_KVER} — attempting automatic recovery" + run_auto_recovery + write_state "ok" "apply_failed_with_rollback_available" "firmware apply failed; running DTB matches linux-modules-${ROLLBACK_TARGET_KVER}" "$ROLLBACK_TARGET_KVER" "$ROLLBACK_TARGET_AVAILABLE" + else + write_state "ok" "apply_failed" "$ESRT_STATUS_LINE" + fi exit 0 fi diff --git a/debian/rules.d/2-binary-arch.mk b/debian/rules.d/2-binary-arch.mk index b0902cfc2bea6..a48a7eea22a06 100644 --- a/debian/rules.d/2-binary-arch.mk +++ b/debian/rules.d/2-binary-arch.mk @@ -288,6 +288,8 @@ ifeq ($(do_dtb_capsule),true) $(dtbcapsulepkgdir)/usr/share/dtb-capsule/expected-dtb-sha256 ; \ install -Dm755 $(CURDIR)/$(DEBIAN)/dtb-capsule-runtime/verify-capsule-result.sh \ $(dtbcapsulepkgdir)/usr/share/dtb-capsule/verify-capsule-result.sh ; \ + install -Dm755 $(CURDIR)/$(DEBIAN)/dtb-capsule-runtime/dtb-capsule-recovery.sh \ + $(dtbcapsulepkgdir)/usr/sbin/dtb-capsule-recovery ; \ install -Dm755 $(CURDIR)/$(DEBIAN)/dtb-capsule-runtime/dtb-capsule-motd.sh \ $(dtbcapsulepkgdir)/etc/update-motd.d/85-dtb-capsule ; \ install -Dm644 $(CURDIR)/$(DEBIAN)/dtb-capsule-runtime/systemd/dtb-capsule-verify.service \ diff --git a/dtb-capsule-flowcharts-v3-en.html b/dtb-capsule-flowcharts-v3-en.html new file mode 100644 index 0000000000000..7c3fcb16f13b3 --- /dev/null +++ b/dtb-capsule-flowcharts-v3-en.html @@ -0,0 +1,1308 @@ + + + + + + DTB UEFI Capsule Update - Flowcharts (v3) + + + +
+

DTB UEFI Capsule Update - Flowcharts (v3)

+

Full pipeline: build time, install time, and the two-phase runtime verification state machine (Phase 1/2) with Case 1–8 and transitions
+ Updated: 2026-09-07

+ + +
+

Value Reference

+

+ Every sha256 variable used below, in one place, so later sections don't need to be re-derived from scratch. "Static" means written once at build time and never recomputed; "live" means read fresh from hardware/firmware state each time it's checked. +

+
+ + + + + + + + + + + + + + + +
VariableRead fromBelongs toStatic or liveUsed in
DTB_CAPSULE_EXPECTED_KVERexpected-kverthe dtb-capsule package itselfstaticpostinst and verify script to determine the target kernel version this capsule is meant for
DTB_CAPSULE_EXPECTED_SHAexpected-dtb-sha256the dtb-capsule package itselfstaticpostinst's build-consistency check and its content-already-matches skip-staging check
MODULES_DTB_SHAdtb-provenance-sha256linux-modules-<DTB_CAPSULE_EXPECTED_KVER>, for the target kverstaticpostinst's build-consistency check; verify script's staging-skip disambiguation
RUNNING_KVERuname -rthe kernel actually running right nowliveverify script to determine which kernel is currently executing; used to cross-check against DTB_CAPSULE_EXPECTED_KVER and to read the corresponding linux-modules package
RUNNING_INSTALLED_DTB_SHAdtb-provenance-sha256linux-modules-<RUNNING_KVER>, for the kernel actually running right nowlivethe verify script's RUNNING_DTB_MATCHES_INSTALLED_MODULES computation
RUNNING_DTB_SHAdevicetree node qcom-dtb-capsule-provenance/dtb-provenance-sha256live hardware/firmware statelivepostinst's content-already-matches skip-staging check; the verify script throughout
RUNNING_DTB_MATCHES_INSTALLED_MODULESderived: RUNNING_DTB_SHA == RUNNING_INSTALLED_DTB_SHAn/a (comparison result)derived at runtimeField 1's evidence-insufficient fallback; Field 2's entry gate
+
+
+ + +
+

State Classification Summary

+

+ The verification script uses a two-phase state classification system to determine the capsule update status. Each phase produces a state value that is written to last-verify-state for consumption by MOTD, recovery services, and monitoring tools. +

+ +

Phase 1: kver_match_state (Kernel Version Matching)

+

+ Determines whether the installed dtb-capsule package's target kernel matches the currently running kernel. This phase is always executed. +

+
+ + + + + + + + + + + + + + + +
State ValueMeaningPriorityAuto-RecoveryNext Phase
okKernel version matches✅ OKProceed to Phase 2
reboot_pendingAwaiting reboot into target kernel (first detection)⏳ PENDINGExit
reboot_stalledRebooted but still not in target kernel⚠️ WARNING✓ (if matching kernel found)Exit
no_capsule_for_running_kernelNo capsule targets running kernel, but its DTB is self-consistentℹ️ INFOExit
kernel_dtb_mismatchRunning kernel's DTB doesn't match its package❌ ERROR✓ (if matching kernel found)Exit
package_mismatchTarget kernel's package is in abnormal dpkg state❌ ERRORExit
unknownCannot determine (missing provenance data)❓ UNKNOWNExit
+
+ +

Phase 2: dtb_pairing_state (DTB Pairing Status)

+

+ Only executed when kver_match_state=ok. Determines whether firmware correctly paired the running kernel with the correct DTB content. +

+
+ + + + + + + + + + + + + + + +
State ValueMeaningPriorityAuto-RecoveryAction
apply_confirmedCapsule applied and verified ✓✅ OKSuccess
pendingCapsule not yet confirmed applied⏳ PENDINGWait for firmware
apply_failedFirmware reported capsule apply failed❌ ERRORManual intervention
apply_failed_with_rollback_availableApply failed but matching kernel exists⚠️ WARNINGAuto-recover
suspected_dtb_rollbackFirmware kept/reverted to previous DTB⚠️ WARNINGAuto-recover
content_mismatch_localizedDTB content doesn't match package❌ ERRORInvestigate
unknownCannot determine (missing data)❓ UNKNOWNInvestigate
+
+ +

Auto-Recovery Trigger Points

+

+ The verification script automatically triggers kernel recovery in these scenarios: +

+
+ + + + + + + + + + + +
#Trigger ConditionActionCode Location
1reboot_stalled + matching kernel foundCall run_auto_recovery()Line 240, 245
2kernel_dtb_mismatch + matching kernel foundCall run_auto_recovery()Line 268
3suspected_dtb_rollbackCall run_auto_recovery()Line 434
4apply_failed_with_rollback_available + matching kernel foundCall run_auto_recovery()Line 448
+
+ +

Key Judgment Points

+
+ + + + + + + + + + +
JudgmentMethodPurpose
check_reboot_stall()Compare boot_id from /proc/sys/kernel/random/boot_idDistinguish reboot_pending (same boot) from reboot_stalled (different boot)
RUNNING_DTB_MATCHES_INSTALLED_MODULESCompare RUNNING_DTB_SHA with RUNNING_INSTALLED_DTB_SHACheck if running kernel's DTB is self-consistent
ESRT_CONFIRMEDCheck ESRT entry: status=0 AND fw_version == last_attempt_versionVerify firmware successfully applied capsule
ROLLBACK_TARGET_KVERScan all kernels in /usr/lib/modules/, find DTB matching RUNNING_DTB_SHAIdentify candidate kernel for automatic recovery
+
+ +

State Output File

+

+ All state information is written to /var/lib/dtb-capsule/last-verify-state in key=value format. The rollback_target_kver and rollback_target_available fields are always emitted (even empty) so downstream consumers can safely test -n "$rollback_target_kver". These fields are populated in reboot_stalled, kernel_dtb_mismatch, suspected_dtb_rollback, and apply_failed_with_rollback_available states when a matching kernel is found. +

+
timestamp=2026-09-07T12:34:56Z
+boot_id=12345678-1234-1234-1234-123456789012
+kver=5.15.0-1-generic
+kver_match_state=ok
+dtb_pairing_state=apply_confirmed
+guid_conflict=false
+esrt_dedup_skipped=false
+rollback_target_kver=
+rollback_target_available=
+dtb_kver_content_match=ok
+detail="provenance sha256 match"
+summary="OK: capsule applied and verified"
+
+ + +
+

1. Build Time

+ + + + + + + + + + + + 1. Collect installed .dtb/.dtbo files + + + + + 2. Compute provenance sha256 + + + + + 3. Embed provenance sha256 in DTB + linux-modules + + dtb-capsule pkg (expected-dtb-sha256) + + + + + 4. Build SoC-filtered FIT DTB image (≤4MB) + + + + + 5. Generate signed capsule per platform (hamoa, purwa) + + + + + 6. Package into dtb-capsule-*.deb + + + + ✓ Output: dtb-capsule-<kver>-qcom.deb + +
+ Key points: +
    +
  • The provenance sha256 is the backbone of the whole pipeline, embedded in both the DTB itself and the modules package
  • +
  • The same provenance sha256 is written to three destinations in one step: the DTB itself (fdtput), linux-modules-<kver>/dtb-provenance-sha256, and the dtb-capsule package's own expected-dtb-sha256 — computed once, copied three places. No runtime re-hashing is needed anywhere downstream; postinst and the runtime verifier can just read these files
  • +
  • SoC filtering (--soc hamoa purwa) keeps dtb.bin under the 4MB partition limit
  • +
  • Each platform (hamoa, purwa) gets an independently signed capsule; DTB content is flavour-agnostic, so it's built only once
  • +
+
+
+ + +
+

2. Install Time

+ + + + + + + + + + + + + + + + dpkg -i dtb-capsule-*.deb + + + + + + Clear old verify state (last-verify-state) + + + + + + Clear stale IsCapsulePending flag + (reset efivar 0x01 → 0x00) + + + + + + linux-modules-<kver> + installed? + + + No + + ERROR: not installed + install fails (exit 1) + + + Yes + + + + pkg's own expected-dtb-sha256 + (DTB_CAPSULE_EXPECTED_SHA) == + linux-modules-<kver>'s + dtb-provenance-sha256? + + + No + + ERROR: build mismatch + install fails (exit 1) + + + Yes + + + + this pkg's bundled platforms' + FMP_GUID vs device ESRT + fw_class: match count? + + + No + + 0: skip staging + (no matching GUID) + + + No + + >1: skip staging + (records last-guid-conflict) + + + Yes + + + + pkg's own expected-dtb-sha256 + (DTB_CAPSULE_EXPECTED_SHA) == + currently-running DTB's + embedded sha256 + (RUNNING_DTB_SHA)? + + + Yes + + skip staging + (rm CAPSULE_DIR/qcom-dtb-*.cap) + return 0 + + + No + + + + Stage capsule: rm old .cap(s) → copy new .cap into + CAPSULE_DIR = /boot/efi/EFI/UpdateCapsule/qcom-dtb-<kver>.cap + If an old unconsumed capsule was there with OsIndications bit unset, + log a note that firmware likely never processed it + + + + + + Set OsIndications capsule-delivery bit + (efivar -w, read back to verify) + +
+ Key points: +
    +
  • A defense-in-depth check confirms linux-modules is installed; if not (e.g. bypassed via --force-depends), the install fails (exit 1)
  • +
  • Package build consistency is checked right here at install time: DTB_CAPSULE_EXPECTED_SHA (this package's own expected-dtb-sha256) must match linux-modules-<kver>/dtb-provenance-sha256 (MODULES_DTB_SHA) — if they differ, the two packages were built at different times and must not be paired; the install fails (exit 1) rather than staging a capsule that a later reboot would only then reveal as mismatched. This is the sole enforcement point — if it's bypassed (e.g. --force-depends), the mismatch isn't silently lost: it still surfaces after reboot as a content_mismatch_localized verdict (Case 2 in the Runtime Case Summary table), just without the specific "build mismatch" diagnosis
  • +
  • ESRT GUID matching compares each platform bundled inside this dtb-capsule package (its capsule.env FMP_GUID) against this device's ESRT fw_class entries (which firmware components this device can actually accept capsule updates for) — a three-way decision: 0 matches, exactly 1 match, or >1 matches (conflict, recorded to last-guid-conflict) — staging proceeds only on exactly 1 match
  • +
  • DTB_CAPSULE_EXPECTED_SHA is read directly from this package's own expected-dtb-sha256 file (shipped by the build, see below — not re-hashed here); RUNNING_DTB_SHA is the provenance sha256 already embedded in the DTB the device booted with right now (read from devicetree). If the running DTB already matches this package's bundled DTB (RUNNING_DTB_SHA == DTB_CAPSULE_EXPECTED_SHA), staging is skipped outright — and any leftover .cap in CAPSULE_DIR from a previous attempt is cleared right there, so the next boot's verifier doesn't mistake it for an unconsumed capsule (reboot_pending false positive)
  • +
  • Staging always clears any old .cap in CAPSULE_DIR before copying the new one in — there is no separate branch that leaves an old capsule in place; the only variation is whether a diagnostic note is logged (if an old unconsumed capsule was found with the OsIndications bit unset, meaning firmware likely never processed it)
  • +
  • expected-dtb-sha256 is written once at build time directly into the dtb-capsule package (from the same dtb-provenance-content-sha256sums.txt the package carries) — nothing downstream re-derives or rewrites it; both decision boxes above just read the shipped value as-is
  • +
  • The OsIndications bit is read back and verified after writing, not just trusted from efivar's own exit code
  • +
+
+ +
+ + + + + + + + + + + + +
CaseScenarioOutcome
Alinux-modules-<kver> not installed (e.g. --force-depends)✗ Error, install fails (exit 1)
BBuild hash mismatch: DTB_CAPSULE_EXPECTED_SHAMODULES_DTB_SHA✗ Error, install fails (exit 1)
C0 packaged platforms' FMP_GUID match this device's ESRT entries⚠ Skip staging (no matching GUID)
D>1 packaged platforms' FMP_GUID match this device's ESRT entries⚠ Skip staging (ambiguous, recorded to last-guid-conflict)
EExactly 1 match, and RUNNING_DTB_SHA == DTB_CAPSULE_EXPECTED_SHA (content already matches)✓ Skip staging (no-op), clears any stale .cap
FExactly 1 match, content differs✓ Stage capsule, set OsIndications bit (normal path)
+

Case B replaces what used to be checked only at runtime ("Case 0") — the build-consistency check now runs here, before any capsule is staged, so a mismatched pairing is caught immediately rather than after a wasted reboot.

+
+
+ + +
+

3. Runtime Verification

+

+ Two phases are written to last-verify-state. Phase 1 (kver_match_state) asks "does the installed dtb-capsule package's target match the kernel running right now?" using dpkg records and CAPSULE_DIR — deliberately direction-agnostic: EXPECTED may name a kernel that's newer (an upgrade not yet rebooted into) or older (a rollback target) than RUNNING, and both are handled by the same check. Phase 2 (dtb_pairing_state, only reached once Phase 1 = ok) asks "did firmware pair the running kernel with the correct DTB content?" using ESRT and a scan of installed kernels — content is checked first, and ESRT is consulted only to diagnose a mismatch. Package build consistency (dtb-capsule vs. its target linux-modules-<kver>) is enforced once, earlier, at install time (see the Install Time Case table, Case B) — it is not re-checked here. +

+ +

3a. kver_match_state (Phase 1)

+ + + + + + + + + + + + + + + + Read DTB_CAPSULE_EXPECTED_KVER (package target) & RUNNING_KVER + + + + + + DTB_CAPSULE_EXPECTED_KVER empty + OR RUNNING==EXPECTED? + + + Yes + + kver_match_state=ok + → proceed to Phase 2 (3b) + + + No + + + + EXPECTED's dpkg record + exists but NOT + install-ok-installed? + + + Yes + + package_mismatch + (exit — device/package mismatch) + + + No + + + + CAPSULE_DIR non-empty, + OR MODULES_DTB_SHA == + RUNNING_DTB_SHA (staging + was skipped, content already + == EXPECTED)? + + + Yes + + check_reboot_stall() → + reboot_pending / reboot_stalled + (EXPECTED may be newer OR older) + + + No + + + + RUNNING_DTB_MATCHES_INSTALLED_MODULES + (running kernel's own + content)? + + + ok + + no_capsule_for_running_kernel + (different kernel, running + kernel's content self-consistent) + + + mismatch + + kernel_dtb_mismatch + + + unknown + + kver_match_state=unknown + (no provenance data available) + + + + On reaching kver_match_state=ok (fresh or deferred): + clear the reboot-pending-since tracking file (see 3c) + + +
+ + + + + + + + + + + + + + + +
#DTB_CAPSULE_EXPECTED_KVEREXPECTED dpkg recordCAPSULE_DIR / staging-skippedRUNNING_DTB_MATCHES_INSTALLED_MODULESkver_match_state
1empty, or == RUNNING_KVERok
2set, ≠ RUNNING_KVERexists, not install-ok-installedpackage_mismatch
3set, ≠ RUNNING_KVERok / missingpending / skipped-stagingreboot_pending / reboot_stalled
4set, ≠ RUNNING_KVERok / missingnookno_capsule_for_running_kernel
5set, ≠ RUNNING_KVERok / missingnomismatchkernel_dtb_mismatch
6set, ≠ RUNNING_KVERok / missingnounknownunknown (no provenance marker)
+

There is deliberately no "version direction" column — EXPECTED can legitimately name a kernel that is either newer or older than RUNNING, and rows 3–6 treat both the same way. Row 6 is the "evidence insufficient" exit, landing on ok so Phase 2's content self-consistency check can speak instead of guessing.

+
+ +

3b. dtb_pairing_state (Phase 2, only reached once kver_match_state=ok)

+ + + + + + + + + + + + + + + Entry: kver_match_state=ok + + + + + RUNNING_DTB_MATCHES_INSTALLED_MODULES + == ok? + + + Yes + + dtb_pairing_state=apply_confirmed ✓ + (content match wins regardless of ESRT) + + + No + + + no ESRT match, OR + CAPSULE_DIR non-empty? + + + Yes + + dtb_pairing_state=pending + + + No + + + RUNNING_DTB_SHA empty? + (no provenance node in devicetree) + + + Yes + + dtb_pairing_state=unknown + (no provenance node) + + + No + + + RUNNING_DTB_MATCHES_INSTALLED_MODULES + == unknown? + + + Yes + + dtb_pairing_state=unknown + (no marker in own package) + + + No (mismatch, provenance present) + + + Scan every OTHER installed kernel's + dtb-provenance-sha256 == RUNNING_DTB_SHA? + + + + + match found among + other installed kernels? + + + Yes + + dtb_pairing_state=suspected_dtb_rollback + (rollback_target_kver=X) + + + No + + + ESRT_CONFIRMED + == 0? + + + Yes + + + + ROLLBACK_TARGET_KVER + found? + + + Yes + + dtb_pairing_state=apply_failed_with_rollback_available + + + No + + dtb_pairing_state=apply_failed + + + No + + + Localize: diff each packaged .dtb/.dtbo + sha256 against the build-time manifest + + + + + dtb_pairing_state=content_mismatch_localized + + +

3c. reboot_stalled persistence tracking (check_reboot_stall)

+ + + + + + + + + + + + + + + reboot_pending point reached (DTB_CAPSULE_EXPECTED_KVER, BOOT_ID) + + + + + no tracking file, OR tracked + expected_kver != DTB_CAPSULE_EXPECTED_KVER? + + + Yes + + (Re)write file: + expected_kver + first_boot_id=BOOT_ID + → reboot_pending + + + No + + + tracked_first_boot_id + == BOOT_ID? + + + Yes + + reboot_pending + (same boot; file untouched) + + + No + + + reboot_stalled + (rebooted ≥1x, still not matching; + file NOT updated — keeps original first_boot_id) + + +
+ Key points: +
    +
  • 3a (kver_match_state): direction-agnostic — splits into package_mismatch, reboot_pending/reboot_stalled, no_capsule_for_running_kernel, and kernel_dtb_mismatch. Whenever DTB self-consistency itself is unknown, it defers to Phase 2 instead of guessing
  • +
  • 3b (dtb_pairing_state): content is checked first — a match short-circuits to apply_confirmed regardless of ESRT. On mismatch, every other installed kernel's provenance sha256 is scanned first to name a suspected_dtb_rollback target; only if no match is found does ESRT decide between apply_failed_with_rollback_available (if a matching kernel exists) and apply_failed (if no match)
  • +
  • 3c (reboot_stalled tracking): keyed on boot_id so clock skew can't distort it; deliberately left untouched once escalated to reboot_stalled, so the state keeps escalating consistently across further reboots
  • +
+
+
+ + +
+

4. Case Summary (Case 1–8)

+

Every runtime scenario the design accounts for, in the order Phase 1/2 would actually reach them.

+
+ + + + + + + + + + + + + + + + +
CaseScenariokver_match_statedtb_pairing_stateOverall
1Matched linux-image/module + dtb-capsule installokapply_confirmed✓ Normal
2Matched install, but content mismatched (ESRT false success, or DTB altered)okcontent_mismatch_localized✗ Error
3linux-image/module only, dtb-capsule never installedokapply_confirmed✓ Normal
4Kernel upgraded, DTB not updated along with itkernel_dtb_mismatch✗ Error
5New dtb-capsule installed, matching linux-image/module not yet installed/rebooted (incl. 5b: staging skipped, content already matches)reboot_pending / reboot_stalled⏳ Pending
6DTB manually reflashed, dtb-capsule never installedoksuspected_dtb_rollback / apply_failed✗ Error
7Kernel upgraded, DTB content unchanged — postinst skipped stagingokapply_confirmed✓ Normal
8linux-modules-<RUNNING_KVER> has been removed; no provenance marker to check againstokunknown? Cannot verify
+

Package build-consistency mismatches (dtb-capsule vs. its target linux-modules-<kver>) are now caught earlier, at install time — see the Install Time Case table, Case B — so they no longer appear as a runtime case here. Case 5's variant 5b is detected via MODULES_DTB_SHA == RUNNING_DTB_SHA rather than a non-empty CAPSULE_DIR.

+
+
+ + +
+

4. Kernel Rollback Recovery Flow

+

+ When kernel/DTB pairing is mismatched (kernel_dtb_mismatch, reboot_stalled, apply_failed, or suspected_dtb_rollback), + verify-capsule-result.sh checks if a matching kernel exists. If found, dtb-capsule-recovery --auto scans installed kernels + to find one whose DTB matches the running DTB, then updates GRUB to boot that kernel on next reboot, converging the device back to a known-good state. +

+ + + + + + + + + + + + + + + + + kernel_dtb_mismatch or reboot_stalled detected + verify-capsule-result.sh calls dtb-capsule-recovery --auto + + + + + + Read running DTB's provenance sha256 + + + + + + matching kernel + exists? + + + No + + ERROR: no matching + kernel found + + + Yes + + + + Scan all installed kernels + compare each kernel's dtb-provenance-sha256 + against running DTB sha256 + + + + + + matching kernel + found? + + + No + + ERROR: no kernel + matches running DTB + + + Yes + + + + Select latest matching kernel version + + + + + + Update GRUB default boot entry + grub-set-default + update /etc/default/grub + update-grub + + +
+ Key points: +
    +
  • Automatic trigger: verify-capsule-result.sh checks for matching kernel before invoking dtb-capsule-recovery --auto on kernel_dtb_mismatch, reboot_stalled, suspected_dtb_rollback, or apply_failed_with_rollback_available
  • +
  • Matching kernel check: Scans /usr/lib/modules for kernels whose dtb-provenance-sha256 matches the running DTB's sha256
  • +
  • Kernel scan: Iterates through all installed kernels in version order, comparing each kernel's dtb-provenance-sha256 against the running DTB's sha256
  • +
  • Latest selection: When multiple kernels match, selects the highest version number
  • +
  • GRUB persistence: Updates both grub-set-default (EFI variable) and /etc/default/grub (config file), then calls update-grub to regenerate grub.cfg
  • +
  • Convergence: Device reboots into the selected kernel, which has matching DTB content, converging to apply_confirmed state
  • +
+
+
+ + +
+

5. Case Transition Diagrams

+ +
+

Normal upgrade: Case 1 → Case 5 → Case 1

+ + + + + + + + Case 1 + apply_confirmed + + + install new dtb-capsule + + + Case 5 + reboot_pending + + + reboot + + + Case 1 + apply_confirmed (new kver) + +
+ +
+

Kernel upgraded without a matching capsule: Case 1 → Case 4

+ + + + + + + + Case 1 + apply_confirmed + + + install linux-image only, + no new dtb-capsule + + + Case 4 + kernel_dtb_mismatch + +
+ +
+

Manual DTB reflash, then dtb-capsule install: Case 6 → Case 1 → Case 7

+ + + + + + + + Case 1 + apply_confirmed + + + manual DTB flash + + + Case 6 + suspected_dtb_rollback + + + auto-recovery triggers + reboot with matching kernel + + + Case 1 + apply_confirmed + +
+ +
+ Case transitions confirm the state machine is direction-agnostic and idempotent: whichever case a device is in, installing the correct capsule package for the running kernel and rebooting (if needed) always converges back to an apply_confirmed case (1/3/7), never leaving a dead end. +
+
+ + +
+

6. End-to-End

+ + + + + + + + + + + Build + Time + Generate capsule pkg + + + + dtb-capsule-*.deb + + + + + Install + Time + Stage capsule + + + + dpkg -i + + + + + Reboot + Firmware applies + + + + firmware + + + + + Runtime + Verify result + + + + systemd + + + + + Login + Time + MOTD display + + + + + Key data flows: + + + dtb_provenance_sha256 + embedded in DTB + linux-modules + dtb-capsule pkg (expected-dtb-sha256) + + + <machine>-dtb.cap + signed UEFI capsule + + + ESRT result + firmware apply status + + + last-verify-state + kver_match_state, dtb_pairing_state + + + reboot-pending-since + boot_id-keyed reboot_stalled tracking + + + journalctl -t dtb-capsule-verify + detailed verification log + + +
+ Full-pipeline overview: +
    +
  • Build time: generate the signed capsule package, embedding provenance info into the DTB, linux-modules, and the dtb-capsule package itself
  • +
  • Install time: select the platform via ESRT GUID matching, skip staging when content already matches, otherwise stage the capsule to the EFI partition
  • +
  • Reboot: firmware reads the capsule from the EFI partition and flashes the DTB partition
  • +
  • Runtime: a systemd service checks kver_match_state (Phase 1), then dtb_pairing_state (Phase 2); package build consistency between dtb-capsule and linux-modules is enforced once, at install time, not re-checked here
  • +
  • Login time: MOTD surfaces the verification result so admins see it at a glance
  • +
+
+
+ + +
+

Legend

+
+
+
+ Process step +
+
+
+ Success / done +
+
+
+ Pending / warning +
+
+
+ Error / failure +
+
+
+ Decision point +
+
+
+ Fall-through / no verdict yet +
+
+
+
+ + From b7e7b9137e6d3d3a6eaa55ade477df46e92a3aed Mon Sep 17 00:00:00 2001 From: Guanquan Tian Date: Tue, 8 Sep 2026 15:25:57 +0800 Subject: [PATCH 7/8] ci: enable secrets inheritance in premerge PR workflow Add secrets: inherit to allow premerge-pr.yml to access GitHub Secrets (FMPCERT, FMPROOT, RMPSUB) needed for DTB capsule certificate injection during kernel build. Signed-off-by: Guanquan Tian --- .github/workflows/premerge-pr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/premerge-pr.yml b/.github/workflows/premerge-pr.yml index 9e6fdaf3f3a37..03cd0bf2653b7 100644 --- a/.github/workflows/premerge-pr.yml +++ b/.github/workflows/premerge-pr.yml @@ -39,3 +39,4 @@ jobs: dbgsym: false flavours: qcom s3_prefix: premerge + secrets: inherit From 9d7b0cc312d48ea46ebd5e839155b0e31d367b83 Mon Sep 17 00:00:00 2001 From: Guanquan Tian Date: Tue, 15 Sep 2026 12:52:49 +0800 Subject: [PATCH 8/8] qcom: consolidate dtb-capsule design docs under debian.qcom/docs Removes the stray FLOWCHART-UPDATES-v3.md and IMPLEMENTATION-SUMMARY-P1-P4.md that accidentally got committed alongside the dtb-capsule-recovery code change, and replaces them with an up-to-date implementation summary and flowchart set under debian.qcom/docs/, so design docs live with the code they describe instead of the repo root. Signed-off-by: Guanquan Tian --- FLOWCHART-UPDATES-v3.md | 191 ------ IMPLEMENTATION-SUMMARY-P1-P4.md | 161 ----- .../docs/DTB-CAPSULE-IMPLEMENTATION.md | 621 ++++++++++++++++++ .../docs/dtb-capsule-flowcharts-en.html | 304 +++------ 4 files changed, 712 insertions(+), 565 deletions(-) delete mode 100644 FLOWCHART-UPDATES-v3.md delete mode 100644 IMPLEMENTATION-SUMMARY-P1-P4.md create mode 100644 debian.qcom/docs/DTB-CAPSULE-IMPLEMENTATION.md rename dtb-capsule-flowcharts-v3-en.html => debian.qcom/docs/dtb-capsule-flowcharts-en.html (78%) diff --git a/FLOWCHART-UPDATES-v3.md b/FLOWCHART-UPDATES-v3.md deleted file mode 100644 index 59cdf00fa2134..0000000000000 --- a/FLOWCHART-UPDATES-v3.md +++ /dev/null @@ -1,191 +0,0 @@ -# DTB Capsule 流程图更新 (v3) - 与 P1-P4 代码优化同步 - -## 更新日期 -2026-09-07 - -## 更新原因 -代码实现了 P1-P4 优化后,流程图需要同步更新以反映最新的实现细节。 - -## 具体改动 - -### 1. 自动恢复触发点表格 (行 397-414) -**改动内容**: -- 第 4 项从 "apply_failed + matching kernel found" 改为 "apply_failed_with_rollback_available + matching kernel found" -- 更新所有代码行号以反映 P1-P4 优化后的新行号 - -**原因**: -- 代码中 apply_failed 分为两种情况: - - 有匹配内核 → apply_failed_with_rollback_available(触发自动恢复) - - 无匹配内核 → apply_failed(不触发自动恢复) -- 流程图之前的描述不够准确 - -**更新前**: -``` -4 | apply_failed + matching kernel found | Call run_auto_recovery() | Line 448 -``` - -**更新后**: -``` -4 | apply_failed_with_rollback_available + matching kernel found | Call run_auto_recovery() | Line 448 -``` - -**行号更新**: -- reboot_stalled: Line 224, 241 → Line 240, 245 -- kernel_dtb_mismatch: Line 265 → Line 268 -- suspected_dtb_rollback: Line 427 → Line 434 -- apply_failed_with_rollback_available: Line 448 → Line 448 (不变) - ---- - -### 2. Phase 2 流程图增强 (行 779-888) -**改动内容**: -- 在 "ESRT_CONFIRMED == 0?" 的判断后添加子决策节点 -- 区分 "有 ROLLBACK_TARGET_KVER" 和 "无 ROLLBACK_TARGET_KVER" 两种情况 -- 分别导向 apply_failed_with_rollback_available 和 apply_failed - -**原因**: -- 代码中 ESRT_CONFIRMED == 0 时的处理逻辑有两个分支 -- 流程图之前只显示了一个分支(apply_failed),缺少了 apply_failed_with_rollback_available 的处理 - -**改动前**: -``` -ESRT_CONFIRMED == 0? - ├─ Yes → apply_failed - └─ No → (continue to content_mismatch_localized) -``` - -**改动后**: -``` -ESRT_CONFIRMED == 0? - ├─ Yes - │ ├─ ROLLBACK_TARGET_KVER found? - │ │ ├─ Yes → apply_failed_with_rollback_available - │ │ └─ No → apply_failed - │ └─ (end) - └─ No → (continue to content_mismatch_localized) -``` - -**SVG 调整**: -- 增加了一个新的决策菱形(ROLLBACK_TARGET_KVER found?) -- 调整了后续元素的位置 -- 更新 SVG viewBox 从 "0 0 1200 1260" 改为 "0 0 1400 1350" - ---- - -### 3. State Output File 说明增强 (行 431-446) -**改动内容**: -- 添加说明:rollback_target_kver 和 rollback_target_available 现在在多个状态中被填充 -- 明确指出这些字段在哪些状态下会有值 - -**原因**: -- P3 优化后,reboot_stalled 和 kernel_dtb_mismatch 状态也会上报这两个字段 -- 需要在文档中说明这一变化 - -**更新前**: -``` -All state information is written to `/var/lib/dtb-capsule/last-verify-state` in key=value format: -``` - -**更新后**: -``` -All state information is written to `/var/lib/dtb-capsule/last-verify-state` in key=value format. -The `rollback_target_kver` and `rollback_target_available` fields are always emitted (even empty) -so downstream consumers can safely test `-n "$rollback_target_kver"`. These fields are populated in -`reboot_stalled`, `kernel_dtb_mismatch`, `suspected_dtb_rollback`, and -`apply_failed_with_rollback_available` states when a matching kernel is found. -``` - ---- - -### 4. Key Points 说明更新 (行 942-950) -**改动内容**: -- 更新 3b (dtb_pairing_state) 的说明 -- 明确区分 apply_failed_with_rollback_available 和 apply_failed 的条件 - -**原因**: -- 之前的说明没有明确提到 apply_failed_with_rollback_available 的存在 -- 需要说明 ESRT 决策的完整逻辑 - -**更新前**: -``` -only if no match is found does ESRT decide between `apply_failed` and `content_mismatch_localized` -``` - -**更新后**: -``` -only if no match is found does ESRT decide between `apply_failed_with_rollback_available` -(if a matching kernel exists) and `apply_failed` (if no match) -``` - ---- - -### 5. Kernel Rollback Recovery Flow 说明更新 (行 1061-1071) -**改动内容**: -- 更新自动恢复触发条件的说明 -- 改为 "apply_failed_with_rollback_available" 而不是 "apply_failed" - -**原因**: -- 与自动恢复触发点表格的改动保持一致 - -**更新前**: -``` -verify-capsule-result.sh checks for matching kernel before invoking dtb-capsule-recovery --auto -on kernel_dtb_mismatch, reboot_stalled, apply_failed, or suspected_dtb_rollback -``` - -**更新后**: -``` -verify-capsule-result.sh checks for matching kernel before invoking dtb-capsule-recovery --auto -on `kernel_dtb_mismatch`, `reboot_stalled`, `suspected_dtb_rollback`, or `apply_failed_with_rollback_available` -``` - ---- - -### 6. 文档更新日期 -- 从 2026-09-03 更新为 2026-09-07 - ---- - -## 验证清单 - -- ✅ 自动恢复触发点表格准确反映代码实现 -- ✅ Phase 2 流程图包含 apply_failed_with_rollback_available 的处理 -- ✅ State Output File 说明包含 P3 改动信息 -- ✅ Key Points 说明准确描述 ESRT 决策逻辑 -- ✅ 所有代码行号已更新 -- ✅ SVG 尺寸已调整以容纳新的决策节点 -- ✅ 文档日期已更新 - ---- - -## 与代码的对应关系 - -### 自动恢复触发点 -| 流程图 | 代码位置 | 状态 | -|--------|---------|------| -| reboot_stalled + matching kernel | 行 240, 245 | ✅ | -| kernel_dtb_mismatch + matching kernel | 行 268 | ✅ | -| suspected_dtb_rollback | 行 434 | ✅ | -| apply_failed_with_rollback_available + matching kernel | 行 448 | ✅ | - -### Phase 2 决策路径 -| 条件 | 代码位置 | 状态 | -|------|---------|------| -| RUNNING_DTB_MATCHES_INSTALLED_MODULES == ok | 行 295-298 | ✅ | -| ESRT_CONFIRMED == 0 + ROLLBACK_TARGET_KVER found | 行 442-449 | ✅ | -| ESRT_CONFIRMED == 0 + no ROLLBACK_TARGET_KVER | 行 450-451 | ✅ | -| ESRT_CONFIRMED == 1 + content mismatch | 行 456-503 | ✅ | - ---- - -## 总结 - -✅ **流程图已与 P1-P4 代码优化同步** - -- 自动恢复触发点表格更准确 -- Phase 2 流程图更完整 -- 所有说明都反映了最新的代码实现 -- 代码行号已更新 -- 文档日期已更新 - -**流程图现在完全准确反映了代码实现的最新状态。** diff --git a/IMPLEMENTATION-SUMMARY-P1-P4.md b/IMPLEMENTATION-SUMMARY-P1-P4.md deleted file mode 100644 index a213a5b969578..0000000000000 --- a/IMPLEMENTATION-SUMMARY-P1-P4.md +++ /dev/null @@ -1,161 +0,0 @@ -# DTB Capsule 冗余优化实施总结 (P1-P4) - -## 实施日期 -2026-09-07 - -## 改动范围 -文件: `debian.qcom/dtb-capsule-runtime/verify-capsule-result.sh` - -## 实施内容 - -### P1: 内核扫描循环一次性计算 ✅ -**目标**: 消除 5 处重复的内核扫描循环 - -**改动**: -- 行 129-134: 在 Phase 1 开始前,一次性计算 `ROLLBACK_TARGET_KVER` -- 所有 Phase 1 分支直接引用该变量,无需重复扫描 -- 行 240, 245, 269: Phase 1 分支使用预计算的 `$ROLLBACK_TARGET_KVER` - -**代码行数变化**: 净减少约 20 行 - -**验证**: ✓ 只有 1 处内核扫描循环(行 130) - ---- - -### P2: ESRT 缓存文件合并 ✅ -**目标**: 将 3 个独立的 ESRT 缓存文件合并为 1 个 - -**改动**: -- 行 53: 新增 `LAST_ESRT_CACHE_FILE` 变量 -- 行 312-321: 从统一缓存文件读取 kver、confirmed、detail -- 行 390-395: 写入统一缓存文件(key=value 格式) - -**代码行数变化**: 净减少约 10 行 - -**向后兼容**: 旧的 3 个缓存文件变量仍然定义(行 38-40),但不再使用 - -**验证**: ✓ 缓存文件从 3 个减少到 1 个 - ---- - -### P3: reboot_stalled/kernel_dtb_mismatch 上报回滚目标 ✅ -**目标**: 修复可观测性缺口,让这两个状态也上报 `rollback_target_kver` - -**改动**: -- 行 223-251: 合并两个重复的 reboot_stalled 判断(D3 决策),统一处理 capsule_unconsumed 和 staging_skipped 两种情况 -- 行 245: reboot_stalled 状态现在传入 `$ROLLBACK_TARGET_KVER` 和 `$_rollback_available` -- 行 262-274: kernel_dtb_mismatch 状态现在传入 `$ROLLBACK_TARGET_KVER` 和 `$_rollback_available` - -**代码行数变化**: -- P3a (D3 合并): 净减少约 15 行 -- P3b (上报 rollback_target): 增加约 6 行 -- 总计: 净减少约 9 行 - -**外部影响**: 增量式,新增两个原本为空的字段取值;不删除、不改名现有字段 - -**验证**: ✓ 两处 write_state 调用都传入了 rollback_target 参数 - ---- - -### P4: Phase 2 回滚目标扫描复用 ✅ -**目标**: 消除 Phase 2 中的重复内核扫描 - -**改动**: -- 行 425-426: 注释说明 `ROLLBACK_TARGET_KVER` 已在 P1 计算,此处复用 -- 行 428-437: suspected_dtb_rollback 分支直接使用 `$ROLLBACK_TARGET_KVER` -- 行 439-454: apply_failed_with_rollback_available 分支直接使用 `$ROLLBACK_TARGET_KVER` - -**代码行数变化**: 净减少约 20 行(消除了 2 处重复的内核扫描循环) - -**验证**: ✓ Phase 2 中不再有内核扫描循环 - ---- - -## 总体改动统计 - -| 层级 | 改动 | 代码变化 | 风险 | 状态 | -|------|------|---------|------|------| -| P1 | 内核扫描一次性计算 | -20 行 | 低 | ✅ 完成 | -| P2 | ESRT 缓存合并 | -10 行 | 低 | ✅ 完成 | -| P3 | 上报 rollback_target | -9 行 | 低-中 | ✅ 完成 | -| P4 | Phase 2 扫描复用 | -20 行 | 中 | ✅ 完成 | -| **总计** | **全部优化** | **-59 行** | **可控** | **✅ 完成** | - ---- - -## 验证清单 - -### 语法检查 -- ✅ `sh -n verify-capsule-result.sh` 通过 - -### 逻辑验证 -- ✅ P1: 内核扫描循环只出现 1 次(行 130) -- ✅ P2: ESRT 缓存文件统一为 1 个(行 53) -- ✅ P3: reboot_stalled 传入 rollback_target(行 245) -- ✅ P3: kernel_dtb_mismatch 传入 rollback_target(行 269) -- ✅ P4: Phase 2 中无重复的内核扫描循环 - -### 代码质量 -- ✅ 注释清晰,说明最终状态而非过程 -- ✅ 变量命名一致(`_rollback_available`, `_detail_reason` 等) -- ✅ 错误处理保持不变 -- ✅ 日志输出保持不变 - ---- - -## 设计说明 - -### P1 的设计原理 -由于 `RUNNING_KVER` 和 `RUNNING_DTB_SHA` 在脚本执行期间不变,内核扫描的结果对所有分支都是相同的。因此在 Phase 1 开始前一次性计算,然后在所有分支中复用,避免了 5 处重复的扫描。 - -### P2 的设计原理 -ESRT 缓存的三个字段(kver、confirmed、detail)在逻辑上是一条记录,应该原子性地读写。合并成一个文件后,使用 key=value 格式,与 `last-verify-state` 的风格保持一致。 - -### P3 的设计原理 -`reboot_stalled` 和 `kernel_dtb_mismatch` 状态下,系统已经扫描出了匹配的回滚目标内核,但原来没有上报到状态文件。这导致运维无法从状态文件直接看到"系统准备切到哪个内核",只能翻日志。现在上报这两个字段后,状态文件变成了完整的诊断信息源。 - -### P4 的设计原理 -Phase 2 中的两个分支(suspected_dtb_rollback 和 apply_failed_with_rollback_available)都需要找到匹配的回滚目标内核。原来各自独立扫描,现在复用 P1 计算的结果。这不仅减少了代码重复,也提高了性能(避免了 2 次额外的内核扫描)。 - ---- - -## 后续验证步骤 - -### 单元测试(需要手动执行) -为每个既有的状态转换场景(14 个 case)构造 mock 文件树,用覆盖后的环境变量跑脚本,验证: -- P1/P2/P4: 所有 14 个场景的输出逐字节不变(纯重构) -- P3: 只有 `reboot_stalled`/`kernel_dtb_mismatch` 且存在匹配内核的场景里,新增字段从空变为有值 - -### 集成测试 -在实际设备上运行 `dtb-capsule-verify` 和 `dtb-capsule-motd.sh`,确认: -- 状态文件格式正确 -- MOTD 输出不变(对新字段无感) -- 自动恢复流程正常 - ---- - -## 注意事项 - -### 向后兼容性 -- 旧的 3 个 ESRT 缓存文件(`last-verified-kver`, `last-esrt-confirmed`, `last-esrt-detail`)不再使用,但变量定义保留 -- 可以在后续的清理任务中删除这些旧文件 -- `last-verify-state` 的字段格式不变,只是 `rollback_target_kver` 和 `rollback_target_available` 在某些状态下从空变为有值 - -### 外部消费者影响 -- `dtb-capsule-motd.sh`: 对新字段无感,行为不变 -- `dtb-capsule-recovery.sh`: 不受影响(独立工具) -- Fleet agents / recovery services: 新字段是增量式的,不会破坏现有的消费逻辑 - ---- - -## 总结 - -✅ **P1-P4 全部改动已完成** - -- 代码行数减少 59 行(净减少) -- 消除了 5 处重复的内核扫描循环 -- 修复了 1 个真实的可观测性缺口 -- 所有改动都是低风险的重构或增量式改进 -- 语法检查通过,逻辑验证通过 - -**初始化方案已就绪,可进行后续的单元测试和集成测试。** diff --git a/debian.qcom/docs/DTB-CAPSULE-IMPLEMENTATION.md b/debian.qcom/docs/DTB-CAPSULE-IMPLEMENTATION.md new file mode 100644 index 0000000000000..6f3bbf7af0520 --- /dev/null +++ b/debian.qcom/docs/DTB-CAPSULE-IMPLEMENTATION.md @@ -0,0 +1,621 @@ +# DTB Capsule Update & Recovery — Implementation Summary + +Branch: `dtb-capsule-resolute-devel-tip` + +Scope: adds a new `dtb-capsule--qcom` Debian package that ships a UEFI +capsule for updating the Qualcomm device tree blob (DTB), plus build-time +generation, install-time staging, and post-reboot runtime verification of +that update — **including automatic GRUB-default recovery when firmware +applies the wrong DTB**. §8.2 flags a known gap that still needs design +work. + +## 1. Problem being solved + +The kernel package updates DTB files on disk +(`/usr/lib/firmware//device-tree/qcom/*.dtb`), but on these Qualcomm +platforms the DTB actually consumed by the bootloader/firmware pre-OS comes +from a dedicated flash partition (`dtb_a`/`dtb_b`, spinor), not from the +filesystem. Updating the kernel package alone does not update what firmware +boots. + +UEFI Capsule Update is the standard mechanism to get firmware to flash a new +image into that partition on the next boot. This work adds the tooling to +(a) build a signed capsule containing the new DTB content at kernel-package +build time, (b) stage it for firmware to consume at package-install time, +and (c) verify after reboot that firmware actually applied it — including +detecting rollback/mismatch conditions and, where possible, automatically +recovering from them. + +## 2. End-to-end pipeline + +``` +Build time (kernel package build) + └─ collect installed .dtb/.dtbo → compute provenance sha256 + └─ embed sha256 into DTB itself (qcom-dtb-capsule-provenance node) + └─ embed sha256 into linux-modules- package + └─ build SoC-filtered FIT DTB image (dtb.bin) + └─ run qcom_capsule_tool → signed -dtb.cap per platform + └─ package into dtb-capsule--qcom.deb + (cap files, capsule.env, expected-kver, expected-dtb-sha256, + verify script, recovery tool, MOTD script, systemd unit, postinst/prerm) + +Install time (dpkg --configure dtb-capsule--qcom) + └─ postinst: gate on linux-modules- installed + same-build sha256 + └─ match device's ESRT FMP_GUID → pick platform's .cap + └─ skip if running DTB already matches expected content + └─ copy .cap to /boot/efi/EFI/UpdateCapsule/ + └─ set OsIndications capsule-delivery bit (efivar), verify read-back + +Reboot + └─ firmware drains /boot/efi/EFI/UpdateCapsule/, flashes DTB partition, + records result in ESRT (fw_version / last_attempt_status) + +Runtime (post-boot, systemd oneshot, every boot) + └─ dtb-capsule-verify.service → verify-capsule-result.sh + └─ cross-checks kernel version, ESRT result, DTB content sha256 + └─ on mismatch/rollback/failure with a matching kernel available: + best-effort calls dtb-capsule-recovery --auto to switch GRUB default + └─ writes /var/lib/dtb-capsule/last-verify-state + └─ /etc/update-motd.d/85-dtb-capsule surfaces problems on login + +Removal (dpkg remove dtb-capsule--qcom) + └─ prerm: deletes this package's staged-but-unconsumed .cap, if present +``` + +## 3. Build-time changes (compile / package build) + +### 3.1 New build flags and package wiring + +- `debian.qcom/rules.d/arm64.mk`: `do_dtb_capsule = true` (arm64-only, + alongside existing `do_dtbs`/`do_fitimage`). +- `debian/rules.d/0-common-vars.mk`: `dtb_capsule_pkg_name = + dtb-capsule-$(abi_release)-qcom`, plus the cert paths/firmware-version + defaults consumed by the capsule signing step (`dtb_capsule_cert_leaf`/ + `_root`/`_sub` under `$(DEBIAN)/certs/`, `dtb_capsule_fwver ?= 0.0.2.0`, + `dtb_capsule_lfwver ?= 0.0.0.0`, `dtb_capsule_storage_type ?= NORUFS`). +- `debian.qcom/control.stub.in`: + - New `Build-Depends: mtools [arm64]` (used to build a FAT image without + requiring a loop device / root). + - New binary package stanza: `dtb-capsule-PKGVER-ABINUM-qcom` (arch: + arm64, not per-flavour), `Depends: linux-modules-PKGVER-ABINUM-qcom`, + `Recommends: grub2-common`, and a version-independent + `Provides/Conflicts/Replaces: dtb-capsule-qcom` so upgrading to a new + kver's package lets dpkg cleanly replace the previous one instead of + erroring on shared, non-kver-scoped paths (`verify-capsule-result.sh`, + `expected-kver`, the systemd unit, etc.). +- `debian/rules.d/2-binary-arch.mk`: new `do_dtb_capsule` blocks in the + install stage, the packaging/`dh_systemd_enable` stage, and `dh_prep`. + Key design: DTB content is flavour-agnostic (both `qcom` and `qcom-rt` + flavours produce identical `.dtb`/`.dtbo` files), so the capsule package + is built only once using the first flavour's DTBs — same + `if [ $* = $(firstword $(flavours)) ]` guard already used for + `linux-bpf-dev`. + +### 3.2 Vendored source + +Two upstream sources were vendored (as-is, unmodified) into this tree, with +the exact upstream commit recorded in the vendoring commit message for +traceability: + +| What | Vendored from | Commit | +|---|---|---| +| `debian.qcom/scripts/qcom_capsule_tool/*.py` (14 modules) | `qualcomm/cbsp-boot-utilities` | `8a0f1deef97beae600910506bfba488976465828` | +| `debian.qcom/qcom-ptool/platforms/iq-x7181-evk/spinor/partitions.conf` | `qualcomm-linux/qcom-ptool` | `fb8c99c308732eaaba427233029f33c5327beebf` | +| `debian.qcom/fitimage/build-dtb-image.sh` | `qualcomm-linux/qcom-dtb-metadata` | `f1596a6b726c232743f968786de375a91d954eca` | + +`qcom_capsule_tool` is a Python package invoked as `python3 -m +qcom_capsule_tool.cli `. The dispatcher (`cli.py`) exposes +`create` (the full pipeline used by this build), plus the internal steps it +composes — `sysfw-version-create`, `update-fv-xml`, `fv-create`, +`generate-capsule`, `update-json`, `bin-to-hex` — and a `patch-capsule-cert` +subcommand that exists in the vendored tool (patches a root cert into +`uefi_dtbs`/`xbl_config` ELF images, including transparent `.xz` handling) +but is **not** exercised by the current pipeline, since only the `dtb` +partition is updated. + +### 3.3 Build steps added to `2-binary-arch.mk` (per-build, first flavour only) + +1. **Collect shipped DTBs**: copy every `.dtb`/`.dtbo` actually installed + under `usr/lib/firmware/-/device-tree/qcom/` (i.e. + the output of `dtbs_install`, run earlier in the same rule) into a + staging dir. This directory holds only the final `dtb-y` targets actually + shipped to the device — not `.dtbo` overlay fragments that exist solely + as FIT-image inputs and are never installed standalone — so the manifest + below doesn't later get reported as "missing" by the runtime verifier + for files that were never meant to be on the device in the first place. +2. **Compute provenance sha256**: `sha256sum` every collected file, sorted + by filename (`sort -k2,2`) into + `dtb-provenance-content-sha256sums.txt`, then `sha256sum` that manifest + file itself → one `dtb_provenance_sha256` value representing the exact + set of DTB content shipped in this build. This value is the backbone + that ties build, install, and runtime together (see §6). +3. **Embed provenance into the DTB itself**: `fdtput -p -t s + /qcom-dtb-capsule-provenance dtb-provenance-sha256 ` on every + collected `.dtb` — so a running kernel can read back which build produced + the DTB it's currently booted with, via + `/sys/firmware/devicetree/base/qcom-dtb-capsule-provenance/`. +4. **Embed provenance into `linux-modules-`**: writes the same sha256 + to `/usr/lib/modules//dtb-provenance-sha256` inside the kernel + modules package — the "what should be installed for this kernel version" + reference value, readable even when that DTB isn't the one currently + active. +5. **Build the FIT DTB image**: `build-dtb-image.sh --dtb-src --soc + hamoa purwa --size 4 --out dtb.bin --prune`. Filters the FIT `.its` down + to only the DTBs needed for the `hamoa`/`purwa` SoCs (excludes DTBs for + other platforms), keeping `dtb.bin` under the spinor partition's 4MB + size cap. Builds an external-data FIT image with `mkimage`, wraps it in + a FAT image via `mtools` (no loop device / root required). `--prune` + skips any DTB the `.its` references but that's missing from the source + directory, instead of failing the build. +6. **Generate a signed capsule per platform** (`hamoa`, `purwa`), via + `qcom_capsule_tool.cli create`: + - `-S NORUFS -T ` (storage type / target platform, + from each machine's `capsule.env`) + - `--ptool-path --update-partitions dtb` (only the + `dtb`/`dtb_a`/`dtb_b` partitions are marked `Operation=UPDATE`; every + other partition in `partitions.conf` stays `Operation=IGNORE`) + - `-guid ` (Hamoa: `0F6D58FC-2258-4D27-9E23-D77219B0897C`, + Purwa: `185a798b-13b2-4595-bd08-e2770a4bb190`) + - signs with `$(dtb_capsule_cert_leaf)`/`_root`/`_sub` + (`QcFMPCert.pem`/`QcFMPRoot.pub.pem`/`QcFMPSub.pub.pem`, supplied via + the CI secrets wired in §3.5) + - produces `-dtb.cap` +7. **Package the outputs** into `dtb-capsule--qcom.deb`: + - `/usr/share/dtb-capsule//-dtb.cap` + `capsule.env` + (per platform) + - `/usr/share/dtb-capsule/dtb-provenance-content-sha256sums.txt` + (per-file manifest, used later to localize a content mismatch to a + specific file) + - `/usr/share/dtb-capsule/expected-kver` (this package's target kernel + version) + - `/usr/share/dtb-capsule/expected-dtb-sha256` (this kver's expected + aggregate provenance sha256) + - `/usr/share/dtb-capsule/verify-capsule-result.sh` + - `/usr/sbin/dtb-capsule-recovery` (from `dtb-capsule-recovery.sh`) + - `/etc/update-motd.d/85-dtb-capsule` + - `/lib/systemd/system/dtb-capsule-verify.service` + - rendered `postinst`/`prerm` (from `debian.qcom/templates/dtb-capsule. + postinst.in` / `.prerm.in`) + - systemd wiring: `dh_systemd_enable`/`dh_systemd_start` for + `dtb-capsule-verify.service` — this package ships only a systemd unit + (no init.d script), matching this repo's existing convention for other + systemd-only units; the unit's own `ConditionPathExists` keeps + postinst-time start from doing anything until a capsule is actually + staged. + +### 3.4 Per-platform capsule parameters (`capsule.env`) + +Two platform config files, each just an `FMP_GUID` + `TARGET`: + +- **hamoa** (`IQ-X7181`): GUID from the cbsp-boot-utilities + `uefi_capsule_generation` README, confirmed by reading it directly off + real Hamoa hardware's ESRT (`/sys/firmware/efi/esrt/.../fw_class`). +- **purwa** (`IQ-X5121`): GUID from the same README; maps to the same + `qcom-ptool` platform directory (`iq-x7181-evk`) as Hamoa + (`UpdateFvXml.py`'s `SUPPORTED_PLATFORMS` dict: both `IQ-X7181` and + `IQ-X5121` point at `iq-x7181-evk`). + +### 3.5 CI wiring + +`.github/workflows/premerge-pr.yml` adds `secrets: inherit` on the reusable +`build-kernel.yml` call so the pre-merge build can read the `FMPCERT`/ +`FMPROOT`/`FMPSUB` secrets needed to populate `debian.qcom/certs/QcFMP*.pem` +for capsule signing — without this, the `-p`/`-x`/`-oc` cert args passed to +`qcom_capsule_tool.cli create` in §3.3 step 6 would have nothing to sign +with in CI. + +## 4. Install-time behavior (`dtb-capsule.postinst.in`, runs on `dpkg --configure`) + +Runs `stage_capsule()` on every `configure` of the package: + +1. **Clears previous verify-state result** (`last-verify-state` file only — + the ESRT dedup cache and reboot-stall tracking files are owned by + `verify-capsule-result.sh` and not touched here) — every configure is a + fresh staging attempt, so the old verification result is stale. +2. **Clears a stale `IsCapsulePendingInPersistedMedia` flag** if set to + `0x01`: if a prior capsule update failed (e.g. UEFI crash), this EFI + variable may be stuck in the "update in progress" state, causing + firmware to reject new capsules. Detects and resets it to `0x00` via + `efivar -w`; logs a warning if the reset fails (efivar not available, or + efivarfs not writable). +3. **Determines target kernel version** from the package's own baked-in + `expected-kver` file; bails out (warn, `return 0` — never fails the + install) if missing/empty. +4. **Gate 1 — dependency defense-in-depth**: confirms + `linux-modules-` is actually `install ok installed` (guards against + `--force-depends` bypassing the declared package `Depends`). If not, + logs an error and **fails the install** (`exit 1`). Under a normal `apt + install`, this never fires — the declared `Depends` already guarantees + `linux-modules-` is installed first. +5. **Gate 2 — same-build check**: this package's `expected-dtb-sha256` must + equal `linux-modules-/dtb-provenance-sha256`; mismatch → `exit 1`. + Ensures the dtb-capsule package and the kernel package were actually + built in the same build event. +6. **Platform auto-selection via ESRT**: reads every packaged platform's + `FMP_GUID` (from its `capsule.env`) and checks it against this device's + actual ESRT entries (`/sys/firmware/efi/esrt/entries/entry*/fw_class`): + - 0 matches → skip staging (device doesn't have any of the packaged + platforms' GUIDs). + - **>1 match (ambiguous)** → skip staging, record a `last-guid-conflict` + state file (timestamp, kver, all matched machines) for the runtime + verifier / MOTD to surface. + - exactly 1 match → proceeds with that platform's `.cap`. +7. **Already-matches shortcut**: if the running DTB's provenance sha256 + already equals this package's `expected-dtb-sha256`, staging is skipped + entirely and any stale `.cap` left in `UpdateCapsule/` is removed — so + next boot's verifier doesn't see it as an unconsumed capsule. +8. **Stale-capsule detection**: if `/boot/efi/EFI/UpdateCapsule/` already + has an unconsumed capsule from a previous attempt and the `OsIndications` + capsule-delivery bit is **not** set, logs a warning (firmware likely + never got the earlier request) but still proceeds to (re)stage. +9. **Stage the capsule**: copies `-dtb.cap` → + `/boot/efi/EFI/UpdateCapsule/qcom-dtb-.cap` (clears any other + `qcom-dtb-*.cap` first). +10. **Tell firmware to process it**: sets bit 2 of the `OsIndications` EFI + variable via `efivar -w` (creating the variable if it doesn't exist). + The little-endian 8-byte value is hand-assembled via `\NNN` octal + escapes (dash's `printf` builtin does not understand `\xHH`). Reads the + value back afterward (`le64_from_offset4`) and logs an explicit error if + the bit did not actually stick — `efivar -w`'s own exit code only + reflects whether the write syscall was accepted, not whether firmware + will really honor it. +11. **No reboot is triggered here** — firmware applies the capsule pre-OS on + the next boot, whenever that happens. + +### 4.1 Removal behavior (`dtb-capsule.prerm.in`, runs on package removal) + +On `remove`, deletes this package's staged-but-unconsumed capsule at +`/boot/efi/EFI/UpdateCapsule/qcom-dtb-.cap`, if it's still there. This +file is written by postinst at runtime and is outside dpkg's file list, so +ordinary package removal would otherwise leave it behind — firmware would +still flash the now-uninstalled DTB pre-OS on the next boot even though the +package that shipped it is gone. Does not touch the `OsIndications` +capsule-delivery bit or `/var/lib/dtb-capsule/` state files. + +## 5. Runtime verification (post-reboot) + +### 5.1 Trigger + +`dtb-capsule-verify.service` — a oneshot systemd unit, +`ConditionPathExists=/boot/efi/EFI/UpdateCapsule`, runs +`verify-capsule-result.sh` after `multi-user.target` on every boot. (The +condition only checks the directory *exists*, not that it's non-empty, so +the script itself has to dedup re-checks across boots — see §5.4.) + +### 5.2 What it checks — `kver_match_state` (Phase 1) + +Whether the installed dtb-capsule package's `expected-kver` matches the +kernel actually running right now (`DTB_CAPSULE_EXPECTED_KVER` vs +`RUNNING_KVER`). See the matrix in §5.8 for the full list of states and +their meaning; two mechanisms behind specific states are worth calling out +here since the matrix doesn't have room for them: + +- `reboot_pending` vs. `reboot_stalled` are the same underlying condition + (expected kernel doesn't match running kernel, with an unconsumed or + content-already-matched capsule) at two different points in time. + `check_reboot_stall()` tells them apart via `boot_id`, not a counter, so + clock skew can't distort it: "just detected this boot" → `reboot_pending` + (wait for next reboot); "already survived a reboot without resolving" → + `reboot_stalled` (triggers auto-recovery). +- `kernel_dtb_mismatch` and `no_capsule_for_running_kernel` are + distinguished by whether the running kernel's own DTB content is + self-consistent with its installed package — mismatch is the error case, + self-consistent is benign (a kernel-only install with no matching + dtb-capsule package). + +### 5.3 What it checks — `dtb_pairing_state` (Phase 2, only when `kver_match_state=ok`) + +See the matrix in §5.8 for the full list of states. Two mechanisms behind +specific states are worth calling out here: + +- `apply_confirmed` is a content-level short-circuit: if the running DTB's + provenance sha256 already matches `linux-modules-`'s + recorded value, every ESRT check below is skipped entirely — content + match wins regardless of what firmware reports. +- `apply_failed` decodes the UEFI capsule status code into a human-readable + reason (`ErrorUnsuccessful`, `ErrorInsufficientResources`, + `ErrorIncorrectVersion`, `ErrorInvalidFormat`, `ErrorAuthError` (signature + failure), `ErrorPwrEvtAC`/`ErrorPwrEvtBatt`, `ErrorUnsatisfiedDependencies`). +- `content_mismatch_localized` cross-checks every individual `.dtb`/`.dtbo` + against the per-file `dtb-provenance-content-sha256sums.txt` manifest to + name the specific file(s) that differ (or reports "not localized to any + packaged .dtb/.dtbo" if the differing file isn't a tracked one). +- `ROLLBACK_TARGET_KVER` (used by both `suspected_dtb_rollback` and + `apply_failed_with_rollback_available`, but classified differently + depending on whether ESRT reported success or failure — see §5.7) is + computed **once**, at the top of the script, by scanning every kernel + under `/usr/lib/modules/*` (`sort -V`, ties resolve to the highest + version) for one whose own `dtb-provenance-sha256` matches the running + DTB's. Every branch reuses this single scan result instead of re-scanning. + +### 5.4 ESRT dedup + +The ESRT scan result is cached per running kernel version +(`last-esrt-cache`: kver/confirmed/detail) — once confirmed for a given +`RUNNING_KVER`, subsequent boots on the same kernel skip the sysfs scan and +recall the cached verdict (`esrt_dedup_skipped=true` in the state file). The +cache is only written when `UpdateCapsule/` is confirmed empty, so an +unconsumed-capsule boot never poisons the cache with a stale result. + +### 5.5 Output + +Every run writes `/var/lib/dtb-capsule/last-verify-state` (sourceable +`key=value` file): `timestamp`, `boot_id`, `kver`, `kver_match_state`, +`dtb_pairing_state`, `guid_conflict`/`guid_conflict_detail`, +`esrt_dedup_skipped`, `rollback_target_kver`/`rollback_target_available`, +`dtb_kver_content_match`, `detail`, and a one-line human-readable `summary` +(always last, so `tail -1` alone tells you if anything needs attention). +Also logs everything via `logger -t dtb-capsule-verify` (visible via +`journalctl -t dtb-capsule-verify`). + +### 5.6 Login-time surfacing + +`/etc/update-motd.d/85-dtb-capsule` (runs on every interactive login via +`pam_motd`): prints nothing if the last verify state was fully healthy +(`kver_match_state=ok`, `dtb_pairing_state=apply_confirmed`, no GUID +conflict). Otherwise prints the summary line, the raw state fields, and +points at `journalctl -t dtb-capsule-verify` for detail; for +`kernel_dtb_mismatch`/`reboot_stalled` it also tells the admin to run +`dtb-capsule-recovery` directly. Also detects a stale state file (`boot_id` +mismatch — this boot's check hasn't run/finished yet) and tells the admin to +check back shortly instead of showing a possibly-outdated verdict. + +### 5.7 Automatic recovery (`dtb-capsule-recovery.sh`, `/usr/sbin/dtb-capsule-recovery`) + +Called best-effort (`run_auto_recovery()` — no-op if the tool is missing or +not executable) from four Phase-1/Phase-2 branches, always gated on a +matching kernel already having been found: + +- `kernel_dtb_mismatch` (Phase 1, when `ROLLBACK_TARGET_KVER` is non-empty) +- `reboot_stalled` (Phase 1, when `ROLLBACK_TARGET_KVER` is non-empty) +- `suspected_dtb_rollback` (Phase 2) +- `apply_failed_with_rollback_available` (Phase 2) + +**Matching**: scans `/usr/lib/modules/*`, finds every kernel whose own +`dtb-provenance-sha256` equals the running DTB's, resolves ties to the +highest `sort -V` version (`find_matching_kernels_for_dtb`). + +**Switching** (`set_grub_default`): locates the target kernel's +`menuentry` in `/boot/grub/grub.cfg` via `awk` (excluding `recovery` +entries), builds the `Advanced options for Ubuntu>` submenu path, +then `grub-set-default` → patches `GRUB_DEFAULT=` in `/etc/default/grub` via +`sed` → runs `update-grub` if present. + +**Modes**: `--auto` (scripted; errors out with no changes made if no match +found), `--list` (prints every installed kernel with `match`/`mismatch`/ +`unknown` against the running DTB), and a bare interactive mode (numbered +selection). + +**Known limitation (accepted scope)**: the matching logic only cares +whether DTB *content* matches — it has no notion of whether that kernel has +actually been verified healthy at runtime. If a newly-applied kernel +repeatedly crashes or hangs before `verify-capsule-result.sh` can complete, +there is currently no automatic mechanism to detect this and fall back to the +previously-known-good kernel. The device will remain stuck in a crash loop. +This scenario — where the kernel fails to boot successfully but the DTB was +already applied by firmware — is addressed by the boot-counter mechanism +proposed in §8.2. + +### 5.8 Verification Result Classification Matrix + +#### 5.8.1 Kernel Version × Capsule Application State Matrix + +| `kver_match_state` | `dtb_pairing_state` | Meaning | Auto-recovery? | +|---|---|---|---| +| `package_mismatch` | (N/A) | Package targets a kernel whose `linux-modules` is in an abnormal dpkg state | No | +| `reboot_pending` | (N/A) | Capsule staged/content-matched for a different kernel, awaiting first reboot into it | No — wait | +| `reboot_stalled` | (N/A) | Same as above, but survived a reboot without resolving | **Yes**, if a matching kernel exists | +| `no_capsule_for_running_kernel` | (N/A) | No capsule targets the running kernel, but its own DTB content is self-consistent | No — benign | +| `kernel_dtb_mismatch` | (N/A) | Running kernel's own DTB content doesn't match its installed package | **Yes**, if a matching kernel exists | +| `unknown` | (N/A) | Can't determine running kernel's DTB self-consistency | No | +| `ok` | `pending` | Capsule not yet confirmed applied by firmware | No — wait | +| `ok` | `apply_failed` | Firmware reported the capsule update failed, no rollback target | No | +| `ok` | `apply_failed_with_rollback_available` | Firmware reported apply failed, but a matching kernel exists | **Yes** | +| `ok` | `suspected_dtb_rollback` | ESRT reports success but running DTB content belongs to another installed kernel | **Yes**, if that kernel's package is still installed | +| `ok` | `apply_confirmed` | ✅ DTB content matches the running kernel's own package | No action needed | +| `ok` | `content_mismatch_localized` | ESRT success but DTB content matches neither the running kernel nor any other installed kernel | No — needs manual investigation | +| `ok` | `unknown` | Can't confirm capsule result (missing provenance node/reference) | No | + +## 6. Provenance / traceability mechanism (cross-cutting) + +A single `dtb_provenance_sha256` value — computed once at build time from +the sorted sha256 list of every `.dtb`/`.dtbo` actually shipped — is the +backbone that ties build, install, and runtime together: + +- Embedded in the DTB itself (`qcom-dtb-capsule-provenance` FDT node) → + readable by a running kernel regardless of which package installed it. +- Embedded in `linux-modules-` → the "expected" value for that + specific kernel version, readable without needing that DTB to be + currently active — this is what lets the runtime verifier and the + recovery tool scan *every installed kernel* (not just the running one) + for a content match. +- A per-file manifest (`dtb-provenance-content-sha256sums.txt`) is also + shipped, so a mismatch in the aggregate hash can be localized to the + specific `.dtb`/`.dtbo` that differs, rather than only reporting + "something changed." + +This is what lets `verify-capsule-result.sh` distinguish, after any reboot: +same kernel with matching DTB, same kernel with a stale/rolled-back DTB +(belonging to some other installed kernel), or a kernel/DTB pairing that +matches nothing installed at all — purely from sysfs + package-installed +files, with no dependency on network or a build-time database, and (for the +first two cases) hand off to `dtb-capsule-recovery` to actually fix it. + +### 6.1 Why a content hash, not just the kernel version string + +The kernel version string (`abi_release`, e.g. `6.8.0-1013-qcom`) only +changes when a developer deliberately bumps the ABI number in the +changelog. DTB/`.dts` content changes far more often than that during +development: rebuilding the same PR/branch, cherry-picking the same kver +onto different branches, or CI re-running the same source at a different +commit can all produce a different DTB while the version **string** stays +byte-for-byte identical. A comparison based purely on the version string +cannot see any of this — it would report "match" even when the DTB actually +installed does not correspond to what was actually built for that string. + +This is exactly the failure mode Gate 2 in `dtb-capsule.postinst.in` (§4, +step 5) is closing: even when `linux-modules-` is installed and the +version string matches, the postinst still refuses to stage the capsule +unless this package's `expected-dtb-sha256` equals +`linux-modules-/dtb-provenance-sha256` — i.e. unless the dtb-capsule +package and the kernel package actually came from the *same build event*, +not just a build that happens to share the same version string. Dropping +the content hash in favor of a bare version-string comparison would remove +this check's ability to catch that case; the version string alone cannot +distinguish "same string, same content" from "same string, different +content." + +### 6.2 Alternative: version string + build commit hash + +A candidate approach to reduce the overhead of computing and embedding full +content hashes would be to combine the kernel version string (`abi_release`) +with a short build commit hash (e.g. the first 12 hex digits of the kernel +source tree's HEAD commit at build time). This hybrid identifier would be: + +- **Stable across rebuilds of the same source**: the version string + commit + hash pair uniquely identifies a specific kernel source snapshot, so + rebuilding the same commit produces the same identifier. +- **Sensitive to source changes**: cherry-picking, rebasing, or CI re-running + at a different commit changes the hash, so the identifier differs even if + the version string stays the same. +- **Cheaper to compute**: requires only a git rev-parse at build time, no + need to collect, sort, and hash all DTB files. +- **Smaller to store**: a 12-char hex string is much smaller than a full + sha256 hex digest (64 chars). + +**Trade-offs vs. content hash**: +- **Pro**: simpler, faster, smaller. Catches source-level changes (commits, + cherry-picks, rebases) that the version string alone misses. +- **Con**: does not catch content drift caused by toolchain changes (compiler + version, device-tree-compiler version, build flags) that produce different + DTB output from the same source. If the kernel source is identical but the + build environment differs, the version+commit pair would still match even + though the DTB content differs. + +This approach would be suitable if the build environment is tightly +controlled (e.g. always built in the same CI container with pinned tool +versions) and source-level changes are the primary concern. The current +content-hash approach is more robust for environments where toolchain +versions or build flags may vary. + +## 7. Known temporary workaround + +`debian.qcom/qcom-ptool/platforms/iq-x7181-evk/spinor/partitions.conf`: +renamed the `dtb`/`dtb_BACKUP` partition entries to `dtb_a`/`dtb_b`. This is +a workaround: the real on-device flash meta table already uses `dtb_a`/ +`dtb_b` naming, but the vendored `partitions.conf` still had the older +`dtb`/`dtb_BACKUP` naming, so a capsule built against the vendored file +failed to apply on real hardware. The proper fix — updating the meta's own +partition table to match — is future work; this rename is what makes +partition matching work correctly against real hardware today. + +## 8. Known Limitations & Future Work + +### 8.1 Current scope + +- Only the `dtb` partition (`dtb_a`/`dtb_b`) is updated (not `xbl_config`, + `uefi_dtbs`). +- Only Hamoa (`IQ-X7181`) and Purwa (`IQ-X5121`) platforms supported. +- Capsule signing uses pre-generated certs from CI secrets (no HSM + integration). +- Auto-recovery is content-matching only, with the crash-loop gap described + in §5.7 and §8.2. + +### 8.2 Future work + +- Partition table alignment (§7 workaround → proper fix on the device meta + side). +- Support for additional SoCs/platforms. +- **Boot-counter mechanism for crash recovery**: When a newly-applied kernel + fails to boot (crashes, hangs, or panics before `verify-capsule-result.sh` + can run), the device needs a way to automatically fall back to the + previously-known-good kernel without manual intervention. The current + content-matching recovery in `dtb-capsule-recovery` cannot handle this case + because it has no notion of kernel health — it only knows whether DTB + content matches. + + **Proposed design**: A GRUB boot-counter state machine layered on grubenv: + - **Arm phase**: Before switching to a newly-applied kernel, a helper tool + records the trial kernel, the current known-good kernel as fallback, and + a retry budget (e.g. 3 attempts) into grubenv. + - **Trial phase**: A GRUB script fragment reads this state on each boot. If + the trial is not yet confirmed, it decrements the retry counter and boots + the trial kernel. Once the counter reaches zero, GRUB automatically boots + the fallback kernel instead. + - **Confirm phase**: `verify-capsule-result.sh` confirms the trial (clears + the armed state) once it successfully observes `apply_confirmed` for the + new kernel, indicating the kernel booted and DTB content is correct. + + **Open design questions**: + - How to prevent the rejected kernel from being selected again by + `dtb-capsule-recovery`'s content-matching logic after the boot-counter + has already rejected it? (Needs a rejection marker or exclusion list.) + - What to do with the rejected kernel afterward: leave it installed but + excluded from auto-recovery, or uninstall it? + - How to preserve diagnostic data (logs, coredumps) for post-mortem analysis + while managing storage and avoiding repeated collection of the same + failure? + - How to integrate with existing GRUB configuration and ensure the + boot-counter survives across GRUB updates? + +- `dtb-capsule.postinst.in` currently gates staging only on + `linux-modules-` being installed; it does not check that + `linux-image-` is installed or that grub.cfg actually has a + menuentry for it before letting firmware make the DTB flash. + +## 10. Troubleshooting Guide + +### 10.1 Capsule not applied + +- Check: `tail -1 /var/lib/dtb-capsule/last-verify-state` (one-line summary) + or `cat` the whole file for all fields. +- Check: `journalctl -t dtb-capsule-verify` +- Check: ESRT entry exists (`cat /sys/firmware/efi/esrt/entries/entry*/fw_class`) +- Check: `OsIndications` bit set (`efivar -p -n + 8be4df61-93ca-11d2-aa0d-00e098032b8c-OsIndications`) + +### 10.2 Content mismatch (`content_mismatch_localized`) + +- Check the per-file manifest: + `cat /usr/share/dtb-capsule/dtb-provenance-content-sha256sums.txt` +- Compare against the actually-installed files under + `/usr/lib/firmware//device-tree/qcom/` +- The `detail` field in `last-verify-state` names which specific + `.dtb`/`.dtbo` file(s) differ (or says "not localized" if the differing + file isn't a tracked one). + +### 10.3 GUID conflict + +- Check: `cat /var/lib/dtb-capsule/last-guid-conflict` +- Check: device's ESRT entries (`ls /sys/firmware/efi/esrt/entries/`) +- Investigate: whether more than one packaged platform's `FMP_GUID` matches + this device's ESRT — this is treated as ambiguous and staging is skipped + entirely. + +### 10.4 Capsule not staged (dependency issue) + +Under normal `apt install`, this cannot happen — the declared `Depends` +guarantees `linux-modules-` is installed first. If `dtb-capsule` was +installed with `--force-depends` while `linux-modules` was missing, +postinst fails the install outright (`exit 1`) rather than silently +skipping, so the package is left unconfigured (not "installed with no +capsule staged"): + +- Check: `journalctl -t dtb-capsule-verify` for the "linux-modules is not + installed" error, or the postinst's own stderr from the failed + `dpkg --configure`. +- Fix: `apt install linux-modules--qcom` first, then + `dpkg --configure dtb-capsule--qcom`. +- Avoid `--force-depends` to bypass dependency checks — it defeats Gate 1 + in §4. + +### 10.5 Mismatch persists after `dtb-capsule-recovery` ran + +- Run `dtb-capsule-recovery --list` to see every installed kernel's match + status against the running DTB. +- If no kernel shows `match`, the recovery tool has nothing to switch to — + this is the §8.1/§5.7 known limitation (no healthy content-matching + kernel installed), not a bug in the tool. +- Check `grub-set-default`/`update-grub` actually ran without error in + `journalctl -t dtb-capsule-recovery`; a missing `menuentry` for the target + kernel in `/boot/grub/grub.cfg` will make `set_grub_default` fail loudly. diff --git a/dtb-capsule-flowcharts-v3-en.html b/debian.qcom/docs/dtb-capsule-flowcharts-en.html similarity index 78% rename from dtb-capsule-flowcharts-v3-en.html rename to debian.qcom/docs/dtb-capsule-flowcharts-en.html index 7c3fcb16f13b3..4bf857ea175a7 100644 --- a/dtb-capsule-flowcharts-v3-en.html +++ b/debian.qcom/docs/dtb-capsule-flowcharts-en.html @@ -3,7 +3,7 @@ - DTB UEFI Capsule Update - Flowcharts (v3) + DTB UEFI Capsule Update - Flowcharts