git: 6b6aa45f9a1a - main - pw: fix at job removal when deleting a user

From: Baptiste Daroussin <bapt_at_FreeBSD.org>
Date: Fri, 04 Sep 2026 14:28:56 UTC
The branch main has been updated by bapt:

URL: https://cgit.FreeBSD.org/src/commit/?id=6b6aa45f9a1a35acb7fef68824c1a5eb08503909

commit 6b6aa45f9a1a35acb7fef68824c1a5eb08503909
Author:     Baptiste Daroussin <bapt@FreeBSD.org>
AuthorDate: 2026-09-04 12:12:03 +0000
Commit:     Baptiste Daroussin <bapt@FreeBSD.org>
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
 }