summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaul Eggert <eggert@cs.ucla.edu>2007-12-01 10:09:57 +0100
committerJim Meyering <meyering@redhat.com>2007-12-01 10:09:57 +0100
commitb64119bc54791809b9fc3a225761b1c913fe66bc (patch)
tree15566919c6c92780e8677a300cef940668fe2bad /src
parent317413874c935c8605ae3549e7575e03427b953e (diff)
downloadcoreutils-b64119bc54791809b9fc3a225761b1c913fe66bc.tar.xz
Fix a security race with "cp -p A B" when B already exists.
* src/copy.h (struct cp_options): New member owner_privileges. * src/copy.c (USE_ACL): Define to 0 if not defined, for convenience. (owner_failure_ok): New function. (set_owner): Avoid a security-related race by doing an extra chmod first if it looks like there might be trouble right after a chown. Accept a source struct stat rather than a uid and gid, and accept a boolean NEW_DST and destination struct stat. All callers changed. * src/copy.h (cp_options_default): New function, replacing the old chown_privileges. * src/copy.c (cp_options_default): Likewise. * src/cp.c (cp_option_init): Use it. * src/install.c (cp_option_init): Likewise. * src/mv.c (cp_option_init): Likewise.
Diffstat (limited to 'src')
-rw-r--r--src/copy.c81
-rw-r--r--src/copy.h9
-rw-r--r--src/cp.c2
-rw-r--r--src/install.c2
-rw-r--r--src/mv.c2
5 files changed, 75 insertions, 21 deletions
diff --git a/src/copy.c b/src/copy.c
index 975890750..1e803ec2d 100644
--- a/src/copy.c
+++ b/src/copy.c
@@ -66,6 +66,10 @@
# define lchown(name, uid, gid) chown (name, uid, gid)
#endif
+#ifndef USE_ACL
+# define USE_ACL 0
+#endif
+
#define SAME_OWNER(A, B) ((A).st_uid == (B).st_uid)
#define SAME_GROUP(A, B) ((A).st_gid == (B).st_gid)
#define SAME_OWNER_AND_GROUP(A, B) (SAME_OWNER (A, B) && SAME_GROUP (A, B))
@@ -87,6 +91,7 @@ static bool copy_internal (char const *src_name, char const *dst_name,
bool command_line_arg,
bool *copy_into_self,
bool *rename_succeeded);
+static bool owner_failure_ok (struct cp_options const *x);
/* Pointers to the file names: they're used in the diagnostic that is issued
when we detect the user is trying to copy a directory into itself. */
@@ -173,13 +178,43 @@ copy_dir (char const *src_name_in, char const *dst_name_in, bool new_dst,
symbolic links should be involved. DEST_DESC must
refer to the same file as DEST_NAME if defined.
Upon failure to set both UID and GID, try to set only the GID.
+ NEW_DST is true if the file was newly created; otherwise,
+ DST_SB is the status of the destination.
Return 1 if the initial syscall succeeds, 0 if it fails but it's OK
not to preserve ownership, -1 otherwise. */
static int
set_owner (const struct cp_options *x, char const *dst_name, int dest_desc,
- uid_t uid, gid_t gid)
+ struct stat const *src_sb, bool new_dst,
+ struct stat const *dst_sb)
{
+ uid_t uid = src_sb->st_uid;
+ gid_t gid = src_sb->st_gid;
+
+ /* Naively changing the ownership of an already-existing file before
+ changing its permissions would create a window of vulnerability if
+ the file's old permissions are too generous for the new owner and
+ group. Avoid the window by first changing to a restrictive
+ temporary mode if necessary. */
+
+ if (!new_dst & (x->preserve_mode | x->move_mode | x->set_mode))
+ {
+ mode_t old_mode = dst_sb->st_mode;
+ mode_t new_mode =
+ (x->preserve_mode | x->move_mode ? src_sb->st_mode : x->mode);
+ mode_t restrictive_temp_mode = old_mode & new_mode & S_IRWXU;
+
+ if ((USE_ACL
+ || (old_mode & CHMOD_MODE_BITS
+ & (~new_mode | S_ISUID | S_ISGID | S_ISVTX)))
+ && qset_acl (dst_name, dest_desc, restrictive_temp_mode) != 0)
+ {
+ if (! owner_failure_ok (x))
+ error (0, errno, _("clearing permissions for %s"), quote (dst_name));
+ return -x->require_preserve;
+ }
+ }
+
if (HAVE_FCHOWN && dest_desc != -1)
{
if (fchown (dest_desc, uid, gid) == 0)
@@ -627,8 +662,7 @@ copy_reg (char const *src_name, char const *dst_name,
if (x->preserve_ownership && ! SAME_OWNER_AND_GROUP (*src_sb, sb))
{
- switch (set_owner (x, dst_name, dest_desc,
- src_sb->st_uid, src_sb->st_gid))
+ switch (set_owner (x, dst_name, dest_desc, src_sb, *new_dst, &sb))
{
case -1:
return_val = false;
@@ -1928,7 +1962,7 @@ copy_internal (char const *src_name, char const *dst_name,
if (x->preserve_ownership
&& (new_dst || !SAME_OWNER_AND_GROUP (src_sb, dst_sb)))
{
- switch (set_owner (x, dst_name, -1, src_sb.st_uid, src_sb.st_gid))
+ switch (set_owner (x, dst_name, -1, &src_sb, new_dst, &dst_sb))
{
case -1:
return false;
@@ -2059,23 +2093,26 @@ copy (char const *src_name, char const *dst_name,
options, true, copy_into_self, rename_succeeded);
}
-/* Return true if this process has appropriate privileges to chown a
- file whose owner is not the effective user ID. */
+/* Set *X to the default options for a value of type struct cp_options. */
-extern bool
-chown_privileges (void)
+void
+cp_options_default (struct cp_options *x)
{
+ memset (x, 0, sizeof *x);
#ifdef PRIV_FILE_CHOWN
- bool result;
- priv_set_t *pset = priv_allocset ();
- if (!pset)
- xalloc_die ();
- result = (getppriv (PRIV_EFFECTIVE, pset) == 0
- && priv_ismember (pset, PRIV_FILE_CHOWN));
- priv_freeset (pset);
- return result;
+ {
+ priv_set_t *pset = priv_allocset ();
+ if (!pset)
+ xalloc_die ();
+ if (getppriv (PRIV_EFFECTIVE, pset) == 0)
+ {
+ x->chown_privileges = priv_ismember (pset, PRIV_FILE_CHOWN);
+ x->owner_privileges = priv_ismember (pset, PRIV_FILE_OWNER);
+ }
+ priv_freeset (pset);
+ }
#else
- return (geteuid () == 0);
+ x->chown_privileges = x->owner_privileges = (geteuid () == 0);
#endif
}
@@ -2093,6 +2130,16 @@ chown_failure_ok (struct cp_options const *x)
return ((errno == EPERM || errno == EINVAL) && !x->chown_privileges);
}
+/* Similarly, return true if it's OK for chmod and similar operations
+ to fail, where errno is the error number that chmod failed with and
+ X is the copying option set. */
+
+static bool
+owner_failure_ok (struct cp_options const *x)
+{
+ return ((errno == EPERM || errno == EINVAL) && !x->owner_privileges);
+}
+
/* Return the user's umask, caching the result. */
extern mode_t
diff --git a/src/copy.h b/src/copy.h
index 47c97f389..14104e642 100644
--- a/src/copy.h
+++ b/src/copy.h
@@ -124,6 +124,13 @@ struct cp_options
whose owner is not the effective user ID. */
bool chown_privileges;
+ /* Whether this process has appropriate privileges to do the
+ following operations on a file even when it is owned by some
+ other user: set the file's atime, mtime, mode, or ACL; remove or
+ rename an entry in the file even though it is a sticky directory,
+ or to mount on the file. */
+ bool owner_privileges;
+
/* If true, when copying recursively, skip any subdirectories that are
on different file systems from the one we started on. */
bool one_file_system;
@@ -230,7 +237,7 @@ bool copy (char const *src_name, char const *dst_name,
void dest_info_init (struct cp_options *);
void src_info_init (struct cp_options *);
-bool chown_privileges (void);
+void cp_options_default (struct cp_options *);
bool chown_failure_ok (struct cp_options const *);
mode_t cached_umask (void);
diff --git a/src/cp.c b/src/cp.c
index 599498dd7..99e1f73ec 100644
--- a/src/cp.c
+++ b/src/cp.c
@@ -737,13 +737,13 @@ do_copy (int n_files, char **file, const char *target_directory,
static void
cp_option_init (struct cp_options *x)
{
+ cp_options_default (x);
x->copy_as_regular = true;
x->dereference = DEREF_UNDEFINED;
x->unlink_dest_before_opening = false;
x->unlink_dest_after_failed_open = false;
x->hard_link = false;
x->interactive = I_UNSPECIFIED;
- x->chown_privileges = chown_privileges ();
x->move_mode = false;
x->one_file_system = false;
diff --git a/src/install.c b/src/install.c
index 802dfcf77..db08751e4 100644
--- a/src/install.c
+++ b/src/install.c
@@ -163,6 +163,7 @@ static struct option const long_options[] =
static void
cp_option_init (struct cp_options *x)
{
+ cp_options_default (x);
x->copy_as_regular = true;
x->dereference = DEREF_ALWAYS;
x->unlink_dest_before_opening = true;
@@ -170,7 +171,6 @@ cp_option_init (struct cp_options *x)
x->hard_link = false;
x->interactive = I_UNSPECIFIED;
x->move_mode = false;
- x->chown_privileges = chown_privileges ();
x->one_file_system = false;
x->preserve_ownership = false;
x->preserve_links = false;
diff --git a/src/mv.c b/src/mv.c
index 3b1ba8e9e..44f5bfc51 100644
--- a/src/mv.c
+++ b/src/mv.c
@@ -123,6 +123,7 @@ cp_option_init (struct cp_options *x)
{
bool selinux_enabled = (0 < is_selinux_enabled ());
+ cp_options_default (x);
x->copy_as_regular = false; /* FIXME: maybe make this an option */
x->dereference = DEREF_NEVER;
x->unlink_dest_before_opening = false;
@@ -130,7 +131,6 @@ cp_option_init (struct cp_options *x)
x->hard_link = false;
x->interactive = I_UNSPECIFIED;
x->move_mode = true;
- x->chown_privileges = chown_privileges ();
x->one_file_system = false;
x->preserve_ownership = true;
x->preserve_links = true;