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 diff --git a/debian.qcom/control.stub.in b/debian.qcom/control.stub.in index a81cfde6c207b..40e2acf8f354d 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 , @@ -91,3 +92,19 @@ 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 +Recommends: grub2-common +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/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/debian.qcom/docs/dtb-capsule-flowcharts-en.html b/debian.qcom/docs/dtb-capsule-flowcharts-en.html new file mode 100644 index 0000000000000..4bf857ea175a7 --- /dev/null +++ b/debian.qcom/docs/dtb-capsule-flowcharts-en.html @@ -0,0 +1,1186 @@ + + + + + + DTB UEFI Capsule Update - Flowcharts + + + +
+

DTB UEFI Capsule Update - Flowcharts

+

Build time, install time, and the two-phase runtime verification state machine (Phase 1/2), with an 8-case scenario summary
+ Updated: 2026-09-15

+ +
+

+ TL;DR — Every boot, a systemd service checks whether the running kernel and its DTB actually match what was intended (Phase 1: kernel version; Phase 2: DTB content), verified against firmware's own ESRT report plus a fresh sha256 read from devicetree — never trusting either signal alone, since firmware can claim success on a stale DTB and content can drift independently of what the kernel package expects. On four mismatch states (reboot_stalled, kernel_dtb_mismatch, suspected_dtb_rollback, apply_failed_with_rollback_available), it automatically switches the GRUB default to whichever installed kernel's DTB actually matches, converging the device back to a known-good boot without operator intervention. +

+
+ + +
+

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

+

+ Two phases, each producing one state value written to last-verify-state. Phase 1 (kver_match_state) always runs; Phase 2 (dtb_pairing_state) only runs once Phase 1 = ok. Full decision logic for each state is in the flowcharts below (3a/3b/3c) — this table is a quick-reference index, not a restatement of that logic. +

+
+ + + + + + + + + + + + + + + + + + + + + + +
PhaseState ValueMeaningSeverityAuto-Recovery
1okKernel version matches — proceed to Phase 2OK
1reboot_pendingAwaiting reboot into target kernel (first detection)PENDING
1reboot_stalledRebooted but still not in target kernelWARNING✓ if matching kernel found
1no_capsule_for_running_kernelNo capsule targets running kernel, but its DTB is self-consistentINFO
1kernel_dtb_mismatchRunning kernel's DTB doesn't match its own packageERROR✓ if matching kernel found
1package_mismatchTarget kernel's package is in an abnormal dpkg stateERROR
1unknownCannot determine (missing provenance data)UNKNOWN
2apply_confirmedCapsule applied and content-verifiedOK
2pendingCapsule not yet confirmed applied by firmwarePENDING
2suspected_dtb_rollbackESRT reports success, but content belongs to another installed kernelWARNING
2apply_failed_with_rollback_availableESRT reports failure, but a matching kernel existsWARNING
2apply_failedESRT reports failure, no matching kernelERROR
2content_mismatch_localizedESRT reports success, but content matches no installed kernelERROR
2unknownCannot determine (missing provenance data)UNKNOWN
+

Auto-recovery always means the same action: call dtb-capsule-recovery --auto to switch the GRUB default to a matching kernel (see ੵ. Kernel Rollback Recovery Flow). "if matching kernel found" flags states that can occur without a match too (nothing to recover to, so no-op); the other ✓ states are only reached when a match already exists.

+
+ +

State Output File

+

+ Every run writes /var/lib/dtb-capsule/last-verify-state in key=value format. rollback_target_kver/rollback_target_available are always emitted (even empty) so downstream consumers can safely test -n "$rollback_target_kver". +

+
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)? + + + match + + 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? + + + + + + ESRT_CONFIRMED == 1? + (firmware reported apply success) + + + Yes + + + match found among + other installed kernels? + + + Yes + + dtb_pairing_state=suspected_dtb_rollback + (rollback_target_kver=X) + + + + No (confirmed, no target found) + + + No + + + + ROLLBACK_TARGET_KVER + found? + + + No + + dtb_pairing_state=apply_failed + + + Yes + + dtb_pairing_state=apply_failed_with_rollback_available + + + 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 once for a candidate (ROLLBACK_TARGET_KVER), but ESRT_CONFIRMED is checked before that candidate is used, so the same candidate is classified differently depending on what firmware actually reported: ESRT_CONFIRMED=1 (firmware claims success) + a candidate found → suspected_dtb_rollback; ESRT_CONFIRMED=0 (firmware reports failure) + a candidate found → apply_failed_with_rollback_available; ESRT_CONFIRMED=0 + no candidate → apply_failed. Checking the candidate before ESRT would misroute a genuine apply failure as a rollback — this was a real bug in verify-capsule-result.sh, fixed by gating the rollback branch on ESRT_CONFIRMED=1
  • +
  • 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
6aDTB manually reflashed, dtb-capsule never installed — ESRT reports successoksuspected_dtb_rollback✗ Error
6bSame, but ESRT reports failure and a matching kernel existsokapply_failed_with_rollback_available✗ Error
6cSame, but ESRT reports failure and no matching kernel existsokapply_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.

+
+
+ + +
+

5. 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
  • +
+
+
+ + +
+

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 +
+
+
+
+ + 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..bae53eee86829 --- /dev/null +++ b/debian.qcom/dtb-capsule-runtime/dtb-capsule-motd.sh @@ -0,0 +1,50 @@ +#!/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." +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/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..aa4dc68220e02 --- /dev/null +++ b/debian.qcom/dtb-capsule-runtime/verify-capsule-result.sh @@ -0,0 +1,502 @@ +#!/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" + +# 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" +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 "")" + +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" +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: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) + 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 + _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 [ "$_capsule_unconsumed" -eq 1 ]; then + STALL_STATE="$(check_reboot_stall)" + 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 + + # 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" + 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 + ;; + *) + 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_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")")" + 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_ESRT_CACHE_FILE" + cat > "$LAST_ESRT_CACHE_FILE" < | --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 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..91700f2089bc1 --- /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_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 +--partition --name=uefisecapp_BACKUP --size=220KB --type-guid=538CBDBA-D4A4-4438-A466-D7B356FAC165 --filename=uefi_sec.mbn 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/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() 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..a48a7eea22a06 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,99 @@ 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. + # 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 $(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 ; \ + 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 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 \ + --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 ; \ + 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-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 \ + $(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 +706,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 #