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 Test code - Trixie 13.6 & Forky 14

Find the test code here.

#!/usr/bin/env bash
# ==============================================================================
# test-fwupd.sh
# ------------------------------------------------------------------------------
# Version:      1.0.4
# Created:      14 Jul 2026
# Updated:      18 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/Debian 14) 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 on Debian 14, or in a Forky/sid VM or LXC.
#                  UEFI capsule / Secure Boot updates apply on reboot -- do not
#                  interrupt power.
#
#   Validation:    BOTH BRANCHES VALIDATED ON REAL BARE-METAL HARDWARE.
#
#                  Both classification paths agree on real hardware (grep fallback
#                  and jq), and both boot modes (BIOS + UEFI) have been run.
#
#                  NOT yet exercised anywhere -- do not assume coverage:
#                    - exit code 3 (no updatable devices supported)
#                    - exit code 2 (nothing to do) on the 2.1.x branch
#                    - APPLY with UEFI gated OFF (the non-UEFI device-ID loop) --
#                      needs a host with a peripheral firmware update pending
#                    - uefi_capsule classification (no test host has had a real
#                      capsule update pending)
#
# Changelog:
#   1.0.4 (18 Jul 2026) - Fix version drift: the runtime banner was hardcoded to
#                         1.0.1 while the header said 1.0.3, so logs misreported
#                         which version produced them -- now a single SCRIPT_VERSION
#                         constant. Strip ANSI escapes from the log file while
#                         keeping colour on the terminal (logs were full of raw
#                         escape codes, making them hard to read/diff/grep). Add the
#                         hostname to the banner. Treat exit 1 as the expected
#                         daemon-unreachable case rather than "unexpected". Correct
#                         the --json failure message, which blamed the fwupd version
#                         when the real cause is usually a dead daemon. Validation
#                         updated: 2.1.x now proven on bare-metal Debian 14.
#   1.0.3 (16 Jul 2026) - Validation update: the jq .Devices[].Plugin classification
#                         path is now confirmed on real 2.0.x hardware (ASUS Proxmox
#                         host) and agrees with the grep fallback, so 2.1.x runtime
#                         is the sole remaining gap. Noted the two paths still not
#                         hit anywhere (exit 3; APPLY with UEFI gated off). No
#                         behavioural code change.
#   1.0.2 (14 Jul 2026) - Document validation status: 2.0.x proven end-to-end on
#                         bare metal; 2.1.x confirmed for version/capability logic
#                         only, inside an unprivileged LXC where the daemon cannot
#                         start -- 2.1.x runtime paths and the jq path still need a
#                         VM+OVMF or real hardware. No behavioural code change.
#   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

readonly SCRIPT_VERSION="1.0.4"   # single source of truth -- banner reads this

# ------------------------------------------------------------------------------
# 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"

# Host identity -- logs are collected from several machines, so name the source.
HOST="$(hostname 2>/dev/null || cat /etc/hostname 2>/dev/null || echo unknown)"

# 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
# ------------------------------------------------------------------------------
# Colour is decided on the TTY *before* stdout is redirected, so an interactive
# run stays colourised. The log copy is passed through sed to strip ANSI escapes
# (ours and fwupd's own), keeping the file greppable and diffable.
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

if ! : >> "${LOG}" 2>/dev/null; then
    LOG="./test-fwupd-$(date +%Y%m%d-%H%M%S).log"
fi
# CSI sequences out of the log copy; carriage returns from progress lines too.
exec > >(tee >(sed -u -e 's/\x1b\[[0-9;?]*[a-zA-Z]//g' -e 's/\r$//' >> "${LOG}")) 2>&1

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; }

# Give tee/sed a moment to flush before the shell exits.
cleanup() { sleep 0.2; }
trap cleanup EXIT

# ------------------------------------------------------------------------------
# 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 ${SCRIPT_VERSION}  |  host: ${HOST}"
info "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 -- 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
DAEMON_OK=1
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" ;;
    1)   # Generic failure. Overwhelmingly this is the daemon failing to start:
         # unprivileged containers have no efivars / /sys/firmware, so the D-Bus
         # activation times out and every later call fails the same way.
         if printf '%s' "${UPDATE_OUT}" | grep -qi "connect to daemon\|StartServiceByName\|org.freedesktop.fwupd"; then
             DAEMON_OK=0
             warn "Path: DAEMON UNREACHABLE -- fwupd could not be activated."
             warn "Expected inside unprivileged containers (no efivars/sysfs access)."
         else
             error "Path: ERROR (exit 1)"
         fi
         printf '%s\n' "${UPDATE_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
elif [[ "${DAEMON_OK}" -eq 0 ]]; then
    warn "--json unavailable: the daemon is unreachable (not a version limitation)."
else
    warn "--json returned nothing -- check daemon state, or the version if pre-2.0."
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 on ${HOST}. Full log: ${LOG}"; rule

Thats interesting - Laptop 2 didnt take on CLI command reboot but did take on rebooting from the GUI. Strange.

Tested

  1. Debian 13.6 sytem installed via BIOS - no Secureboot - legacy enabled
  2. Debian 13.6 system installed via UEFI - Secureboot
  3. Debian 14 system installed via BIOS - no Secureboot - legacy enabled
  4. Debian 14 system installed via UEFI - Secureboot
  5. None of my system had outdated firmware with exception of para 6, which cant be fixed via fmupdate.
  6. Intel firmware updates (found 1) will require manufacturer issued update

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.


Testing complete - no further work on this code at this time.

#enoughsaid