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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 109 additions & 18 deletions scripts/test-daemon-lifecycle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,25 @@ 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
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 ;;
Expand All @@ -32,34 +42,115 @@ 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
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 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"
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" = 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
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" ]
[ ! -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"
31 changes: 29 additions & 2 deletions scripts/uninstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -32,7 +41,23 @@ 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
[ ! -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"
Comment on lines +50 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve operator access when uninstall aborts

When the installation lock is busy or ployz.service fails to stop, execution exits after this changes /run/ployz from the installer-established ployz:ployz ownership to root:root with mode 0750. The surviving installation's socket remains group-owned by ployz, but ordinary operators in that group can no longer traverse the runtime directory, so all non-root CLI connections fail even though uninstall reported that it aborted before cleanup. Preserve the ployz group or restore the prior ownership on these failure paths; the new busy and daemon-stop-failure scenarios currently accept the inaccessible directory.

Useful? React with 👍 / 👎.

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.
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
Expand Down Expand Up @@ -60,7 +85,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"
Expand Down
Loading