fwupdate - Trixie & Forky

fwupdate - Trixie & Forky
Firmware Updates LInux

The following post relates to the fwupdate function on Debian

In accordance with the following, there have been numerous updates.

Releases · fwupd/fwupd
A system daemon to allow session software to update firmware - fwupd/fwupd

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+deb13u1trixie-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 20112023 ("Secure Boot KEK Configuration Update", signed by Dell's Platform Key).
  • UEFI db (UEFI CA): currently 20112023 — 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) and CVE-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}"; rule

Okay - 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