Skip site navigation (1)Skip section navigation (2)
Date:      Sun, 5 Oct 2025 13:13:34 GMT
From:      Mark Johnston <markj@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-branches@FreeBSD.org
Subject:   git: 806af8240801 - stable/14 - pw: Add a metalog output mode
Message-ID:  <202510051313.595DDY2N075309@gitrepo.freebsd.org>

next in thread | raw e-mail | index | archive | help
The branch stable/14 has been updated by markj:

URL: https://cgit.FreeBSD.org/src/commit/?id=806af8240801ccc823dc790dae802b17d8e6fb19

commit 806af8240801ccc823dc790dae802b17d8e6fb19
Author:     Mark Johnston <markj@FreeBSD.org>
AuthorDate: 2025-09-18 22:40:00 +0000
Commit:     Mark Johnston <markj@FreeBSD.org>
CommitDate: 2025-10-05 13:13:01 +0000

    pw: Add a metalog output mode
    
    When creating OS images as a non-root user, it may be useful to
    pre-create users in the staged tree.  The useradd operation adds files
    to the new user's home directory, copied from the skeleton directory
    (/usr/share/skel), which makes it inconvient for use in this scenario
    since the added files are not recorded in the mtree metalog.
    
    To cover this gap, this change adds a new -M <metalog> option to pw's
    useradd operation, causing pw to add mtree entries for newly added
    files.
    
    Extend an existing regression test to validate this mode.
    
    Reviewed by:    bapt, emaste
    MFC after:      1 week
    Sponsored by:   The FreeBSD Foundation
    Sponsored by:   Klara, Inc.
    Differential Revision:  https://reviews.freebsd.org/D52590
    
    (cherry picked from commit 182ed3c0755f1bf161d8be02016b5f6cf9b57556)
---
 usr.sbin/pw/cpdir.c                  | 43 +++++++++++++++++++++------------
 usr.sbin/pw/pw.8                     | 16 +++++++++++++
 usr.sbin/pw/pw.c                     | 30 +++++++++++++++++++++--
 usr.sbin/pw/pw.h                     |  5 ++++
 usr.sbin/pw/pw_user.c                |  9 ++++++-
 usr.sbin/pw/pw_utils.c               | 46 ++++++++++++++++++++++++++++++++++++
 usr.sbin/pw/pwupd.h                  |  1 +
 usr.sbin/pw/tests/pw_useradd_test.sh | 20 ++++++++++++----
 8 files changed, 148 insertions(+), 22 deletions(-)

