The branch main has been updated by bapt: URL: https://cgit.FreeBSD.org/src/commit/?id=6b6aa45f9a1a35acb7fef68824c1a5eb08503909
commit 6b6aa45f9a1a35acb7fef68824c1a5eb08503909 Author: Baptiste Daroussin <[email protected]> AuthorDate: 2026-09-04 12:12:03 +0000 Commit: Baptiste Daroussin <[email protected]> CommitDate: 2026-09-04 14:28:47 +0000 pw: fix at job removal when deleting a user rmat() used stat() with a path relative to the current working directory, so it never found the job files in /var/at/jobs and the at(1) jobs of a deleted user were never removed. ef7d0eb9489f also broke it by introducing a typo: /usr/sbin/atrm instead of /usr/bin/artm. Use fstatat() with the directory fd to stat the job files relative to the at jobs directory, and unlinkat() them directly instead of spawning atrm. Those changes allow us to make it works with pw -R. MFC After: 1 week --- usr.sbin/pw/pw_user.c | 27 +++++++++++++-------------- usr.sbin/pw/tests/pw_userdel_test.sh | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/usr.sbin/pw/pw_user.c b/usr.sbin/pw/pw_user.c index 269344232912..9c48ef3fcb5e 100644 --- a/usr.sbin/pw/pw_user.c +++ b/usr.sbin/pw/pw_user.c @@ -680,31 +680,30 @@ pw_checkname(char *name, int gecos) static void rmat(uid_t uid) { - DIR *d = opendir("/var/at/jobs"); + DIR *d; struct dirent *e; - const char *argv[] = { "/usr/sbin/atrm", NULL, NULL }; + int atfd; - if (d == NULL) + atfd = openat(conf.rootfd, "var/at/jobs", O_DIRECTORY | O_CLOEXEC); + if (atfd == -1) return; + d = fdopendir(atfd); + if (d == NULL) { + close(atfd); + return; + } while ((e = readdir(d)) != NULL) { struct stat st; - pid_t pid; if (strncmp(e->d_name, ".lock", 5) == 0) continue; - if (stat(e->d_name, &st) != 0) + if (fstatat(atfd, e->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) continue; if (S_ISDIR(st.st_mode) || st.st_uid != uid) continue; - argv[1] = e->d_name; - if (posix_spawn(&pid, argv[0], NULL, NULL, - (char *const *) argv, environ)) { - warn("Failed to execute '%s %s'", - argv[0], argv[1]); - } else - (void) waitpid(pid, NULL, 0); - } + if (unlinkat(atfd, e->d_name, 0) != 0) + warn("Failed to remove at job '%s'", e->d_name); } closedir(d); } @@ -1008,7 +1007,7 @@ pw_user_del(int argc, char **argv, char *arg1) unlinkat(conf.rootfd, file + 1, 0); /* Remove at jobs */ - if (!PWALTDIR() && getpwuid(id) == NULL) + if (PWALTDIR() != PWF_ALT && GETPWUID(id) == NULL) rmat(id); /* Remove home directory and contents */ diff --git a/usr.sbin/pw/tests/pw_userdel_test.sh b/usr.sbin/pw/tests/pw_userdel_test.sh index 2e11c3534d58..647384615fb4 100755 --- a/usr.sbin/pw/tests/pw_userdel_test.sh +++ b/usr.sbin/pw/tests/pw_userdel_test.sh @@ -82,6 +82,34 @@ home_regular_dir_body() { [ ! -d ${HOME}/foo ] || atf_fail "Home has not been removed" } +atf_test_case delete_at_jobs cleanup +delete_at_jobs_body() { + populate_root_etc_skel + + mkdir -p ${HOME}/var/at/jobs + + atf_check -s exit:0 ${RPW} useradd foo + + uid=$(awk -F: '/^foo:/ {print $3}' ${HOME}/etc/master.passwd) + + job="c00001000000000" + atf_check -s exit:0 touch ${HOME}/var/at/jobs/${job} + atf_check -s exit:0 chown ${uid} ${HOME}/var/at/jobs/${job} + + # A job owned by root must not be removed + otherjob="c00002000000000" + atf_check -s exit:0 touch ${HOME}/var/at/jobs/${otherjob} + atf_check -s exit:0 chown 0 ${HOME}/var/at/jobs/${otherjob} + + atf_check -s exit:0 ${RPW} userdel foo + + [ ! -e ${HOME}/var/at/jobs/${job} ] || atf_fail "at job not removed" + [ -e ${HOME}/var/at/jobs/${otherjob} ] || atf_fail "at job of another user was removed" +} +delete_at_jobs_cleanup() { + rm -rf ${HOME}/var/at/jobs +} + atf_init_test_cases() { atf_add_test_case rmuser_seperate_group atf_add_test_case user_do_not_try_to_delete_root_if_user_unknown @@ -90,4 +118,5 @@ atf_init_test_cases() { atf_add_test_case home_not_a_dir atf_add_test_case home_shared atf_add_test_case home_regular_dir + atf_add_test_case delete_at_jobs }
