From e8eff25043c12a7d30dc347be60ce516a5afd05d Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 8 Jul 2026 12:55:55 -0500 Subject: fix(guard): allow same-version reinstalls through the live-update hook The hypr-live-update-guard hook blocked on target names alone, so a same-version reinstall of hyprland (the maintenance console's integrity REINSTALL, right after a full update) aborted identically to a real upgrade. A pure reinstall writes identical bytes over identical bytes and can't crash the live compositor: the SIGABRT hazard needs the on-disk version to actually change. The guard now compares each target's installed version (pacman -Q) against the sync-db candidate (expac -S %v). Both are read-only queries and safe under the transaction lock, pinned live with db.lck present. Targets that match pass as reinstalls, and the block message lists only the version-changing packages. Unresolvable versions (AUR targets, -U transactions, expac missing) still block conservatively. Verified live on ratio: the previously blocked six-package reinstall ran clean with Hyprland up. The block path is pinned by the test suite (13 tests, four new). I deployed the script to /usr/local/bin on both hosts by hand since the installer only copies it on fresh systems. --- scripts/hypr-live-update-guard | 90 +++++++++++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 14 deletions(-) (limited to 'scripts') diff --git a/scripts/hypr-live-update-guard b/scripts/hypr-live-update-guard index 4f561ae..614414b 100755 --- a/scripts/hypr-live-update-guard +++ b/scripts/hypr-live-update-guard @@ -3,15 +3,28 @@ # hypr-live-update-guard - abort a live GPU/compositor library upgrade. # # Installed as a pacman PreTransaction hook. When an upgrade transaction -# includes GPU/compositor runtime libraries (mesa, hyprland, wayland, GPU -# drivers, ...) AND a Hyprland session is running, this aborts the -# transaction BEFORE any package is swapped. Replacing those libraries out -# from under a live compositor makes the next GPU-lib call hit a now -# "(deleted)" file and SIGABRT, taking the Wayland clients down with it -# (hit on ratio 2026-06-07: mesa + hyprland upgraded live, Hyprland crashed -# and took awww/insync/emacs with it). Aborting at PreTransaction is the -# safe point: nothing has been replaced yet, so the running session is -# untouched and the user can re-run the upgrade from a TTY. +# would CHANGE the on-disk version of GPU/compositor runtime libraries +# (mesa, hyprland, wayland, GPU drivers, ...) AND a Hyprland session is +# running, this aborts the transaction BEFORE any package is swapped. +# Replacing those libraries out from under a live compositor makes the next +# GPU-lib call hit a now "(deleted)" file and SIGABRT, taking the Wayland +# clients down with it (hit on ratio 2026-06-07: mesa + hyprland upgraded +# live, Hyprland crashed and took awww/insync/emacs with it). Aborting at +# PreTransaction is the safe point: nothing has been replaced yet, so the +# running session is untouched and the user can re-run from a TTY. +# +# Version-aware (2026-07-08): a same-version reinstall writes identical +# bytes over identical bytes and is harmless to the live session — the +# maintenance console's integrity REINSTALL is exactly that after an +# update, and the name-only guard was blocking it. Each target's installed +# version (pacman -Q) is compared against the sync-db candidate +# (expac -S %v); targets that match are pure reinstalls and pass. Both are +# read-only queries and run fine inside a hook — the ALPM lock only gates +# other transactions (pinned live with db.lck present). A target whose +# versions can't be resolved (AUR package, a -U transaction installing a +# local file, expac absent) reads as unknown and blocks conservatively — +# a -U of a guard-listed package at repo-matching version would slip +# through, which is accepted as pathological. # # Pacman feeds the matched package names on stdin (NeedsTargets). # @@ -21,6 +34,9 @@ # HYPR_GUARD_SENTINEL path whose existence also proceeds anyway # (default /run/archsetup-allow-live-gpu-update, # cleared on reboot since /run is tmpfs) +# HYPR_GUARD_VERSIONS "pkg installed candidate" lines replacing the +# pacman/expac lookups; when set, a package absent +# from the map reads as unknown (blocks) set -u @@ -43,17 +59,63 @@ hyprland_running() { # this is exactly the from-a-TTY-after-logout path the warning points to. hyprland_running || exit 0 -# Collect the triggering packages (stdin from NeedsTargets) for the message. -pkgs=$(cat 2>/dev/null | sort -u | tr '\n' ' ') +# Collect the triggering packages (stdin from NeedsTargets). +targets=$(cat 2>/dev/null | sort -u) + +# "installed candidate" for a package, or nothing when unknown. +versions_for() { + if [ -n "${HYPR_GUARD_VERSIONS+x}" ]; then + printf '%s\n' "$HYPR_GUARD_VERSIONS" \ + | awk -v p="$1" '$1 == p { print $2, $3; exit }' + else + inst=$(pacman -Q -- "$1" 2>/dev/null | awk '{ print $2 }') + # expac prints one line per repo carrying the package; the first is + # the repo pacman resolves from + cand=$(expac -S '%v' -- "$1" 2>/dev/null | head -n 1) + printf '%s %s\n' "$inst" "$cand" + fi +} + +# Split the targets into version-changing (dangerous live) and same-version +# reinstalls (harmless). Unknown versions count as dangerous. +changed="" +count=0 +for pkg in $targets; do + count=$((count + 1)) + # shellcheck disable=SC2046 # splitting "inst cand" into $1 $2 is the point + set -- $(versions_for "$pkg") + inst=${1:-} + cand=${2:-} + if [ -n "$inst" ] && [ -n "$cand" ] && [ "$inst" = "$cand" ]; then + continue # pure reinstall: identical bytes, no swap hazard + fi + changed="$changed $pkg" +done + +# Every guarded target is a pure reinstall -- nothing changes on disk that +# the live session has mapped. Let it through silently. +if [ "$count" -gt 0 ] && [ -z "$changed" ]; then + exit 0 +fi + +# An empty target list shouldn't normally happen (the hook only fires when +# dangerous targets exist); if Hyprland is up, stay safe and abort. +pkg_lines="" +for pkg in $changed; do + pkg_lines="${pkg_lines} - ${pkg} +" +done +[ -n "$pkg_lines" ] || pkg_lines=" (GPU/compositor runtime libraries) +" cat >&2 <