fwupdate - Trixie & Forky
The following post relates to the fwupdate function on Debian
In accordance with the following, there have been numerous updates.
What's changed in fwupd
The project is running two parallel branches, which is the key thing to hold in your head:
- 2.0.x — the maintenance branch. This is what Debian stable tracks. Latest points are 2.0.18 → 2.0.20.
- 2.1.x — the new feature branch (2.1.1 through 2.1.3+), currently in Debian testing/unstable. This is where the structural changes live.
- 1.9.x — effectively EOL; the 1.9.34 notes explicitly tell distributors to move to 2.0.x as soon as possible. github
The changes that actually matter operationally:
Trust / signing (2.1.x). 2.1.1 drops support for GPG signing of metadata and firmware, moves JCat parsing into libfwupd, and removes json-glib, libarchive and protobuf-c as dependencies. Earlier, 2.0.17 added post-quantum signature support. Direction of travel is JCat/PKCS7 + PQ, GPG gone, smaller dependency surface. githubgithub
Secure Boot certificate migration. This is the one with a deadline. 2.0.17 added the Framework-specific KEK and db hashes, offline hashes for the Microsoft 20250902 dbx, and a guard preventing PK/KEK updates when a test key is installed. 2.0.20 added support for changing the AMD UMA carveout size and warns if you're still using the blocked-firmware functionality. The reason this matters: the 2013 UEFI Secure Boot certificate has expired, and fwupd was bumped to 2.0.20 specifically to handle the migration — letting you update the CA, KEK and revocation databases on the fly, alongside updated shim packages. Without applying these, future bootloader patches could leave Secure Boot systems unable to boot. github + 3
Parser hardening. A large share of recent fixes are attacker-supplied-input hardening on the CAB/metadata/PE parsing path — buffer overreads in malicious PE files, a decompression ratio limit to stop emulation ZIP bombs, a cap on files parsed from ZIP archives, overflow-checked arithmetic for offset calculations, and a 4GiB sanity check on UEFI capsule payloads. Relevant because fwupd parses untrusted cabinets and metadata as root. github
Behavioural removals / server awareness. 2.1.1 removes the concept of blocked firmware entirely and allows systems to use the udev event source without systemd. 2.1.2 adds server platform detection to suspend HSI checks. 2.0.18 creates a reboot-required file when an update needs a reboot, adds a MOTD message for staged updates, and refactored hypervisor/container detection. github + 2
Ramifications for Debian Trixie
The concrete facts:
- Trixie stable ships fwupd 2.0.8-3+deb13u1; trixie-backports has 2.0.20-1~bpo13+1; testing is on the 2.1.x line.
- So none of the 2.1.x structural changes touch you yet — GPG-drop, blocked-firmware removal, PQ signatures, the dependency purge are all downstream of what you're running. No action needed there, just be aware it's coming when you eventually move to forky.
- The Secure Boot cert rollover does affect you now. If any of these boxes run Secure Boot (your Dell Latitude, potentially the Proxmox host), the KEK/db/dbx migration is delivered through exactly this fwupd path. That elevates this script section from "nice housekeeping" to "genuinely load-bearing." Worth pulling 2.0.20 from backports on Secure Boot machines so you get the newer dbx/KEK handling.
- Version-gap gotcha for your code: the reboot-required file landed in 2.0.18. Stock Trixie is 2.0.8, so that marker file does not exist on a non-backports box. Don't build reboot detection around it unless you've confirmed ≥2.0.18.
- Output parsing is officially unstable. The Trixie manpage states it directly: terminal output between versions is not guaranteed to be stable, and if you plan on parsing results, --json is what you need. Your grep approach is exactly what that warning is about. Debian Manpages
The AI and I then rebuilt my old code and we deployed it on a MintOS LMDE 7 Dell latitude system. This is what it found.
Yes the Dell Latitude system had all official firmware patches supplied.
This is the 2011→2023 Secure Boot certificate migration I flagged earlier, showing up live on your machine. Three related updates are pending, and all of them are UEFI/Secure Boot, not peripheral firmware:
- KEK CA: currently 2011 → 2023 ("Secure Boot KEK Configuration Update", signed by Dell's Platform Key).
- UEFI db (UEFI CA): currently 2011 → 2023 — this is the Microsoft 2023 UEFI CA plus the OptionROM db.
- UEFI dbx: currently 20250507, with updates to 20250902 and 20260402 on offer. Those revocations are closing real Secure Boot bypasses —
CVE-2025-47827(vulnerable IGEL bootloader) andCVE-2026-8863(a batch including PC-Doctor, WhiteCanyon blancco, and some ancient OpenSUSE/Oracle/Red Hat shims).
Two things worth noting. First, your actual System Firmware (the BIOS capsule) is listed as up to date — so there is no capsule flash pending, only authenticated UEFI variable writes. That's the low-brick-risk category: they need a reboot to commit but they're not rewriting your BIOS chip. Second, your HSI runtime suffix flags "UEFI db: Invalid" — that's a direct consequence of still carrying the 2011 db. Applying the db update to 2023 should clear it.
So I have a problem on at least 4 Dell laptops.
Rebuild the code based on the bugs we found and run again. No where nearly Debian 14 Forky yet (refer above)
The second run removed minor errors from the code and we run the code in update mode, and rebooted.
So your KEK and db are now on the 2023 Microsoft certificates and the dbx carries the current revocations (IGEL, the CVE-2026-8863 batch). The migration you were flagged for is done, and it committed cleanly through the reboot with Secure Boot still enabled — which is the outcome you want.
Success
Debian 13 Test code
Find the test code here. Note Debian 13 only on standard repository version of fwupdate
#!/usr/bin/env bash
# ==============================================================================
# test-fwupd.sh
# ------------------------------------------------------------------------------
# Version: 1.0.1
# Created: 14 Jul 2026
# Updated: 14 Jul 2026
#
# Summary:
# Purpose: Validation harness for the fwupd firmware-update logic used in
# the provisioning scripts (Section 10). Detects the installed
# fwupd version and branch, maps which behavioural code path the
# update logic will take, and reports the capability differences
# between the 2.0.x (Trixie) and 2.1.x (Forky/testing) branches.
# Report-only by default; never flashes firmware unless forced.
#
# Usage: sudo ./test-fwupd.sh # probe + report
# sudo APPLY=1 ./test-fwupd.sh # non-UEFI updates
# sudo APPLY=1 APPLY_BIOS=1 ./test-fwupd.sh # + UEFI/SecureBoot
# sudo ALLOW_VIRT=1 ./test-fwupd.sh # run in a VM/LXC
#
# Prerequisites: fwupd installed; root for daemon/security queries; Debian
# dpkg for version comparison (falls back to sort -V otherwise).
# jq recommended for exact device classification and BIOS gating.
#
# Warnings: Do NOT apt-pin Forky/sid packages into Trixie to obtain 2.1.x.
# Test the 2.1.x branch in a throwaway Forky/sid VM or LXC, or
# build from source. UEFI capsule / Secure Boot updates apply on
# reboot -- do not interrupt power.
#
# Changelog:
# 1.0.1 (14 Jul 2026) - Fix systemd-detect-virt "none" duplication (it exits 1
# on bare metal, which triggered the || fallback and made
# bare metal look virtualised); switch LC_ALL=C to C.UTF-8
# so fwupd's glyph output stays readable; classify UEFI /
# Secure Boot updates by plugin name and split capsule
# (BIOS flash) from Secure Boot key/dbx; make APPLY_BIOS
# actually gate (apply only non-UEFI device IDs when off).
# 1.0.0 (14 Jul 2026) - Initial version.
# ==============================================================================
set -euo pipefail
# ------------------------------------------------------------------------------
# Config
# ------------------------------------------------------------------------------
: "${APPLY:=0}" # 1 = apply non-UEFI updates
: "${APPLY_BIOS:=0}" # 1 = also apply UEFI capsule / Secure Boot updates
: "${ALLOW_VIRT:=0}" # 1 = run even inside a VM/container
LOG="/var/log/test-fwupd-$(date +%Y%m%d-%H%M%S).log"
# Prefer C.UTF-8: locale-neutral (untranslated, English messages) but keeps
# fwupd's UTF-8 tree/glyph output readable. Fall back to C only if it is absent.
if locale -a 2>/dev/null | grep -qiE '^C\.utf-?8$'; then
export LC_ALL=C.UTF-8
else
export LC_ALL=C
fi
export LANGUAGE="" # stop gettext's LANGUAGE from overriding the locale
# ------------------------------------------------------------------------------
# Logging / colour helpers (ASCII-only; ANSI colour gated on a TTY)
# ------------------------------------------------------------------------------
if [[ -t 1 ]]; then
C_INFO=$'\033[0;36m'; C_WARN=$'\033[0;33m'
C_ERR=$'\033[0;31m'; C_OK=$'\033[0;32m'; C_RST=$'\033[0m'
else
C_INFO=""; C_WARN=""; C_ERR=""; C_OK=""; C_RST=""
fi
# Mirror everything to the log file.
if ! exec > >(tee -a "${LOG}") 2>&1; then
LOG="./test-fwupd-$(date +%Y%m%d-%H%M%S).log"
exec > >(tee -a "${LOG}") 2>&1
fi
info() { printf '%s[INFO]%s %s\n' "${C_INFO}" "${C_RST}" "$*"; }
warn() { printf '%s[WARN]%s %s\n' "${C_WARN}" "${C_RST}" "$*"; }
error() { printf '%s[ERROR]%s %s\n' "${C_ERR}" "${C_RST}" "$*"; }
ok() { printf '%s[OK]%s %s\n' "${C_OK}" "${C_RST}" "$*"; }
rule() { printf -- '------------------------------------------------------------\n'; }
die() { error "$*"; exit 1; }
# ------------------------------------------------------------------------------
# Version comparison helper (Debian dpkg preferred, sort -V fallback)
# ------------------------------------------------------------------------------
ver_ge() {
if command -v dpkg &>/dev/null; then
dpkg --compare-versions "$1" ge "$2"
else
[[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -n1)" == "$2" ]]
fi
}
# ==============================================================================
# A. Environment
# ==============================================================================
rule; info "test-fwupd.sh 1.0.1 | log: ${LOG}"; rule
if [[ "${EUID}" -ne 0 ]]; then
warn "Not running as root -- daemon and security queries may be limited."
fi
# systemd-detect-virt prints "none" AND exits 1 on bare metal, so capture its
# output and swallow the exit code rather than appending a second "none".
virt="$(systemd-detect-virt 2>/dev/null || true)"
virt="${virt:-none}"
if [[ "${virt}" != "none" ]]; then
if [[ "${ALLOW_VIRT}" -ne 1 ]]; then
warn "Detected virtualisation (${virt})."
warn "Host firmware is not managed from inside a guest; skipping."
warn "Re-run with ALLOW_VIRT=1 to probe fwupd behaviour here anyway."
exit 0
fi
info "Detected virtualisation (${virt}) -- proceeding (ALLOW_VIRT=1)."
else
info "Bare-metal host detected."
fi
command -v fwupdmgr &>/dev/null || die "fwupdmgr not found -- install fwupd first."
# ==============================================================================
# B. Version + branch + capability matrix
# ==============================================================================
rule; info "Version and branch detection"; rule
FW_VER=""
if command -v dpkg-query &>/dev/null; then
FW_VER="$(dpkg-query -W -f='${Version}' fwupd 2>/dev/null || true)"
fi
if [[ -z "${FW_VER}" ]]; then
FW_VER="$(fwupdmgr --version 2>/dev/null \
| grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1 || true)"
fi
[[ -n "${FW_VER}" ]] || die "Could not determine fwupd version."
# Upstream x.y.z (strip epoch and Debian revision) for branch classification.
FW_UP="$(printf '%s' "${FW_VER}" | sed -E 's/^[0-9]+://; s/-.*$//')"
info "Installed fwupd: ${FW_VER} (upstream ${FW_UP})"
if ver_ge "${FW_UP}" "2.1.0"; then BRANCH="2.1.x (feature / Forky+sid)"
elif ver_ge "${FW_UP}" "2.0.0"; then BRANCH="2.0.x (maintenance / Trixie)"
elif ver_ge "${FW_UP}" "1.9.0"; then BRANCH="1.9.x (END OF LIFE -- upgrade)"
else BRANCH="pre-1.9 (unsupported)"; fi
info "Branch: ${BRANCH}"
rule; info "Capability matrix for this version"; rule
cap() { if ver_ge "${FW_UP}" "$1"; then ok "$2 (>= $1)"; else warn "$2 (needs >= $1, absent)"; fi; }
cap "2.0.18" "reboot-required marker file + staged-update MOTD"
cap "2.0.18" "hypervisor/container detection (refactored)"
cap "2.0.17" "post-quantum signature support"
cap "2.0.17" "Framework KEK/db + refreshed offline dbx hashes"
cap "2.0.20" "blocked-firmware deprecation warning"
cap "2.1.0" "GPG signing of metadata/firmware DROPPED"
cap "2.1.0" "blocked-firmware concept REMOVED"
cap "2.1.0" "udev event source without systemd"
cap "2.1.2" "server platform detection suspends HSI checks"
# ==============================================================================
# C. Metadata refresh (report-only, non-fatal)
# ==============================================================================
rule; info "LVFS metadata refresh (non-fatal)"; rule
if timeout 60 fwupdmgr refresh --force >/dev/null 2>&1; then
ok "Metadata refreshed from LVFS."
else
warn "Refresh failed -- continuing with cached metadata (check connectivity)."
fi
# ==============================================================================
# D. get-updates exit-code probe -> which Section 10 path fires
# ==============================================================================
rule; info "get-updates exit-code probe"; rule
UPDATE_RC=0
UPDATE_OUT="$(timeout 60 fwupdmgr get-updates 2>&1)" || UPDATE_RC=$?
info "fwupdmgr get-updates returned exit code: ${UPDATE_RC}"
HAS_UPDATES=0
case "${UPDATE_RC}" in
0) ok "Path: UPDATES AVAILABLE"; HAS_UPDATES=1
printf '%s\n' "${UPDATE_OUT}" ;;
2) ok "Path: NOTHING TO DO (firmware up to date)" ;;
3) warn "Path: NOT SUPPORTED (no updatable devices on this hardware)" ;;
124) warn "Path: TIMED OUT" ;;
*) error "Path: ERROR (unexpected code ${UPDATE_RC})"
printf '%s\n' "${UPDATE_OUT}" ;;
esac
info "Note: relying on exit codes, not localised text -- locale-proof."
# ==============================================================================
# E. JSON availability + UEFI/Secure Boot classification (by plugin name)
# ==============================================================================
rule; info "JSON output + UEFI/Secure Boot classification"; rule
HAS_BIOS=0 # any UEFI/system-firmware or Secure Boot update
HAS_CAPSULE=0 # real UEFI capsule flash (brick risk if interrupted)
HAS_SBKEYS=0 # Secure Boot KEK/db/dbx variable updates (reboot, low risk)
JSON_OUT=""
if JSON_OUT="$(timeout 30 fwupdmgr get-updates --json 2>/dev/null)"; then
ok "--json output available."
if command -v jq &>/dev/null; then
info "jq present -- classifying via .Devices[].Plugin (authoritative)."
if jq -e '.Devices[]? | select((.Plugin // "") | test("uefi_capsule";"i"))' \
<<<"${JSON_OUT}" >/dev/null 2>&1; then HAS_CAPSULE=1; fi
if jq -e '.Devices[]? | select((.Plugin // "") | test("uefi_(dbx|db|kek|pk)";"i"))' \
<<<"${JSON_OUT}" >/dev/null 2>&1; then HAS_SBKEYS=1; fi
else
info "jq absent -- grepping plugin names (install jq for exact matching)."
if grep -Eqi '"uefi_capsule"' <<<"${JSON_OUT}"; then HAS_CAPSULE=1; fi
if grep -Eqi '"uefi_(dbx|db|kek|pk)"' <<<"${JSON_OUT}"; then HAS_SBKEYS=1; fi
fi
if [[ "${HAS_CAPSULE}" -eq 1 || "${HAS_SBKEYS}" -eq 1 ]]; then HAS_BIOS=1; fi
if [[ "${HAS_CAPSULE}" -eq 1 ]]; then
warn "UEFI capsule update present (real BIOS flash -- do not interrupt power)."
fi
if [[ "${HAS_SBKEYS}" -eq 1 ]]; then
warn "Secure Boot key/revocation update present (KEK/db/dbx -- reboot to commit)."
fi
if [[ "${HAS_BIOS}" -eq 0 && "${HAS_UPDATES}" -eq 1 ]]; then
info "Updates present, none are UEFI/system firmware."
fi
else
warn "--json not available on this version; text parsing would be required."
fi
# ==============================================================================
# F. Host security / HSI report (read-only)
# ==============================================================================
rule; info "Host security (HSI) report -- read-only"; rule
if [[ "${EUID}" -eq 0 ]]; then
if ! timeout 30 fwupdmgr security 2>/dev/null; then
warn "Security report unavailable (server platform, VM, or unsupported)."
fi
else
warn "Skipping HSI report -- requires root."
fi
# ==============================================================================
# G. Apply (only when explicitly forced) -- mirrors corrected Section 10
# ==============================================================================
rule; info "Apply stage"; rule
if [[ "${HAS_UPDATES}" -ne 1 ]]; then
info "No updates to apply."
elif [[ "${APPLY}" -ne 1 ]]; then
info "Report-only mode. To apply non-UEFI updates: sudo APPLY=1 $0"
if [[ "${HAS_BIOS}" -eq 1 ]]; then
info "To also apply UEFI/Secure Boot updates: sudo APPLY=1 APPLY_BIOS=1 $0"
fi
elif [[ "${HAS_BIOS}" -eq 1 && "${APPLY_BIOS}" -ne 1 ]]; then
# UEFI updates present but not authorised: apply only the non-UEFI devices.
if command -v jq &>/dev/null; then
info "Applying non-UEFI updates only (UEFI gated; set APPLY_BIOS=1 to include)..."
mapfile -t NONUEFI_IDS < <(jq -r '
.Devices[]? | select(((.Plugin // "") | test("uefi";"i")) | not)
| .DeviceId // empty' <<<"${JSON_OUT}" 2>/dev/null || true)
if [[ "${#NONUEFI_IDS[@]}" -eq 0 ]]; then
info "No non-UEFI updates to apply; nothing done."
else
for dev_id in "${NONUEFI_IDS[@]}"; do
info "Updating device ${dev_id}..."
if ! timeout 600 fwupdmgr update "${dev_id}" -y --no-reboot-check 2>&1; then
warn "Update failed or deferred for ${dev_id}."
fi
done
fi
else
warn "UEFI updates present and jq absent -- cannot selectively exclude them."
warn "Install jq, or re-run with APPLY_BIOS=1 to apply everything."
fi
else
info "Applying all eligible updates (fwupd governs power/safety)..."
APPLY_RC=0
timeout 600 fwupdmgr update -y --no-reboot-check 2>&1 || APPLY_RC=$?
if [[ "${APPLY_RC}" -eq 0 ]]; then
ok "Firmware update pass completed."
else
warn "One or more updates failed or were deferred (code ${APPLY_RC})."
fi
if [[ "${HAS_BIOS}" -eq 1 && "${APPLY_BIOS}" -eq 1 ]]; then
warn "UEFI/Secure Boot update staged -- REBOOT required. Do not interrupt power."
fi
fi
rule; ok "Done. Full log: ${LOG}"; ruleOkay - I will update the other systems in a minute.
Now for forky. Hmmm - I need another real system. Where did I put that laptop.
WARNING: I have not tested this script via a Live ISO, in other words a Linux distribution that is running from a USB stick but not actually installed, so if you are considering this please be careful. Say if you have a Windows system installed maybe, would be a possible use case.
Back in a minute, an hour maybe a day or two. Having a think (actually I need to find the laptop and create a USB installation of Debian Forky, get it going and rerun the tests.
Busy busy.
#enoughsaid