From 2a304691cb778b6e419f632dac3a73dd6e82bfa5 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Fri, 17 Jul 2026 07:43:07 +0200 Subject: [PATCH] fix(pacman): wrap makepkg so an orphaned db.lck can't hang `makepkg -i` Vendored makepkg's run_pacman() waits on /var/lib/pacman/db.lck existence with no timeout BEFORE invoking pacman, so an orphaned lock hangs `makepkg -i` forever ("Pacman is currently in use, please wait...") and the pacman wrapper's stale-lock cleanup never gets a turn. Wrap makepkg (mirroring the pacman-wrapper pattern) to clear an orphaned lock up front. - Extract stale-lock logic into shared pacman_lock_lib.sh; pacman_wrapper sources it after integrity verification and it is in the SHA256 manifest - makepkg_wrapper: pre-flight clear for -i/--install; bypass on FAKEROOTKEY and non-install; FAIL OPEN if the lib is missing (never brick makepkg) - Cross-user pacman_process_running() pgrep guard: unprivileged makepkg can detect a root pacman its own fuser cannot see (avoids removing a live lock) - Upgrade survival: rewrap hook (Install/Upgrade only) + check_makepkg_wrapper drift verifier + both wrappers wired into the hourly periodic driver - Fix reorg path bugs: stale ~/linux-configuration periodic path, $CONFIG_DIR/hosts -> scripts/periodic_background/hosts, test WRAPPER_DIR; refresh .orig only when target isn't already our symlink (no exec loop) - Tests: test_makepkg_wrapper.sh (18) incl. hook Operation validity; add lib-integrity coverage to test_pacman_wrapper_security.sh (also path-fixed) Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1 --- ...makepkg-stale-lock-wrapper-2026-07-16.json | 20 ++ ...makepkg-stale-lock-wrapper-2026-07-16.json | 38 ++++ .../check_and_enable_services.sh | 89 ++++++++- .../pacman/96-restore-pkg-wrappers.hook | 16 ++ .../pacman/README_FOR_LLM.md | 39 +++- .../pacman/install_makepkg_wrapper.sh | 87 ++++++++ .../pacman/install_pacman_wrapper.sh | 19 +- .../pacman/makepkg_wrapper.sh | 69 +++++++ .../pacman/pacman_lock_lib.sh | 186 ++++++++++++++++++ .../pacman/pacman_wrapper.sh | 144 ++------------ .../pacman/rewrap_pkg_managers.sh | 34 ++++ .../setup_periodic_system.sh | 8 +- .../bin/periodic-system-maintenance.sh | 2 + .../tests/test_makepkg_wrapper.sh | 120 +++++++++++ .../tests/test_pacman_wrapper_security.sh | 40 +++- 15 files changed, 775 insertions(+), 136 deletions(-) create mode 100644 docs/superpowers/contracts/makepkg-stale-lock-wrapper-2026-07-16.json create mode 100644 docs/superpowers/evidence/makepkg-stale-lock-wrapper-2026-07-16.json create mode 100644 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/96-restore-pkg-wrappers.hook create mode 100755 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_makepkg_wrapper.sh create mode 100755 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/makepkg_wrapper.sh create mode 100755 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_lock_lib.sh create mode 100755 linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/rewrap_pkg_managers.sh create mode 100755 linux_configuration/tests/test_makepkg_wrapper.sh diff --git a/docs/superpowers/contracts/makepkg-stale-lock-wrapper-2026-07-16.json b/docs/superpowers/contracts/makepkg-stale-lock-wrapper-2026-07-16.json new file mode 100644 index 00000000..279a4f52 --- /dev/null +++ b/docs/superpowers/contracts/makepkg-stale-lock-wrapper-2026-07-16.json @@ -0,0 +1,20 @@ +{ + "title": "Wrap makepkg so an orphaned pacman db.lck cannot hang `makepkg -i` (system-wide)", + "objective": "Vendored makepkg's run_pacman() waits on /var/lib/pacman/db.lck existence with no timeout and no owner check, BEFORE it ever invokes pacman — so an orphaned lock hangs `makepkg -i` (and therefore ~/todo/install_arch.sh) forever, and the pacman wrapper's existing stale-lock cleanup never gets a turn. Fix this system-wide by wrapping /usr/bin/makepkg (mirroring the pacman-wrapper pattern, since makepkg is vendored and must not be edited), clearing an orphaned lock before makepkg's own wait, while never removing a lock a live pacman holds.", + "acceptance_criteria": [ + "With an orphaned /var/lib/pacman/db.lck and no pacman running, `makepkg -i --noconfirm` clears the lock and proceeds instead of looping on 'Pacman is currently in use, please wait...'.", + "With a real (root) pacman process running, the wrapper does NOT remove the lock — detection works cross-user, since makepkg runs unprivileged and its own fuser cannot see a root process's fds.", + "makepkg still works for non-install invocations and inside the fakeroot build sandbox (FAKEROOTKEY), and FAILS OPEN: a missing/unreadable shared lib must never break makepkg or pacman.", + "Stale-lock logic lives in exactly one shared library (pacman_lock_lib.sh) used by both wrappers, sourced only after integrity verification and covered by the SHA256 integrity manifest.", + "Wrapper symlinks survive a pacman-git upgrade via a PostTransaction hook plus a drift verifier; the hook must use only valid alpm Operation values (an invalid one aborts every pacman transaction).", + "`sudo pacman -S ` (a real transaction) still succeeds end-to-end with the hook installed.", + "shellcheck --severity=warning clean; test_makepkg_wrapper.sh and test_pacman_wrapper_security.sh pass; pre-commit green." + ], + "out_of_scope": [ + "Editing vendored /usr/bin/makepkg (overwritten on pacman-git upgrades)", + "Changing pacman policy/blocklist/greylist/challenge behaviour", + "Broader hosts-guard subsystem work beyond correcting the reorg-stale $CONFIG_DIR/hosts paths that blocked the periodic regeneration", + "AUR helpers (yay/paru) that invoke pacman internally" + ], + "verifier": "bash linux_configuration/tests/test_makepkg_wrapper.sh (18 checks) + bash linux_configuration/tests/test_pacman_wrapper_security.sh; shellcheck --severity=warning; real-system: sudo touch /var/lib/pacman/db.lck then `makepkg -i --noconfirm` clears it, a root pacman process is respected, and `sudo pacman -S --noconfirm licenses` completes with no hook errors; check_and_enable_services.sh --status reports pacman_wrapper=ok and makepkg_wrapper=ok" +} diff --git a/docs/superpowers/evidence/makepkg-stale-lock-wrapper-2026-07-16.json b/docs/superpowers/evidence/makepkg-stale-lock-wrapper-2026-07-16.json new file mode 100644 index 00000000..e1aa8b1e --- /dev/null +++ b/docs/superpowers/evidence/makepkg-stale-lock-wrapper-2026-07-16.json @@ -0,0 +1,38 @@ +{ + "intent": "Stop `makepkg -i` (and thus ~/todo/install_arch.sh) from hanging forever on an orphaned /var/lib/pacman/db.lck. Vendored makepkg's run_pacman() waits on file-existence only, with no timeout, before pacman is ever called, so the existing pacman wrapper's stale-lock cleanup never runs. Fix system-wide by wrapping makepkg, mirroring the pacman-wrapper pattern.", + "scope": [ + "linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/ (new: pacman_lock_lib.sh, makepkg_wrapper.sh, install_makepkg_wrapper.sh, rewrap_pkg_managers.sh, 96-restore-pkg-wrappers.hook; edit: pacman_wrapper.sh, install_pacman_wrapper.sh, README_FOR_LLM.md)", + "linux_configuration/scripts/periodic_background/ (edit: check_and_enable_services.sh, setup_periodic_system.sh, system-maintenance/bin/periodic-system-maintenance.sh)", + "linux_configuration/tests/ (new: test_makepkg_wrapper.sh; edit: test_pacman_wrapper_security.sh)", + "Non-goals: editing vendored /usr/bin/makepkg; changing pacman policy/challenge behaviour" + ], + "changes": [ + "Extracted stale-lock helpers into shared pacman_lock_lib.sh (single source of truth); pacman_wrapper.sh now sources it AFTER integrity verification, and the lib is added to the SHA256 integrity manifest.", + "New makepkg_wrapper.sh: for install-bound invocations clears an orphaned lock (best-effort) then execs real makepkg; bypasses on FAKEROOTKEY/non-install; FAILS OPEN if lib missing so it can never brick makepkg.", + "Added cross-user pacman_process_running() (pgrep -x) guard: unprivileged makepkg can still detect a root pacman that its own fuser cannot see, preventing wrongful removal of a live lock.", + "Upgrade survival: install_makepkg_wrapper.sh + rewrap_pkg_managers.sh + PostTransaction hook (Install/Upgrade only); drift verifier check_makepkg_wrapper() in check_and_enable_services.sh; both wrappers wired into the hourly periodic driver.", + "Fixed pre-existing reorg path bugs: stale ~/linux-configuration path in the periodic driver, $CONFIG_DIR/hosts -> $CONFIG_DIR/scripts/periodic_background/hosts in setup + check scripts, and the test's WRAPPER_DIR path.", + "Fixed .orig-staleness bug in both installers (refresh backup only when target is not our symlink)." + ], + "verification": [ + { + "command": "bash linux_configuration/tests/test_makepkg_wrapper.sh; bash linux_configuration/tests/test_pacman_wrapper_security.sh", + "result": "pass", + "evidence": "test_makepkg_wrapper.sh: 18 passed, 0 failed. test_pacman_wrapper_security.sh: All Tests Passed. shellcheck --severity=warning clean on all shell files." + }, + { + "command": "deploy + real-system: sudo touch db.lck; makepkg -i --noconfirm", + "result": "pass", + "evidence": "Deployed wrapper cleared orphaned lock ('Stale pacman lock detected... Removing it automatically') and ran real makepkg; a running root 'pacman' process was respected (lock kept). sudo pacman -S licenses (real txn) rc=0 after hook Operation fix." + } + ], + "risks": [ + "Wrapper sits in front of makepkg system-wide; mitigated by fail-open source guard + isolation/real-system tests.", + "A rare hung/zombie process literally named 'pacman' can block stale-lock removal (safety over a rare re-hang).", + "pacman-git upgrade briefly clobbers the symlink until the PostTransaction hook/drift verifier restores it." + ], + "rollback": [ + "Restore stock binaries: sudo cp -f /usr/bin/pacman.orig /usr/bin/pacman; sudo cp -f /usr/bin/makepkg.orig /usr/bin/makepkg; sudo rm -f /etc/pacman.d/hooks/96-restore-pkg-wrappers.hook", + "Validate after rollback: 'pacman -Q' and 'makepkg --version' work; a real 'sudo pacman -S --noconfirm licenses' transaction completes with no hook errors." + ] +} diff --git a/linux_configuration/scripts/periodic_background/check_and_enable_services.sh b/linux_configuration/scripts/periodic_background/check_and_enable_services.sh index 40a312bb..fdd7fc78 100755 --- a/linux_configuration/scripts/periodic_background/check_and_enable_services.sh +++ b/linux_configuration/scripts/periodic_background/check_and_enable_services.sh @@ -31,12 +31,13 @@ CONFIG_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" # Script paths PACMAN_WRAPPER_INSTALL="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh" +MAKEPKG_WRAPPER_INSTALL="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/pacman/install_makepkg_wrapper.sh" MIDNIGHT_SHUTDOWN_SCRIPT="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh" STARTUP_MONITOR_SCRIPT="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/setup_pc_startup_monitor.sh" PERIODIC_SYSTEM_SCRIPT="$CONFIG_DIR/scripts/periodic_background/setup_periodic_system.sh" -HOSTS_INSTALL_SCRIPT="$CONFIG_DIR/hosts/install.sh" -HOSTS_GUARD_SCRIPT="$CONFIG_DIR/hosts/guard/setup_hosts_guard.sh" -HOSTS_PACMAN_HOOKS_SCRIPT="$CONFIG_DIR/hosts/guard/install_pacman_hooks.sh" +HOSTS_INSTALL_SCRIPT="$CONFIG_DIR/scripts/periodic_background/hosts/install.sh" +HOSTS_GUARD_SCRIPT="$CONFIG_DIR/scripts/periodic_background/hosts/guard/setup_hosts_guard.sh" +HOSTS_PACMAN_HOOKS_SCRIPT="$CONFIG_DIR/scripts/periodic_background/hosts/guard/install_pacman_hooks.sh" THESIS_TRACKER_SCRIPT="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/setup_thesis_work_tracker.sh" FOCUS_MODE_SCRIPT="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/install_focus_mode_daemon.sh" COMPULSIVE_BLOCK_SCRIPT="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh" @@ -254,6 +255,85 @@ check_pacman_wrapper() { SERVICE_STATUS["pacman_wrapper"]=$status } +check_makepkg_wrapper() { + header "Makepkg Wrapper" + + local status="ok" + local issues=() + + # Wrapper symlink + if [[ -L /usr/bin/makepkg ]]; then + local target + target=$(readlink -f /usr/bin/makepkg) + if [[ $target == "/usr/local/bin/makepkg_wrapper" ]]; then + msg "Makepkg symlink points to wrapper" + else + issues+=("Makepkg symlink points to: $target (expected /usr/local/bin/makepkg_wrapper)") + status="error" + fi + else + issues+=("Makepkg is not a symlink (wrapper not installed)") + status="error" + fi + + # Original makepkg backup + if [[ -f /usr/bin/makepkg.orig ]]; then + msg "Original makepkg backed up at /usr/bin/makepkg.orig" + else + issues+=("Original makepkg backup not found at /usr/bin/makepkg.orig") + status="error" + fi + + # Wrapper, shared lib, rewrap helper, survival hook + if [[ -f /usr/local/bin/makepkg_wrapper ]]; then + msg "Wrapper script exists at /usr/local/bin/makepkg_wrapper" + else + issues+=("Wrapper script not found at /usr/local/bin/makepkg_wrapper") + status="error" + fi + if [[ -f /usr/local/bin/pacman_lock_lib.sh ]]; then + msg "Shared lock library exists at /usr/local/bin/pacman_lock_lib.sh" + else + issues+=("Shared lock library not found at /usr/local/bin/pacman_lock_lib.sh") + status="error" + fi + if [[ -f /usr/local/bin/rewrap_pkg_managers.sh ]]; then + msg "Rewrap helper exists at /usr/local/bin/rewrap_pkg_managers.sh" + else + issues+=("Rewrap helper not found at /usr/local/bin/rewrap_pkg_managers.sh") + status="error" + fi + if [[ -f /etc/pacman.d/hooks/96-restore-pkg-wrappers.hook ]]; then + msg "Upgrade-survival hook installed" + else + issues+=("Upgrade-survival hook not installed at /etc/pacman.d/hooks/96-restore-pkg-wrappers.hook") + status="error" + fi + + # Report and fix + if [[ $status == "error" ]]; then + for issue in "${issues[@]}"; do + err "$issue" + done + ((ISSUES_FOUND++)) || true + + if [[ $STATUS_ONLY -eq 0 ]]; then + note "Installing makepkg wrapper..." + if [[ -f $MAKEPKG_WRAPPER_INSTALL ]]; then + run bash "$MAKEPKG_WRAPPER_INSTALL" + ((FIXES_APPLIED++)) || true + if [[ $DRY_RUN -eq 0 ]] && [[ -L /usr/bin/makepkg ]] && [[ -f /usr/bin/makepkg.orig ]] && [[ -f /usr/local/bin/makepkg_wrapper ]]; then + status="ok" + fi + else + err "Installer script not found: $MAKEPKG_WRAPPER_INSTALL" + fi + fi + fi + + SERVICE_STATUS["makepkg_wrapper"]=$status +} + check_midnight_shutdown() { header "Midnight Shutdown (Day-Specific Auto-Shutdown)" @@ -1160,7 +1240,7 @@ print_summary() { printf "%-25s %s\n" "Service" "Status" printf "%-25s %s\n" "-------" "------" - for service in pacman_wrapper midnight_shutdown startup_monitor periodic_systems hosts thesis_tracker focus_mode compulsive_blocker thorium_startup leechblock guest_mode_removal vbox_hosts workout_locker; do + for service in pacman_wrapper makepkg_wrapper midnight_shutdown startup_monitor periodic_systems hosts thesis_tracker focus_mode compulsive_blocker thorium_startup leechblock guest_mode_removal vbox_hosts workout_locker; do local status="${SERVICE_STATUS[$service]:-unknown}" local color case "$status" in @@ -1212,6 +1292,7 @@ main() { fi check_pacman_wrapper + check_makepkg_wrapper check_midnight_shutdown check_startup_monitor check_periodic_systems diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/96-restore-pkg-wrappers.hook b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/96-restore-pkg-wrappers.hook new file mode 100644 index 00000000..bf41cc58 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/96-restore-pkg-wrappers.hook @@ -0,0 +1,16 @@ +# Restore the pacman + makepkg wrapper symlinks after a pacman-git upgrade +# overwrites /usr/bin/pacman and /usr/bin/makepkg with the stock binaries. +# PostTransaction + file-ops-only Exec (never calls pacman) => safe inside the +# transaction. See rewrap_pkg_managers.sh. +[Trigger] +Type = Package +Operation = Upgrade +Operation = Install +Target = pacman +Target = pacman-git + +[Action] +Description = Restoring pacman/makepkg wrapper symlinks... +When = PostTransaction +Exec = /usr/local/bin/rewrap_pkg_managers.sh +Depends = coreutils diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/README_FOR_LLM.md b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/README_FOR_LLM.md index 68126156..c850c761 100644 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/README_FOR_LLM.md +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/README_FOR_LLM.md @@ -241,11 +241,46 @@ remove_installed_blocked_packages() { ## Stale Lock Handling +The stale-lock logic lives in **`pacman_lock_lib.sh`** (shared, sourced by both +`pacman_wrapper.sh` and `makepkg_wrapper.sh` — single source of truth). The +wrapper sources it only AFTER `verify_policy_integrity`, and the lib is listed in +the integrity manifest, so a tampered lib is rejected before it runs. + If `/var/lib/pacman/db.lck` exists but no pacman is running: - Interactive: Prompts user to remove (15s timeout) -- Non-interactive (`--noconfirm`): Auto-removes if lock is >10 minutes old -- If another pacman is actually running: Blocks with error +- Non-interactive (`--noconfirm`): Auto-removes; also auto-removes if lock is + > 10 minutes old +- If a real pacman/pamac process is running: Blocks with error. Detection uses + `fuser`/`lsof` on the lock file AND a system-wide `pgrep -x pacman` guard + (`pacman_process_running`) so an UNPRIVILEGED caller (makepkg is never root) + can still see a ROOT `pacman -Syu` that its own `fuser` cannot. + +### Makepkg wrapper + +`/usr/bin/makepkg` is symlinked to `makepkg_wrapper` (real binary at +`/usr/bin/makepkg.orig`). Vendored `makepkg`'s `run_pacman()` has its own +lock-wait that checks ONLY file existence with no timeout, so an orphaned +`db.lck` hangs `makepkg -i` forever ("Pacman is currently in use, please +wait...") _before_ pacman is ever called — the pacman wrapper's cleanup never +gets a turn. `makepkg_wrapper` closes this: for install-bound invocations +(`-i`/`--install`) it clears an orphaned lock up front, then execs real makepkg. +It bypasses (execs real makepkg unchanged) inside a fakeroot build sandbox +(`FAKEROOTKEY` set) or for non-install invocations, and **fails open** — if the +shared lib is missing it execs real makepkg rather than break all builds. + +### Upgrade survival + +A `pacman-git` upgrade reinstalls `/usr/bin/pacman` and `/usr/bin/makepkg` as +stock binaries, clobbering the wrapper symlinks. Two mechanisms restore them: + +- **PostTransaction hook** `/etc/pacman.d/hooks/96-restore-pkg-wrappers.hook` + (Target = pacman/pacman-git) runs `rewrap_pkg_managers.sh` (file-ops only, + never calls pacman) to re-establish both symlinks and refresh the `.orig` + backups. NOTE: alpm `Operation` accepts only Install/Upgrade/Remove — an + invalid value aborts EVERY transaction; validate with a real `-S` install. +- **Drift verifier** `check_makepkg_wrapper()` in `check_and_enable_services.sh` + plus the hourly periodic driver re-running both installers. ## Maintenance Auto-Setup diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_makepkg_wrapper.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_makepkg_wrapper.sh new file mode 100755 index 00000000..1cab0821 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_makepkg_wrapper.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# install_makepkg_wrapper.sh — wrap /usr/bin/makepkg so an orphaned pacman db.lck +# cannot hang `makepkg -i` forever (see makepkg_wrapper.sh for the full why). +# +# Mirrors install_pacman_wrapper.sh: preserves the real binary at +# /usr/bin/makepkg.orig and points /usr/bin/makepkg at the wrapper. Also installs +# the shared stale-lock library (idempotent) and the upgrade-survival hook that +# re-establishes BOTH wrapper symlinks after a pacman-git upgrade. + +set -euo pipefail + +# Auto-sudo +if [ "$EUID" -ne 0 ]; then + echo "Executing with sudo..." + exec sudo "$0" "$@" +fi + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +SRC_DIR="$(dirname "$(readlink -f "$0")")" +INSTALL_DIR="/usr/local/bin" + +LOCK_LIB_SOURCE="$SRC_DIR/pacman_lock_lib.sh" +LOCK_LIB_DEST="${INSTALL_DIR}/pacman_lock_lib.sh" +WRAPPER_SOURCE="$SRC_DIR/makepkg_wrapper.sh" +WRAPPER_DEST="${INSTALL_DIR}/makepkg_wrapper" +REWRAP_SOURCE="$SRC_DIR/rewrap_pkg_managers.sh" +REWRAP_DEST="${INSTALL_DIR}/rewrap_pkg_managers.sh" +HOOK_SOURCE="$SRC_DIR/96-restore-pkg-wrappers.hook" +HOOK_DEST="/etc/pacman.d/hooks/96-restore-pkg-wrappers.hook" + +# Copy a file, unlocking an immutable destination first (the shared lib may be +# chattr +i from install_pacman_wrapper.sh's integrity protection). +deploy_file() { # + local src="$1" dest="$2" mode="$3" + if [[ ! -f $src ]]; then + echo -e "${RED}Error:${NC} missing source $src" >&2 + exit 1 + fi + if command -v chattr >/dev/null 2>&1 && [[ -e $dest ]] && + [[ $(lsattr -d "$dest" 2>/dev/null | awk '{print $1}') == *i* ]]; then + chattr -i "$dest" 2>/dev/null || true + fi + cp -f "$src" "$dest" + chmod "$mode" "$dest" +} + +echo -e "${CYAN}Installing makepkg wrapper...${NC}" + +# Shared stale-lock library (idempotent — install_pacman_wrapper.sh also deploys +# and integrity-protects it; the makepkg wrapper needs it present regardless). +deploy_file "$LOCK_LIB_SOURCE" "$LOCK_LIB_DEST" 644 + +# makepkg wrapper itself +deploy_file "$WRAPPER_SOURCE" "$WRAPPER_DEST" 755 + +# Preserve the real makepkg. Refresh the backup whenever /usr/bin/makepkg is a +# real file (fresh install, or a pacman-git upgrade replaced our symlink); NEVER +# when it is already our symlink — copying the symlink's target would put the +# wrapper into makepkg.orig and cause an exec loop. +if [ ! -L /usr/bin/makepkg ]; then + echo -e "${BLUE}Backing up original makepkg to /usr/bin/makepkg.orig...${NC}" + cp -f /usr/bin/makepkg /usr/bin/makepkg.orig +fi + +# Sanity: refuse to symlink if we have no real binary to fall back to. +if [ ! -e /usr/bin/makepkg.orig ]; then + echo -e "${RED}Error: /usr/bin/makepkg.orig missing; refusing to create wrapper symlink.${NC}" >&2 + exit 1 +fi + +# Upgrade-survival: rewrap helper + PostTransaction hook restoring BOTH symlinks. +deploy_file "$REWRAP_SOURCE" "$REWRAP_DEST" 755 +mkdir -p /etc/pacman.d/hooks +deploy_file "$HOOK_SOURCE" "$HOOK_DEST" 644 + +# Point /usr/bin/makepkg at the wrapper. +echo -e "${BLUE}Creating symbolic link /usr/bin/makepkg -> ${WRAPPER_DEST}...${NC}" +ln -sf "$WRAPPER_DEST" /usr/bin/makepkg + +echo -e "${GREEN}makepkg wrapper installed.${NC} Original preserved at ${CYAN}/usr/bin/makepkg.orig${NC}" +echo -e "Upgrade-survival hook installed at ${CYAN}${HOOK_DEST}${NC}" diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh index dcde07e5..0a01b79b 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh @@ -20,6 +20,7 @@ NC='\033[0m' # No Color # Script locations WRAPPER_SOURCE="$(dirname "$0")/pacman_wrapper.sh" +LOCK_LIB_SOURCE="$(dirname "$0")/pacman_lock_lib.sh" WORDS_SOURCE="$(dirname "$0")/words.txt" BLOCKED_SOURCE="$(dirname "$0")/pacman_blocked_keywords.txt" WHITELIST_SOURCE="$(dirname "$0")/pacman_whitelist.txt" @@ -28,6 +29,7 @@ MAKEPKG_CAPPED_SOURCE="$(dirname "$0")/makepkg_capped.sh" MKPKG_SOURCE="$(dirname "$0")/mkpkg.sh" INSTALL_DIR="/usr/local/bin" WRAPPER_DEST="${INSTALL_DIR}/pacman_wrapper" +LOCK_LIB_DEST="${INSTALL_DIR}/pacman_lock_lib.sh" WORDS_DEST="${INSTALL_DIR}/words.txt" BLOCKED_DEST="${INSTALL_DIR}/pacman_blocked_keywords.txt" WHITELIST_DEST="${INSTALL_DIR}/pacman_whitelist.txt" @@ -115,6 +117,8 @@ echo -e "${CYAN}Installing pacman wrapper...${NC}" # Install the wrapper script echo -e "${BLUE}Copying wrapper script to ${WRAPPER_DEST}...${NC}" copy_managed_file "$WRAPPER_SOURCE" "$WRAPPER_DEST" required "wrapper script" +copy_managed_file "$LOCK_LIB_SOURCE" "$LOCK_LIB_DEST" required "stale-lock library" +chmod 644 "$LOCK_LIB_DEST" copy_managed_file "$WORDS_SOURCE" "$WORDS_DEST" required "words list" copy_managed_file "$BLOCKED_SOURCE" "$BLOCKED_DEST" required "blocked keywords list" copy_managed_file "$WHITELIST_SOURCE" "$WHITELIST_DEST" optional "whitelist" @@ -129,10 +133,13 @@ chmod 644 "$WORDS_DEST" "$BLOCKED_DEST" "$WHITELIST_DEST" "$GREYLIST_DEST" 2> /d # Automatically use symbolic link installation method echo -e "${YELLOW}Installing using symbolic link method...${NC}" -# Backup original pacman -if [ ! -f "/usr/bin/pacman.orig" ]; then +# Backup original pacman. Refresh the backup whenever /usr/bin/pacman is a real +# file (e.g. a pacman-git upgrade replaced our symlink with the new binary), but +# NEVER when it is already our symlink — copying the symlink's target would put +# the wrapper into pacman.orig and cause an exec loop. +if [ ! -L /usr/bin/pacman ]; then echo -e "${BLUE}Backing up original pacman to /usr/bin/pacman.orig...${NC}" - cp /usr/bin/pacman /usr/bin/pacman.orig + cp -f /usr/bin/pacman /usr/bin/pacman.orig fi # Update the PACMAN_BIN variable in the wrapper to point to the original @@ -150,6 +157,7 @@ unlock_immutable_file_if_needed "$INTEGRITY_FILE" missing_files=() [[ ! -f "$BLOCKED_DEST" ]] && missing_files+=("$BLOCKED_DEST") [[ ! -f "$GREYLIST_DEST" ]] && missing_files+=("$GREYLIST_DEST") +[[ ! -f "$LOCK_LIB_DEST" ]] && missing_files+=("$LOCK_LIB_DEST") if [[ ${#missing_files[@]} -gt 0 ]]; then echo -e "${RED}Error: Critical policy files are missing:${NC}" @@ -161,6 +169,10 @@ fi { sha256sum "$BLOCKED_DEST" || { echo -e "${RED}Failed to checksum blocked list${NC}" >&2; exit 1; } sha256sum "$GREYLIST_DEST" || { echo -e "${RED}Failed to checksum greylist${NC}" >&2; exit 1; } + # The shared stale-lock library is executed (sourced) by the wrapper, so it is + # integrity-checked too: pacman_wrapper.sh sources it only AFTER + # verify_policy_integrity passes, so a tampered lib is rejected before it runs. + sha256sum "$LOCK_LIB_DEST" || { echo -e "${RED}Failed to checksum lock library${NC}" >&2; exit 1; } # Whitelist is optional if [[ -f "$WHITELIST_DEST" ]]; then sha256sum "$WHITELIST_DEST" || { echo -e "${RED}Failed to checksum whitelist${NC}" >&2; exit 1; } @@ -184,6 +196,7 @@ echo -e "${BLUE}Protecting policy files from modification...${NC}" if command -v chattr > /dev/null 2>&1; then chattr +i "$BLOCKED_DEST" 2>/dev/null || echo -e "${YELLOW}Warning: Could not make blocked list immutable${NC}" chattr +i "$GREYLIST_DEST" 2>/dev/null || echo -e "${YELLOW}Warning: Could not make greylist immutable${NC}" + chattr +i "$LOCK_LIB_DEST" 2>/dev/null || echo -e "${YELLOW}Warning: Could not make lock library immutable${NC}" # Note: whitelist is intentionally left modifiable for user convenience else echo -e "${YELLOW}Warning: chattr not available, policy files will not be immutable${NC}" diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/makepkg_wrapper.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/makepkg_wrapper.sh new file mode 100755 index 00000000..298db92b --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/makepkg_wrapper.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# makepkg_wrapper.sh — thin wrapper around /usr/bin/makepkg. +# +# WHY THIS EXISTS +# --------------- +# Vendored makepkg (shipped by the `pacman` package) has its own DB-lock wait in +# run_pacman(): `while [[ -f $lockfile ]]; do ... done`. It checks ONLY whether +# /var/lib/pacman/db.lck exists — never whether a live pacman actually owns it — +# and the outer loop has no timeout. So an ORPHANED lock (left by a pacman that +# was killed mid-transaction) makes `makepkg -i` hang forever, printing +# "Pacman is currently in use, please wait..." every ~30s. +# +# makepkg blocks in that loop BEFORE it ever invokes pacman, so the system's +# pacman_wrapper (which clears stale locks correctly) never gets a turn. This +# wrapper closes the gap: for install-bound invocations it clears an *orphaned* +# lock up front, then execs the real makepkg. A legitimate lock (held by a live +# pacman) is left untouched so makepkg waits as designed. +# +# makepkg is vendored and overwritten on `pacman-git` upgrades, so it must not be +# edited directly — wrapping it (mirroring the pacman_wrapper pattern) is the +# upgrade-safe fix. + +set -euo pipefail + +# Real makepkg, preserved by install_makepkg_wrapper.sh. Overridable via env so +# the shell test harness can point it at a stub (mirrors $PACMAN_LOCK_FILE); +# unset in production, so the default holds. +MAKEPKG_BIN="${MAKEPKG_BIN:-/usr/bin/makepkg.orig}" + +# Source shared stale-lock helpers (co-located next to this wrapper). Fail SAFE: +# if the lib is missing/unreadable, do NOT break makepkg — exec the real binary +# and skip the pre-flight. A broken helper must never take down all builds. +_LIB_DIR="$(dirname "$(readlink -f "$0")")" +if [[ -r "$_LIB_DIR/pacman_lock_lib.sh" ]]; then + # shellcheck source=pacman_lock_lib.sh + source "$_LIB_DIR/pacman_lock_lib.sh" +else + exec "$MAKEPKG_BIN" "$@" +fi + +# Does this invocation ask makepkg to install (-i / --install)? Only install +# invocations reach makepkg's pacman lock-wait, so only then do we pre-clear. +# Handles bundled short flags (-i, -si, -sif, -if, ...). Stops at `--`. +invocation_installs() { + local arg + for arg in "$@"; do + [[ $arg == "--" ]] && return 1 + [[ $arg == "--install" ]] && return 0 + # Single-dash short-flag cluster containing 'i' (makepkg's -i = install). + [[ $arg == -[!-]* && $arg == *i* ]] && return 0 + done + return 1 +} + +# Bypass fast-path: inside a fakeroot build sandbox (makepkg re-execs itself for +# the package() stage — FAKEROOTKEY is set), or for any invocation that won't +# install (won't hit the lock-wait). Mirrors the pacman_wrapper FAKEROOTKEY +# guard. Exec real makepkg unchanged. +if [[ -n ${FAKEROOTKEY:-} ]] || ! invocation_installs "$@"; then + exec "$MAKEPKG_BIN" "$@" +fi + +# Best-effort pre-flight clear. `check_and_handle_db_lock` removes the lock only +# when no live pacman owns it; if a real pacman holds it, it returns non-zero and +# we deliberately DO NOT abort — makepkg's own wait will (correctly) wait it out. +# Hence `|| true`: a live-holder verdict is not a makepkg failure here. +check_and_handle_db_lock "$@" || true + +exec "$MAKEPKG_BIN" "$@" diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_lock_lib.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_lock_lib.sh new file mode 100755 index 00000000..83583bd3 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_lock_lib.sh @@ -0,0 +1,186 @@ +#!/bin/bash +# pacman_lock_lib.sh — shared stale pacman DB-lock handling. +# +# Single source of truth for detecting and clearing an orphaned +# /var/lib/pacman/db.lck. Sourced by BOTH pacman_wrapper.sh and +# makepkg_wrapper.sh so the stale-lock policy lives in exactly one place. +# +# This file only defines functions/vars — it must be *sourced*, never executed. +# It intentionally does NOT enable `set -euo pipefail`: the sourcing script owns +# its own shell options, and toggling them from a library would surprise callers. +# +# Testability: the lock path is taken from $PACMAN_LOCK_FILE (default +# /var/lib/pacman/db.lck) so the shell test harness can point it at a temp file +# instead of the real system lock. Production behaviour is unchanged when the +# variable is unset. + +# Guard against double-sourcing (both wrappers may source transitively). +# This file is meant to be sourced, so `return` is the correct exit here. +if [[ -n ${_PACMAN_LOCK_LIB_LOADED:-} ]]; then + return 0 +fi +_PACMAN_LOCK_LIB_LOADED=1 + +# Colors — only set if the caller has not already defined them, so a wrapper +# that defines its own palette keeps it. Values use literal escape sequences to +# match the callers' `echo -e` style. +: "${RED:=\033[0;31m}" +: "${GREEN:=\033[0;32m}" +: "${YELLOW:=\033[0;33m}" +: "${BLUE:=\033[0;34m}" +: "${CYAN:=\033[0;36m}" +: "${NC:=\033[0m}" + +# Lock file location (overridable for tests). +PACMAN_LOCK_FILE="${PACMAN_LOCK_FILE:-/var/lib/pacman/db.lck}" + +# Helper: detect if current invocation includes --noconfirm +has_noconfirm_flag() { + for arg in "$@"; do + if [[ $arg == "--noconfirm" ]]; then + return 0 + fi + done + return 1 +} + +# Current unix epoch (bash builtin, no fork). +current_epoch() { + printf '%(%s)T\n' -1 +} + +# Helper: get list of PIDs holding a lock file (excluding our own PID) +# Populates the $holders array +get_lock_holders() { + local lock_file="$1" + holders=() + if command -v fuser >/dev/null 2>&1; then + read -r -a holders <<< "$(fuser "$lock_file" 2>/dev/null || true)" + elif command -v lsof >/dev/null 2>&1; then + mapfile -t holders < <(lsof -t "$lock_file" 2>/dev/null || true) + fi + # Filter out our own PID + if [[ ${#holders[@]} -gt 0 ]]; then + local -a filtered=() + for pid in "${holders[@]}"; do + [[ $pid -eq $$ ]] && continue + filtered+=("$pid") + done + holders=("${filtered[@]}") + fi +} + +# Helper: is any real pacman/pamac transaction process running system-wide? +# Unlike fuser on the lock file, pgrep reads /proc/*/comm which is visible across +# users, so an UNPRIVILEGED caller (e.g. this lib running under makepkg, which is +# never root) can still detect a ROOT `pacman -Syu`. This closes the gap where a +# non-root fuser cannot see a root process's open fds and would wrongly conclude +# the lock is orphaned. Trade-off: a (rare) hung/zombie process literally named +# `pacman` will also block removal — erring toward safety over a rare re-hang. +pacman_process_running() { + pgrep -x pacman >/dev/null 2>&1 && return 0 + pgrep -x pacman.orig >/dev/null 2>&1 && return 0 + pgrep -x pamac >/dev/null 2>&1 && return 0 + return 1 +} + +# Handle stale pacman database lock if present and no package managers are running. +# Returns 0 if the lock is absent, was cleared, or the caller may proceed. +# Returns 1 if a genuine pacman/pamac transaction (or an un-freeable updater) +# holds the lock, or the user declined to remove it — caller must NOT proceed. +check_and_handle_db_lock() { + local lock_file="$PACMAN_LOCK_FILE" + # Quick exit if no lock + if [[ ! -e $lock_file ]]; then + return 0 + fi + + # Determine which processes actually have the lock open + local -a holders=() + get_lock_holders "$lock_file" + + if [[ ${#holders[@]} -gt 0 ]]; then + local pac_holder=0 + local gui_holder=0 + for pid in "${holders[@]}"; do + local comm args lower + comm=$(ps -p "$pid" -o comm= 2>/dev/null || true) + args=$(ps -p "$pid" -o args= 2>/dev/null || true) + lower="${comm,,} ${args,,}" + if [[ $lower == *" pacman"* || $lower == pacman* || $lower == *"/pacman "* || $lower == *" pamac"* ]]; then + pac_holder=1 + elif [[ $lower == *packagekit* || $lower == *gnome-software* || $lower == *discover* ]]; then + gui_holder=1 + fi + done + + if [[ $pac_holder -eq 1 ]]; then + echo -e "${RED}Another pacman/pamac transaction is holding the database lock. Try again later.${NC}" >&2 + return 1 + fi + + if [[ $gui_holder -eq 1 ]]; then + echo -e "${YELLOW}A background software updater is holding the pacman lock. Attempting to stop it...${NC}" >&2 + if command -v systemctl >/dev/null 2>&1; then + systemctl --quiet stop packagekit.service 2>/dev/null || true + systemctl --quiet stop packagekit 2>/dev/null || true + fi + pkill -x packagekitd 2>/dev/null || true + pkill -f gnome-software 2>/dev/null || true + pkill -f discover 2>/dev/null || true + sleep 1 + + # Re-check holders + get_lock_holders "$lock_file" + if [[ ${#holders[@]} -gt 0 ]]; then + echo -e "${RED}Cannot free the pacman lock; another process still holds it. Try again later.${NC}" >&2 + return 1 + fi + fi + fi + + # Cross-user safety net: even if fuser (possibly run unprivileged) could not + # see the holder, refuse to remove the lock while any real pacman/pamac + # process is running system-wide. + if pacman_process_running; then + echo -e "${RED}A pacman/pamac process is running; not removing the database lock. Try again later.${NC}" >&2 + return 1 + fi + + # Helper to remove a file with sudo if needed + remove_file_as_root() { + local f="$1" + if [[ $EUID -ne 0 ]]; then + sudo rm -f "$f" + else + rm -f "$f" + fi + } + + # Decide whether to remove the lock + local now epoch age + if epoch=$(stat -c %Y "$lock_file" 2>/dev/null); then + now=$(current_epoch) + age=$((now - epoch)) + else + age=999999 + fi + + # Auto-remove in non-interactive mode (--noconfirm) or if the lock is older than 10 minutes + if has_noconfirm_flag "$@" || [[ $age -ge 600 ]]; then + echo -e "${YELLOW}Stale pacman lock detected (age: ${age}s). Removing it automatically...${NC}" >&2 + remove_file_as_root "$lock_file" || return 1 + return 0 + fi + + # Interactive prompt (15s timeout) + echo -e "${YELLOW}A pacman lock exists but no active pacman is running.${NC}" >&2 + echo -e "${CYAN}Lock path:${NC} $lock_file (age: ${age}s)" >&2 + read -r -t 15 -p $'Remove stale lock and continue? [y/N]: ' reply || reply="n" + if [[ ${reply,,} == "y" || ${reply,,} == "yes" ]]; then + remove_file_as_root "$lock_file" || return 1 + return 0 + fi + echo -e "${RED}Aborting due to existing pacman lock. Close other updaters and retry, or run with --noconfirm to auto-clear stale locks.${NC}" >&2 + return 1 +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh index c5ddbb0e..75deeeae 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh @@ -378,20 +378,6 @@ function is_greylisted_package_name() { return 1 } -# Helper: detect if current invocation includes --noconfirm -function has_noconfirm_flag() { - for arg in "$@"; do - if [[ $arg == "--noconfirm" ]]; then - return 0 - fi - done - return 1 -} - -current_epoch() { - printf '%(%s)T\n' -1 -} - current_day_of_week() { printf '%(%u)T\n' -1 } @@ -404,116 +390,10 @@ current_day_name() { printf '%(%A)T\n' -1 } -# Helper: get list of PIDs holding a lock file (excluding our own PID) -# Populates the $holders array -get_lock_holders() { - local lock_file="$1" - holders=() - if command -v fuser >/dev/null 2>&1; then - read -r -a holders <<< "$(fuser "$lock_file" 2>/dev/null || true)" - elif command -v lsof >/dev/null 2>&1; then - mapfile -t holders < <(lsof -t "$lock_file" 2>/dev/null || true) - fi - # Filter out our own PID - if [[ ${#holders[@]} -gt 0 ]]; then - local -a filtered=() - for pid in "${holders[@]}"; do - [[ $pid -eq $$ ]] && continue - filtered+=("$pid") - done - holders=("${filtered[@]}") - fi -} - -# Handle stale pacman database lock if present and no package managers are running -check_and_handle_db_lock() { - local lock_file="/var/lib/pacman/db.lck" - # Quick exit if no lock - if [[ ! -e $lock_file ]]; then - return 0 - fi - - # Determine which processes actually have the lock open - local -a holders=() - get_lock_holders "$lock_file" - - if [[ ${#holders[@]} -gt 0 ]]; then - local pac_holder=0 - local gui_holder=0 - for pid in "${holders[@]}"; do - local comm args lower - comm=$(ps -p "$pid" -o comm= 2>/dev/null || true) - args=$(ps -p "$pid" -o args= 2>/dev/null || true) - lower="${comm,,} ${args,,}" - if [[ $lower == *" pacman"* || $lower == pacman* || $lower == *"/pacman "* || $lower == *" pamac"* ]]; then - pac_holder=1 - elif [[ $lower == *packagekit* || $lower == *gnome-software* || $lower == *discover* ]]; then - gui_holder=1 - fi - done - - if [[ $pac_holder -eq 1 ]]; then - echo -e "${RED}Another pacman/pamac transaction is holding the database lock. Try again later.${NC}" >&2 - return 1 - fi - - if [[ $gui_holder -eq 1 ]]; then - echo -e "${YELLOW}A background software updater is holding the pacman lock. Attempting to stop it...${NC}" >&2 - if command -v systemctl >/dev/null 2>&1; then - systemctl --quiet stop packagekit.service 2>/dev/null || true - systemctl --quiet stop packagekit 2>/dev/null || true - fi - pkill -x packagekitd 2>/dev/null || true - pkill -f gnome-software 2>/dev/null || true - pkill -f discover 2>/dev/null || true - sleep 1 - - # Re-check holders - get_lock_holders "$lock_file" - if [[ ${#holders[@]} -gt 0 ]]; then - echo -e "${RED}Cannot free the pacman lock; another process still holds it. Try again later.${NC}" >&2 - return 1 - fi - fi - fi - - # Helper to remove a file with sudo if needed - remove_file_as_root() { - local f="$1" - if [[ $EUID -ne 0 ]]; then - sudo rm -f "$f" - else - rm -f "$f" - fi - } - - # Decide whether to remove the lock - local now epoch age - if epoch=$(stat -c %Y "$lock_file" 2>/dev/null); then - now=$(current_epoch) - age=$((now - epoch)) - else - age=999999 - fi - - # Auto-remove in non-interactive mode (--noconfirm) or if the lock is older than 10 minutes - if has_noconfirm_flag "$@" || [[ $age -ge 600 ]]; then - echo -e "${YELLOW}Stale pacman lock detected (age: ${age}s). Removing it automatically...${NC}" >&2 - remove_file_as_root "$lock_file" || return 1 - return 0 - fi - - # Interactive prompt (15s timeout) - echo -e "${YELLOW}A pacman lock exists but no active pacman is running.${NC}" >&2 - echo -e "${CYAN}Lock path:${NC} $lock_file (age: ${age}s)" >&2 - read -r -t 15 -p $'Remove stale lock and continue? [y/N]: ' reply || reply="n" - if [[ ${reply,,} == "y" || ${reply,,} == "yes" ]]; then - remove_file_as_root "$lock_file" || return 1 - return 0 - fi - echo -e "${RED}Aborting due to existing pacman lock. Close other updaters and retry, or run with --noconfirm to auto-clear stale locks.${NC}" >&2 - return 1 -} +# Stale pacman DB-lock detection/handling (get_lock_holders, +# check_and_handle_db_lock) plus has_noconfirm_flag and current_epoch now live in +# the shared pacman_lock_lib.sh, sourced after the integrity check below. This is +# the single source of truth shared with makepkg_wrapper.sh. # Generic function to remove installed packages matching a filter # Args: check_function label_prefix @@ -791,6 +671,22 @@ if ! verify_policy_integrity; then exit 1 fi +# Load shared stale-lock helpers (get_lock_holders, check_and_handle_db_lock, +# has_noconfirm_flag, current_epoch), AFTER integrity verification so a tampered +# lib (listed in the integrity manifest) is caught before it is sourced. Sourced +# here — past the fast-path — so build-time non-root calls skip it entirely. +_WRAPPER_DIR="$(dirname "$(readlink -f "$0")")" +if [[ -r "$_WRAPPER_DIR/pacman_lock_lib.sh" ]]; then + # shellcheck source=pacman_lock_lib.sh + source "$_WRAPPER_DIR/pacman_lock_lib.sh" +else + # Fail open: a missing lib must not brick pacman (you'd need pacman to + # reinstall it). Skip stale-lock handling with safe no-op fallbacks. + echo -e "${YELLOW}Warning: pacman_lock_lib.sh missing; stale-lock handling disabled.${NC}" >&2 + check_and_handle_db_lock() { return 0; } + current_epoch() { printf '%(%s)T\n' -1; } +fi + # Before any pacman action, ensure maintenance services exist ensure_periodic_maintenance diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/rewrap_pkg_managers.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/rewrap_pkg_managers.sh new file mode 100755 index 00000000..c44380a8 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/rewrap_pkg_managers.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# rewrap_pkg_managers.sh — restore the pacman + makepkg wrapper symlinks. +# +# A `pacman-git` upgrade reinstalls /usr/bin/pacman and /usr/bin/makepkg as the +# stock binaries, clobbering our wrapper symlinks. This helper re-establishes +# them (refreshing the .orig backups from the freshly installed binaries) so the +# wrappers survive upgrades. +# +# It is invoked by the PostTransaction pacman hook (96-restore-pkg-wrappers.hook) +# and by the periodic drift verifier. It does FILE OPS ONLY — it never calls +# pacman — so it is safe to run inside a pacman transaction hook without deadlock. + +set -euo pipefail + +# rewrap +rewrap() { + local real="$1" orig="$2" wrapper="$3" + # Nothing to point at — skip rather than create a dangling symlink. + if [[ ! -e $wrapper ]]; then + echo "rewrap: wrapper missing, skipping: $wrapper" >&2 + return 0 + fi + if [[ ! -L $real ]]; then + # Real binary present (fresh install, or an upgrade replaced our symlink): + # refresh the .orig backup, then re-point the symlink at the wrapper. + # When $real IS already our symlink we must NOT copy — that would put the + # wrapper into .orig and cause an exec loop. + [[ -e $real ]] && cp -f "$real" "$orig" + fi + ln -sf "$wrapper" "$real" +} + +rewrap /usr/bin/pacman /usr/bin/pacman.orig /usr/local/bin/pacman_wrapper +rewrap /usr/bin/makepkg /usr/bin/makepkg.orig /usr/local/bin/makepkg_wrapper diff --git a/linux_configuration/scripts/periodic_background/setup_periodic_system.sh b/linux_configuration/scripts/periodic_background/setup_periodic_system.sh index d5c0cfa5..58d33961 100755 --- a/linux_configuration/scripts/periodic_background/setup_periodic_system.sh +++ b/linux_configuration/scripts/periodic_background/setup_periodic_system.sh @@ -19,7 +19,8 @@ CONFIG_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" # Define paths PACMAN_WRAPPER_SCRIPT="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh" PACMAN_WRAPPER_INSTALL="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/pacman/install_pacman_wrapper.sh" -HOSTS_INSTALL_SCRIPT="$CONFIG_DIR/hosts/install.sh" +MAKEPKG_WRAPPER_INSTALL="$CONFIG_DIR/scripts/periodic_background/digital_wellbeing/pacman/install_makepkg_wrapper.sh" +HOSTS_INSTALL_SCRIPT="$CONFIG_DIR/scripts/periodic_background/hosts/install.sh" echo "" echo "Configuration directory: $CONFIG_DIR" @@ -62,6 +63,10 @@ verify_files() { missing_files+=("$PACMAN_WRAPPER_INSTALL") fi + if [[ ! -f $MAKEPKG_WRAPPER_INSTALL ]]; then + missing_files+=("$MAKEPKG_WRAPPER_INSTALL") + fi + if [[ ! -f $HOSTS_INSTALL_SCRIPT ]]; then missing_files+=("$HOSTS_INSTALL_SCRIPT") fi @@ -106,6 +111,7 @@ create_execution_script() { # Install from template with path substitutions sed \ -e "s|__PACMAN_WRAPPER_INSTALL__|$PACMAN_WRAPPER_INSTALL|g" \ + -e "s|__MAKEPKG_WRAPPER_INSTALL__|$MAKEPKG_WRAPPER_INSTALL|g" \ -e "s|__HOSTS_INSTALL_SCRIPT__|$HOSTS_INSTALL_SCRIPT|g" \ "$TEMPLATE_MAINT_SCRIPT" > "$exec_script" diff --git a/linux_configuration/scripts/periodic_background/system-maintenance/bin/periodic-system-maintenance.sh b/linux_configuration/scripts/periodic_background/system-maintenance/bin/periodic-system-maintenance.sh index 2127dafa..44fb83b1 100755 --- a/linux_configuration/scripts/periodic_background/system-maintenance/bin/periodic-system-maintenance.sh +++ b/linux_configuration/scripts/periodic_background/system-maintenance/bin/periodic-system-maintenance.sh @@ -9,6 +9,7 @@ LOG_FILE="/var/log/periodic-system-maintenance.log" # Path placeholders replaced at install time PACMAN_WRAPPER_INSTALL="__PACMAN_WRAPPER_INSTALL__" +MAKEPKG_WRAPPER_INSTALL="__MAKEPKG_WRAPPER_INSTALL__" HOSTS_INSTALL_SCRIPT="__HOSTS_INSTALL_SCRIPT__" # Function to log with timestamp @@ -44,6 +45,7 @@ log_message "=== Periodic System Maintenance Started ===" # Install pacman wrapper execute_with_log "$PACMAN_WRAPPER_INSTALL" "Pacman Wrapper Installation" +execute_with_log "$MAKEPKG_WRAPPER_INSTALL" "Makepkg Wrapper Installation" # Update hosts file execute_with_log "$HOSTS_INSTALL_SCRIPT" "Hosts File Update" diff --git a/linux_configuration/tests/test_makepkg_wrapper.sh b/linux_configuration/tests/test_makepkg_wrapper.sh new file mode 100755 index 00000000..60f4c411 --- /dev/null +++ b/linux_configuration/tests/test_makepkg_wrapper.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# Tests for the makepkg wrapper + shared stale-lock library. +# +# Covers: script syntax, structural invariants (fail-open source guard, +# FAKEROOTKEY bypass, .orig-refresh, integrity), and the actual stale-lock +# behaviour exercised against a TEMP lock file (never the real system lock). + +set -u + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PAC_DIR="$SCRIPT_DIR/../scripts/periodic_background/digital_wellbeing/pacman" +LIB="$PAC_DIR/pacman_lock_lib.sh" +WRAPPER="$PAC_DIR/makepkg_wrapper.sh" +INSTALLER="$PAC_DIR/install_makepkg_wrapper.sh" +REWRAP="$PAC_DIR/rewrap_pkg_managers.sh" +HOOK="$PAC_DIR/96-restore-pkg-wrappers.hook" + +pass=0 +fail=0 +ok() { echo "✓ $1"; pass=$((pass + 1)); } +bad() { echo "✗ $1"; fail=$((fail + 1)); } + +echo "=== Testing makepkg wrapper + shared stale-lock library ===" + +# --- Syntax --------------------------------------------------------------- +for f in "$LIB" "$WRAPPER" "$INSTALLER" "$REWRAP"; do + if bash -n "$f"; then ok "syntax valid: $(basename "$f")"; else bad "syntax error: $(basename "$f")"; fi +done + +# --- Structural invariants ------------------------------------------------ +if grep -q 'PACMAN_LOCK_FILE:-/var/lib/pacman/db.lck' "$LIB"; then + ok "lib lock path is overridable for tests (defaults to real path)" +else bad "lib does not parameterise PACMAN_LOCK_FILE"; fi + +if grep -q 'pacman_process_running' "$LIB"; then + ok "lib has cross-user pgrep guard (pacman_process_running)" +else bad "lib missing cross-user pgrep guard"; fi + +if grep -q 'FAKEROOTKEY' "$WRAPPER"; then + ok "wrapper has FAKEROOTKEY bypass fast-path" +else bad "wrapper missing FAKEROOTKEY bypass"; fi + +# Fail-open: a missing lib must exec real makepkg rather than break it. +if awk '/-r "\$_LIB_DIR\/pacman_lock_lib.sh"/{g=1} g&&/exec "\$MAKEPKG_BIN"/{seen=1} END{exit !seen}' "$WRAPPER"; then + ok "wrapper fails OPEN when lib missing (execs real makepkg)" +else bad "wrapper does not fail open on missing lib"; fi + +if grep -q 'if \[ ! -L /usr/bin/makepkg \]' "$INSTALLER"; then + ok "installer refreshes makepkg.orig only when not our symlink (no exec loop)" +else bad "installer .orig guard missing/incorrect"; fi + +if grep -q 'When = PostTransaction' "$HOOK" && grep -q 'rewrap_pkg_managers.sh' "$HOOK"; then + ok "survival hook is PostTransaction and runs rewrap helper" +else bad "survival hook malformed"; fi + +# Invalid alpm Operation values abort EVERY pacman transaction (alpm parses all +# hooks up front), so the hook must use only Install/Upgrade/Remove. +if grep -E '^Operation' "$HOOK" | grep -qvE '^Operation = (Install|Upgrade|Remove)$'; then + bad "hook has an invalid alpm Operation value (would break all pacman transactions)" +else ok "hook uses only valid alpm Operation values"; fi + +# --- Functional (temp lock, no root) -------------------------------------- +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"; jobs -p | xargs -r kill 2>/dev/null || true' EXIT +LOCK="$TMP/db.lck" +STUB="$TMP/makepkg.stub" +printf '#!/bin/bash\nprintf "%%s" "$*" > "%s/args"\n' "$TMP" > "$STUB" +chmod +x "$STUB" + +# Source the lib once. check_and_handle_db_lock reads $PACMAN_LOCK_FILE at call +# time, so pointing it at the temp lock before sourcing is enough for all cases. +export PACMAN_LOCK_FILE="$LOCK" +# shellcheck source=../scripts/periodic_background/digital_wellbeing/pacman/pacman_lock_lib.sh +source "$LIB" + +# orphaned + --noconfirm -> cleared +: > "$LOCK" +check_and_handle_db_lock --noconfirm >/dev/null 2>&1 || true +if [[ ! -e $LOCK ]]; then ok "orphaned lock + --noconfirm cleared"; else bad "orphaned lock not cleared"; fi + +# absent -> no-op, returns 0 +if check_and_handle_db_lock --noconfirm >/dev/null 2>&1; then ok "absent lock is a no-op"; else bad "absent lock returned non-zero"; fi + +# old (>=600s) -> auto-removed without --noconfirm +: > "$LOCK" +touch -d '2 hours ago' "$LOCK" +check_and_handle_db_lock >/dev/null 2>&1 || true +if [[ ! -e $LOCK ]]; then ok "old orphaned lock auto-removed without --noconfirm"; else bad "age-based removal failed"; fi + +# running 'pacman' process -> removal blocked (cross-user pgrep guard) +: > "$LOCK" +FAKE="$TMP/pacman" +cp /bin/sleep "$FAKE" +setsid "$FAKE" 20 & +fp=$! +sleep 0.3 +if check_and_handle_db_lock --noconfirm >/dev/null 2>&1; then blocked=0; else blocked=1; fi +if [[ $blocked -eq 1 && -e $LOCK ]]; then ok "running 'pacman' process blocks removal"; else bad "pgrep guard failed"; fi +kill "$fp" 2>/dev/null || true +sleep 0.2 +rm -f "$LOCK" + +# wrapper: non-install invocation bypasses, leaves lock, execs real makepkg +: > "$LOCK" +MAKEPKG_BIN="$STUB" bash "$WRAPPER" --version >/dev/null 2>&1 || true +if [[ -e $LOCK && "$(cat "$TMP/args" 2>/dev/null)" == "--version" ]]; then ok "wrapper: non-install bypasses, lock untouched"; else bad "wrapper non-install bypass failed"; fi + +# wrapper: -sif clears orphaned lock then execs real makepkg +: > "$LOCK" +MAKEPKG_BIN="$STUB" bash "$WRAPPER" -sif --noconfirm >/dev/null 2>&1 || true +if [[ ! -e $LOCK && "$(cat "$TMP/args" 2>/dev/null)" == "-sif --noconfirm" ]]; then ok "wrapper: -sif clears orphaned lock then execs makepkg"; else bad "wrapper install pre-flight failed"; fi + +# wrapper: FAKEROOTKEY (build sandbox) bypasses, keeps lock +: > "$LOCK" +MAKEPKG_BIN="$STUB" FAKEROOTKEY=deadbeef bash "$WRAPPER" -sif --noconfirm >/dev/null 2>&1 || true +if [[ -e $LOCK ]]; then ok "wrapper: FAKEROOTKEY bypass keeps lock"; else bad "wrapper fakeroot bypass failed"; fi + +echo "" +echo "=== Result: $pass passed, $fail failed ===" +if [[ $fail -eq 0 ]]; then echo "=== All Tests Passed! ==="; else exit 1; fi diff --git a/linux_configuration/tests/test_pacman_wrapper_security.sh b/linux_configuration/tests/test_pacman_wrapper_security.sh index 045b0aea..530b4352 100755 --- a/linux_configuration/tests/test_pacman_wrapper_security.sh +++ b/linux_configuration/tests/test_pacman_wrapper_security.sh @@ -4,8 +4,8 @@ set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -WRAPPER_DIR="$SCRIPT_DIR/../scripts/digital_wellbeing/pacman" -VBOX_DIR="$SCRIPT_DIR/../scripts/digital_wellbeing/virtualbox" +WRAPPER_DIR="$SCRIPT_DIR/../scripts/periodic_background/digital_wellbeing/pacman" +VBOX_DIR="$SCRIPT_DIR/../scripts/periodic_background/digital_wellbeing/virtualbox" echo "=== Testing Pacman Wrapper Security Enhancements ===" echo "" @@ -182,6 +182,42 @@ else exit 1 fi +# Test: shared lock library syntax +echo "[TEST] Checking shared lock library syntax..." +if bash -n "$WRAPPER_DIR/pacman_lock_lib.sh"; then + echo "✓ pacman_lock_lib.sh syntax is valid" +else + echo "✗ pacman_lock_lib.sh has syntax errors" + exit 1 +fi + +# Test: wrapper sources the shared lock library +echo "[TEST] Verifying wrapper sources the shared lock library..." +if grep -q 'source .*pacman_lock_lib.sh' "$WRAPPER_DIR/pacman_wrapper.sh"; then + echo "✓ Wrapper sources pacman_lock_lib.sh" +else + echo "✗ Wrapper does not source pacman_lock_lib.sh" + exit 1 +fi + +# Test: the lock library is covered by the integrity manifest +echo "[TEST] Verifying installer checksums the lock library..." +if grep -qF 'sha256sum "$LOCK_LIB_DEST"' "$WRAPPER_DIR/install_pacman_wrapper.sh"; then + echo "✓ Lock library is included in the integrity manifest" +else + echo "✗ Lock library not checksummed by installer" + exit 1 +fi + +# Test: lib is sourced AFTER integrity verification (tamper caught before source) +echo "[TEST] Verifying lock library is sourced after integrity check..." +if awk '/verify_policy_integrity/{v=NR} /source .*pacman_lock_lib.sh/{s=NR} END{exit !(v && s && s>v)}' "$WRAPPER_DIR/pacman_wrapper.sh"; then + echo "✓ Lock library sourced after verify_policy_integrity" +else + echo "✗ Lock library not sourced after integrity verification" + exit 1 +fi + echo "" echo "=== All Tests Passed! ===" echo ""