From f3bd10cf0fc66bb7bfb0057b042ba03ebee43661 Mon Sep 17 00:00:00 2001 From: alvinlollo Date: Sun, 27 Sep 2026 14:03:47 +1000 Subject: [PATCH] fix(iso): sweep ii-builder leftovers via NSpid before userdel end-4's ./setup sudo keepalive orphans a `sleep 60` child as ii-builder; userdel then exits 8 and aborts the ISO build (CI exit 8). Sweep uid-matching processes inside the build chroot before userdel, mirroring shadow's user_busy_processes() checks (real/effective/saved uid + root device/inode). Because arch-chroot mounts proc before `unshare --fork --pid`, /proc entry numbers are outer-namespace pids: signal the last NSpid field instead, require matching NSpid depth, and surface kill errors instead of swallowing them. userdel keeps its stderr and, on final failure, dumps the leftover process list before aborting. --- airootfs/root/customize_airootfs.sh | 86 ++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/airootfs/root/customize_airootfs.sh b/airootfs/root/customize_airootfs.sh index 56c3bb5..0aae764 100644 --- a/airootfs/root/customize_airootfs.sh +++ b/airootfs/root/customize_airootfs.sh @@ -195,9 +195,91 @@ if [[ -d "${DOTS_REPO}/sdata/dist-arch" ]]; then \( -name src -o -name pkg \) -prune -exec rm -rf {} + fi rm -f /etc/sudoers.d/99-ii-builder -if ! userdel -r "${BUILD_USER}" 2>/dev/null; then + +# Processes still running as BUILD_USER make userdel exit 8 and abort the +# build. end-4's ./setup is the usual source: sudo_init_keepalive forks +# `(sleep 60; sudo true) &` and its EXIT trap kills only the subshell, +# orphaning the sleep child for up to a minute. Mirrors shadow's +# user_busy_processes(): match real/effective/saved uid AND require the +# process to live inside this build chroot - the runner shares our PID +# namespace, and a host process could carry the same uid. +# +# mkarchiso runs this script via arch-chroot, which mounts proc *before* +# `unshare --fork --pid`: /proc entry numbers belong to the outer pid +# namespace, while kill(2) resolves pids in our (nested) namespace. Use the +# last NSpid field - the pid in our own namespace - for signalling; keep the +# /proc entry number for logging, since that is what userdel reports. +nspid_fields() { + awk -F'\t' '/^NSpid:/{ $1=""; sub(/^ +/, ""); print; exit }' "$1" +} + +builder_procs() { + local uid broot st p key real eff saved kpid cmd + local -a nsf self_nsf=() + uid=$(id -u "${BUILD_USER}") || return 0 + broot=$(stat -Lc '%d:%i' /) + read -r -a self_nsf < <(nspid_fields /proc/self/status) + for st in /proc/[0-9]*/status; do + [[ -r "$st" ]] || continue + real=""; eff=""; saved="" + while read -r key real eff saved _; do + [[ $key == "Uid:" ]] && break + done < "$st" || continue + [[ $real == "$uid" || $eff == "$uid" || $saved == "$uid" ]] || continue + p=${st#/proc/}; p=${p%/status} + # Stat of /proc//root needs CAP_SYS_PTRACE (or same-uid): exactly + # what shadow's userdel uses, so a process we cannot see here is one + # userdel cannot flag either. Compare by device+inode; skip on failure. + [[ $(stat -Lc '%d:%i' "/proc/$p/root" 2>/dev/null) == "$broot" ]] || continue + # NSpid chain length must equal ours, i.e. the process must live in our + # pid namespace - only then is its last field killable from here. + nsf=() + read -r -a nsf < <(nspid_fields "/proc/$p/status") || continue + if ((${#nsf[@]} > 0 && ${#nsf[@]} == ${#self_nsf[@]})); then + kpid=${nsf[${#nsf[@]} - 1]} + else + kpid="-" + fi + cmd=$(tr '\0' ' ' < "/proc/$p/cmdline" 2>/dev/null) + printf '%s\t%s\t%s\n' "$p" "$kpid" "$cmd" + done + return 0 +} + +sweep_builder_procs() { + local sig=$1 p kpid cmd kerr + local -a pids=() + while IFS=$'\t' read -r p kpid cmd; do + [[ -n $p ]] || continue + if [[ -z $kpid || $kpid == "-" ]]; then + echo "[customize] ${BUILD_USER} process outside our pid namespace (cannot signal): pid $p: $cmd" + continue + fi + echo "[customize] ${BUILD_USER} process ($sig): pid $p: $cmd" + pids+=("$kpid") + done < <(builder_procs) + if ((${#pids[@]} > 0)); then + # Surface signal failures (they used to be swallowed, hiding a + # pid-namespace mismatch as a silent no-op sweep). + if ! kerr=$(kill -"$sig" "${pids[@]}" 2>&1); then + [[ -n $kerr ]] && printf '[customize] kill -%s: %s\n' "$sig" "$kerr" + fi + fi + return 0 +} + +sweep_builder_procs TERM +sleep 2 +sweep_builder_procs KILL +sleep 1 + +if ! userdel -r "${BUILD_USER}"; then rm -rf "/home/${BUILD_USER:?}" - userdel "${BUILD_USER}" + if ! userdel "${BUILD_USER}"; then + echo "[customize] FATAL: cannot remove user ${BUILD_USER}; leftover processes:" >&2 + builder_procs >&2 || true + exit 1 + fi fi chown -R root:root "${DOTS_REPO}"