[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