diff --git a/usr.sbin/pw/cpdir.c b/usr.sbin/pw/cpdir.c --- a/usr.sbin/pw/cpdir.c +++ b/usr.sbin/pw/cpdir.c @@ -40,49 +40,48 @@ 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 @@ 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 @@ 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 @@ 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.h b/usr.sbin/pw/pw.h --- a/usr.sbin/pw/pw.h +++ b/usr.sbin/pw/pw.h @@ -70,6 +70,11 @@ 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.8 b/usr.sbin/pw/pw.8 --- 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 @@ 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 --- a/usr.sbin/pw/pw.c +++ b/usr.sbin/pw/pw.c @@ -132,7 +132,11 @@ 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 @@ "%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 @@ 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 @@ 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_user.c b/usr.sbin/pw/pw_user.c --- a/usr.sbin/pw/pw_user.c +++ b/usr.sbin/pw/pw_user.c @@ -86,10 +86,13 @@ { 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 @@ 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 @@ 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 --- a/usr.sbin/pw/pw_utils.c +++ b/usr.sbin/pw/pw_utils.c @@ -92,3 +92,49 @@ 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 --- a/usr.sbin/pw/pwupd.h +++ b/usr.sbin/pw/pwupd.h @@ -76,6 +76,7 @@ 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 --- 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 @@ 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