diff --git a/usr.sbin/pw/cpdir.c b/usr.sbin/pw/cpdir.c
index 3839a039495a..979323d64342 100644
--- a/usr.sbin/pw/cpdir.c
+++ b/usr.sbin/pw/cpdir.c
@@ -40,49 +40,48 @@ copymkdir(int rootfd, char const *dir, int skelfd, mode_t mode, uid_t uid,
     gid_t gid, int flags)
 {
 	char		*p, lnk[MAXPATHLEN];
-	int		len, homefd, srcfd, destfd;
+	int		len, srcfd, destfd;
 	ssize_t		sz;
 	struct stat     st;
 	struct dirent  *e;
 	DIR		*d;
+	mode_t		pumask;
 
 	if (*dir == '/')
 		dir++;
 
+	pumask = umask(0);
+	umask(pumask);
+
 	if (mkdirat(rootfd, dir, mode) != 0) {
-		mode_t pumask;
 
 		if (errno != EEXIST) {
 			warn("mkdir(%s)", dir);
 			return;
 		}
 
-		pumask = umask(0);
-		umask(pumask);
-
 		if (fchmodat(rootfd, dir, mode & ~pumask,
 		    AT_SYMLINK_NOFOLLOW) == -1)
 			warn("chmod(%s)", dir);
 	}
-
 	if (fchownat(rootfd, dir, uid, gid, AT_SYMLINK_NOFOLLOW) == -1)
 		warn("chown(%s)", dir);
-
 	if (flags > 0 && chflagsat(rootfd, dir, flags,
 	    AT_SYMLINK_NOFOLLOW) == -1)
 		warn("chflags(%s)", dir);
+	metalog_emit(dir, (mode | S_IFDIR) & ~pumask, uid, gid, flags);
 
 	if (skelfd == -1)
 		return;
 
-	homefd = openat(rootfd, dir, O_DIRECTORY);
 	if ((d = fdopendir(skelfd)) == NULL) {
 		close(skelfd);
-		close(homefd);
 		return;
 	}
 
 	while ((e = readdir(d)) != NULL) {
+		char path[MAXPATHLEN];
+
 		if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
 			continue;
 
@@ -92,19 +91,32 @@ copymkdir(int rootfd, char const *dir, int skelfd, mode_t mode, uid_t uid,
 
 		if (strncmp(p, "dot.", 4) == 0)	/* Conversion */
 			p += 3;
+		(void)snprintf(path, sizeof(path), "%s/%s", dir, p);
 
 		if (S_ISDIR(st.st_mode)) {
-			copymkdir(homefd, p, openat(skelfd, e->d_name, O_DIRECTORY),
-			    st.st_mode & _DEF_DIRMODE, uid, gid, st.st_flags);
+			int fd;
+
+			fd = openat(skelfd, e->d_name, O_DIRECTORY);
+			if (fd == -1) {
+				warn("openat(%s)", e->d_name);
+				continue;
+			}
+			copymkdir(rootfd, path, fd, st.st_mode & _DEF_DIRMODE,
+			    uid, gid, st.st_flags);
 			continue;
 		}
 
 		if (S_ISLNK(st.st_mode) &&
-		    (len = readlinkat(skelfd, e->d_name, lnk, sizeof(lnk) -1))
+		    (len = readlinkat(skelfd, e->d_name, lnk, sizeof(lnk) - 1))
 		    != -1) {
 			lnk[len] = '\0';
-			symlinkat(lnk, homefd, p);
-			fchownat(homefd, p, uid, gid, AT_SYMLINK_NOFOLLOW);
+			if (symlinkat(lnk, rootfd, path) != 0)
+				warn("symlink(%s)", path);
+			else if (fchownat(rootfd, path, uid, gid,
+			    AT_SYMLINK_NOFOLLOW) != 0)
+				warn("chown(%s)", path);
+			metalog_emit_symlink(path, lnk, st.st_mode & ~pumask,
+			    uid, gid);
 			continue;
 		}
 
@@ -113,7 +125,7 @@ copymkdir(int rootfd, char const *dir, int skelfd, mode_t mode, uid_t uid,
 
 		if ((srcfd = openat(skelfd, e->d_name, O_RDONLY)) == -1)
 			continue;
-		destfd = openat(homefd, p, O_RDWR | O_CREAT | O_EXCL,
+		destfd = openat(rootfd, path, O_RDWR | O_CREAT | O_EXCL,
 		    st.st_mode);
 		if (destfd == -1) {
 			close(srcfd);
@@ -135,6 +147,7 @@ copymkdir(int rootfd, char const *dir, int skelfd, mode_t mode, uid_t uid,
 			warn("chown(%s)", p);
 		if (fchflags(destfd, st.st_flags) != 0)
 			warn("chflags(%s)", p);
+		metalog_emit(path, st.st_mode & ~pumask, uid, gid, st.st_flags);
 		close(destfd);
 	}
 	closedir(d);
diff --git a/usr.sbin/pw/pw.8 b/usr.sbin/pw/pw.8
index 5eae810b6732..f6d9ebca6308 100644
--- a/usr.sbin/pw/pw.8
+++ b/usr.sbin/pw/pw.8
@@ -30,6 +30,7 @@
 .Nd create, remove, modify & display system users and groups
 .Sh SYNOPSIS
 .Nm
+.Op Fl M Ar metalog
 .Op Fl R Ar rootdir
 .Op Fl V Ar etcdir
 .Cm useradd
@@ -464,6 +465,21 @@ option, bearing the name of the new account.
 This can be overridden by the
 .Fl d
 option on the command line, if desired.
+.It Fl M Ar metalog
+Specify a path to a
+.Xr mtree 5
+metalog file.
+.Nm
+will add entries for all files added to a user's home directory.
+This is useful when building images as a non-root user, as the
+metalog can be used as input to
+.Xr tar 1
+or
+.Xr makefs 8 .
+Note that this option must precede the
+.Ql useradd
+string on the command line, otherwise it will be interpreted as the mode
+option.
 .It Fl M Ar mode
 Create the user's home directory with the specified
 .Ar mode ,
diff --git a/usr.sbin/pw/pw.c b/usr.sbin/pw/pw.c
index 6f59e392bdd0..7cb5dd160e12 100644
--- a/usr.sbin/pw/pw.c
+++ b/usr.sbin/pw/pw.c
@@ -132,7 +132,11 @@ main(int argc, char *argv[])
 	while (argc > 1) {
 		if (*argv[1] == '-') {
 			/*
-			 * Special case, allow pw -V<dir> <operation> [args] for scripts etc.
+			 * Special case, allow pw -V<dir> <operation> [args] for
+			 * scripts etc.
+			 *
+			 * The -M option before the keyword is handled
+			 * differently from -M after a keyword.
 			 */
 			arg = argv[1][1];
 			if (arg == 'V' || arg == 'R') {
@@ -164,6 +168,23 @@ main(int argc, char *argv[])
 				    "%s%s", optarg, arg == 'R' ?
 				    _PATH_PWD : "");
 				conf.altroot = true;
+			} else if (mode == -1 && which == -1 && arg == 'M') {
+				int fd;
+
+				optarg = &argv[1][2];
+				if (*optarg == '\0') {
+					optarg = argv[2];
+					++argv;
+					--argc;
+				}
+				fd = open(optarg,
+				    O_WRONLY | O_APPEND | O_CREAT | O_CLOEXEC,
+				    0644);
+				if (fd == -1)
+					errx(EX_OSERR,
+					    "Cannot open metalog `%s'",
+					    optarg);
+				conf.metalog = fdopen(fd, "ae");
 			} else
 				break;
 		} else if (mode == -1 && (tmp = getindex(Modes, argv[1])) != -1)
@@ -195,6 +216,10 @@ main(int argc, char *argv[])
 	if (conf.rootfd == -1)
 		errx(EXIT_FAILURE, "Unable to open '%s'", conf.rootdir);
 
+	if (conf.metalog != NULL && (which != W_USER || mode != M_ADD))
+		errx(EXIT_FAILURE,
+	    "metalog can only be specified with 'useradd'");
+
 	return (cmdfunc[which][mode](argc, argv, arg1));
 }
 
@@ -233,10 +258,11 @@ cmdhelp(int mode, int which)
 		static const char *help[W_NUM][M_NUM] =
 		{
 			{
-				"usage: pw useradd [name] [switches]\n"
+				"usage: pw [-M metalog] useradd [name] [switches]\n"
 				"\t-V etcdir      alternate /etc location\n"
 				"\t-R rootdir     alternate root directory\n"
 				"\t-C config      configuration file\n"
+				"\t-M metalog     mtree file, must precede 'useradd'\n"
 				"\t-q             quiet operation\n"
 				"  Adding users:\n"
 				"\t-n name        login name\n"
diff --git a/usr.sbin/pw/pw.h b/usr.sbin/pw/pw.h
index c3725693f91d..ceb843d79503 100644
--- a/usr.sbin/pw/pw.h
+++ b/usr.sbin/pw/pw.h
@@ -70,6 +70,11 @@ struct userconf *get_userconfig(const char *cfg);
 struct userconf *read_userconfig(char const * file);
 int write_userconfig(struct userconf *cnf, char const * file);
 
+void metalog_emit(const char *path, mode_t mode, uid_t uid, gid_t gid,
+    int flags);
+void metalog_emit_symlink(const char *path, const char *target, mode_t mode,
+    uid_t uid, gid_t gid);
+
 int pw_group_add(int argc, char **argv, char *name);
 int pw_group_del(int argc, char **argv, char *name);
 int pw_group_mod(int argc, char **argv, char *name);
diff --git a/usr.sbin/pw/pw_user.c b/usr.sbin/pw/pw_user.c
index 007f750c7d1a..413eac4882cc 100644
--- a/usr.sbin/pw/pw_user.c
+++ b/usr.sbin/pw/pw_user.c
@@ -86,10 +86,13 @@ mkdir_home_parents(int dfd, const char *dir)
 {
 	struct stat st;
 	char *dirs, *tmp;
+	mode_t pumask;
+
+	pumask = umask(0);
+	umask(pumask);
 
 	if (*dir != '/')
 		errx(EX_DATAERR, "invalid base directory for home '%s'", dir);
-
 	dir++;
 
 	if (fstatat(dfd, dir, &st, 0) != -1) {
@@ -120,6 +123,9 @@ mkdir_home_parents(int dfd, const char *dir)
 					    dirs);
 				if (fchownat(dfd, dirs, 0, 0, 0) != 0)
 					warn("chown(%s)", dirs);
+				metalog_emit(dir,
+				    (_DEF_DIRMODE | S_IFDIR) & ~pumask, 0, 0,
+				    0);
 			}
 			*tmp = '/';
 		}
@@ -129,6 +135,7 @@ mkdir_home_parents(int dfd, const char *dir)
 			err(EX_OSFILE,  "'%s' (home parent) is not a directory", dirs);
 		if (fchownat(dfd, dirs, 0, 0, 0) != 0)
 			warn("chown(%s)", dirs);
+		metalog_emit(dirs, (_DEF_DIRMODE | S_IFDIR) & ~pumask, 0, 0, 0);
 	}
 
 	free(dirs);
diff --git a/usr.sbin/pw/pw_utils.c b/usr.sbin/pw/pw_utils.c
index 9be1656bcfe1..87dd421ca8a3 100644
--- a/usr.sbin/pw/pw_utils.c
+++ b/usr.sbin/pw/pw_utils.c
@@ -92,3 +92,49 @@ nis_update(void) {
 		errx(i, "make exited with status %d", i);
 	return (i);
 }
+
+static void
+metalog_emit_record(const char *path, const char *target, mode_t mode,
+    uid_t uid, gid_t gid, int flags)
+{
+	const char *flagstr, *type;
+	int error;
+
+	if (conf.metalog == NULL)
+		return;
+
+	if (target != NULL)
+		type = "link";
+	else if (S_ISDIR(mode))
+		type = "dir";
+	else if (S_ISREG(mode))
+		type = "file";
+	else
+		errx(1, "metalog_emit: unhandled file type for %s", path);
+
+	flagstr = fflagstostr(flags &
+	    (UF_IMMUTABLE | UF_APPEND | SF_IMMUTABLE | SF_APPEND));
+	if (flagstr == NULL)
+		errx(1, "metalog_emit: fflagstostr failed");
+
+	error = fprintf(conf.metalog,
+	    "./%s type=%s mode=0%03o uid=%u gid=%u%s%s%s%s\n",
+	    path, type, mode & ACCESSPERMS, uid, gid,
+	    target != NULL ? " link=" : "", target != NULL ? target : "",
+	    *flagstr != '\0' ? " flags=" : "", *flagstr != '\0' ? flagstr : "");
+	if (error < 0)
+		errx(1, "metalog_emit: write error");
+}
+
+void
+metalog_emit(const char *path, mode_t mode, uid_t uid, gid_t gid, int flags)
+{
+	metalog_emit_record(path, NULL, mode, uid, gid, flags);
+}
+
+void
+metalog_emit_symlink(const char *path, const char *target, mode_t mode,
+    uid_t uid, gid_t gid)
+{
+	metalog_emit_record(path, target, mode, uid, gid, 0);
+}
diff --git a/usr.sbin/pw/pwupd.h b/usr.sbin/pw/pwupd.h
index 7e62d9fe402c..18d2a2f41fcb 100644
--- a/usr.sbin/pw/pwupd.h
+++ b/usr.sbin/pw/pwupd.h
@@ -77,6 +77,7 @@ struct userconf {
 struct pwconf {
 	char		 rootdir[MAXPATHLEN];
 	char		 etcpath[MAXPATHLEN];
+	FILE		 *metalog;
 	int		 fd;
 	int		 rootfd;
 	bool		 altroot;
diff --git a/usr.sbin/pw/tests/pw_useradd_test.sh b/usr.sbin/pw/tests/pw_useradd_test.sh
index 6413c063d482..75e96a64ba8e 100755
--- a/usr.sbin/pw/tests/pw_useradd_test.sh
+++ b/usr.sbin/pw/tests/pw_useradd_test.sh
@@ -1,4 +1,3 @@
-
 # Import helper functions
 . $(atf_get_srcdir)/helper_functions.shin
 
@@ -357,15 +356,28 @@ user_add_skel_body() {
 	echo "c" > ${HOME}/skel/c/d/dot.c
 	mkdir ${HOME}/home
 	ln -sf /nonexistent ${HOME}/skel/c/foo
-	atf_check -s exit:0 ${RPW} useradd foo -k /skel -m
+	atf_check -s exit:0 ${RPW} -M METALOG useradd foo -k /skel -m
 	test -d ${HOME}/home/foo || atf_fail "Directory not created"
 	test -f ${HOME}/home/foo/.a || atf_fail "File not created"
 	atf_check -o file:${HOME}/skel/.a -s exit:0 cat ${HOME}/home/foo/.a
 	atf_check -o file:${HOME}/skel/b -s exit:0 cat ${HOME}/home/foo/b
-	test -d ${HOME}/home/foo/c || atf_fail "Dotted directory in skel not copied"
-	test -d ${HOME}/home/foo/.plop || atf_fail "Directory in skell not created"
+	test -d ${HOME}/home/foo/c || atf_fail "Directory in skel not copied"
+	test -d ${HOME}/home/foo/.plop || atf_fail "Dotted directory in skel not created"
 	atf_check -o inline:"/nonexistent\n" -s ignore readlink -f ${HOME}/home/foo/c/foo
 	atf_check -o file:${HOME}/skel/c/d/dot.c -s exit:0 cat ${HOME}/home/foo/c/d/.c
+
+	cat <<__EOF__ >METALOG.expected
+./home/foo type=dir mode=0755 uid=1001 gid=1001
+./home/foo/.a type=file mode=0644 uid=1001 gid=1001
+./home/foo/.plop type=dir mode=0755 uid=1001 gid=1001
+./home/foo/b type=file mode=0644 uid=1001 gid=1001
+./home/foo/c type=dir mode=0755 uid=1001 gid=1001
+./home/foo/c/d type=dir mode=0755 uid=1001 gid=1001
+./home/foo/c/d/.c type=file mode=0644 uid=1001 gid=1001
+./home/foo/c/foo type=link mode=0755 uid=1001 gid=1001 link=/nonexistent
+__EOF__
+	atf_check -o save:METALOG.out sort METALOG
+	atf_check diff METALOG.out METALOG.expected
 }
 
 atf_test_case user_add_uid0



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?202510051313.595DDY2N075309>