Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions classes/updatehub-image.bbclass
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@
# The active and inactive image schema requires a backend to identify and choose the image to be
# used for next boot. It supports: 'u-boot', 'grub' or 'grub-efi'.
#
# UPDATEHUB_VALIDATION_TIMEOUT
#
# How long a freshly installed image has to validate itself before updatehub-rollback-guard
# reboots it so the bootloader can roll back. Defaults to '5min', accepts any systemd time span.
# Enabled automatically with the 'u-boot' active/inactive backend when 'systemd' is in
# DISTRO_FEATURES; there is no sysvinit equivalent. Keep it comfortably above the time the agent
# needs to reach the validation callback on the slowest supported hardware.
#
# UPDATEHUB_INSTALL_MODE
#
# There are multiple installation modes supported. This is usually machine dependent as it depends
Expand Down
6 changes: 6 additions & 0 deletions classes/updatehub-runtime.bbclass
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ python () {
raise bb.parse.SkipRecipe("'%s' in UPDATEHUB_ACTIVE_INACTIVE_BACKEND is not a valid active/inactive backend. Valid active/inactive backends are: %s" % (active_inactive_backend, ' '.join(valid_active_inactive_backends)))
elif active_inactive_backend:
d.appendVar('UPDATEHUB_RUNTIME_PACKAGES', ' updatehub-active-inactive-backend-%s' % active_inactive_backend)

# Guards against updates that never validate (see UPDATEHUB_VALIDATION_TIMEOUT).
# Requires the u-boot backend (reads its boot counter) and systemd (it's a timer).
if active_inactive_backend == 'u-boot' and \
bb.utils.contains('DISTRO_FEATURES', 'systemd', True, False, d):
d.appendVar('UPDATEHUB_RUNTIME_PACKAGES', ' updatehub-rollback-guard')
}

def sanitise_version(ver):
Expand Down
55 changes: 55 additions & 0 deletions recipes-core/updatehub/updatehub-rollback-guard.bb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright 2026 (C) O.S. Systems Software LTDA.

SUMMARY = "Roll back an update which fails to validate itself"
DESCRIPTION = "Reboots the system when a freshly installed image does not \
validate itself within UPDATEHUB_VALIDATION_TIMEOUT, so U-Boot can count the \
boot attempt and roll back to the previously working image."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = " \
file://${BPN} \
file://${BPN}.service \
file://${BPN}.timer \
"

S = "${WORKDIR}"

UPDATEHUB_VALIDATION_TIMEOUT ?= "5min"

# The timeout is baked into the installed files, so two machines configuring it
# differently must not share a package.
PACKAGE_ARCH = "${MACHINE_ARCH}"

# Nothing is built; do not stage a cross toolchain for three text files.
INHIBIT_DEFAULT_DEPS = "1"

# The grace period is a systemd timer. Fail loudly rather than install units
# which no init system will ever run.
REQUIRED_DISTRO_FEATURES = "systemd"

inherit features_check systemd

do_configure[noexec] = "1"
do_compile[noexec] = "1"

# The timer is what gets enabled; it pulls in the service when it elapses.
SYSTEMD_SERVICE:${PN} = "${BPN}.timer"

do_install() {
install -Dm 0755 ${WORKDIR}/${BPN} ${D}${bindir}/${BPN}
install -Dm 0644 ${WORKDIR}/${BPN}.service ${D}${systemd_system_unitdir}/${BPN}.service
install -Dm 0644 ${WORKDIR}/${BPN}.timer ${D}${systemd_system_unitdir}/${BPN}.timer

sed -i -e 's,@VALIDATION_TIMEOUT@,${UPDATEHUB_VALIDATION_TIMEOUT},g' \
-e 's,@BINDIR@,${bindir},g' \
${D}${bindir}/${BPN} \
${D}${systemd_system_unitdir}/${BPN}.service \
${D}${systemd_system_unitdir}/${BPN}.timer
}

# systemd.bbclass doesn't follow the timer's Unit= to package the .service.
FILES:${PN} += "${systemd_system_unitdir}/${BPN}.service"

# fw_printenv, to read the boot counter state from the U-Boot environment.
RDEPENDS:${PN} += "u-boot-fw-utils"
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/bin/sh
# -*- shell-script -*-
#
# Copyright 2026 (C) O.S. Systems Software LTDA.
#
# Force a reboot when a freshly installed image fails to validate itself.
#
# U-Boot only counts boot attempts (bootcount) while upgrade_available=1; a
# healthy boot clears that flag via updatehub-active-validated. A boot that
# fails services but leaves systemd running is not a hang -- systemd keeps
# petting the watchdog -- so bootcount never advances and a broken image can
# sit in the active slot forever. This script reboots once the timeout below
# elapses so U-Boot counts the attempt and can roll back to the other slot.
#
# No 'set -e': runs in an already-broken boot, so a failing diagnostic must
# never stop the script short of the reboot.

TIMEOUT="@VALIDATION_TIMEOUT@"

# BOOTCOUNT_ENV=0 means the counter isn't in the U-Boot environment: nothing to police.
BOOTCOUNT_ENV=1
if [ -f /etc/default/updatehub-active ]; then
. /etc/default/updatehub-active
fi
[ "$BOOTCOUNT_ENV" = "1" ] || exit 0

# Log to kmsg too: a boot broken enough to need this often has no working journal.
log() {
logger -t updatehub "updatehub-rollback-guard: $*" 2>/dev/null
echo "updatehub-rollback-guard: $*" > /dev/kmsg 2>/dev/null
}

# Single call: a second fw_printenv re-reads storage, which may be what's broken.
{ read -r upgrade_available; read -r bootcount; } <<EOF
$(fw_printenv -n upgrade_available bootcount 2>/dev/null)
EOF

# Not a probationary boot.
if [ "$upgrade_available" != "1" ]; then
exit 0
fi

log "image did not validate itself within $TIMEOUT (bootcount=${bootcount:-?}); rebooting so U-Boot counts the attempt and can roll back"

# Not touching bootcount/updatehub_active: that's U-Boot's and the boot script's job.
# sysrq is the real fallback, for when PID 1 can't be reached.
systemctl --no-block reboot || echo b > /proc/sysrq-trigger

exit 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Unit]
Description=Roll back an update that failed to validate itself
# No deps: must run even when the data partition/agent/targets pulling them in have failed.
DefaultDependencies=no
IgnoreOnIsolate=yes

[Service]
Type=oneshot
ExecStart=@BINDIR@/updatehub-rollback-guard
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[Unit]
Description=Grace period before rolling back an unvalidated update
# Default deps order this after sysinit.target, which a broken-enough boot never reaches.
DefaultDependencies=no
IgnoreOnIsolate=yes
Conflicts=shutdown.target
Before=shutdown.target

[Timer]
# From boot, not from timer start, so a slow/partial boot can't extend the grace period.
OnBootSec=@VALIDATION_TIMEOUT@
Unit=updatehub-rollback-guard.service

[Install]
# emergency/rescue.target isolate and don't pull in timers.target, so list them explicitly.
WantedBy=timers.target emergency.target rescue.target
Loading