fix(install): clean stale cron spools (recycled-uid "rename: not permitted")

userdel does NOT remove /var/spool/cron/crontabs/<user>, so across an
uninstall->reinstall the manager's uid can be recycled (e.g. 1001 -> 1003)
while the old spool file stays owned by the dead uid. The spool dir is
sticky (1730), so the new manager can't rename its temp over the
old-uid-owned file → "crontab: crontabs/libreportal: rename: Operation
not permitted", and the crontab silently never updates (the "added"
success message doesn't check the rename). Same class as the stale
easydocker spool left by the pre-rename migration.

Two fixes:
- runFullUninstall removes each torn-down user's cron spool (+ the legacy
  easydocker one) so teardown stops leaving orphans.
- initUsers defensively drops a manager cron spool owned by a different
  uid (recycled) before the manager-run crontab setup runs — fixes an
  already-dirty box and any uid drift, in both modes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-24 23:17:47 +01:00
parent 6c95b7b1a2
commit 3515d06e0a

21
init.sh
View File

@ -712,6 +712,22 @@ initUsers()
sudo systemctl restart docker sudo systemctl restart docker
isSuccessful "User $sudo_user_name created successfully." isSuccessful "User $sudo_user_name created successfully."
fi fi
# Drop a stale cron spool from a prior install. userdel doesn't remove
# /var/spool/cron/crontabs/<user>, so if the manager's uid was recycled the
# leftover (owned by the dead uid) can't be replaced by the new user in the
# sticky spool dir → "crontab: rename: Operation not permitted". Remove it
# only when it's owned by a different uid (a current-uid spool is valid), plus
# the legacy easydocker artifact, so the manager-run crontab setup writes a
# clean, correctly-owned spool.
local spool_dir="/var/spool/cron/crontabs"
local mgr_uid; mgr_uid=$(id -u "$sudo_user_name" 2>/dev/null)
if [[ -f "$spool_dir/$sudo_user_name" \
&& "$(stat -c %u "$spool_dir/$sudo_user_name" 2>/dev/null)" != "$mgr_uid" ]]; then
sudo rm -f "$spool_dir/$sudo_user_name"
isNotice "Removed a stale cron spool for $sudo_user_name (recycled uid)."
fi
sudo rm -f "$spool_dir/easydocker"
# Install-phase sudo: the heavy install runs AS this user (see the handoff in # Install-phase sudo: the heavy install runs AS this user (see the handoff in
# completeInitMessage) and needs BROAD root — useradd for the docker-install # completeInitMessage) and needs BROAD root — useradd for the docker-install
# user, rootless setup, apt, sysctl, etc. So grant a temporary validated # user, rootless setup, apt, sysctl, etc. So grant a temporary validated
@ -1460,7 +1476,12 @@ runFullUninstall()
pkill -9 -u "$u" >/dev/null 2>&1 || true pkill -9 -u "$u" >/dev/null 2>&1 || true
userdel -r "$u" >/dev/null 2>&1 || true userdel -r "$u" >/dev/null 2>&1 || true
[[ -n "$u" ]] && rm -rf "/home/$u" [[ -n "$u" ]] && rm -rf "/home/$u"
# userdel does NOT remove the cron spool; a leftover (owned by this
# now-removed uid) blocks the next install's user from replacing it in the
# sticky spool dir → "crontab: rename: Operation not permitted".
[[ -n "$u" ]] && rm -f "/var/spool/cron/crontabs/$u"
done done
rm -f /var/spool/cron/crontabs/easydocker # legacy pre-rename artifact
if [[ "$keep_docker" == "true" ]]; then if [[ "$keep_docker" == "true" ]]; then
sed -i "/^${mgr}:/d" /etc/subuid /etc/subgid 2>/dev/null || true sed -i "/^${mgr}:/d" /etc/subuid /etc/subgid 2>/dev/null || true
isSuccessful "Removed user '$mgr' (+ home); kept '$iuser' + its docker image store" isSuccessful "Removed user '$mgr' (+ home); kept '$iuser' + its docker image store"