diff --git a/classes-recipe/image_types_qcom.bbclass b/classes-recipe/image_types_qcom.bbclass index e81e3874a..f2087256f 100644 --- a/classes-recipe/image_types_qcom.bbclass +++ b/classes-recipe/image_types_qcom.bbclass @@ -171,7 +171,15 @@ create_qcomflash_pkg() { fi # uefi dtb - if [ -n "${QCOM_UEFI_DTB}" ] && \ + # Prefer the OEM-cert-injected uefi_dtbs deployed by + # firmware-qcom-oem-cert when available. Mirrors the xbl_config + # substitution above, for SPI-NOR-boot targets (e.g. hamoa) that + # carry QcCapsuleRootCert in uefi_dtbs.elf rather than + # xbl_config.elf. + if [ -n "${QCOM_CAPSULE_FIRMWARE}" ] && \ + [ -f "${DEPLOY_DIR_IMAGE}/uefi_dtbs-with-oem-cert.xz" ]; then + install -m 0644 "${DEPLOY_DIR_IMAGE}/uefi_dtbs-with-oem-cert.xz" spinor/uefi_dtbs.xz + elif [ -n "${QCOM_UEFI_DTB}" ] && \ [ -f "${DEPLOY_DIR_IMAGE}/${QCOM_BOOT_FILES_SUBDIR}/spinor/${QCOM_UEFI_DTB}" ]; then install -m 0644 "${DEPLOY_DIR_IMAGE}/${QCOM_BOOT_FILES_SUBDIR}/spinor/${QCOM_UEFI_DTB}" spinor/uefi_dtbs.xz fi diff --git a/classes-recipe/qcom-capsule.bbclass b/classes-recipe/qcom-capsule.bbclass index 9219760a3..56b6fd1c2 100644 --- a/classes-recipe/qcom-capsule.bbclass +++ b/classes-recipe/qcom-capsule.bbclass @@ -34,21 +34,15 @@ CAPSULE_ROOT_PUB ?= "" CAPSULE_SUB_PUB ?= "" # --------------------------------------------------------------------------- -# XBLConfig DTB certificate injection +# OEM root certificate injection # --------------------------------------------------------------------------- -# The class automatically detects the post-DDR DTB by parsing the output of -# xblconfig_parser.py dump (looks for the first entry matching post-ddr*.dtb). -# Both the filename and the section index are extracted from the dump output. -# -# XBLCONFIG_DTB overrides auto-detection when set to an explicit filename. -# XBLCONFIG_DTB_SECTION overrides the auto-detected section index. -# -# When a post-DDR DTB is found (auto or explicit), the class will: -# 1. dump XBLConfig sections -# 2. patch QcCapsuleRootCert in the DTB with the converted root cert -# 3. re-pack the updated DTB back into xbl_config.elf -XBLCONFIG_DTB ?= "" -XBLCONFIG_DTB_SECTION ?= "" +# QcCapsuleRootCert is injected into the boot config ELFs by +# firmware-qcom-oem-cert (classes-recipe/qcom-oem-cert.bbclass), not here. +# It has to happen there because the certificate must be in place before +# the config ELFs are signed, and this recipe runs after the boot firmware +# has already been deployed. This class consumes the result: the +# cert-bearing copies are staged over the pristine boot binaries so the +# capsule firmware volume is built from the same images the device runs. # --------------------------------------------------------------------------- # Boot binaries location @@ -89,7 +83,7 @@ inherit python3native deploy CAPSULE_DIR = "${WORKDIR}/capsule_gen" do_compile[depends] += "cbsp-boot-utilities-native:do_populate_sysroot \ - edk2-basetools-native:do_populate_sysroot" + firmware-qcom-oem-cert:do_deploy" do_compile[dirs] = "${CAPSULE_DIR}" do_compile[cleandirs] = "${CAPSULE_DIR}" @@ -204,68 +198,8 @@ python generate_fvupdate() { do_compile[prefuncs] += "generate_fvupdate" -# Inject the OEM root certificate into xbl_config.elf. -# Dumps the config sections, auto-detects the post-DDR DTB (or uses -# XBLCONFIG_DTB / XBLCONFIG_DTB_SECTION overrides), patches QcCapsuleRootCert -# in that DTB, and repacks the updated DTB back into xbl_config.elf in place. -# $1 - path to xbl_config.elf (modified in place on success) -patch_xblconfig_cert() { - local xbl_config="$1" - local staged_dir - staged_dir=$(dirname "${xbl_config}") - - XBL_DUMP_LOG="${CAPSULE_DIR}/xbl_dump.log" - qcom-capsule-tool parse-config \ - "${xbl_config}" dump \ - --out-dir "${staged_dir}" | tee "${XBL_DUMP_LOG}" - - DTB_PATCH="${XBLCONFIG_DTB}" - DTB_SECTION="${XBLCONFIG_DTB_SECTION}" - if [ -z "${DTB_PATCH}" ]; then - # Parse a line like: - # [+] config_item[6] -> PH# 8 -> './post-ddr-kodiak-1.0.dtb' (90280 bytes) - POST_DDR_LINE=$(grep -m1 "post-ddr.*\.dtb" "${XBL_DUMP_LOG}" || true) - if [ -n "${POST_DDR_LINE}" ]; then - DTB_PATCH=$(echo "${POST_DDR_LINE}" | sed "s|.* -> '||;s|'.*||" | xargs basename) - DTB_SECTION=$(echo "${POST_DDR_LINE}" | sed "s/.*PH# \([0-9]*\).*/\1/") - fi - fi - - if [ -n "${DTB_PATCH}" ]; then - ORIG_DTB="${staged_dir}/${DTB_PATCH}" - UPDATED_DTB="${staged_dir}/${DTB_PATCH%.dtb}-updated.dtb" - - qcom-capsule-tool set-dtb-property \ - "${ORIG_DTB}" \ - /sw/uefi/uefiplat \ - QcCapsuleRootCert \ - "@list:${ROOT_INC}" \ - "${UPDATED_DTB}" - - qcom-capsule-tool parse-config \ - "${xbl_config}" replace \ - "${DTB_SECTION}" \ - "${UPDATED_DTB}" \ - "${staged_dir}/xbl_config_patched.elf" - - mv "${staged_dir}/xbl_config_patched.elf" \ - "${xbl_config}" - - touch "${CAPSULE_DIR}/.xbl_with_oem_cert" - fi -} - do_compile() { CBSP_DATA="${STAGING_DATADIR_NATIVE}/cbsp-boot-utilities" - EDK2_BASETOOLS="${STAGING_DATADIR_NATIVE}/edk2-basetools" - - # GenFfs/GenFv are staged to ${STAGING_BINDIR_NATIVE} (in PATH) by - # upstream meta-arm's edk2-basetools-native and resolved by - # qcom-capsule-tool via shutil.which. GenerateCapsule.py and its - # Common/ Python package live under ${EDK2_BASETOOLS}; add that to - # PYTHONPATH so `import Common` works when we invoke the script - # directly below. - export PYTHONPATH="${EDK2_BASETOOLS}${PYTHONPATH:+:$PYTHONPATH}" # Use a board-specific FvUpdate.xml if provided via SRC_URI:append or # generated from CAPSULE_ENTRIES, otherwise fall back to the default @@ -280,9 +214,6 @@ do_compile() { cd "${CAPSULE_DIR}" - ROOT_INC="${CAPSULE_DIR}/QcFMPRoot.inc" - qcom-capsule-tool bin-to-hex "${CAPSULE_ROOT_CER}" "${ROOT_INC}" - # Stage boot binaries so they are writable (XBLConfig patching modifies # xbl_config.elf in place) BOOTBINS_STAGED="${CAPSULE_DIR}/bootbins" @@ -299,10 +230,12 @@ do_compile() { "${BOOTBINS_STAGED}/dtb.bin" fi - # Inject OEM root cert into xbl_config.elf when present. Platforms - # without xbl_config.elf (e.g. hamoa) skip this step. - if [ -f "${BOOTBINS_STAGED}/xbl_config.elf" ]; then - patch_xblconfig_cert "${BOOTBINS_STAGED}/xbl_config.elf" + # Overlay the cert-bearing config ELFs deployed by + # firmware-qcom-oem-cert, so the capsule firmware volume carries the + # same images the device boots. Optional: hamoa has no xbl_config.elf. + if [ -f "${DEPLOY_DIR_IMAGE}/xbl_config-with-oem-cert.elf" ]; then + install -m 0644 "${DEPLOY_DIR_IMAGE}/xbl_config-with-oem-cert.elf" \ + "${BOOTBINS_STAGED}/${QCOM_XBL_CONFIG}" fi qcom-capsule-tool sysfw-version-create \ @@ -327,7 +260,7 @@ do_compile() { -oc "${CAPSULE_SUB_PUB}" \ -g "${CAPSULE_GUID}" - python3 "${EDK2_BASETOOLS}/GenerateCapsule.py" \ + qcom-capsule-tool generate-capsule \ -e \ -j config.json \ -o "${PN}.cap" \ @@ -346,14 +279,6 @@ FILES:${PN} = "${nonarch_base_libdir}/firmware/efi/${PN}.cap" do_deploy() { install -d "${DEPLOYDIR}" install -m 0644 "${CAPSULE_DIR}/${PN}.cap" "${DEPLOYDIR}/" - - # When XBLConfig was injected with the OEM root cert, deploy the updated - # binary under a distinct name to avoid a deploy-manifest conflict with - # firmware-qcom-bootbins (which already owns xbl_config.elf). - if [ -f "${CAPSULE_DIR}/.xbl_with_oem_cert" ]; then - install -m 0644 "${CAPSULE_DIR}/bootbins/xbl_config.elf" \ - "${DEPLOYDIR}/xbl_config-with-oem-cert.elf" - fi } addtask deploy before do_build after do_compile diff --git a/classes-recipe/qcom-oem-cert.bbclass b/classes-recipe/qcom-oem-cert.bbclass new file mode 100644 index 000000000..dc890bfb1 --- /dev/null +++ b/classes-recipe/qcom-oem-cert.bbclass @@ -0,0 +1,138 @@ +# +# Copyright (c) 2026 Qualcomm Innovation Center, Inc. All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause-Clear +# +# Inject the OEM capsule root certificate into the boot config ELFs. +# +# A recipe of its own, not part of qcom-capsule.bbclass, because the order +# is forced: the certificate must be in the config ELF before that ELF is +# signed (editing a DTB inside it invalidates any signature it carried), +# and the capsule built afterwards is verified against that same +# certificate. By the time the capsule recipe runs the boot firmware is +# already deployed and signed, so the injection cannot live there. Staging +# into ${OEM_CERT_STAGE} between do_compile and do_deploy leaves the seam a +# signing step needs. + +# Shared with qcom-capsule.bbclass; ci/capsule-test-keys.yml sets it for CI. +CAPSULE_ROOT_CER ?= "" + +BOOTBINS_DIR ?= "${DEPLOY_DIR_IMAGE}/${QCOM_BOOT_FILES_SUBDIR}" + +# Named for the signing step that belongs in this seam. +OEM_CERT_STAGE = "${B}/firmware-to-sign" + +inherit python3native deploy + +do_configure[noexec] = "1" +do_install[noexec] = "1" + +do_compile[depends] += "qdte-lite-native:do_populate_sysroot \ + cbsp-boot-utilities-native:do_populate_sysroot" +do_compile[depends] += "${@'${QCOM_BOOT_FIRMWARE}:do_deploy' if d.getVar('QCOM_BOOT_FIRMWARE') else ''}" +do_compile[cleandirs] = "${OEM_CERT_STAGE}" + +python () { + if not d.getVar('CAPSULE_ROOT_CER'): + raise bb.parse.SkipRecipe( + '%s: CAPSULE_ROOT_CER is not set. Point it at the DER-encoded OEM ' + 'root certificate (see ci/capsule-test-keys.yml for a ' + 'CI/development overlay).' % d.getVar('PN')) +} + +# Inject the certificate into one boot config ELF. +# +# The DTB names are asked for rather than configured per machine: they are +# assigned during disassembly, from container metadata in one container and +# from each DTB's /compatible in another, so hardcoding them goes stale. +# More than one line is normal -- on hamoa the property appears in a base +# DTB and in a .dtbo overlay, at different node paths -- and the ops are +# joined with '&' so a single pass applies them all. +# +# bin-to-hex owns the DER-to-cells conversion so the padding of a trailing +# partial cell lives in one place rather than being reimplemented here. +# +# $1 - path to the config ELF or its .xz (rewritten in place) +patch_config_elf_cert() { + local config_elf="$1" + + local stem + stem=$(basename "${config_elf}") + stem="${stem%.xz}" + stem="${stem%.elf}" + + local targets + targets=$(qdte-lite --nogui --input_file "${config_elf}" \ + --find_property QcCapsuleRootCert) || { + bbwarn "No DTB in ${stem} defines QcCapsuleRootCert; skipping OEM cert injection." + return + } + + local root_inc="${B}/QcFMPRoot.inc" + qcom-capsule-tool bin-to-hex "${CAPSULE_ROOT_CER}" "${root_inc}" + + local modify_arg="" target + for target in ${targets}; do + if [ -z "${modify_arg}" ]; then + modify_arg="${target}=@list:${root_inc}" + else + modify_arg="${modify_arg}&${target}=@list:${root_inc}" + fi + done + + local outdir="${B}/qdte_out/${stem}" + rm -rf "${outdir}" + mkdir -p "${outdir}" + + qdte-lite --nogui \ + --input_file "${config_elf}" \ + --output_path "${outdir}" \ + --output_file "${stem}.elf" \ + --modify "${modify_arg}" + + # qdte-lite always writes a plain ELF; restore the input's compression. + case "${config_elf}" in + *.xz) xz -c "${outdir}/${stem}.elf" > "${config_elf}" ;; + *) install -m 0644 "${outdir}/${stem}.elf" "${config_elf}" ;; + esac +} + +do_compile() { + install -d "${OEM_CERT_STAGE}" + + # QCOM_XBL_CONFIG is xbl_config_kvm.elf on kvm machines. Both ELFs are + # optional: hamoa has no XBLConfig, UFS-boot platforms no uefi_dtbs.xz. + if [ -f "${BOOTBINS_DIR}/${QCOM_XBL_CONFIG}" ]; then + install -m 0644 "${BOOTBINS_DIR}/${QCOM_XBL_CONFIG}" "${OEM_CERT_STAGE}/" + patch_config_elf_cert "${OEM_CERT_STAGE}/${QCOM_XBL_CONFIG}" + fi + + # uefi_dtbs.xz can sit in a SPI-NOR subdirectory of the boot bins. + UEFI_DTBS_XZ=$(find "${BOOTBINS_DIR}" -name "uefi_dtbs.xz" -print -quit) + if [ -n "${UEFI_DTBS_XZ}" ]; then + install -m 0644 "${UEFI_DTBS_XZ}" "${OEM_CERT_STAGE}/" + patch_config_elf_cert "${OEM_CERT_STAGE}/uefi_dtbs.xz" + fi + + if [ -z "$(ls -A ${OEM_CERT_STAGE} 2>/dev/null)" ]; then + bbfatal "No boot config ELF carrying QcCapsuleRootCert was found under ${BOOTBINS_DIR}." + fi +} + +do_deploy() { + install -d "${DEPLOYDIR}" + + # Fixed deploy name whatever the machine calls its XBLConfig, and + # distinct from it so this does not collide in the deploy manifest with + # the boot firmware recipe that owns the unmodified copy. + if [ -f "${OEM_CERT_STAGE}/${QCOM_XBL_CONFIG}" ]; then + install -m 0644 "${OEM_CERT_STAGE}/${QCOM_XBL_CONFIG}" \ + "${DEPLOYDIR}/xbl_config-with-oem-cert.elf" + fi + + if [ -f "${OEM_CERT_STAGE}/uefi_dtbs.xz" ]; then + install -m 0644 "${OEM_CERT_STAGE}/uefi_dtbs.xz" \ + "${DEPLOYDIR}/uefi_dtbs-with-oem-cert.xz" + fi +} +addtask deploy before do_build after do_compile diff --git a/dynamic-layers/meta-arm/recipes-firmware/firmware/firmware-qcom-capsule_%.bbappend b/dynamic-layers/meta-arm/recipes-firmware/firmware/firmware-qcom-capsule_%.bbappend deleted file mode 100644 index 373a63afe..000000000 --- a/dynamic-layers/meta-arm/recipes-firmware/firmware/firmware-qcom-capsule_%.bbappend +++ /dev/null @@ -1,13 +0,0 @@ -CAPSULE_FLASH_TYPE:iq-x7181-evk = "NORUFS" -CAPSULE_ENTRIES:iq-x7181-evk = "dtb" - -# Hamoa stores the Linux DTB FIT image in SPINOR. The firmware uses a -# main/backup model: dtb is always the active partition; dtb_BACKUP holds a -# rollback copy that is overwritten by before dtb is updated. -CAPSULE_ENTRY_dtb[binary] = "dtb.bin" -CAPSULE_ENTRY_dtb[dest_disk] = "SPINOR" -CAPSULE_ENTRY_dtb[dest_partition] = "dtb" -CAPSULE_ENTRY_dtb[dest_guid] = "{2A1A52FC-AA0B-401C-A808-5EA0F91068F8}" -CAPSULE_ENTRY_dtb[backup_disk] = "SPINOR" -CAPSULE_ENTRY_dtb[backup_partition] = "dtb_BACKUP" -CAPSULE_ENTRY_dtb[backup_guid] = "{A166F11A-2B39-4FAA-B7E7-F8AA080D0587}" diff --git a/recipes-bsp/firmware-boot/firmware-qcom-oem-cert_1.0.bb b/recipes-bsp/firmware-boot/firmware-qcom-oem-cert_1.0.bb new file mode 100644 index 000000000..7e420c4b7 --- /dev/null +++ b/recipes-bsp/firmware-boot/firmware-qcom-oem-cert_1.0.bb @@ -0,0 +1,14 @@ +DESCRIPTION = "Boot config ELFs carrying the OEM capsule root certificate" +LICENSE = "MIT" +LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" + +COMPATIBLE_MACHINE = "hamoa|qcm6490|qcs615|qcs8300|qcs9100" + +# Deploy-only. PACKAGES = "" would leave do_package running with nothing to +# split, and buildhistory then fails listing a packages-split that was +# never created. +inherit nopackages + +inherit qcom-oem-cert + +PACKAGE_ARCH = "${MACHINE_ARCH}" diff --git a/recipes-bsp/firmware/firmware-qcom-capsule_%.bbappend b/recipes-bsp/firmware/firmware-qcom-capsule_%.bbappend new file mode 100644 index 000000000..06ead6f19 --- /dev/null +++ b/recipes-bsp/firmware/firmware-qcom-capsule_%.bbappend @@ -0,0 +1,29 @@ +CAPSULE_FLASH_TYPE:iq-x7181-evk = "NORUFS" +CAPSULE_ENTRIES:iq-x7181-evk = "dtb" + +# Hamoa keeps the Linux DTB FIT image in SPINOR, with dtb as the active +# partition and dtb_BACKUP a rollback copy written before it. +# +# These are varflags, and varflags take no part in override resolution -- +# CAPSULE_ENTRY_dtb[dest_disk]:iq-x7181-evk does not exist. Assigned +# plainly they would apply everywhere, so any other board declaring a "dtb" +# entry would silently inherit these SPINOR destinations. Guard on +# MACHINEOVERRIDES to get the scope the override was meant to give. +QCOM_CAPSULE_DTB_ENTRY_MACHINE ?= "iq-x7181-evk" + +python () { + machine = d.getVar('QCOM_CAPSULE_DTB_ENTRY_MACHINE') + if machine not in (d.getVar('MACHINEOVERRIDES') or '').split(':'): + return + + for flag, value in ( + ('binary', 'dtb.bin'), + ('dest_disk', 'SPINOR'), + ('dest_partition', 'dtb'), + ('dest_guid', '{2A1A52FC-AA0B-401C-A808-5EA0F91068F8}'), + ('backup_disk', 'SPINOR'), + ('backup_partition', 'dtb_BACKUP'), + ('backup_guid', '{A166F11A-2B39-4FAA-B7E7-F8AA080D0587}'), + ): + d.setVarFlag('CAPSULE_ENTRY_dtb', flag, value) +} diff --git a/dynamic-layers/meta-arm/recipes-firmware/firmware/firmware-qcom-capsule_1.0.bb b/recipes-bsp/firmware/firmware-qcom-capsule_1.0.bb similarity index 100% rename from dynamic-layers/meta-arm/recipes-firmware/firmware/firmware-qcom-capsule_1.0.bb rename to recipes-bsp/firmware/firmware-qcom-capsule_1.0.bb diff --git a/dynamic-layers/meta-arm/recipes-devtools/cbsp-boot-utilities/cbsp-boot-utilities-native_1.0.bb b/recipes-devtools/cbsp-boot-utilities/cbsp-boot-utilities-native_1.0.bb similarity index 76% rename from dynamic-layers/meta-arm/recipes-devtools/cbsp-boot-utilities/cbsp-boot-utilities-native_1.0.bb rename to recipes-devtools/cbsp-boot-utilities/cbsp-boot-utilities-native_1.0.bb index d33aead02..0906f908a 100644 --- a/dynamic-layers/meta-arm/recipes-devtools/cbsp-boot-utilities/cbsp-boot-utilities-native_1.0.bb +++ b/recipes-devtools/cbsp-boot-utilities/cbsp-boot-utilities-native_1.0.bb @@ -6,18 +6,20 @@ LICENSE = "BSD-3-Clause-Clear" LIC_FILES_CHKSUM = "file://LICENSE;md5=8e1eb38e3de3966193d29f31f5d7e684" SRC_URI = "git://github.com/quic/cbsp-boot-utilities.git;protocol=https;branch=main" -SRCREV = "a19e5b6f75cd4aa08aa5ced82f9767f1858d766d" +SRCREV = "8a0f1deef97beae600910506bfba488976465828" S = "${UNPACKDIR}/${BPN}-${PV}/uefi_capsule_generation" inherit python_poetry_core native +# edk2-basetools is no longer needed: the tool now carries its own +# GenFfs/GenFv (fv_builder.py) and a generate-capsule that is a drop-in for +# edk2 BaseTools GenerateCapsule.py, producing identical bytes. requests +# went with the setup step that used to fetch them. DEPENDS += " \ dtc-native \ - edk2-basetools-native \ python3-dtc-native \ python3-pyelftools-native \ - python3-requests-native \ " # FvUpdate.xml ships alongside pyproject.toml (not inside the Python diff --git a/recipes-devtools/qdte-lite/files/0001-fix-emit-trees-whose-size-lands-on-an-FdtSw-growth-bo.patch b/recipes-devtools/qdte-lite/files/0001-fix-emit-trees-whose-size-lands-on-an-FdtSw-growth-bo.patch new file mode 100644 index 000000000..928145bcf --- /dev/null +++ b/recipes-devtools/qdte-lite/files/0001-fix-emit-trees-whose-size-lands-on-an-FdtSw-growth-bo.patch @@ -0,0 +1,155 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Igor Opaniuk +Date: Wed, 2 Sep 2026 11:48:44 +0200 +Subject: [PATCH] fix: emit trees whose size lands on an FdtSw growth boundary + +Serializing a device tree fails with FDT_ERR_NOSPACE for particular +trees, and succeeds for almost identical ones. Two builds of the same +hamoa boot container differ here: the DTB is 153568 bytes in one and +153600 in the next, and only the second one fails. + +153600 is exactly 150 x 1024. FdtSw grows its buffer in INC_SIZE (1024) +byte steps, and every write method retries through check_space() when it +runs out, so writing the tree always succeeds. as_fdt() does not retry -- +it calls fdt_finish() once, and fdt_finish() still has to place the +strings block and fix up the header. A tree that fills the buffer to an +exact multiple of INC_SIZE leaves nothing for it, so emission fails on +size alone. Roughly one tree in every 256 lands there. + +That makes it a lottery rather than a limit, and an unusually confusing +one: the same code, tool and certificate work on one firmware drop and +fail on the next, and the exception surfaces from a load rather than +from the write that caused it, because the load path serializes the tree +to hash it. + +Seed the buffer past that point and retry with more room if the finish +still does not fit. + +The selftest gains --emit-sizes, which sweeps a padding property in +4-byte steps across three growth increments so several emissions land +exactly on a boundary, and asserts that at least one did -- a sweep that +missed every boundary would pass without testing anything. It fails on +the unfixed backend at precisely the three boundary sizes. + +Upstream-Status: Submitted [https://github.com/qualcomm/qdte-lite/pull/4] + +Signed-off-by: Igor Opaniuk +--- + qdte_lite/core/_fdt_selftest.py | 50 +++++++++++++++++++++++++++++++-- + qdte_lite/core/fdt_backend.py | 35 +++++++++++++++++------ + 2 files changed, 75 insertions(+), 10 deletions(-) + +diff --git a/qdte_lite/core/_fdt_selftest.py b/qdte_lite/core/_fdt_selftest.py +index f6c927a..21ed0da 100644 +--- a/qdte_lite/core/_fdt_selftest.py ++++ b/qdte_lite/core/_fdt_selftest.py +@@ -139,15 +139,61 @@ def check_roundtrip(paths): + print("ROUNDTRIP OK: %s (%d bytes)" % (path, len(blob_a))) + + ++def check_emit_sizes(): ++ """Emit trees whose serialized size lands on every offset across an ++ FdtSw growth boundary. ++ ++ FdtSw grows its buffer in INC_SIZE steps and every write method retries ++ through check_space(), but as_fdt() calls fdt_finish() once with no ++ retry. A tree that serializes to an exact multiple of INC_SIZE leaves no ++ slack for the strings block, so emission used to fail with ++ FDT_ERR_NOSPACE -- for that size only, which made it a lottery rather ++ than a limit. Sweep a padding property in 4-byte steps so several ++ emissions land exactly on a boundary. ++ """ ++ ++ import libfdt ++ ++ inc = libfdt.FdtSw.INC_SIZE ++ sizes, failures = set(), [] ++ for pad_cells in range(1, (inc * 3) // 4): ++ root = fdt_backend.FdtNode("/") ++ root.append(fdt_backend.FdtPropertyWords("pad", [0xDEADBEEF] * pad_cells)) ++ try: ++ blob = fdt_backend.Fdt(root).to_dtb() ++ except Exception as ex: ++ failures.append((pad_cells, ex)) ++ continue ++ sizes.add(len(blob)) ++ ++ on_boundary = sorted(n for n in sizes if n % inc == 0) ++ assert on_boundary, ( ++ "sweep never produced a blob on an INC_SIZE (%d) boundary, so this " ++ "would not have caught the bug it exists for" % inc ++ ) ++ assert not failures, "emission failed at %d size(s), first: pad=%d %s" % ( ++ len(failures), ++ failures[0][0], ++ failures[0][1], ++ ) ++ print( ++ "emit sizes: %d blobs, %d exactly on a %d-byte boundary, no failures" ++ % (len(sizes), len(on_boundary), inc) ++ ) ++ ++ + def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--parity", action="store_true") + mode.add_argument("--roundtrip", action="store_true") +- parser.add_argument("dtbs", nargs="+", help="DTB files to test") ++ mode.add_argument("--emit-sizes", action="store_true") ++ parser.add_argument("dtbs", nargs="*", help="DTB files to test") + opts = parser.parse_args(argv) + +- if opts.parity: ++ if opts.emit_sizes: ++ check_emit_sizes() ++ elif opts.parity: + check_parity(opts.dtbs) + else: + check_roundtrip(opts.dtbs) +diff --git a/qdte_lite/core/fdt_backend.py b/qdte_lite/core/fdt_backend.py +index 90fe5c8..1a8b869 100644 +--- a/qdte_lite/core/fdt_backend.py ++++ b/qdte_lite/core/fdt_backend.py +@@ -246,14 +246,33 @@ class Fdt: + """Serialize deterministically via libfdt's sequential writer. + When ``mappings`` is a dict, fill it with {path: (start, end)} + byte ranges of each node header / property record in the blob.""" +- sw = libfdt.FdtSw() +- for addr, size in self.memrsv: +- sw.add_reservemap_entry(addr, size) +- sw.finish_reservemap() +- sw.begin_node("") +- self._emit_node(sw, self.root) +- sw.end_node() +- fdt = sw.as_fdt() ++ # FdtSw grows its buffer in INC_SIZE steps, and every write method ++ # retries through check_space() when it runs out. as_fdt() does not: ++ # it calls fdt_finish() once and raises FDT_ERR_NOSPACE if the strings ++ # block and header fixups do not fit in what is left. A tree whose ++ # serialized size lands on a multiple of INC_SIZE leaves zero slack ++ # and fails, which makes this a size lottery rather than a real limit. ++ # ++ # Seed the buffer past the point where that can happen and retry with ++ # more room if it still does. ++ size_hint = libfdt.FdtSw.INC_SIZE ++ while True: ++ sw = libfdt.FdtSw(size_hint) ++ for addr, size in self.memrsv: ++ sw.add_reservemap_entry(addr, size) ++ sw.finish_reservemap() ++ sw.begin_node("") ++ self._emit_node(sw, self.root) ++ sw.end_node() ++ try: ++ fdt = sw.as_fdt() ++ break ++ except libfdt.FdtException as ex: ++ if ex.err != -libfdt.NOSPACE: ++ raise ++ # len(sw) is the grown buffer; ask for meaningfully more than ++ # the increment so this terminates in one further attempt. ++ size_hint = len(sw.as_bytearray()) + libfdt.FdtSw.INC_SIZE * 16 + fdt.pack() + buf = bytearray(fdt.as_bytearray()) + if self.boot_cpuid: diff --git a/recipes-devtools/qdte-lite/qdte-lite-native_2.0.0.bb b/recipes-devtools/qdte-lite/qdte-lite-native_2.0.0.bb new file mode 100644 index 000000000..10aa68548 --- /dev/null +++ b/recipes-devtools/qdte-lite/qdte-lite-native_2.0.0.bb @@ -0,0 +1,14 @@ +SUMMARY = "Device Tree Editor Lite (qdte-lite)" +DESCRIPTION = "Lightweight fork of the Qualcomm Device Tree Editor" +HOMEPAGE = "https://github.com/qualcomm/qdte-lite" +LICENSE = "BSD-3-Clause-Clear" +LIC_FILES_CHKSUM = "file://LICENSE.txt;md5=57272fa9cc740c745feb331231cca6f2" + +SRC_URI = "git://github.com/qualcomm/qdte-lite.git;protocol=https;branch=main;tag=v${PV} \ + file://0001-fix-emit-trees-whose-size-lands-on-an-FdtSw-growth-bo.patch \ + " +SRCREV = "21db12fa954d009b6e501d8bd12d22c8e6e7aaa1" + +inherit python_setuptools_build_meta native + +DEPENDS += "python3-dtc-native"