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}"