diff options
Diffstat (limited to 'src/chown-core.c')
-rw-r--r-- | src/chown-core.c | 581 |
1 files changed, 581 insertions, 0 deletions
diff --git a/src/chown-core.c b/src/chown-core.c new file mode 100644 index 0000000..428c96d --- /dev/null +++ b/src/chown-core.c @@ -0,0 +1,581 @@ +/* chown-core.c -- core functions for changing ownership. + Copyright (C) 2000-2022 Free Software Foundation, Inc. + + 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 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 + 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 General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Extracted from chown.c/chgrp.c and librarified by Jim Meyering. */ + +#include <config.h> +#include <stdio.h> +#include <sys/types.h> +#include <pwd.h> +#include <grp.h> + +#include "system.h" +#include "chown-core.h" +#include "error.h" +#include "ignore-value.h" +#include "root-dev-ino.h" +#include "xfts.h" + +#define FTSENT_IS_DIRECTORY(E) \ + ((E)->fts_info == FTS_D \ + || (E)->fts_info == FTS_DC \ + || (E)->fts_info == FTS_DP \ + || (E)->fts_info == FTS_DNR) + +enum RCH_status + { + /* we called fchown and close, and both succeeded */ + RC_ok = 2, + + /* required_uid and/or required_gid are specified, but don't match */ + RC_excluded, + + /* SAME_INODE check failed */ + RC_inode_changed, + + /* open/fchown isn't needed, isn't safe, or doesn't work due to + permissions problems; fall back on chown */ + RC_do_ordinary_chown, + + /* open, fstat, fchown, or close failed */ + RC_error + }; + +extern void +chopt_init (struct Chown_option *chopt) +{ + chopt->verbosity = V_off; + chopt->root_dev_ino = NULL; + chopt->affect_symlink_referent = true; + chopt->recurse = false; + chopt->force_silent = false; + chopt->user_name = NULL; + chopt->group_name = NULL; +} + +extern void +chopt_free (struct Chown_option *chopt) +{ + free (chopt->user_name); + free (chopt->group_name); +} + +/* Convert the numeric user-id, UID, to a string stored in xmalloc'd memory, + and return it. Use the decimal representation of the ID. */ + +static char * +uid_to_str (uid_t uid) +{ + char buf[INT_BUFSIZE_BOUND (intmax_t)]; + return xstrdup (TYPE_SIGNED (uid_t) ? imaxtostr (uid, buf) + : umaxtostr (uid, buf)); +} + +/* Convert the numeric group-id, GID, to a string stored in xmalloc'd memory, + and return it. Use the decimal representation of the ID. */ + +static char * +gid_to_str (gid_t gid) +{ + char buf[INT_BUFSIZE_BOUND (intmax_t)]; + return xstrdup (TYPE_SIGNED (gid_t) ? imaxtostr (gid, buf) + : umaxtostr (gid, buf)); +} + +/* Convert the numeric group-id, GID, to a string stored in xmalloc'd memory, + and return it. If there's no corresponding group name, use the decimal + representation of the ID. */ + +extern char * +gid_to_name (gid_t gid) +{ + struct group *grp = getgrgid (gid); + return grp ? xstrdup (grp->gr_name) : gid_to_str (gid); +} + +/* Convert the numeric user-id, UID, to a string stored in xmalloc'd memory, + and return it. If there's no corresponding user name, use the decimal + representation of the ID. */ + +extern char * +uid_to_name (uid_t uid) +{ + struct passwd *pwd = getpwuid (uid); + return pwd ? xstrdup (pwd->pw_name) : uid_to_str (uid); +} + +/* 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. + If USER is NULL, give the group-oriented messages. + CHANGED describes what (if anything) has happened. */ + +static void +describe_change (char const *file, enum Change_status changed, + char const *old_user, char const *old_group, + char const *user, char const *group) +{ + char const *fmt; + char *old_spec; + char *spec; + + if (changed == CH_NOT_APPLIED) + { + printf (_("neither symbolic link %s nor referent has been changed\n"), + quoteaf (file)); + return; + } + + 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 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: + 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")); + break; + default: + abort (); + } + + printf (fmt, quoteaf (file), old_spec, spec); + + 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. + + 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. + We first call fstat and verify that the dev/inode match those from + the preceding stat call, and only then, if appropriate (given the + required_uid and required_gid constraints) do we call fchown. + + Return RC_do_ordinary_chown if we can't open FILE, or if FILE is a + special file that might have undesirable side effects when opening. + In this case the caller can use the less-safe ordinary chown. + + Return one of the RCH_status values. */ + +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) +{ + enum RCH_status status = RC_ok; + struct stat st; + int open_flags = O_NONBLOCK | O_NOCTTY; + int fd; + + if (required_uid == (uid_t) -1 && required_gid == (gid_t) -1) + return RC_do_ordinary_chown; + + if (! S_ISREG (orig_st->st_mode)) + { + if (S_ISDIR (orig_st->st_mode)) + open_flags |= O_DIRECTORY; + else + 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))))) + return (errno == EACCES ? RC_do_ordinary_chown : RC_error); + + if (fstat (fd, &st) != 0) + status = RC_error; + 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)) + { + if (fchown (fd, uid, gid) == 0) + { + status = (close (fd) == 0 + ? RC_ok : RC_error); + return status; + } + else + { + status = RC_error; + } + } + + 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 + to UID and/or GID as appropriate. + If REQUIRED_UID is not -1, then skip files with any other user ID. + If REQUIRED_GID is not -1, then skip files with any other group ID. + CHOPT specifies additional options. + 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) +{ + char const *file_full_name = ent->fts_path; + char const *file = ent->fts_accpath; + struct stat const *file_stats; + struct stat stat_buf; + bool ok = true; + bool do_chown; + bool symlink_changed = true; + + switch (ent->fts_info) + { + 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. */ + ignore_value (fts_read (fts)); + return false; + } + return true; + } + break; + + case FTS_DP: + if (! chopt->recurse) + 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". */ + if (ent->fts_level == 0 && ent->fts_number == 0) + { + 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: + if (! chopt->force_silent) + error (0, ent->fts_errno, "%s", quotef (file_full_name)); + ok = false; + break; + + case FTS_DNR: + 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; + } + + if (!ok) + { + do_chown = false; + 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) + { + do_chown = true; + file_stats = ent->fts_statp; + } + else + { + file_stats = ent->fts_statp; + + /* If this is a symlink and we're dereferencing them, + 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) + { + 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)); + } + + /* This happens when chown -LR --preserve-root encounters a symlink-to-/. */ + if (ok + && FTSENT_IS_DIRECTORY (ent) + && ROOT_DEV_INO_CHECK (chopt->root_dev_ino, file_stats)) + { + ROOT_DEV_INO_WARN (file_full_name); + return false; + } + + 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; + } + } + 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., 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")), + 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))); + + 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); + 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; + char *new_usr = chopt->user_name + ? chopt->user_name : uid != -1 + ? uid_to_str (uid) : NULL; + char *new_grp = chopt->group_name + ? chopt->group_name : gid != -1 + ? gid_to_str (gid) : NULL; + describe_change (file_full_name, ch_status, + old_usr, old_grp, + new_usr, new_grp); + free (old_usr); + free (old_grp); + if (new_usr != chopt->user_name) + free (new_usr); + if (new_grp != chopt->group_name) + free (new_grp); + } + } + + if ( ! chopt->recurse) + fts_set (fts, ent, FTS_SKIP); + + return ok; +} + +/* Change the owner and/or group of the specified FILES. + BIT_FLAGS specifies how to treat each symlink-to-directory + that is encountered during a recursive traversal. + CHOPT specifies additional options. + If UID is not -1, then change the owner id of each file to UID. + If GID is not -1, then change the group id of each file to GID. + If REQUIRED_UID and/or REQUIRED_GID is not -1, then change only + files with user ID and group ID that match the non-(-1) value(s). + 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) +{ + 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); + + FTS *fts = xfts_open (files, bit_flags | stat_flags, NULL); + + while (true) + { + FTSENT *ent; + + ent = fts_read (fts); + if (ent == NULL) + { + 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); + } + + if (fts_close (fts) != 0) + { + error (0, errno, _("fts_close failed")); + ok = false; + } + + return ok; +} |