mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-15 09:10:47 +01:00
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsCs1GUyiinA7bg68YvAh1
This commit is contained in:
parent
93308e15ab
commit
2a304691cb
@ -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 <pkg>` (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"
|
||||
}
|
||||
@ -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."
|
||||
]
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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() { # <src> <dest> <mode>
|
||||
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}"
|
||||
@ -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}"
|
||||
|
||||
@ -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" "$@"
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 <real_path> <orig_backup> <wrapper_dest>
|
||||
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
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
120
linux_configuration/tests/test_makepkg_wrapper.sh
Executable file
120
linux_configuration/tests/test_makepkg_wrapper.sh
Executable file
@ -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
|
||||
@ -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 ""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user