From d8decf6a3fd83eb1fefc2f56a68080e687ec39d9 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 9 Sep 2026 09:26:57 +0000 Subject: [PATCH 1/2] Stop upgrade workers and exclude installation during uninstall --- scripts/test-daemon-lifecycle.sh | 93 ++++++++++++++++++++++++++------ scripts/uninstall.sh | 24 ++++++++- 2 files changed, 98 insertions(+), 19 deletions(-) diff --git a/scripts/test-daemon-lifecycle.sh b/scripts/test-daemon-lifecycle.sh index ca84459f77..2f3de40eff 100755 --- a/scripts/test-daemon-lifecycle.sh +++ b/scripts/test-daemon-lifecycle.sh @@ -7,12 +7,22 @@ TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT mkdir -p "$TMP/bin" "$TMP/install" "$TMP/systemd" "$TMP/state" "$TMP/run" LOG=$TMP/commands.log +SCENARIO=active +export SCENARIO export LOG : > "$LOG" cat > "$TMP/bin/docker" <<'EOF' #!/bin/sh echo "docker $*" >> "$LOG" +[ ! -e "$PLOYZ_RUN_DIR/worker" ] && [ ! -e "$PLOYZ_RUN_DIR/daemon" ] || { + echo "unsafe cleanup: active upgrade worker or daemon" | tee -a "$LOG" >&2 + exit 1 +} +if flock -n "$PLOYZ_RUN_DIR/.install.lock" true; then + echo "unsafe cleanup: no installation ownership" | tee -a "$LOG" >&2 + exit 1 +fi case "$*" in 'ps -aq --filter label=ployz.managed') echo managed-container ;; 'ps -aq --filter name=^/ployz-corrosion$') echo corrosion-container ;; @@ -32,6 +42,30 @@ EOF cat > "$TMP/bin/systemctl" <<'EOF' #!/bin/sh echo "systemctl $*" >> "$LOG" +case "$*" in + 'list-units '*) + [ "$SCENARIO" != list-failure ] || exit 1 + case "$*" in + *'ployz-upgrade-*.service') + [ ! -e "$PLOYZ_RUN_DIR/worker" ] || echo 'ployz-upgrade-test.service loaded active running Upgrade' + ;; + *'ployz.service') + [ ! -e "$PLOYZ_RUN_DIR/daemon" ] || echo 'ployz.service loaded active running Daemon' + ;; + esac + ;; + 'stop ployz-upgrade-test.service') + [ "$SCENARIO" != worker-stop-failure ] || exit 1 + rm -f "$PLOYZ_RUN_DIR/worker" + ;; + 'stop ployz.service') + [ "$SCENARIO" != daemon-stop-failure ] || exit 1 + rm -f "$PLOYZ_RUN_DIR/daemon" + # An accepted worker can appear after the initial worker stop. + [ "$SCENARIO" != late ] || touch "$PLOYZ_RUN_DIR/worker" + ;; +esac +exit 0 EOF cat > "$TMP/bin/ip" <<'EOF' #!/bin/sh @@ -39,27 +73,52 @@ echo "ip $*" >> "$LOG" exit 0 EOF for command in getent userdel groupdel; do - cp "$TMP/bin/systemctl" "$TMP/bin/$command" + cp "$TMP/bin/ip" "$TMP/bin/$command" done chmod 0755 "$TMP/bin"/* -touch "$TMP/install/ployzd" "$TMP/install/ployz-uninstall" "$TMP/install/ployz-corrosion" -chmod 0755 "$TMP/install/ployzd" "$TMP/install/ployz-uninstall" -touch "$TMP/docker" "$TMP/images" "$TMP/volumes" "$TMP/docker-config" +run_uninstall() { + sudo env PATH="$TMP/bin:$PATH" LOG="$LOG" SCENARIO="$SCENARIO" PLOYZ_AUTO_CONFIRM=true \ + INSTALL_BIN_DIR="$TMP/install" INSTALL_SYSTEMD_DIR="$TMP/systemd" \ + PLOYZ_DATA_DIR="$TMP/state" PLOYZ_RUN_DIR="$TMP/run" bash "$ROOT/scripts/uninstall.sh" +} -sudo env PATH="$TMP/bin:$PATH" LOG="$LOG" PLOYZ_AUTO_CONFIRM=true INSTALL_BIN_DIR="$TMP/install" \ - INSTALL_SYSTEMD_DIR="$TMP/systemd" PLOYZ_DATA_DIR="$TMP/state" PLOYZ_RUN_DIR="$TMP/run" \ - bash "$ROOT/scripts/uninstall.sh" +for SCENARIO in active late absent worker-stop-failure list-failure daemon-stop-failure busy; do + : > "$LOG" + mkdir -p "$TMP/state" "$TMP/run" + touch "$TMP/state/receipt" "$TMP/run/socket" + if [ "$SCENARIO" != absent ]; then touch "$TMP/run/worker" "$TMP/run/daemon"; fi + touch "$TMP/install/ployzd" "$TMP/install/ployz-uninstall" "$TMP/install/ployz-corrosion" + chmod 0755 "$TMP/install/ployzd" "$TMP/install/ployz-uninstall" + touch "$TMP/docker" "$TMP/images" "$TMP/volumes" "$TMP/docker-config" -[ ! -e "$TMP/install/ployzd" ] -[ ! -e "$TMP/install/ployz-uninstall" ] -[ -e "$TMP/install/ployz-corrosion" ] -[ ! -e "$TMP/state" ] -[ ! -e "$TMP/run" ] -[ -f "$TMP/docker" ] && [ -f "$TMP/images" ] && [ -f "$TMP/volumes" ] && [ -f "$TMP/docker-config" ] -grep -Fq 'docker rm -f managed-container' "$LOG" -grep -Fq 'docker rm -f corrosion-container' "$LOG" -grep -Fq 'docker network rm ployz-network' "$LOG" -grep -Fq 'ip link delete ployz' "$LOG" + if [ "$SCENARIO" = busy ]; then + exec {lock_fd}>"$TMP/run/.install.lock" + flock -n "$lock_fd" + fi + if run_uninstall; then + case "$SCENARIO" in *failure|busy) echo "unexpected uninstall success: $SCENARIO" >&2; exit 1 ;; esac + [ ! -e "$TMP/install/ployzd" ] + [ ! -e "$TMP/install/ployz-uninstall" ] + [ ! -e "$TMP/state" ] + # Keep the lock inode: unlinking it lets a concurrent installer bypass ownership. + [ "$(ls -A "$TMP/run")" = .install.lock ] + sudo flock -n "$TMP/run/.install.lock" true + grep -Fq 'docker rm -f managed-container' "$LOG" + grep -Fq 'docker rm -f corrosion-container' "$LOG" + grep -Fq 'docker network rm ployz-network' "$LOG" + grep -Fq 'ip link delete ployz' "$LOG" + else + case "$SCENARIO" in *failure|busy) ;; *) echo "unexpected uninstall failure: $SCENARIO" >&2; exit 1 ;; esac + [ -e "$TMP/install/ployzd" ] && [ -e "$TMP/install/ployz-uninstall" ] + [ -e "$TMP/state/receipt" ] && [ -e "$TMP/run/socket" ] + if grep -q '^docker ' "$LOG"; then exit 1; fi + fi + if grep -q '^unsafe cleanup:' "$LOG"; then exit 1; fi + if [ "$SCENARIO" = busy ]; then exec {lock_fd}>&-; fi + [ -e "$TMP/install/ployz-corrosion" ] + [ -f "$TMP/docker" ] && [ -f "$TMP/images" ] && [ -f "$TMP/volumes" ] && [ -f "$TMP/docker-config" ] + sudo rm -rf "$TMP/run" "$TMP/state" +done echo "destructive daemon uninstall contract passed" diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index c5ddcbe099..ba821c3624 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -22,6 +22,15 @@ safe_owned_directory() { case "$1" in /var/lib/ployz|/run/ployz|/tmp/*) return ;; *) return 1 ;; esac } +stop_loaded_units() { + local units unit rest + units=$(systemctl list-units --all --plain --no-legend "$1") || error "Cannot inspect $1; uninstall aborted" + while read -r unit rest; do + [ -n "$unit" ] || continue + systemctl stop "$unit" || error "Cannot stop $unit; uninstall aborted" + done <<< "$units" +} + main() { [ "$EUID" -eq 0 ] || error "Run this uninstaller with sudo or as root" safe_owned_directory "$PLOYZ_DATA_DIR" || error "Refusing unsafe data directory: $PLOYZ_DATA_DIR" @@ -32,7 +41,16 @@ main() { return fi - systemctl stop ployz.service 2>/dev/null || true + stop_loaded_units 'ployz-upgrade-*.service' + # Share MutationGate's flock so installation cannot race destructive cleanup. + umask 077 + mkdir -p "$PLOYZ_RUN_DIR" + chmod 0750 "$PLOYZ_RUN_DIR" + exec {installation_lock}>"$PLOYZ_RUN_DIR/.install.lock" + flock -n "$installation_lock" || error "Ployz mutation or installation is active; retry uninstall" + stop_loaded_units ployz.service + # Catch an accepted worker launched during the first stop, now blocked on our lock. + stop_loaded_units 'ployz-upgrade-*.service' if command -v docker >/dev/null 2>&1; then readarray -t containers < <(docker ps -aq --filter label=ployz.managed) if [ "${#containers[@]}" -gt 0 ]; then @@ -60,7 +78,9 @@ main() { ip link delete ployz fi - rm -rf "$PLOYZ_DATA_DIR" "$PLOYZ_RUN_DIR" + rm -rf "$PLOYZ_DATA_DIR" + # Preserve the locked inode until reboot; replacing it would bypass exclusion. + find "$PLOYZ_RUN_DIR" -mindepth 1 -maxdepth 1 ! -name .install.lock -exec rm -rf -- {} + if id "$PLOYZ_USER" >/dev/null 2>&1; then userdel "$PLOYZ_USER"; fi if getent group "$PLOYZ_USER" >/dev/null 2>&1; then groupdel "$PLOYZ_USER"; fi rm -f "$INSTALL_BIN_DIR/ployz-uninstall" From 17d1fec2da0df6ea31f49d7a2d01f22ba19a0665 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 9 Sep 2026 10:15:03 +0000 Subject: [PATCH 2/2] Protect uninstall lock ownership and reject unsafe lock entries --- scripts/test-daemon-lifecycle.sh | 38 +++++++++++++++++++++++++++++--- scripts/uninstall.sh | 9 +++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/scripts/test-daemon-lifecycle.sh b/scripts/test-daemon-lifecycle.sh index 2f3de40eff..8656286756 100755 --- a/scripts/test-daemon-lifecycle.sh +++ b/scripts/test-daemon-lifecycle.sh @@ -4,7 +4,7 @@ set -euo pipefail ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) TMP=$(mktemp -d) -trap 'rm -rf "$TMP"' EXIT +trap 'sudo rm -rf "$TMP"' EXIT mkdir -p "$TMP/bin" "$TMP/install" "$TMP/systemd" "$TMP/state" "$TMP/run" LOG=$TMP/commands.log SCENARIO=active @@ -83,7 +83,7 @@ run_uninstall() { PLOYZ_DATA_DIR="$TMP/state" PLOYZ_RUN_DIR="$TMP/run" bash "$ROOT/scripts/uninstall.sh" } -for SCENARIO in active late absent worker-stop-failure list-failure daemon-stop-failure busy; do +for SCENARIO in symlink-failure dangling-failure fifo-failure directory-failure active late absent worker-stop-failure list-failure daemon-stop-failure busy; do : > "$LOG" mkdir -p "$TMP/state" "$TMP/run" touch "$TMP/state/receipt" "$TMP/run/socket" @@ -92,11 +92,43 @@ for SCENARIO in active late absent worker-stop-failure list-failure daemon-stop- chmod 0755 "$TMP/install/ployzd" "$TMP/install/ployz-uninstall" touch "$TMP/docker" "$TMP/images" "$TMP/volumes" "$TMP/docker-config" + if [ "$SCENARIO" = symlink-failure ]; then + printf 'protected target\n' | sudo tee "$TMP/protected-target" >/dev/null + sudo chmod 0600 "$TMP/protected-target" + ln -s "$TMP/protected-target" "$TMP/run/.install.lock" + fi + case "$SCENARIO" in + dangling-failure) ln -s "$TMP/missing-target" "$TMP/run/.install.lock" ;; + fifo-failure) mkfifo "$TMP/run/.install.lock" ;; + directory-failure) mkdir "$TMP/run/.install.lock" ;; + active) printf 'existing lock\n' > "$TMP/run/.install.lock" ;; + esac if [ "$SCENARIO" = busy ]; then exec {lock_fd}>"$TMP/run/.install.lock" flock -n "$lock_fd" fi - if run_uninstall; then + result=0 + run_uninstall || result=$? + case "$SCENARIO" in + worker-stop-failure|list-failure) ;; + *) [ "$(sudo stat -c %u "$TMP/run")" = 0 ] || { + echo "runtime directory is not root-controlled" >&2 + exit 1 + } ;; + esac + # Restore test access only after uninstall has exited. + sudo chown "$(id -u):$(id -g)" "$TMP/run" + [ ! -e "$TMP/missing-target" ] + if [ "$SCENARIO" = active ]; then + [ "$(sudo cat "$TMP/run/.install.lock")" = 'existing lock' ] + fi + if [ "$SCENARIO" = symlink-failure ]; then + [ "$(sudo cat "$TMP/protected-target")" = 'protected target' ] || { + echo "uninstall truncated the symlink target" >&2 + exit 1 + } + fi + if [ "$result" -eq 0 ]; then case "$SCENARIO" in *failure|busy) echo "unexpected uninstall success: $SCENARIO" >&2; exit 1 ;; esac [ ! -e "$TMP/install/ployzd" ] [ ! -e "$TMP/install/ployz-uninstall" ] diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index ba821c3624..3046f5b860 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -44,9 +44,16 @@ main() { stop_loaded_units 'ployz-upgrade-*.service' # Share MutationGate's flock so installation cannot race destructive cleanup. umask 077 + [ ! -L "$PLOYZ_RUN_DIR" ] || error "Refusing symlink runtime directory: $PLOYZ_RUN_DIR" mkdir -p "$PLOYZ_RUN_DIR" + # Remove the service account's ability to swap the lock between inspection and open. + chown root:root "$PLOYZ_RUN_DIR" chmod 0750 "$PLOYZ_RUN_DIR" - exec {installation_lock}>"$PLOYZ_RUN_DIR/.install.lock" + local lock_path="$PLOYZ_RUN_DIR/.install.lock" + if [ -L "$lock_path" ] || { [ -e "$lock_path" ] && [ ! -f "$lock_path" ]; }; then + error "Refusing symlink or non-regular installation lock: $lock_path" + fi + exec {installation_lock}<>"$lock_path" flock -n "$installation_lock" || error "Ployz mutation or installation is active; retry uninstall" stop_loaded_units ployz.service # Catch an accepted worker launched during the first stop, now blocked on our lock.