fix(iso): sweep ii-builder leftovers via NSpid before userdel
Tests / tests (push) Successful in 2m19s

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.
This commit is contained in:
alvinlollo
2026-09-27 14:03:47 +10:00
parent 83ed87f6a9
commit f3bd10cf0f
+84 -2
View File
@@ -195,9 +195,91 @@ if [[ -d "${DOTS_REPO}/sdata/dist-arch" ]]; then
\( -name src -o -name pkg \) -prune -exec rm -rf {} + \( -name src -o -name pkg \) -prune -exec rm -rf {} +
fi fi
rm -f /etc/sudoers.d/99-ii-builder 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/<pid>/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:?}" 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 fi
chown -R root:root "${DOTS_REPO}" chown -R root:root "${DOTS_REPO}"