diff options
Diffstat (limited to 'src/chown-core.c')
-rw-r--r-- | src/chown-core.c | 469 |
1 files changed, 255 insertions, 214 deletions
diff --git a/src/chown-core.c b/src/chown-core.c index bd987a8..b699993 100644 --- a/src/chown-core.c +++ b/src/chown-core.c @@ -1,10 +1,10 @@ /* chown-core.c -- core functions for changing ownership. - Copyright (C) 2000, 2002, 2003, 2004, 2005, 2006, 2007 Free Software Foundation. + Copyright (C) 2000-2016 Free Software Foundation, Inc. - This program is free software; you can redistribute it and/or modify + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2, or (at your option) - any later version. + the Free Software Foundation, either version 3 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 @@ -12,8 +12,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software Foundation, - Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* Extracted from chown.c/chgrp.c and librarified by Jim Meyering. */ @@ -26,9 +25,7 @@ #include "system.h" #include "chown-core.h" #include "error.h" -#include "inttostr.h" -#include "openat.h" -#include "quote.h" +#include "ignore-value.h" #include "root-dev-ino.h" #include "xfts.h" @@ -70,7 +67,7 @@ chopt_init (struct Chown_option *chopt) } extern void -chopt_free (struct Chown_option *chopt ATTRIBUTE_UNUSED) +chopt_free (struct Chown_option *chopt _GL_UNUSED) { /* Deliberately do not free chopt->user_name or ->group_name. They're not always allocated. */ @@ -86,8 +83,8 @@ gid_to_name (gid_t gid) char buf[INT_BUFSIZE_BOUND (intmax_t)]; struct group *grp = getgrgid (gid); return xstrdup (grp ? grp->gr_name - : TYPE_SIGNED (gid_t) ? imaxtostr (gid, buf) - : umaxtostr (gid, buf)); + : TYPE_SIGNED (gid_t) ? imaxtostr (gid, buf) + : umaxtostr (gid, buf)); } /* Convert the numeric user-id, UID, to a string stored in xmalloc'd memory, @@ -100,8 +97,35 @@ uid_to_name (uid_t uid) char buf[INT_BUFSIZE_BOUND (intmax_t)]; struct passwd *pwd = getpwuid (uid); return xstrdup (pwd ? pwd->pw_name - : TYPE_SIGNED (uid_t) ? imaxtostr (uid, buf) - : umaxtostr (uid, buf)); + : TYPE_SIGNED (uid_t) ? imaxtostr (uid, buf) + : umaxtostr (uid, buf)); +} + +/* Allocate a string representing USER and GROUP. */ + +static char * +user_group_str (char const *user, char const *group) +{ + char *spec = NULL; + + if (user) + { + if (group) + { + spec = xmalloc (strlen (user) + 1 + strlen (group) + 1); + stpcpy (stpcpy (stpcpy (spec, user), ":"), group); + } + else + { + spec = xstrdup (user); + } + } + else if (group) + { + spec = xstrdup (group); + } + + return spec; } /* Tell the user how/if the user and group of FILE have been changed. @@ -110,68 +134,67 @@ uid_to_name (uid_t uid) static void describe_change (const char *file, enum Change_status changed, - char const *user, char const *group) + char const *old_user, char const *old_group, + char const *user, char const *group) { const char *fmt; - char const *spec; - char *spec_allocated = NULL; + char *old_spec; + char *spec; if (changed == CH_NOT_APPLIED) { printf (_("neither symbolic link %s nor referent has been changed\n"), - quote (file)); + quoteaf (file)); return; } - if (user) - { - if (group) - { - spec_allocated = xmalloc (strlen (user) + 1 + strlen (group) + 1); - stpcpy (stpcpy (stpcpy (spec_allocated, user), ":"), group); - spec = spec_allocated; - } - else - { - spec = user; - } - } - else - { - spec = group; - } + spec = user_group_str (user, group); + old_spec = user_group_str (user ? old_user : NULL, group ? old_group : NULL); switch (changed) { case CH_SUCCEEDED: - fmt = (user ? _("changed ownership of %s to %s\n") - : group ? _("changed group of %s to %s\n") - : _("no change to ownership of %s\n")); + fmt = (user ? _("changed ownership of %s from %s to %s\n") + : group ? _("changed group of %s from %s to %s\n") + : _("no change to ownership of %s\n")); break; case CH_FAILED: - fmt = (user ? _("failed to change ownership of %s to %s\n") - : group ? _("failed to change group of %s to %s\n") - : _("failed to change ownership of %s\n")); + if (old_spec) + { + fmt = (user ? _("failed to change ownership of %s from %s to %s\n") + : group ? _("failed to change group of %s from %s to %s\n") + : _("failed to change ownership of %s\n")); + } + else + { + fmt = (user ? _("failed to change ownership of %s to %s\n") + : group ? _("failed to change group of %s to %s\n") + : _("failed to change ownership of %s\n")); + free (old_spec); + old_spec = spec; + spec = NULL; + } break; case CH_NO_CHANGE_REQUESTED: fmt = (user ? _("ownership of %s retained as %s\n") - : group ? _("group of %s retained as %s\n") - : _("ownership of %s retained\n")); + : group ? _("group of %s retained as %s\n") + : _("ownership of %s retained\n")); break; default: abort (); } - printf (fmt, quote (file), spec); + printf (fmt, quoteaf (file), old_spec, spec); - free (spec_allocated); + free (old_spec); + free (spec); } /* Change the owner and/or group of the FILE to UID and/or GID (safely) only if REQUIRED_UID and REQUIRED_GID match the owner and group IDs - of FILE. ORIG_ST must be the result of `stat'ing FILE. + of FILE. ORIG_ST must be the result of 'stat'ing FILE. - The `safely' part above means that we can't simply use chown(2), + The 'safely' part above means that we can't simply use chown(2), since FILE might be replaced with some other file between the time of the preceding stat/lstat and this chown call. So here we open FILE and do everything else via the resulting file descriptor. @@ -187,9 +210,9 @@ describe_change (const char *file, enum Change_status changed, static enum RCH_status restricted_chown (int cwd_fd, char const *file, - struct stat const *orig_st, - uid_t uid, gid_t gid, - uid_t required_uid, gid_t required_gid) + struct stat const *orig_st, + uid_t uid, gid_t gid, + uid_t required_uid, gid_t required_gid) { enum RCH_status status = RC_ok; struct stat st; @@ -202,15 +225,15 @@ restricted_chown (int cwd_fd, char const *file, if (! S_ISREG (orig_st->st_mode)) { if (S_ISDIR (orig_st->st_mode)) - open_flags |= O_DIRECTORY; + open_flags |= O_DIRECTORY; else - return RC_do_ordinary_chown; + return RC_do_ordinary_chown; } fd = openat (cwd_fd, file, O_RDONLY | open_flags); if (! (0 <= fd - || (errno == EACCES && S_ISREG (orig_st->st_mode) - && 0 <= (fd = openat (cwd_fd, file, O_WRONLY | open_flags))))) + || (errno == EACCES && S_ISREG (orig_st->st_mode) + && 0 <= (fd = openat (cwd_fd, file, O_WRONLY | open_flags))))) return (errno == EACCES ? RC_do_ordinary_chown : RC_error); if (fstat (fd, &st) != 0) @@ -218,26 +241,24 @@ restricted_chown (int cwd_fd, char const *file, else if (! SAME_INODE (*orig_st, st)) status = RC_inode_changed; else if ((required_uid == (uid_t) -1 || required_uid == st.st_uid) - && (required_gid == (gid_t) -1 || required_gid == st.st_gid)) + && (required_gid == (gid_t) -1 || required_gid == st.st_gid)) { if (fchown (fd, uid, gid) == 0) - { - status = (close (fd) == 0 - ? RC_ok : RC_error); - return status; - } + { + status = (close (fd) == 0 + ? RC_ok : RC_error); + return status; + } else - { - status = RC_error; - } + { + status = RC_error; + } } - { /* FIXME: remove these curly braces when we assume C99. */ - int saved_errno = errno; - close (fd); - errno = saved_errno; - return status; - } + int saved_errno = errno; + close (fd); + errno = saved_errno; + return status; } /* Change the owner and/or group of the file specified by FTS and ENT @@ -248,9 +269,9 @@ restricted_chown (int cwd_fd, char const *file, Return true if successful. */ static bool change_file_owner (FTS *fts, FTSENT *ent, - uid_t uid, gid_t gid, - uid_t required_uid, gid_t required_gid, - struct Chown_option const *chopt) + uid_t uid, gid_t gid, + uid_t required_uid, gid_t required_gid, + struct Chown_option const *chopt) { char const *file_full_name = ent->fts_path; char const *file = ent->fts_accpath; @@ -264,56 +285,68 @@ change_file_owner (FTS *fts, FTSENT *ent, { case FTS_D: if (chopt->recurse) - { - if (ROOT_DEV_INO_CHECK (chopt->root_dev_ino, ent->fts_statp)) - { - /* This happens e.g., with "chown -R --preserve-root 0 /" - and with "chown -RH --preserve-root 0 symlink-to-root". */ - ROOT_DEV_INO_WARN (file_full_name); - /* Tell fts not to traverse into this hierarchy. */ - fts_set (fts, ent, FTS_SKIP); - /* Ensure that we do not process "/" on the second visit. */ - ent = fts_read (fts); - return false; - } - return true; - } + { + if (ROOT_DEV_INO_CHECK (chopt->root_dev_ino, ent->fts_statp)) + { + /* This happens e.g., with "chown -R --preserve-root 0 /" + and with "chown -RH --preserve-root 0 symlink-to-root". */ + ROOT_DEV_INO_WARN (file_full_name); + /* Tell fts not to traverse into this hierarchy. */ + fts_set (fts, ent, FTS_SKIP); + /* Ensure that we do not process "/" on the second visit. */ + ignore_value (fts_read (fts)); + return false; + } + return true; + } break; case FTS_DP: if (! chopt->recurse) - return true; + return true; break; case FTS_NS: /* For a top-level file or directory, this FTS_NS (stat failed) - indicator is determined at the time of the initial fts_open call. - With programs like chmod, chown, and chgrp, that modify - permissions, it is possible that the file in question is - accessible when control reaches this point. So, if this is - the first time we've seen the FTS_NS for this file, tell - fts_read to stat it "again". */ + indicator is determined at the time of the initial fts_open call. + With programs like chmod, chown, and chgrp, that modify + permissions, it is possible that the file in question is + accessible when control reaches this point. So, if this is + the first time we've seen the FTS_NS for this file, tell + fts_read to stat it "again". */ if (ent->fts_level == 0 && ent->fts_number == 0) - { - ent->fts_number = 1; - fts_set (fts, ent, FTS_AGAIN); - return true; - } - error (0, ent->fts_errno, _("cannot access %s"), quote (file_full_name)); + { + ent->fts_number = 1; + fts_set (fts, ent, FTS_AGAIN); + return true; + } + if (! chopt->force_silent) + error (0, ent->fts_errno, _("cannot access %s"), + quoteaf (file_full_name)); ok = false; break; case FTS_ERR: - error (0, ent->fts_errno, _("%s"), quote (file_full_name)); + if (! chopt->force_silent) + error (0, ent->fts_errno, "%s", quotef (file_full_name)); ok = false; break; case FTS_DNR: - error (0, ent->fts_errno, _("cannot read directory %s"), - quote (file_full_name)); + if (! chopt->force_silent) + error (0, ent->fts_errno, _("cannot read directory %s"), + quoteaf (file_full_name)); ok = false; break; + case FTS_DC: /* directory that causes cycles */ + if (cycle_warning_required (fts, ent)) + { + emit_cycle_warning (file_full_name); + return false; + } + break; + default: break; } @@ -324,9 +357,9 @@ change_file_owner (FTS *fts, FTSENT *ent, file_stats = NULL; } else if (required_uid == (uid_t) -1 && required_gid == (gid_t) -1 - && chopt->verbosity == V_off - && ! chopt->root_dev_ino - && ! chopt->affect_symlink_referent) + && chopt->verbosity == V_off + && ! chopt->root_dev_ino + && ! chopt->affect_symlink_referent) { do_chown = true; file_stats = ent->fts_statp; @@ -336,24 +369,25 @@ change_file_owner (FTS *fts, FTSENT *ent, file_stats = ent->fts_statp; /* If this is a symlink and we're dereferencing them, - stat it to get info on the referent. */ + stat it to get info on the referent. */ if (chopt->affect_symlink_referent && S_ISLNK (file_stats->st_mode)) - { - if (fstatat (fts->fts_cwd_fd, file, &stat_buf, 0) != 0) - { - error (0, errno, _("cannot dereference %s"), - quote (file_full_name)); - ok = false; - } - - file_stats = &stat_buf; - } + { + if (fstatat (fts->fts_cwd_fd, file, &stat_buf, 0) != 0) + { + if (! chopt->force_silent) + error (0, errno, _("cannot dereference %s"), + quoteaf (file_full_name)); + ok = false; + } + + file_stats = &stat_buf; + } do_chown = (ok - && (required_uid == (uid_t) -1 - || required_uid == file_stats->st_uid) - && (required_gid == (gid_t) -1 - || required_gid == file_stats->st_gid)); + && (required_uid == (uid_t) -1 + || required_uid == file_stats->st_uid) + && (required_gid == (gid_t) -1 + || required_gid == file_stats->st_gid)); } /* This happens when chown -LR --preserve-root encounters a symlink-to-/. */ @@ -368,89 +402,94 @@ change_file_owner (FTS *fts, FTSENT *ent, if (do_chown) { if ( ! chopt->affect_symlink_referent) - { - ok = (lchownat (fts->fts_cwd_fd, file, uid, gid) == 0); - - /* Ignore any error due to lack of support; POSIX requires - this behavior for top-level symbolic links with -h, and - implies that it's required for all symbolic links. */ - if (!ok && errno == EOPNOTSUPP) - { - ok = true; - symlink_changed = false; - } - } + { + ok = (lchownat (fts->fts_cwd_fd, file, uid, gid) == 0); + + /* Ignore any error due to lack of support; POSIX requires + this behavior for top-level symbolic links with -h, and + implies that it's required for all symbolic links. */ + if (!ok && errno == EOPNOTSUPP) + { + ok = true; + symlink_changed = false; + } + } else - { - /* If possible, avoid a race condition with --from=O:G and without the - (-h) --no-dereference option. If fts's stat call determined - that the uid/gid of FILE matched the --from=O:G-selected - owner and group IDs, blindly using chown(2) here could lead - chown(1) or chgrp(1) mistakenly to dereference a *symlink* - to an arbitrary file that an attacker had moved into the - place of FILE during the window between the stat and - chown(2) calls. If FILE is a regular file or a directory - that can be opened, this race condition can be avoided safely. */ - - enum RCH_status err - = restricted_chown (fts->fts_cwd_fd, file, file_stats, uid, gid, - required_uid, required_gid); - switch (err) - { - case RC_ok: - break; - - case RC_do_ordinary_chown: - ok = (chownat (fts->fts_cwd_fd, file, uid, gid) == 0); - break; - - case RC_error: - ok = false; - break; - - case RC_inode_changed: - /* FIXME: give a diagnostic in this case? */ - case RC_excluded: - do_chown = false; - ok = false; - break; - - default: - abort (); - } - } - - /* On some systems (e.g., Linux-2.4.x), - the chown function resets the `special' permission bits. - Do *not* restore those bits; doing so would open a window in - which a malicious user, M, could subvert a chown command run - by some other user and operating on files in a directory - where M has write access. */ + { + /* If possible, avoid a race condition with --from=O:G and without the + (-h) --no-dereference option. If fts's stat call determined + that the uid/gid of FILE matched the --from=O:G-selected + owner and group IDs, blindly using chown(2) here could lead + chown(1) or chgrp(1) mistakenly to dereference a *symlink* + to an arbitrary file that an attacker had moved into the + place of FILE during the window between the stat and + chown(2) calls. If FILE is a regular file or a directory + that can be opened, this race condition can be avoided safely. */ + + enum RCH_status err + = restricted_chown (fts->fts_cwd_fd, file, file_stats, uid, gid, + required_uid, required_gid); + switch (err) + { + case RC_ok: + break; + + case RC_do_ordinary_chown: + ok = (chownat (fts->fts_cwd_fd, file, uid, gid) == 0); + break; + + case RC_error: + ok = false; + break; + + case RC_inode_changed: + /* FIXME: give a diagnostic in this case? */ + case RC_excluded: + do_chown = false; + ok = false; + break; + + default: + abort (); + } + } + + /* On some systems (e.g., GNU/Linux 2.4.x), + the chown function resets the 'special' permission bits. + Do *not* restore those bits; doing so would open a window in + which a malicious user, M, could subvert a chown command run + by some other user and operating on files in a directory + where M has write access. */ if (do_chown && !ok && ! chopt->force_silent) - error (0, errno, (uid != (uid_t) -1 - ? _("changing ownership of %s") - : _("changing group of %s")), - quote (file_full_name)); + error (0, errno, (uid != (uid_t) -1 + ? _("changing ownership of %s") + : _("changing group of %s")), + quoteaf (file_full_name)); } if (chopt->verbosity != V_off) { bool changed = - ((do_chown & ok & symlink_changed) - && ! ((uid == (uid_t) -1 || uid == file_stats->st_uid) - && (gid == (gid_t) -1 || gid == file_stats->st_gid))); + ((do_chown && ok && symlink_changed) + && ! ((uid == (uid_t) -1 || uid == file_stats->st_uid) + && (gid == (gid_t) -1 || gid == file_stats->st_gid))); if (changed || chopt->verbosity == V_high) - { - enum Change_status ch_status = - (!ok ? CH_FAILED - : !symlink_changed ? CH_NOT_APPLIED - : !changed ? CH_NO_CHANGE_REQUESTED - : CH_SUCCEEDED); - describe_change (file_full_name, ch_status, - chopt->user_name, chopt->group_name); - } + { + enum Change_status ch_status = + (!ok ? CH_FAILED + : !symlink_changed ? CH_NOT_APPLIED + : !changed ? CH_NO_CHANGE_REQUESTED + : CH_SUCCEEDED); + char *old_usr = file_stats ? uid_to_name (file_stats->st_uid) : NULL; + char *old_grp = file_stats ? gid_to_name (file_stats->st_gid) : NULL; + describe_change (file_full_name, ch_status, + old_usr, old_grp, + chopt->user_name, chopt->group_name); + free (old_usr); + free (old_grp); + } } if ( ! chopt->recurse) @@ -470,18 +509,18 @@ change_file_owner (FTS *fts, FTSENT *ent, Return true if successful. */ extern bool chown_files (char **files, int bit_flags, - uid_t uid, gid_t gid, - uid_t required_uid, gid_t required_gid, - struct Chown_option const *chopt) + uid_t uid, gid_t gid, + uid_t required_uid, gid_t required_gid, + struct Chown_option const *chopt) { bool ok = true; /* Use lstat and stat only if they're needed. */ int stat_flags = ((required_uid != (uid_t) -1 || required_gid != (gid_t) -1 - || chopt->affect_symlink_referent - || chopt->verbosity != V_off) - ? 0 - : FTS_NOSTAT); + || chopt->affect_symlink_referent + || chopt->verbosity != V_off) + ? 0 + : FTS_NOSTAT); FTS *fts = xfts_open (files, bit_flags | stat_flags, NULL); @@ -491,24 +530,26 @@ chown_files (char **files, int bit_flags, ent = fts_read (fts); if (ent == NULL) - { - if (errno != 0) - { - /* FIXME: try to give a better message */ - error (0, errno, _("fts_read failed")); - ok = false; - } - break; - } + { + if (errno != 0) + { + /* FIXME: try to give a better message */ + if (! chopt->force_silent) + error (0, errno, _("fts_read failed")); + ok = false; + } + break; + } ok &= change_file_owner (fts, ent, uid, gid, - required_uid, required_gid, chopt); + required_uid, required_gid, chopt); } - /* Ignore failure, since the only way it can do so is in failing to - return to the original directory, and since we're about to exit, - that doesn't matter. */ - fts_close (fts); + if (fts_close (fts) != 0) + { + error (0, errno, _("fts_close failed")); + ok = false; + } return ok; } |