summaryrefslogtreecommitdiffstats
path: root/src/chown-core.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/chown-core.c')
-rw-r--r--src/chown-core.c581
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;
+}