[libuser/f18] TOCTOU race condition in user home creation, move and removal fixed
Viktor Hercinger
herczy at fedoraproject.org
Fri Mar 29 15:36:12 UTC 2013
commit 873067f56966a0b3fdb9c1cd2a8435e38c84d498
Author: Viktor Hercinger <vhercing at redhat.com>
Date: Fri Mar 29 16:35:53 2013 +0100
TOCTOU race condition in user home creation, move and removal fixed
Resolves: #884685, CVE-2012-5630, CVE-2012-5644
This patch is a backport of some upstream modifications.
libuser-dirtree-TOCTOU-fix.patch | 1581 ++++++++++++++++++++++++++++++++++++++
libuser.spec | 12 +-
2 files changed, 1591 insertions(+), 2 deletions(-)
---
diff --git a/libuser-dirtree-TOCTOU-fix.patch b/libuser-dirtree-TOCTOU-fix.patch
new file mode 100644
index 0000000..e7c0997
--- /dev/null
+++ b/libuser-dirtree-TOCTOU-fix.patch
@@ -0,0 +1,1581 @@
+diff --git a/Makefile.am b/Makefile.am
+index 34bf74f..dbbf206 100644
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -24,7 +24,8 @@ VG_ENVIRONMENT = G_SLICE=always-malloc \
+
+ ## Targets
+ SUBDIRS = po docs
+-TESTS = tests/config_test.sh tests/files_test tests/pwhash_test tests/utils_test
++TESTS = tests/config_test.sh tests/fs_test tests/files_test tests/pwhash_test \
++ tests/utils_test
+ if LDAP
+ TESTS += tests/default_pw_test tests/ldap_test
+ endif
+@@ -40,6 +41,7 @@ EXTRA_DIST = \
+ tests/config_test.sh \
+ tests/default_pw.conf.in tests/default_pw_test tests/default_pw_test.py \
+ tests/files.conf.in tests/files_test tests/files_test.py \
++ tests/fs.conf.in tests/fs_tests tests/fs_test.py \
+ tests/ldap.conf.in tests/ldaprc tests/ldap_skel.ldif tests/ldap_test \
+ tests/ldap_test.py \
+ tests/pwhash.conf.in tests/pwhash.py tests/pwhash_test \
+diff --git a/apps/lnewusers.c b/apps/lnewusers.c
+index 93eda75..1a5a5cb 100644
+--- a/apps/lnewusers.c
++++ b/apps/lnewusers.c
+@@ -246,16 +246,6 @@ main(int argc, const char **argv)
+ _("Refusing to use dangerous home directory `%s' "
+ "for %s by default\n"), homedir, fields[0]);
+ else if (lu_user_add(ctx, ent, &error)) {
+- if (!lu_user_setpass(ctx, ent, fields[1], FALSE,
+- &error)) {
+- fprintf(stderr,
+- _("Error setting initial password for "
+- "%s: %s\n"), fields[0],
+- lu_strerror(error));
+- if (error) {
+- lu_error_free(&error);
+- }
+- }
+ lu_nscd_flush_cache(LU_NSCD_CACHE_PASSWD);
+ /* Unless the nocreatehomedirs flag was given, attempt
+ * to create the user's home directory. */
+@@ -285,6 +275,21 @@ main(int argc, const char **argv)
+ }
+ }
+ }
++ /* Set the password after creating the home directory
++ to prevent the user from seeing an incomplete
++ home, and to prevent the user from interfering with
++ the creation of the home directory. */
++ if (!lu_user_setpass(ctx, ent, fields[1], FALSE,
++ &error)) {
++ fprintf(stderr,
++ _("Error setting initial password for "
++ "%s: %s\n"), fields[0],
++ lu_strerror(error));
++ if (error) {
++ lu_error_free(&error);
++ }
++ }
++ lu_nscd_flush_cache(LU_NSCD_CACHE_PASSWD);
+ } else {
+ fprintf(stderr,
+ _("Error creating user account for %s: %s\n"),
+diff --git a/apps/luseradd.c b/apps/luseradd.c
+index 2de755a..7839183 100644
+--- a/apps/luseradd.c
++++ b/apps/luseradd.c
+@@ -261,27 +261,6 @@ main(int argc, const char **argv)
+ lu_strerror(error));
+ return 3;
+ }
+-
+- if (userPassword != NULL) {
+- if (lu_user_setpass(ctx, ent, userPassword, FALSE, &error)
+- == FALSE) {
+- fprintf(stderr, _("Error setting password for user "
+- "%s: %s.\n"), name,
+- lu_strerror(error));
+- return 3;
+- }
+- }
+-
+- if (cryptedUserPassword != NULL) {
+- if (lu_user_setpass(ctx, ent, cryptedUserPassword, TRUE,
+- &error) == FALSE) {
+- fprintf(stderr, _("Error setting password for user "
+- "%s: %s.\n"), name,
+- lu_strerror(error));
+- return 3;
+- }
+- }
+-
+ lu_nscd_flush_cache(LU_NSCD_CACHE_PASSWD);
+
+ /* If we don't have the the don't-create-home flag, create the user's
+@@ -314,6 +293,29 @@ main(int argc, const char **argv)
+ }
+ }
+
++ /* Set the password after creating the home directory to prevent the
++ user from seeing an incomplete home, and to prevent the user from
++ interfering with the creation of the home directory. */
++ if (userPassword != NULL) {
++ if (lu_user_setpass(ctx, ent, userPassword, FALSE, &error)
++ == FALSE) {
++ fprintf(stderr, _("Error setting password for user "
++ "%s: %s.\n"), name,
++ lu_strerror(error));
++ return 3;
++ }
++ }
++ if (cryptedUserPassword != NULL) {
++ if (lu_user_setpass(ctx, ent, cryptedUserPassword, TRUE,
++ &error) == FALSE) {
++ fprintf(stderr, _("Error setting password for user "
++ "%s: %s.\n"), name,
++ lu_strerror(error));
++ return 3;
++ }
++ }
++ lu_nscd_flush_cache(LU_NSCD_CACHE_PASSWD);
++
+ lu_ent_free(ent);
+
+ lu_end(ctx);
+diff --git a/lib/fs.c b/lib/fs.c
+index 673a650..2cef596 100644
+--- a/lib/fs.c
++++ b/lib/fs.c
+@@ -58,247 +58,528 @@ current_umask(void)
+ return value;
+ }
+
+-/* Copy the "src" directory to "dest", setting all ownerships as given, and
+- setting the mode of the top-level directory as given. The group ID of the
+- copied files is preserved if it is nonzero. If keep_contexts, preserve
+- SELinux contexts in files under dest; use matchpathcon otherwise. Assume
+- umask_value is the current value of umask.
+-
+- Note that keep_contexts does NOT affect the context of dest; the caller must
+- perform an explicit setfscreatecon() before calling lu_homedir_copy() to set
+- the context of dest. The SELinux fscreate context is on return from this
+- function is unspecified. */
++/* What should the ownership and permissions of the copied files be? */
++struct copy_access_options
++{
++ /* Preserve ownership and permissions of the original unmodified;
++ otherwise, use matchpathcon() for SELinux contexts and apply
++ the following fields.. */
++ gboolean preserve_source;
++ /* Ignore EEXIST errors. This will go away in the future. */
++ gboolean ignore_eexist;
++ uid_t uid; /* UID to use for the copy if !preserve_source. */
++ /* GID to use for the copy if !preserve_source and original is owned by
++ GID 0. */
++ gid_t gid;
++ mode_t umask; /* umask to apply to modes if !preserve_source */
++};
++
++/* Return an UID appropriate for a copy of ST given OPTIONS. */
++static uid_t
++uid_for_copy(const struct copy_access_options *options, const struct stat *st)
++{
++ if (options->preserve_source)
++ return st->st_uid;
++ return options->uid;
++}
++
++/* Return a GID appropriate for a copy of ST given OPTIONS. */
++static gid_t
++gid_for_copy(const struct copy_access_options *options, const struct stat *st)
++{
++ if (options->preserve_source)
++ return st->st_gid;
++ if (st->st_gid != 0) /* Skeleton wants us to us a different group */
++ return st->st_gid;
++ return options->gid;
++}
++
++/* Return a mode_t value appropriate for a copy of ST given OPTIONS. */
++static mode_t
++mode_for_copy(const struct copy_access_options *options, const struct stat *st)
++{
++ if (options->preserve_source)
++ return st->st_mode;
++ return st->st_mode & ~options->umask;
++}
++
++/* Copy symlink SYMLINK_NAME in SRC_DIR_FD, which corresponds to SRC_PATH, to
++ SYMLINK_NAME in DEST_DIR_FD, which corresponds to DEST_PATH. Use
++ ACCESS_OPTIONS. Use SRC_STAT for data about SRC_PATH.
++
++ On return from this function, SELinux fscreate context is unspecified.
++
++ Note that SRC_PATH should only be used for error messages, not to access the
++ files; if the user is still logged in, a directory in the path may be
++ replaced by a symbolic link, redirecting the access outside of
++ SRC_DIR_FD/SYMLINK_NAME. Likewise for DEST_*. */
+ static gboolean
+-lu_homedir_copy(const char *src, const char *dest, uid_t owner, gid_t group,
+- mode_t mode, gboolean keep_contexts, mode_t umask_value,
+- struct lu_error **error)
++copy_symlink(int src_dir_fd, const char *src_path, int dest_dir_fd,
++ const char *dest_path, const char *symlink_name,
++ const struct stat *src_stat,
++ const struct copy_access_options *access_options,
++ struct lu_error **error)
+ {
++ char buf[PATH_MAX];
++ ssize_t len;
++ struct timespec timebuf[2];
++
++ LU_ERROR_CHECK(error);
++
++ /* In the worst case here, we end up with a wrong SELinux context for a
++ symbolic link due to a path name lookup race. That's unfortunate,
++ but symlink contents are more or less public anyway... (A possible
++ improvement would be to use Linux-only O_PATH to open src_path
++ first, then see if it is a symlink, and "upgrade" to an O_RDONLY if
++ not. But O_PATH is available only in Linux >= 2.6.39.)
++
++ The symlinkat()/fchownat()/utimensat() calls are also not safe
++ against an user meddling; we might be able to ensure the
++ fchownat()/utimensat() are done on the same file using O_PATH again,
++ but symlinkat()/the rest is definitely unatomic. Rely on having an
++ unwritable the parent directory, same as in the mkdirat()/openat()
++ case of lu_homedir_copy_and_close(). */
++ if (access_options->preserve_source) {
++ if (!lu_util_fscreate_from_lfile(src_path, error))
++ return FALSE;
++ } else if (!lu_util_fscreate_for_path(dest_path,
++ src_stat->st_mode & S_IFMT,
++ error))
++ return FALSE;
++
++ len = readlinkat(src_dir_fd, symlink_name, buf, sizeof(buf) - 1);
++ if (len == -1) {
++ lu_error_new(error, lu_error_generic,
++ _("Error reading `%s': %s"), src_path,
++ strerror(errno));
++ return FALSE;
++ }
++ buf[len] = '\0';
++ if (symlinkat(buf, dest_dir_fd, symlink_name) == -1) {
++ if (errno == EEXIST && access_options->ignore_eexist)
++ return TRUE;
++ lu_error_new(error, lu_error_generic,
++ _("Error creating `%s': %s"), dest_path,
++ strerror(errno));
++ return FALSE;
++ }
++ if (fchownat(dest_dir_fd, symlink_name,
++ uid_for_copy(access_options, src_stat),
++ gid_for_copy(access_options, src_stat),
++ AT_SYMLINK_NOFOLLOW) == -1
++ && errno != EPERM && errno != EOPNOTSUPP) {
++ lu_error_new(error, lu_error_generic,
++ _("Error changing owner of `%s': %s"), dest_path,
++ strerror(errno));
++ return FALSE;
++ }
++ timebuf[0] = src_stat->st_atim;
++ timebuf[1] = src_stat->st_mtim;
++ utimensat(dest_dir_fd, symlink_name, timebuf, AT_SYMLINK_NOFOLLOW);
++ return TRUE;
++}
++
++/* Copy SRC_FD, which corresponds to SRC_PATH, to DEST_NAME in DEST_DIR_FD,
++ which corresponds to DEST_PATH. Use ACCESS_OPTIONS. Use SRC_STAT for data
++ about SRC_PATH.
++
++ On return from this function, SELinux fscreate context is unspecified.
++
++ Note that SRC_PATH should only be used for error messages, not to access the
++ files; if the user is still logged in, a directory in the path may be
++ replaced by a symbolic link, redirecting the access outside of SRC_FD.
++ Likewise for DEST_*. */
++static gboolean
++copy_regular_file(int src_fd, const char *src_path, int dest_dir_fd,
++ const char *dest_name, const char *dest_path,
++ const struct stat *src_stat,
++ const struct copy_access_options *access_options,
++ struct lu_error **error)
++{
++ int dest_fd;
++ struct timespec timebuf[2];
++ gboolean ret = FALSE;
++
++ LU_ERROR_CHECK(error);
++
++ if (access_options->preserve_source) {
++ if (!lu_util_fscreate_from_fd(src_fd, src_path, error))
++ return FALSE;
++ } else if (!lu_util_fscreate_for_path(dest_path,
++ src_stat->st_mode & S_IFMT,
++ error))
++ return FALSE;
++ /* Start with absolutely restrictive permissions; the original file may
++ be e.g. a hardlink to /etc/shadow. */
++ dest_fd = openat(dest_dir_fd, dest_name,
++ O_EXCL | O_CREAT | O_WRONLY | O_NOFOLLOW, 0);
++ if (dest_fd == -1) {
++ if (errno == EEXIST && access_options->ignore_eexist)
++ return TRUE;
++ lu_error_new(error, lu_error_open, _("Error writing `%s': %s"),
++ dest_path, strerror(errno));
++ return FALSE;
++ }
++
++ /* Now just copy the data. */
++ for (;;) {
++ unsigned char buf[BUFSIZ];
++ ssize_t left;
++ unsigned char *p;
++
++ left = read(src_fd, &buf, sizeof(buf));
++ if (left == -1) {
++ if (errno == EINTR)
++ continue;
++ lu_error_new(error, lu_error_read,
++ _("Error reading `%s': %s"), src_path,
++ strerror(errno));
++ goto err_dest_fd;
++ }
++ if (left == 0)
++ break;
++ p = buf;
++ while (left > 0) {
++ ssize_t out;
++
++ out = write(dest_fd, p, left);
++ if (out == -1) {
++ if (errno == EINTR)
++ continue;
++ lu_error_new(error, lu_error_write,
++ _("Error writing `%s': %s"),
++ dest_path, strerror(errno));
++ goto err_dest_fd;
++ }
++ p += out;
++ left -= out;
++ }
++ }
++
++ /* Set the ownership; permissions are still restrictive. */
++ if (fchown(dest_fd, uid_for_copy(access_options, src_stat),
++ gid_for_copy(access_options, src_stat)) == -1
++ && errno != EPERM) {
++ lu_error_new(error, lu_error_generic,
++ _("Error changing owner of `%s': %s"), dest_path,
++ strerror(errno));
++ goto err_dest_fd;
++ }
++
++ /* Set the desired mode. Do this explicitly to preserve S_ISGID and
++ other bits. Do this after chown, because chown is permitted to
++ reset these bits. */
++ if (fchmod(dest_fd, mode_for_copy(access_options, src_stat)) == -1) {
++ lu_error_new(error, lu_error_generic,
++ _("Error setting mode of `%s': %s"), dest_path,
++ strerror(errno));
++ goto err_dest_fd;
++ }
++
++ timebuf[0] = src_stat->st_atim;
++ timebuf[1] = src_stat->st_mtim;
++ futimens(dest_fd, timebuf);
++
++ ret = TRUE;
++ /* Fall through */
++
++err_dest_fd:
++ close(dest_fd);
++ return ret;
++}
++
++/* Forward declaration. */
++static gboolean lu_copy_dir_and_close(int src_dir_fd, GString *src_path_buf,
++ int dest_parent_fd,
++ const char *dest_dir_name,
++ GString *dest_path_buf,
++ const struct stat *src_dir_stat,
++ const struct copy_access_options
++ *access_options, struct lu_error **error);
++
++/* Copy ENT_NAME in SRC_DIR_FD, which corresponds to SRC_PATH_BUF,
++ to DEST_DIR_FD, which corresponds to DEST_PATH_BUF. Use ACCESS_OPTIONS.
++
++ On return from this function, SELinux fscreate context is unspecified. This
++ function may temporarily modify SRC_PATH_BUF and DEST_PATH_BUF, but they
++ will be unchanged on return.
++
++ Note that SRC_PATH_BUF should only be used for error messages, not to access
++ the files; if the user is still logged in, a directory in the path may be
++ replaced by a symbolic link, redirecting the access outside of SRC_DIR_FD.
++ Likewise for DEST_*. */
++static gboolean
++copy_dir_entry(int src_dir_fd, GString *src_path_buf, int dest_dir_fd,
++ GString *dest_path_buf, const char *ent_name,
++ const struct copy_access_options *access_options,
++ struct lu_error **error)
++{
++ struct stat st;
++ int ifd;
++ gboolean ret = FALSE;
++
++ LU_ERROR_CHECK(error);
++
++ /* Open the input entry first, then we can fstat() it and be certain
++ that it is still the same file. O_NONBLOCK protects us against
++ FIFOs and perhaps side-effects of the open() of a device file if
++ there ever was one here, and doesn't matter for regular files or
++ directories. */
++ ifd = openat(src_dir_fd, ent_name,
++ O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
++ if (ifd == -1) {
++ int saved_errno;
++
++ saved_errno = errno;
++ if (errno != ELOOP || fstatat(src_dir_fd, ent_name, &st,
++ AT_SYMLINK_NOFOLLOW) != 0
++ || !S_ISLNK(st.st_mode)) {
++ lu_error_new(error, lu_error_open,
++ _("Error opening `%s': %s"),
++ src_path_buf->str, strerror(saved_errno));
++ return FALSE;
++ }
++
++ return copy_symlink(src_dir_fd, src_path_buf->str, dest_dir_fd,
++ dest_path_buf->str, ent_name, &st,
++ access_options, error);
++ }
++
++ if (fstat(ifd, &st) != 0) {
++ lu_error_new(error, lu_error_stat, _("couldn't stat `%s': %s"),
++ src_path_buf->str, strerror(errno));
++ goto err_ifd;
++ }
++ g_assert(!S_ISLNK(st.st_mode));
++
++ if (S_ISDIR(st.st_mode)) {
++ ret = lu_copy_dir_and_close(ifd, src_path_buf, dest_dir_fd,
++ ent_name, dest_path_buf, &st,
++ access_options, error);
++ ifd = -1;
++ } else if (S_ISREG(st.st_mode))
++ ret = copy_regular_file(ifd, src_path_buf->str, dest_dir_fd,
++ ent_name, dest_path_buf->str, &st,
++ access_options, error);
++ else
++ /* Note that we don't copy device specials. */
++ ret = TRUE;
++ /* Fall through */
++
++err_ifd:
++ if (ifd != -1)
++ close(ifd);
++ return ret;
++}
++
++/* Copy SRC_DIR_FD, which corresponds to SRC_PATH_BUF, to DEST_DIR_NAME under
++ DEST_PARENT_FD, which corresponds to DEST_PATH_BUF. Use ACCESS_OPTIONS. Use
++ SRC_DIR_STAT for data about SRC_PATH_BUF.
++
++ In every case, even on error, close SRC_DIR_FD.
++
++ DEST_PARENT_FD may be AT_FDCWD. On return from this function, SELinux
++ fscreate context is unspecified. This function may temporarily modify
++ SRC_PATH_BUF and DEST_PATH_BUF, but they will be unchanged on return.
++
++ Note that SRC_PATH_BUF should only be used for error messages, not to access
++ the files; if the user is still logged in, a directory in the path may be
++ replaced by a symbolic link, redirecting the access outside of
++ SRC_PARENT_FD/SRC_DIR_NAME. Likewise for DEST_*. */
++static gboolean
++lu_copy_dir_and_close(int src_dir_fd, GString *src_path_buf, int dest_parent_fd,
++ const char *dest_dir_name, GString *dest_path_buf,
++ const struct stat *src_dir_stat,
++ const struct copy_access_options *access_options,
++ struct lu_error **error)
++{
++ size_t orig_src_path_buf_len, orig_dest_path_buf_len;
+ struct dirent *ent;
+ DIR *dir;
+- int ifd, ofd;
++ int dest_dir_fd;
++ struct timespec timebuf[2];
+ gboolean ret = FALSE;
+
+ LU_ERROR_CHECK(error);
++ orig_src_path_buf_len = src_path_buf->len;
++ orig_dest_path_buf_len = dest_path_buf->len;
+
+- if (*dest != '/') {
++ if (*dest_path_buf->str != '/') {
+ lu_error_new(error, lu_error_generic,
+ _("Home directory path `%s' is not absolute"),
+- dest);
+- goto err;
++ dest_path_buf->str);
++ goto err_src_dir_fd;
+ }
+
+- dir = opendir(src);
++ dir = fdopendir(src_dir_fd);
+ if (dir == NULL) {
+ lu_error_new(error, lu_error_generic,
+- _("Error reading `%s': %s"), src,
++ _("Error reading `%s': %s"), src_path_buf->str,
+ strerror(errno));
+- goto err;
++ goto err_src_dir_fd;
+ }
+
+- /* Create the top-level directory. */
+- if (mkdir(dest, mode) == -1 && errno != EEXIST) {
+- lu_error_new(error, lu_error_generic,
+- _("Error creating `%s': %s"), dest,
+- strerror(errno));
++ if (access_options->preserve_source) {
++ if (!lu_util_fscreate_from_fd(src_dir_fd, src_path_buf->str,
++ error))
++ goto err_dir;
++ } else if (!lu_util_fscreate_for_path(dest_path_buf->str,
++ src_dir_stat->st_mode & S_IFMT,
++ error))
+ goto err_dir;
+- }
+
+- /* Set the ownership on the top-level directory. */
+- if (chown(dest, owner, group) == -1 && errno != EPERM) {
++ /* Create the directory. It starts owned by us (presumbaly root), with
++ fairly restrictive permissions that still allow us to use the
++ directory. */
++ if (mkdirat(dest_parent_fd, dest_dir_name, S_IRWXU) == -1
++ && (errno != EEXIST || !access_options->ignore_eexist)) {
+ lu_error_new(error, lu_error_generic,
+- _("Error changing owner of `%s': %s"), dest,
++ _("Error creating `%s': %s"), dest_path_buf->str,
+ strerror(errno));
+ goto err_dir;
+ }
+-
+- /* Set modes explicitly to preserve S_ISGID and other bits. Do this
+- after chown, because chown is permitted to reset these bits. */
+- if (chmod(dest, mode & ~umask_value) == -1) {
+- lu_error_new(error, lu_error_generic,
+- _("Error setting mode of `%s': %s"), dest,
+- strerror(errno));
++ /* FIXME: O_SEARCH would be ideal here, but Linux doesn't currently
++ provide it. */
++ dest_dir_fd = openat(dest_parent_fd, dest_dir_name,
++ O_RDONLY | O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW);
++ if (dest_dir_fd == -1) {
++ lu_error_new(error, lu_error_open, _("Error opening `%s': %s"),
++ dest_path_buf->str, strerror(errno));
+ goto err_dir;
+ }
++ /* The openat() after mkdirat() is not 100% safe; we may be modifying
++ ownership/permissions of another user's directory that was moved to
++ dest_dir_name in the mean time! (Although why there would exist an
++ another user's directory, assuming lack hardlinks of directories, is
++ not clear.)
++
++ There's no way to do this completely atomically; so, rely on
++ permissions of the parent directory (write access to parent is
++ required to rename directories). This holds for the top-level
++ directory, and for the others we achieve this by creating them
++ root-owned and S_IRWXU, and only applying the original ownership and
++ permissions after finishing other work. See also the comment in
++ copy_symlink().
++
++ Handling any preexisting directory structure complicates this -
++ should we temporarily chown/chmod any existing directory to
++ root:root/S_IRWXU? That might be very disruptive, and such
++ structures should not exist in the first place. */
+
+ while ((ent = readdir(dir)) != NULL) {
+- char srcpath[PATH_MAX], path[PATH_MAX], buf[PATH_MAX];
+- struct stat st;
+- struct utimbuf timebuf;
+-
+- /* Iterate through each item in the directory. */
+- /* Skip over self and parent hard links. */
+- if (strcmp(ent->d_name, ".") == 0) {
+- continue;
+- }
+- if (strcmp(ent->d_name, "..") == 0) {
++ if (strcmp(ent->d_name, ".") == 0
++ || strcmp(ent->d_name, "..") == 0)
+ continue;
+- }
+
+ /* Build the path of the source file or directory and its
+ corresponding member in the new tree. */
+- snprintf(srcpath, sizeof(srcpath), "%s/%s", src, ent->d_name);
+- snprintf(path, sizeof(path), "%s/%s", dest, ent->d_name);
++ g_string_append_c(src_path_buf, '/');
++ g_string_append(src_path_buf, ent->d_name);
++ g_string_append_c(dest_path_buf, '/');
++ g_string_append(dest_path_buf, ent->d_name);
++
++ if (!copy_dir_entry(src_dir_fd, src_path_buf, dest_dir_fd,
++ dest_path_buf, ent->d_name, access_options,
++ error))
++ goto err_dest_dir_fd;
++
++ g_string_truncate(src_path_buf, orig_src_path_buf_len);
++ g_string_truncate(dest_path_buf, orig_dest_path_buf_len);
++ }
+
+- /* What we do next depends on the type of entry we're
+- * looking at. */
+- if (lstat(srcpath, &st) != 0)
+- continue;
++ /* Set the ownership on the directory. Permissions are still
++ fairly restrictive. */
++ if (fchown(dest_dir_fd, uid_for_copy(access_options, src_dir_stat),
++ gid_for_copy(access_options, src_dir_stat)) == -1
++ && errno != EPERM) {
++ lu_error_new(error, lu_error_generic,
++ _("Error changing owner of `%s': %s"),
++ dest_path_buf->str, strerror(errno));
++ goto err_dest_dir_fd;
++ }
+
+- if (keep_contexts != 0) {
+- if (!lu_util_fscreate_from_file(srcpath, error))
+- goto err_dir;
+- } else if (!lu_util_fscreate_for_path(path, st.st_mode & S_IFMT,
+- error))
+- goto err_dir;
++ /* Set the desired mode. Do this explicitly to preserve S_ISGID and
++ other bits. Do this after chown, because chown is permitted to
++ reset these bits. */
++ if (fchmod(dest_dir_fd,
++ mode_for_copy(access_options, src_dir_stat)) == -1) {
++ lu_error_new(error, lu_error_generic,
++ _("Error setting mode of `%s': %s"),
++ dest_path_buf->str, strerror(errno));
++ goto err_dest_dir_fd;
++ }
+
+- /* We always want to preserve atime/mtime. */
+- timebuf.actime = st.st_atime;
+- timebuf.modtime = st.st_mtime;
++ timebuf[0] = src_dir_stat->st_atim;
++ timebuf[1] = src_dir_stat->st_mtim;
++ futimens(dest_dir_fd, timebuf);
+
+- /* If it's a directory, descend into it. */
+- if (S_ISDIR(st.st_mode)) {
+- if (!lu_homedir_copy(srcpath, path, owner,
+- st.st_gid ?: group, st.st_mode,
+- keep_contexts, umask_value, error))
+- /* Aargh! Fail up. */
+- goto err_dir;
+- /* Set the date on the directory. */
+- utime(path, &timebuf);
+- continue;
+- }
++ ret = TRUE;
++ /* Fall through */
+
+- /* If it's a symlink, duplicate it. */
+- if (S_ISLNK(st.st_mode)) {
+- ssize_t len;
++ err_dest_dir_fd:
++ close(dest_dir_fd);
++ err_dir:
++ closedir(dir);
++ src_dir_fd = -1;
++ err_src_dir_fd:
++ if (src_dir_fd != -1)
++ close(src_dir_fd);
++ g_string_truncate(src_path_buf, orig_src_path_buf_len);
++ g_string_truncate(dest_path_buf, orig_dest_path_buf_len);
++ return ret;
++}
+
+- len = readlink(srcpath, buf, sizeof(buf) - 1);
+- if (len == -1) {
+- lu_error_new(error, lu_error_generic,
+- _("Error reading `%s': %s"),
+- srcpath, strerror(errno));
+- goto err_dir;
+- }
+- buf[len] = '\0';
+- if (symlink(buf, path) == -1) {
+- if (errno == EEXIST)
+- continue;
+- lu_error_new(error, lu_error_generic,
+- _("Error creating `%s': %s"),
+- path, strerror(errno));
+- goto err_dir;
+- }
+- if (lchown(path, owner, st.st_gid ?: group) == -1
+- && errno != EPERM && errno != EOPNOTSUPP) {
+- lu_error_new(error, lu_error_generic,
+- _("Error changing owner of `%s': "
+- "%s"), dest, strerror(errno));
+- goto err_dir;
+- }
+- utime(path, &timebuf);
+- continue;
+- }
++/* Copy SRC_DIR to DEST_DIR. Use ACCESS_OPTIONS.
+
+- /* If it's a regular file, copy it. */
+- if (S_ISREG(st.st_mode)) {
+- off_t offset;
+-
+- /* Open both the input and output files. If we fail to
+- do either, we have to give up. */
+- ifd = open(srcpath, O_RDONLY);
+- if (ifd == -1) {
+- lu_error_new(error, lu_error_open,
+- _("Error reading `%s': %s"),
+- srcpath, strerror(errno));
+- goto err_dir;
+- }
+- ofd = open(path, O_EXCL | O_CREAT | O_WRONLY,
+- st.st_mode);
+- if (ofd == -1) {
+- if (errno == EEXIST) {
+- close(ifd);
+- continue;
+- }
+- lu_error_new(error, lu_error_open,
+- _("Error writing `%s': %s"),
+- path, strerror(errno));
+- goto err_ifd;
+- }
++ Return TRUE on error.
+
+- /* Now just copy the data. */
+- for (;;) {
+- ssize_t left;
+- char *p;
+-
+- left = read(ifd, &buf, sizeof(buf));
+- if (left == -1) {
+- if (errno == EINTR)
+- continue;
+- lu_error_new(error, lu_error_read,
+- _("Error reading `%s': "
+- "%s"), srcpath,
+- strerror(errno));
+- goto err_ofd;
+- }
+- if (left == 0)
+- break;
+- p = buf;
+- while (left > 0) {
+- ssize_t out;
+-
+- out = write(ofd, p, left);
+- if (out == -1) {
+- if (errno == EINTR)
+- continue;
+- lu_error_new(error,
+- lu_error_write,
+- _("Error writing "
+- "`%s': %s"),
+- path,
+- strerror(errno));
+- goto err_ofd;
+- }
+- p += out;
+- left -= out;
+- }
+- }
++ To be secure, neither SRC_DIR nor DEST_DIR should contain any
++ user-controlled parent directories in the path. SRC_DIR may be an
++ user-owned directory, or even a symlink, but its parent should not be
++ user-writable (so that the user can't replace it with a symlink or change
++ the symlink). */
++static gboolean
++lu_homedir_copy(const char *src_dir, const char *dest_dir,
++ const struct copy_access_options *access_options,
++ struct lu_error **error)
++{
++ lu_security_context_t fscreate;
++ int fd;
++ struct stat st;
++ GString *src_path_buf, *dest_path_buf;
++ gboolean ret;
+
+- /* Close the files. */
+- offset = lseek(ofd, 0, SEEK_CUR);
+- if (offset != ((off_t) -1)) {
+- if (ftruncate(ofd, offset) == -1) {
+- lu_error_new(error, lu_error_generic,
+- _("Error writing `%s': "
+- "%s"), path,
+- strerror(errno));
+- goto err_ofd;
+- }
+- }
+- close (ifd);
+- close (ofd);
++ LU_ERROR_CHECK(error);
+
+- /* Set the ownership and timestamp on the new file. */
+- if (chown(path, owner, st.st_gid ?: group) == -1
+- && errno != EPERM) {
+- lu_error_new(error, lu_error_generic,
+- _("Error changing owner of `%s': "
+- "%s"), dest, strerror(errno));
+- goto err_dir;
+- }
+- utime(path, &timebuf);
+- continue;
+- }
+- /* Note that we don't copy device specials. */
++ ret = FALSE;
++ if (!lu_util_fscreate_save(&fscreate, error))
++ goto err;
++
++ fd = open(src_dir, O_RDONLY | O_CLOEXEC | O_DIRECTORY);
++ if (fd == -1) {
++ lu_error_new(error, lu_error_open, _("Error opening `%s': %s"),
++ src_dir, strerror(errno));
++ goto err_fscreate;
++ }
++ if (fstat(fd, &st) == -1) {
++ lu_error_new(error, lu_error_stat, _("couldn't stat `%s': %s"),
++ src_dir, strerror(errno));
++ goto err_fd;
+ }
+- ret = TRUE;
+- goto err_dir;
+
+- err_ofd:
+- close(ofd);
+- err_ifd:
+- close(ifd);
+- err_dir:
+- closedir(dir);
+- err:
++ src_path_buf = g_string_new(src_dir);
++ dest_path_buf = g_string_new(dest_dir);
++ ret = lu_copy_dir_and_close(fd, src_path_buf, AT_FDCWD, dest_dir,
++ dest_path_buf, &st, access_options, error);
++ g_string_free(dest_path_buf, TRUE);
++ g_string_free(src_path_buf, TRUE);
++ goto err_fscreate;
++
++err_fd:
++ close(fd);
++err_fscreate:
++ lu_util_fscreate_restore(fscreate);
++err:
+ return ret;
+ }
+
+@@ -315,6 +596,10 @@ lu_homedir_copy(const char *src, const char *dest, uid_t owner, gid_t group,
+ *
+ * Creates a new home directory for an user.
+ *
++ * If you want to use this in a hostile environment, ensure that no untrusted
++ * user has write permission to any parent of @skeleton or @directory. Usually
++ * /home is only writable by root, which is safe.
++ *
+ * Returns: %TRUE on success
+ */
+ gboolean
+@@ -322,107 +607,174 @@ lu_homedir_populate(struct lu_context *ctx, const char *skeleton,
+ const char *directory, uid_t owner, gid_t group,
+ mode_t mode, struct lu_error **error)
+ {
+- lu_security_context_t fscreate;
+- gboolean ret;
++ struct copy_access_options access_options;
++
++ LU_ERROR_CHECK(error);
++ g_return_val_if_fail(ctx != NULL, FALSE);
++ g_return_val_if_fail(directory != NULL, FALSE);
+
+ if (skeleton == NULL)
+ skeleton = lu_cfg_read_single(ctx, "defaults/skeleton",
+ "/etc/skel");
+- ret = FALSE;
+- if (!lu_util_fscreate_save(&fscreate, error))
+- goto err;
+- if (!lu_util_fscreate_for_path(directory, S_IFDIR, error))
+- goto err_fscreate;
+- ret = lu_homedir_copy(skeleton, directory, owner, group, mode, 0,
+- current_umask(), error);
+-err_fscreate:
+- lu_util_fscreate_restore(fscreate);
+-err:
+- return ret;
++ access_options.preserve_source = FALSE;
++ access_options.ignore_eexist = TRUE;
++ access_options.uid = owner;
++ access_options.gid = group;
++ access_options.umask = current_umask();
++ if (!lu_homedir_copy(skeleton, directory, &access_options, error))
++ return FALSE;
++
++ /* Now reconfigure the toplevel directory as desired. The directory
++ thus might have incorrect owner/permissions for a while; this is OK
++ because the contents are public anyway (every users sees them on
++ first access), and write access is not allowed because the skeleton
++ is not writable. */
++
++ /* Set the ownership on the top-level directory manually again,
++ lu_homedir_copy() would have preserved st.st_gid if it were not root
++ for some reason; our API promises to use precisely "owner" and
++ "group". */
++ if (chown(directory, owner, group) == -1 && errno != EPERM) {
++ lu_error_new(error, lu_error_generic,
++ _("Error changing owner of `%s': %s"), directory,
++ strerror(errno));
++ return FALSE;
++ }
++ /* Set modes as required instead of preserving st.st_mode. Do this
++ after chown, because chown is permitted to reset these bits. */
++ if (chmod(directory, mode & ~access_options.umask) == -1) {
++ lu_error_new(error, lu_error_generic,
++ _("Error setting mode of `%s': %s"), directory,
++ strerror(errno));
++ return FALSE;
++ }
++
++ return TRUE;
+ }
+
+-/**
+- * lu_homedir_remove:
+- * @directory: Path to the root of the directory tree
+- * @error: Filled with #lu_error if an error occurs
+- *
+- * Recursively removes a user's home (or really, any) directory.
+- *
+- * Note that the implementation is not currently race-free; calling this when
+- * the user may still be logged in is discouraged.
+- *
+- * Returns: %TRUE on success
+- */
+-gboolean
+-lu_homedir_remove(const char *directory, struct lu_error ** error)
++/* Recursively remove directory DIR_NAME under PARENT_FD, which corresponds to
++ PATH_BUF.
++
++ Return TRUE on sucess.
++
++ PARENT_FD may be AT_FDCWD. This function may temporarily modify PATH_BUF,
++ but it will be unchanged on return.
++
++ Note that PATH_BUF should only be used for error messages, not to access
++ the files; if the user is still logged in, a directory in the path may be
++ replaced by a symbolic link, redirecting the access outside of
++ PARENT_FD/DIR_NAME. */
++static gboolean
++remove_subdirectory(int parent_fd, const char *dir_name, GString *path_buf,
++ struct lu_error **error)
+ {
++ size_t orig_path_buf_len;
++ int dir_fd;
+ struct dirent *ent;
+ DIR *dir;
+
+ LU_ERROR_CHECK(error);
++ orig_path_buf_len = path_buf->len;
+
+- /* Open the directory. This catches the case that it's already gone. */
+- dir = opendir(directory);
++ dir_fd = openat(parent_fd, dir_name,
++ O_RDONLY | O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW);
++ if (dir_fd == -1) {
++ lu_error_new(error, lu_error_open,
++ _("Error opening `%s': %s"), path_buf->str,
++ strerror(errno));
++ return FALSE;
++ }
++ dir = fdopendir(dir_fd);
+ if (dir == NULL) {
+- lu_error_new(error, lu_error_generic,
+- _("Error removing `%s': %s"), directory,
++ lu_error_new(error, lu_error_open,
++ _("Error opening `%s': %s"), path_buf->str,
+ strerror(errno));
++ close(dir_fd);
+ return FALSE;
+ }
+
+ /* Iterate over all of its contents. */
+ while ((ent = readdir(dir)) != NULL) {
+ struct stat st;
+- char path[PATH_MAX];
+
+ /* Skip over the self and parent hard links. */
+- if (strcmp(ent->d_name, ".") == 0) {
+- continue;
+- }
+- if (strcmp(ent->d_name, "..") == 0) {
++ if (strcmp(ent->d_name, ".") == 0
++ || strcmp(ent->d_name, "..") == 0)
+ continue;
+- }
+
+ /* Generate the full path of the next victim. */
+- snprintf(path, sizeof(path), "%s/%s", directory, ent->d_name);
++ g_string_append_c(path_buf, '/');
++ g_string_append(path_buf, ent->d_name);
+
+ /* What we do next depends on whether or not the next item to
+- * remove is a directory. */
+- if (lstat(path, &st) != -1) {
+- if (S_ISDIR(st.st_mode)) {
+- /* We decend into subdirectories... */
+- if (lu_homedir_remove(path, error) == FALSE) {
+- closedir(dir);
+- return FALSE;
+- }
+- } else {
+- /* ... and unlink everything else. */
+- if (unlink(path) == -1) {
+- lu_error_new(error,
+- lu_error_generic,
+- _("Error removing "
+- "`%s': %s"),
+- path,
+- strerror
+- (errno));
+- closedir(dir);
+- return FALSE;
+- }
++ remove is a directory. */
++ if (fstatat(dir_fd, ent->d_name, &st,
++ AT_SYMLINK_NOFOLLOW) == -1) {
++ lu_error_new(error, lu_error_stat,
++ _("couldn't stat `%s': %s"), path_buf->str,
++ strerror(errno));
++ goto err_dir;
++ }
++ if (S_ISDIR(st.st_mode)) {
++ /* We descend into subdirectories... */
++ if (remove_subdirectory(dir_fd, ent->d_name, path_buf,
++ error) == FALSE)
++ goto err_dir;
++ } else {
++ /* ... and unlink everything else. */
++ if (unlinkat(dir_fd, ent->d_name, 0) == -1) {
++ lu_error_new(error, lu_error_generic,
++ _("Error removing `%s': %s"),
++ path_buf->str, strerror(errno));
++ goto err_dir;
+ }
+ }
++
++ g_string_truncate(path_buf, orig_path_buf_len);
+ }
+
+ closedir(dir);
+
+ /* As a final step, remove the directory itself. */
+- if (rmdir(directory) == -1) {
++ if (unlinkat(parent_fd, dir_name, AT_REMOVEDIR) == -1) {
+ lu_error_new(error, lu_error_generic,
+- _("Error removing `%s': %s"), directory,
++ _("Error removing `%s': %s"), path_buf->str,
+ strerror(errno));
+ return FALSE;
+ }
+
+ return TRUE;
++
++err_dir:
++ closedir(dir);
++ g_string_truncate(path_buf, orig_path_buf_len);
++ return FALSE;
++}
++
++/**
++ * lu_homedir_remove:
++ * @directory: Path to the root of the directory tree
++ * @error: Filled with #lu_error if an error occurs
++ *
++ * Recursively removes a user's home (or really, any) directory.
++ *
++ * If you want to use this in a hostile environment, ensure that no untrusted
++ * user has write permission to any parent of @directory.
++ *
++ * Returns: %TRUE on success
++ */
++gboolean
++lu_homedir_remove(const char *directory, struct lu_error ** error)
++{
++ gboolean ret;
++ GString *path_buf;
++
++ LU_ERROR_CHECK(error);
++ g_return_val_if_fail(directory != NULL, FALSE);
++ path_buf = g_string_new(directory);
++ ret = remove_subdirectory(AT_FDCWD, directory, path_buf, error);
++ g_string_free(path_buf, TRUE);
++ return ret;
+ }
+
+ /**
+@@ -436,37 +788,29 @@ lu_homedir_remove(const char *directory, struct lu_error ** error)
+ * Currently implemented by first creating a copy, then deleting the original,
+ * expect this to take a long time.
+ *
++ * If you want to use this in a hostile environment, ensure that no untrusted
++ * user has write permission to any parent of @oldhome or @newhome. Usually
++ * /home is only writable by root, which is safe; user's write permission to
++ * @oldhome itself is OK.
++ *
+ * Returns: %TRUE on success
+ */
+ gboolean
+ lu_homedir_move(const char *oldhome, const char *newhome,
+ struct lu_error ** error)
+ {
+- struct stat st;
+- lu_security_context_t fscreate;
++ struct copy_access_options access_options;
+
+ LU_ERROR_CHECK(error);
++ g_return_val_if_fail(oldhome != NULL, FALSE);
++ g_return_val_if_fail(newhome != NULL, FALSE);
+
+- /* If the directory exists... */
+- if (stat(oldhome, &st) != 0)
+- goto err;
++ access_options.preserve_source = TRUE;
++ access_options.ignore_eexist = FALSE;
++ if (!lu_homedir_copy(oldhome, newhome, &access_options, error))
++ return FALSE;
+
+- if (!lu_util_fscreate_save(&fscreate, error))
+- goto err;
+- if (!lu_util_fscreate_from_file(oldhome, error))
+- goto err_fscreate;
+- /* ... and we can copy it ... */
+- if (!lu_homedir_copy(oldhome, newhome, st.st_uid, st.st_gid,
+- st.st_mode, 1, current_umask(), error))
+- goto err_fscreate;
+- lu_util_fscreate_restore(fscreate);
+- /* ... remove the old one. */
+ return lu_homedir_remove(oldhome, error);
+-
+-err_fscreate:
+- lu_util_fscreate_restore(fscreate);
+-err:
+- return FALSE;
+ }
+
+ /**
+diff --git a/lib/user_private.h b/lib/user_private.h
+index c829a71..6563f81 100644
+--- a/lib/user_private.h
++++ b/lib/user_private.h
+@@ -309,7 +309,10 @@ typedef security_context_t lu_security_context_t;
+ gboolean lu_util_fscreate_save(security_context_t *ctx,
+ struct lu_error **error);
+ void lu_util_fscreate_restore(security_context_t ctx);
++gboolean lu_util_fscreate_from_fd(int fd, const char *path,
++ struct lu_error **error);
+ gboolean lu_util_fscreate_from_file(const char *file, struct lu_error **error);
++gboolean lu_util_fscreate_from_lfile(const char *file, struct lu_error **error);
+ gboolean lu_util_fscreate_for_path(const char *path, mode_t mode,
+ struct lu_error **error);
+
+@@ -317,8 +320,12 @@ gboolean lu_util_fscreate_for_path(const char *path, mode_t mode,
+ typedef char lu_security_context_t; /* "Something" */
+ #define lu_util_fscreate_save(CTX, ERROR) ((void)(CTX), (void)(ERROR), TRUE)
+ #define lu_util_fscreate_restore(CTX) ((void)(CTX))
++#define lu_util_fscreate_from_fd(FD, PATH, ERROR) \
++ ((void)(FILE), (void)(PATH), (void)(ERROR), TRUE)
+ #define lu_util_fscreate_from_file(FILE, ERROR) \
+ ((void)(FILE), (void)(ERROR), TRUE)
++#define lu_util_fscreate_from_lfile(FILE, ERROR) \
++ ((void)(FILE), (void)(ERROR), TRUE)
+ #define lu_util_fscreate_for_path(PATH, MODE, ERROR) \
+ ((void)(PATH), (void)(MODE), (void)(ERROR), TRUE)
+ #endif
+diff --git a/lib/util.c b/lib/util.c
+index 0177238..b0ed63b 100644
+--- a/lib/util.c
++++ b/lib/util.c
+@@ -705,6 +705,32 @@ lu_util_fscreate_restore(security_context_t ctx)
+ }
+ }
+
++/* Set fscreate context from context of fd. Use path only for diagnostics. */
++gboolean
++lu_util_fscreate_from_fd(int fd, const char *path, struct lu_error **error)
++{
++ if (is_selinux_enabled() > 0) {
++ security_context_t ctx;
++
++ if (fgetfilecon(fd, &ctx) < 0) {
++ lu_error_new(error, lu_error_stat,
++ _("couldn't get security context of "
++ "`%s': %s"), path, strerror(errno));
++ return FALSE;
++ }
++ if (setfscreatecon(ctx) < 0) {
++ lu_error_new(error, lu_error_generic,
++ _("couldn't set default security context "
++ "to `%s': %s"), ctx, strerror(errno));
++ freecon(ctx);
++ return FALSE;
++ }
++ freecon(ctx);
++ }
++ return TRUE;
++}
++
++
+ /* Set fscreate context from context of file. */
+ gboolean
+ lu_util_fscreate_from_file(const char *file, struct lu_error **error)
+@@ -730,6 +756,32 @@ lu_util_fscreate_from_file(const char *file, struct lu_error **error)
+ return TRUE;
+ }
+
++/* Set fscreate context from context of file, not resolving it if it is a
++ symlink. */
++gboolean
++lu_util_fscreate_from_lfile(const char *file, struct lu_error **error)
++{
++ if (is_selinux_enabled() > 0) {
++ security_context_t ctx;
++
++ if (lgetfilecon(file, &ctx) < 0) {
++ lu_error_new(error, lu_error_stat,
++ _("couldn't get security context of "
++ "`%s': %s"), file, strerror(errno));
++ return FALSE;
++ }
++ if (setfscreatecon(ctx) < 0) {
++ lu_error_new(error, lu_error_generic,
++ _("couldn't set default security context "
++ "to `%s': %s"), ctx, strerror(errno));
++ freecon(ctx);
++ return FALSE;
++ }
++ freecon(ctx);
++ }
++ return TRUE;
++}
++
+ /* Set fscreate context for creating a file at path, with file type specified
+ by mode. */
+ gboolean
+diff --git a/tests/fs.conf.in b/tests/fs.conf.in
+new file mode 100644
+index 0000000..3991b93
+--- /dev/null
++++ b/tests/fs.conf.in
+@@ -0,0 +1,24 @@
++[defaults]
++# non-portable
++moduledir = @TOP_BUILDDIR@/modules/.libs
++skeleton = @WORKDIR@/skel
++modules = files shadow
++create_modules = files shadow
++crypt_style = md5
++
++[userdefaults]
++LU_USERNAME = %n
++LU_UIDNUMBER = 500
++LU_GIDNUMBER = %u
++
++[groupdefaults]
++LU_GROUPNAME = %n
++LU_GIDNUMBER = 500
++
++[files]
++directory = @WORKDIR@/files
++nonroot = yes
++
++[shadow]
++directory = @WORKDIR@/files
++nonroot = yes
+diff --git a/tests/fs_test b/tests/fs_test
+new file mode 100755
+index 0000000..4179fc8
+--- /dev/null
++++ b/tests/fs_test
+@@ -0,0 +1,233 @@
++#! /bin/sh
++# Automated libuser fs function regression tester
++#
++# Copyright (c) 2012 Red Hat, Inc. All rights reserved.
++#
++# This is free software; you can redistribute it and/or modify it under
++# the terms of the GNU Library General Public License as published by
++# the Free Software Foundation; either version 2 of the License, or
++# (at your option) any later version.
++#
++# This program is distributed in the hope that it will be useful, but
++# WITHOUT ANY WARRANTY; without even the implied warranty of
++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++# General Public License for more details.
++#
++# You should have received a copy of the GNU Library General Public
++# License along with this program; if not, write to the Free Software
++# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
++#
++# Author: Miloslav Trmač <mitr at redhat.com>
++
++srcdir=$srcdir/tests
++
++workdir=$(pwd)/test_fs
++
++umask 002
++
++trap 'status=$?; chmod -R u+rwx "$workdir"; rm -rf "$workdir"; exit $status' 0
++trap '(exit 1); exit 1' 1 2 13 15
++
++rm -rf "$workdir"
++mkdir "$workdir"
++
++# Set up an the environment
++mkdir "$workdir"/files
++> "$workdir"/files/passwd
++> "$workdir"/files/shadow
++> "$workdir"/files/group
++> "$workdir"/files/gshadow
++
++# Set up the client
++LIBUSER_CONF=$workdir/libuser.conf
++export LIBUSER_CONF
++sed "s|@WORKDIR@|$workdir|g; s|@TOP_BUILDDIR@|$(pwd)|g" \
++ < "$srcdir"/fs.conf.in > "$LIBUSER_CONF"
++# Ugly non-portable hacks
++LD_LIBRARY_PATH=$(pwd)/lib/.libs
++export LD_LIBRARY_PATH
++PYTHONPATH=$(pwd)/python/.libs
++export PYTHONPATH
++
++# Test lu_homedir_remove()
++fakeroot > rm_output <<EOF
++# User's "own" content
++mkdir -p "$workdir"/rm/root/{dir,unreadable}
++touch "$workdir"/rm/{kept,root/{dir,unreadable}/f}
++mkfifo "$workdir"/rm/root/fifo
++ln -s ../kept "$workdir"/rm/root/symlink
++chown -R 555:555 "$workdir"/rm/root
++chmod 701 "$workdir"/rm/root
++chmod 000 "$workdir"/rm/root/unreadable
++
++# Other content in the home directory
++mknod "$workdir"/rm/root/block b 1 1
++mknod "$workdir"/rm/root/char c 1 1
++echo 'foo' > "$workdir"/rm/secret
++chmod 000 "$workdir"/rm/secret
++ln "$workdir"/rm/{secret,root/hardlink}
++
++$VALGRIND python "$srcdir"/fs_test.py --remove "$workdir/rm/root"
++if [ \$? -ne 0 ]; then
++ exit 1
++fi
++
++LC_ALL=C ls -lnR "$workdir/rm" | tail -n +3 | \
++ awk '{ printf("%.10s %d %4d %4d %8d %s\n",
++ \$1, \$2, \$3, \$4, \$5, \$9) ;}'
++
++EOF
++
++diff rm_output - <<EOF
++-rw-rw-r-- 1 0 0 0 kept
++---------- 1 0 0 4 secret
++EOF
++if [ $? -ne 0 ]; then
++ exit 1
++fi
++
++
++# Prepare an "interesting" home directory, to be used both directly and as
++# a skeleton
++fakeroot -s "$workdir"/save <<EOF
++mkdir -p "$workdir"/common/{dir,unreadable,group-owned}
++cd "$workdir"/common
++# User's "own" content
++for i in '.' dir group-owned; do
++ touch "\$i"/f "\$i"/setuid
++ chmod u+xs "\$i"/setuid
++ mkfifo "\$i"/fifo
++ ln -s ../outside "\$i"/symlink
++done
++mkdir setgid
++chmod g+s setgid
++echo 'content' > unreadable/f
++chmod 000 unreadable/f unreadable
++chown -R 555:555 .
++chgrp -R 444 group-owned
++
++# Other content in the home directory
++mknod block b 1 1
++mknod char c 1 1
++echo 'foo' > secret
++chmod 000 secret
++EOF
++
++
++# Test lu_homedir_move()
++fakeroot -i "$workdir"/save -s "$workdir"/save > mv_output <<EOF
++cp -a "$workdir"/common "$workdir"/home1
++# This seems to be a fakeroot bug
++# (https://bugzilla.redhat.com/show_bug.cgi?id=887001) - in the copy, mode-0
++# files are really unreadable in the underlying filesystem. However, a chmod
++# resets the state.
++chmod 0 "$workdir"/home1/{unreadable,unreadable/f,secret}
++
++$VALGRIND python "$srcdir"/fs_test.py --move "$workdir"/{home1,home2}
++if [ \$? -ne 0 ]; then
++ exit 1
++fi
++
++cd "$workdir"/home2
++LC_ALL=C ls -lnR | \
++ awk 'NF > 3 { printf("%.10s %d %4d %4d %s\n",
++ \$1, \$2, \$3, \$4, \$9); }
++ NF <= 3 && !/total/ { print }'
++EOF
++
++# Special files and fifos are not copied over. Ownership and permissions are
++# preserved.
++diff mv_output - <<EOF
++.:
++drwx------ 2 555 555 dir
++-rw------- 1 555 555 f
++drwx------ 2 555 444 group-owned
++---------- 1 0 0 secret
++drwxrwsr-x 2 555 555 setgid
++-rwsrw-r-- 1 555 555 setuid
++lrwxrwxrwx 1 555 555 symlink
++d--------- 2 555 555 unreadable
++
++./dir:
++-rw------- 1 555 555 f
++-rwsrw-r-- 1 555 555 setuid
++lrwxrwxrwx 1 555 555 symlink
++
++./group-owned:
++-rw------- 1 555 444 f
++-rwsrw-r-- 1 555 444 setuid
++lrwxrwxrwx 1 555 444 symlink
++
++./setgid:
++
++./unreadable:
++---------- 1 555 555 f
++EOF
++
++# Moving onto an existing directory is prohibited
++fakeroot > mv2_output 2>&1 <<EOF
++mkdir "$workdir"/mv2home{1,2}
++
++$VALGRIND python "$srcdir"/fs_test.py --move "$workdir"/mv2home{1,2}
++echo \$?
++EOF
++diff mv2_output - <<EOF
++Error creating \`$workdir/mv2home2': File exists
++1
++EOF
++
++
++# Test lu_homedir_populate()
++fakeroot -i "$workdir"/save -s "$workdir"/save > pop_output <<EOF
++cp -a "$workdir"/common "$workdir"/skel
++# This seems to be a fakeroot bug
++# (https://bugzilla.redhat.com/show_bug.cgi?id=887001) - in the copy, mode-0
++# files are really unreadable in the underlying filesystem. However, a chmod
++# resets the state.
++chmod 0 "$workdir"/skel/{unreadable,unreadable/f,secret}
++chown -R 0:0 "$workdir"/skel
++chgrp -R 444 "$workdir"/skel/group-owned
++
++$VALGRIND python "$srcdir"/fs_test.py --populate "$workdir"/newhome 556 557
++if [ \$? -ne 0 ]; then
++ exit 1
++fi
++
++cd "$workdir"/newhome
++LC_ALL=C ls -lnR | \
++ awk 'NF > 3 { printf("%.10s %d %4d %4d %s\n",
++ \$1, \$2, \$3, \$4, \$9); }
++ NF <= 3 && !/total/ { print }'
++EOF
++
++# Special files and fifos are not copied over. Permissions are preserved,
++# ownership is changed to the desired values except for non-root groups
++diff pop_output - <<EOF
++.:
++drwx------ 2 556 557 dir
++-rw------- 1 556 557 f
++drwx------ 2 556 444 group-owned
++---------- 1 556 557 secret
++drwxrwsr-x 2 556 557 setgid
++-rwsrw-r-- 1 556 557 setuid
++lrwxrwxrwx 1 556 557 symlink
++d--------- 2 556 557 unreadable
++
++./dir:
++-rw------- 1 556 557 f
++-rwsrw-r-- 1 556 557 setuid
++lrwxrwxrwx 1 556 557 symlink
++
++./group-owned:
++-rw------- 1 556 444 f
++-rwsrw-r-- 1 556 444 setuid
++lrwxrwxrwx 1 556 444 symlink
++
++./setgid:
++
++./unreadable:
++---------- 1 556 557 f
++EOF
++if [ $? -ne 0 ]; then
++ exit 1
++fi
+diff --git a/tests/fs_test.py b/tests/fs_test.py
+new file mode 100644
+index 0000000..914bc17
+--- /dev/null
++++ b/tests/fs_test.py
+@@ -0,0 +1,53 @@
++# Helpers for fs_test.
++# -*- coding: utf-8 -*-
++#
++# Copyright (c) 2012 Red Hat, Inc. All rights reserved.
++#
++# This is free software; you can redistribute it and/or modify it under
++# the terms of the GNU Library General Public License as published by
++# the Free Software Foundation; either version 2 of the License, or
++# (at your option) any later version.
++#
++# This program is distributed in the hope that it will be useful, but
++# WITHOUT ANY WARRANTY; without even the implied warranty of
++# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
++# General Public License for more details.
++#
++# You should have received a copy of the GNU Library General Public
++# License along with this program; if not, write to the Free Software
++# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
++#
++# Author: Miloslav Trmač <mitr at redhat.com>
++
++import libuser
++import sys
++
++def main():
++ if sys.argv[1] == '--remove':
++ a = libuser.admin()
++ u = a.initUser('fs_test_remove')
++ u[libuser.HOMEDIRECTORY] = sys.argv[2]
++ a.removeHome(u)
++ elif sys.argv[1] == '--move':
++ a = libuser.admin()
++ u = a.initUser('fs_test_move')
++ u[libuser.HOMEDIRECTORY] = sys.argv[2]
++ try:
++ a.moveHome(u, sys.argv[3])
++ except RuntimeError, e:
++ sys.exit(str(e))
++ elif sys.argv[1] == '--populate':
++ a = libuser.admin()
++ u = a.initUser('fs_test_populate')
++ u[libuser.HOMEDIRECTORY] = sys.argv[2]
++ u[libuser.UIDNUMBER] = int(sys.argv[3])
++ u[libuser.GIDNUMBER] = int(sys.argv[4])
++ try:
++ a.createHome(u)
++ except RuntimeError, e:
++ sys.exit(str(e))
++ else:
++ sys.exit('Unexpected mode')
++
++if __name__ == '__main__':
++ main()
diff --git a/libuser.spec b/libuser.spec
index 6772e78..beaee67 100644
--- a/libuser.spec
+++ b/libuser.spec
@@ -2,13 +2,14 @@
Name: libuser
Version: 0.58
-Release: 2%{?dist}
+Release: 3%{?dist}
Group: System Environment/Base
License: LGPLv2+
URL: https://fedorahosted.org/libuser/
# Upstream commit 51e9d56ed656c3aeceb39b7de5a1db7d976d4e51
-Patch0: libuser-force-secure-getenv.patch
Source: https://fedorahosted.org/releases/l/i/libuser/libuser-%{version}.tar.xz
+Patch0: libuser-force-secure-getenv.patch
+Patch1: libuser-dirtree-TOCTOU-fix.patch
BuildRequires: glib2-devel, linuxdoc-tools, pam-devel, popt-devel, python2-devel
BuildRequires: cyrus-sasl-devel, libselinux-devel, openldap-devel
# To make sure the configure script can find it
@@ -17,6 +18,7 @@ BuildRequires: nscd
BuildRequires: openldap-clients, openldap-servers, openssl
# For regenerating autoconf/automake files
BuildRequires: gtk-doc, libtool, gettext-devel, automake, autoconf
+BuildRequires: fakeroot
Summary: A user and group account administration library
%description
@@ -50,6 +52,8 @@ administering user and group accounts.
%prep
%setup -q
%patch0 -p1 -b .force_secure_getenv
+%patch1 -p1 -b .toctou
+chmod +x tests/fs_test
%build
# Copied from upstream autogen.sh
@@ -109,6 +113,10 @@ python -c "import libuser"
%{_datadir}/gtk-doc/html/*
%changelog
+* Fri Mar 15 2013 Hercinger Viktor <hercinger.viktor at gmail.com> - 0.58-3
+- Fixed TOCTOU race condition when copying, removing or creating directory trees
+ Resolves: #928846, CVE-2012-5630, CVE-2012-5644
+
* Mon Feb 4 2013 Miloslav Trmač <mitr at redhat.com> - 0.58-2
- Always use secure_getenv() or __secure_getenv(), fail build if neither is
available. Patch by Viktor Hercinger <vhercing at redhat.com>.
More information about the scm-commits
mailing list