pw userdel: destroy home dataset if empty

When removing a user's home directory, if the directory is a ZFS
dataset, it cannot be removed.  If the directory has been emptied,
use "zfs destroy" to destroy it.  This complements the automatic
dataset creation in adduser.  Note that datasets within the directory
and snapshots are not handled, as the complete path is not constructed.

While here, add waitpid() calls to rmat() and pw_user_del().

Reviewed by:	des
Differential Revision:	https://reviews.freebsd.org/D45348
This commit is contained in:
Mike Karels 2024-05-29 18:55:14 -05:00
parent 19dbf72a27
commit d2f1f71ec8
4 changed files with 118 additions and 18 deletions

View file

@ -741,6 +741,9 @@ Secondly, it will only remove files and directories that are actually owned by
the user, or symbolic links owned by anyone under the user's home directory.
Finally, after deleting all contents owned by the user only empty directories
will be removed.
If the home directory is a ZFS dataset and has been emptied,
the dataset will be destroyed.
ZFS datasets within the home directory and snapshots are not handled.
If any additional cleanup work is required, this is left to the administrator.
.El
.Pp
@ -1077,7 +1080,8 @@ No base home directory configured.
.Xr passwd 5 ,
.Xr pw.conf 5 ,
.Xr pwd_mkdb 8 ,
.Xr vipw 8
.Xr vipw 8 ,
.Xr zfs 8
.Sh HISTORY
The
.Nm

View file

@ -28,7 +28,7 @@
*/
#include <sys/param.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <ctype.h>
@ -669,6 +669,7 @@ rmat(uid_t uid)
while ((e = readdir(d)) != NULL) {
struct stat st;
pid_t pid;
if (strncmp(e->d_name, ".lock", 5) != 0 &&
stat(e->d_name, &st) == 0 &&
@ -679,11 +680,12 @@ rmat(uid_t uid)
e->d_name,
NULL
};
if (posix_spawn(NULL, argv[0], NULL, NULL,
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);
}
}
closedir(d);
@ -919,11 +921,14 @@ pw_user_del(int argc, char **argv, char *arg1)
"-r",
NULL
};
if (posix_spawnp(NULL, argv[0], NULL, NULL,
pid_t pid;
if (posix_spawnp(&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);
}
}

View file

@ -139,7 +139,7 @@ void vendgrent(void);
void copymkdir(int rootfd, char const * dir, int skelfd, mode_t mode, uid_t uid,
gid_t gid, int flags);
void rm_r(int rootfd, char const * dir, uid_t uid);
bool rm_r(int rootfd, char const * dir, uid_t uid);
__END_DECLS
#endif /* !_PWUPD_H */

View file

@ -26,35 +26,58 @@
* SUCH DAMAGE.
*/
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <libutil.h>
#include <spawn.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "pwupd.h"
void
static bool try_dataset_remove(const char *home);
extern char **environ;
/*
* "rm -r" a directory tree. If the top-level directory cannot be removed
* due to EBUSY, indicating that it is a ZFS dataset, and we have emptied
* it, destroy the dataset. Return true if any files or directories
* remain.
*/
bool
rm_r(int rootfd, const char *path, uid_t uid)
{
int dirfd;
DIR *d;
struct dirent *e;
struct stat st;
const char *fullpath;
bool skipped = false;
fullpath = path;
if (*path == '/')
path++;
dirfd = openat(rootfd, path, O_DIRECTORY);
if (dirfd == -1) {
return;
return (true);
}
d = fdopendir(dirfd);
if (d == NULL) {
(void)close(dirfd);
return;
return (true);
}
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
@ -62,16 +85,84 @@ rm_r(int rootfd, const char *path, uid_t uid)
if (fstatat(dirfd, e->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0)
continue;
if (S_ISDIR(st.st_mode))
rm_r(dirfd, e->d_name, uid);
else if (S_ISLNK(st.st_mode) || st.st_uid == uid)
if (S_ISDIR(st.st_mode)) {
if (rm_r(dirfd, e->d_name, uid) == true)
skipped = true;
} else if (S_ISLNK(st.st_mode) || st.st_uid == uid)
unlinkat(dirfd, e->d_name, 0);
else
skipped = true;
}
closedir(d);
if (fstatat(rootfd, path, &st, AT_SYMLINK_NOFOLLOW) != 0)
return;
if (S_ISLNK(st.st_mode))
unlinkat(rootfd, path, 0);
else if (st.st_uid == uid)
unlinkat(rootfd, path, AT_REMOVEDIR);
return (skipped);
if (S_ISLNK(st.st_mode)) {
if (unlinkat(rootfd, path, 0) == -1)
skipped = true;
} else if (st.st_uid == uid) {
if (unlinkat(rootfd, path, AT_REMOVEDIR) == -1) {
if (errno == EBUSY && skipped == false)
skipped = try_dataset_remove(fullpath);
else
skipped = true;
}
} else
skipped = true;
return (skipped);
}
/*
* If the home directory is a ZFS dataset, attempt to destroy it.
* Return true if the dataset is not destroyed.
* This would be more straightforward as a shell script.
*/
static bool
try_dataset_remove(const char *path)
{
bool skipped = true;
struct statfs stat;
const char *argv[] = {
"/sbin/zfs",
"destroy",
NULL,
NULL
};
int status;
pid_t pid;
/* see if this is an absolute path (top-level directory) */
if (*path != '/')
return (skipped);
/* see if ZFS is loaded */
if (kld_isloaded("zfs") == 0)
return (skipped);
/* This won't work if root dir is not / (-R option) */
if (strcmp(conf.rootdir, "/") != 0) {
warnx("cannot destroy home dataset when -R was used");
return (skipped);
}
/* if so, find dataset name */
if (statfs(path, &stat) != 0) {
warn("statfs %s", path);
return (skipped);
}
/*
* Check that the path refers to the dataset itself,
* not a subdirectory.
*/
if (strcmp(stat.f_mntonname, path) != 0)
return (skipped);
argv[2] = stat.f_mntfromname;
if ((skipped = posix_spawn(&pid, argv[0], NULL, NULL,
(char *const *) argv, environ)) != 0) {
warn("Failed to execute '%s %s %s'",
argv[0], argv[1], argv[2]);
} else {
if (waitpid(pid, &status, 0) != -1 && status != 0) {
warnx("'%s %s %s' exit status %d\n",
argv[0], argv[1], argv[2], status);
}
}
return (skipped);
}