From nobody Sat Oct 4 19:35:16 2025 X-Original-To: dev-commits-src-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4cfG2r6Qqxz69ftx; Sat, 04 Oct 2025 19:35:16 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R12" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 4cfG2r3slqz3r40; Sat, 04 Oct 2025 19:35:16 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1759606516; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=XbiAry+8thT0nLJLSoZvDTmTcTVYU1JMbq0j+UhcULE=; b=rKRPggExqwIbWvTxI4TQFJtPrMGVtkh53TSXL3c+djvVoBHr9vCsmlrPiCEPs2nyEX4HLN p3GvqQSy5eI4NW1BLNsvRhJA0mC7/QfOKCQdN45Fic7eSfj9oqysnyRvxsZAfCI6LAKaU6 xkhVusOKLTraIjo1df0z/MrVN7A+n58VditsVbWqwjaWnzNxpDVnGmn4gunG9eVBauhFWw Pfq9HMaJKqu+HU6KkZyqGcH+Mi/n+7k1lbLQ3CdXOjrWu9ZOkwhVxbi8ZdBls4W8T7GPUn UFL4pey+m1z2tTQAL/mgna/t/6MsIwMKQy3pXn0bsQyYW1fZ/FeUKqJbBFZcAA== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1759606516; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=XbiAry+8thT0nLJLSoZvDTmTcTVYU1JMbq0j+UhcULE=; b=sN9bmBYAskVpeI7hpByE00lgFK4QSJHxlVlSe+yfT2zwTVrqLqCbayl54qNNiX8rODRLdR 5ZQvG+TwCNtEwRMXqBGOpTu3t4E165Kj9Y4ZHntOo85pxBjrg95vpKIs9NPvFfJ6fQDgsl 3cKIC3CjIOXa9qUFLcsOpdO4xQS6jDNg4Bm4DVijBSi/FJTHljUJElmm+POmfEtnMcgJT2 yuNdGUtO1RXjE61sPAkMHeYQ99CUfQRIY2CJZ129DSsO8rwEIdq4QFxbbXmj/XWz8j1HUW vBsqdsVXojFUclPMIJBnAkKORxPSIuGN9r3EmtYvADk1rhW3uQuIlo1mN96csQ== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1759606516; a=rsa-sha256; cv=none; b=pRYJJIDg3hgkY5B+AqBedSrXtYW6rxFMgtKrfU2a4FJmbOx+oU0LMvZco2L88qt3ignJPc DB9nq9kl95S6SMw1bKC+Z2qtTApRoA1y5+rtnXfh90/kCTAlATCB2OPK/1WUX/i8tAxS0s zJDuISZ+xW+sXTQmfZtuEiJAtEomGm2vf2FGvb9UPjLBOi7ytsVM20S19A6v1qnnWlji8d PmTM4Y36kRzRLer8NqtXy1UnXStznB7nsk8kw2F0Lkp3R2riAkRGQohvBeEen9ixOCQgKN vuz3Y5SCRIN/6isOC55JJAxKVyMGrqDc6mEd9d01NOLVP6OBPCNwcCTCEeMFZA== ARC-Authentication-Results: i=1; mx1.freebsd.org; none Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (Client did not present a certificate) by mxrelay.nyi.freebsd.org (Postfix) with ESMTPS id 4cfG2r3QFwzvkg; Sat, 04 Oct 2025 19:35:16 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from gitrepo.freebsd.org ([127.0.1.44]) by gitrepo.freebsd.org (8.18.1/8.18.1) with ESMTP id 594JZGDX079211; Sat, 4 Oct 2025 19:35:16 GMT (envelope-from git@gitrepo.freebsd.org) Received: (from git@localhost) by gitrepo.freebsd.org (8.18.1/8.18.1/Submit) id 594JZG5M079208; Sat, 4 Oct 2025 19:35:16 GMT (envelope-from git) Date: Sat, 4 Oct 2025 19:35:16 GMT Message-Id: <202510041935.594JZG5M079208@gitrepo.freebsd.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-branches@FreeBSD.org From: Mark Johnston Subject: git: 1efb9810ba8c - stable/15 - pw: Add a metalog output mode List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-all@freebsd.org Sender: owner-dev-commits-src-all@FreeBSD.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: markj X-Git-Repository: src X-Git-Refname: refs/heads/stable/15 X-Git-Reftype: branch X-Git-Commit: 1efb9810ba8caa550e0b8459117a36fde0daaec9 Auto-Submitted: auto-generated The branch stable/15 has been updated by markj: URL: https://cgit.FreeBSD.org/src/commit/?id=1efb9810ba8caa550e0b8459117a36fde0daaec9 commit 1efb9810ba8caa550e0b8459117a36fde0daaec9 Author: Mark Johnston AuthorDate: 2025-09-18 22:40:00 +0000 Commit: Mark Johnston CommitDate: 2025-10-04 19:34:57 +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 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 [args] for scripts etc. + * Special case, allow pw -V [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