The branch main has been updated by markj:

URL: 
https://cgit.FreeBSD.org/src/commit/?id=182ed3c0755f1bf161d8be02016b5f6cf9b57556

commit 182ed3c0755f1bf161d8be02016b5f6cf9b57556
Author:     Mark Johnston <[email protected]>
AuthorDate: 2025-09-18 22:40:00 +0000
Commit:     Mark Johnston <[email protected]>
CommitDate: 2025-09-18 22:42:16 +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
---
 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 a39a022ca309..605c51dcec2a 100644
--- a/usr.sbin/pw/pwupd.h
+++ b/usr.sbin/pw/pwupd.h
@@ -76,6 +76,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

Reply via email to