/* chown-core.c -- core functions for changing ownership. Copyright (C) 2000, 2002, 2003 Free Software Foundation. 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. 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, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ /* Extracted from chown.c/chgrp.c and librarified by Jim Meyering. */ #include #include #include #include #include #include "system.h" #include "error.h" #include "lchown.h" #include "quote.h" #include "savedir.h" #include "chown-core.h" /* The number of decimal digits required to represent the largest value of type `unsigned int'. This is enough for an 8-byte unsigned int type. */ #define UINT_MAX_DECIMAL_DIGITS 20 #ifndef _POSIX_VERSION struct group *getgrnam (); struct group *getgrgid (); #endif int lstat (); void chopt_init (struct Chown_option *chopt) { chopt->verbosity = V_off; chopt->dereference = DEREF_NEVER; chopt->recurse = 0; chopt->force_silent = 0; chopt->user_name = 0; chopt->group_name = 0; } void chopt_free (struct Chown_option *chopt) { /* Deliberately do not free chopt->user_name or ->group_name. They're not always allocated. */ } /* Convert N to a string, and return a pointer to that string in memory allocated from the heap. */ static char * uint_to_string (unsigned int n) { char buf[UINT_MAX_DECIMAL_DIGITS + 1]; char *p = buf + sizeof buf; *--p = '\0'; do *--p = '0' + (n % 10); while ((n /= 10) != 0); return xstrdup (p); } /* 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. */ char * gid_to_name (gid_t gid) { struct group *grp = getgrgid (gid); return grp ? xstrdup (grp->gr_name) : uint_to_string (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. */ char * uid_to_name (uid_t uid) { struct passwd *pwd = getpwuid (uid); return pwd ? xstrdup (pwd->pw_name) : uint_to_string (uid); } /* 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 (const char *file, enum Change_status changed, char const *user, char const *group) { const char *fmt; char *spec; int spec_allocated = 0; if (changed == CH_NOT_APPLIED) { printf (_("neither symbolic link %s nor referent has been changed\n"), quote (file)); return; } if (user) { if (group) { spec = xmalloc (strlen (user) + 1 + strlen (group) + 1); stpcpy (stpcpy (stpcpy (spec, user), ":"), group); spec_allocated = 1; } else { spec = (char *) user; } } else { spec = (char *) group; } switch (changed) { case CH_SUCCEEDED: fmt = (user ? _("changed ownership of %s to %s\n") : _("changed group of %s to %s\n")); break; case CH_FAILED: fmt = (user ? _("failed to change ownership of %s to %s\n") : _("failed to change group of %s to %s\n")); break; case CH_NO_CHANGE_REQUESTED: fmt = (user ? _("ownership of %s retained as %s\n") : _("group of %s retained as %s\n")); break; default: abort (); } printf (fmt, quote (file), spec); if (spec_allocated) free (spec); } /* Recursively change the ownership of the files in directory DIR to user-id, UID, and group-id, GID, according to the options specified by CHOPT. Return 0 if successful, 1 if errors occurred. */ static int change_dir_owner (const char *dir, uid_t uid, gid_t gid, uid_t old_uid, gid_t old_gid, struct Chown_option const *chopt) { char *name_space, *namep; char *path; /* Full path of each entry to process. */ unsigned dirlength; /* Length of `dir' and '\0'. */ unsigned filelength; /* Length of each pathname to process. */ unsigned pathlength; /* Bytes allocated for `path'. */ int errors = 0; name_space = savedir (dir); if (name_space == NULL) { if (chopt->force_silent == 0) error (0, errno, "%s", quote (dir)); return 1; } dirlength = strlen (dir) + 1; /* + 1 is for the trailing '/'. */ pathlength = dirlength + 1; /* Give `path' a dummy value; it will be reallocated before first use. */ path = xmalloc (pathlength); strcpy (path, dir); path[dirlength - 1] = '/'; for (namep = name_space; *namep; namep += filelength - dirlength) { filelength = dirlength + strlen (namep) + 1; if (filelength > pathlength) { pathlength = filelength * 2; path = xrealloc (path, pathlength); } strcpy (path + dirlength, namep); errors |= change_file_owner (0, path, uid, gid, old_uid, old_gid, chopt); } free (path); free (name_space); return errors; } /* Change the ownership of FILE to user-id, UID, and group-id, GID, provided it presently has owner OLD_UID and group OLD_GID. Honor the options specified by CHOPT. If FILE is a directory and -R is given, recurse. Return 0 if successful, 1 if errors occurred. */ int change_file_owner (int cmdline_arg, const char *file, uid_t uid, gid_t gid, uid_t old_uid, gid_t old_gid, struct Chown_option const *chopt) { struct stat file_stats; uid_t new_uid; gid_t new_gid; int errors = 0; int is_symlink; int is_directory; if (lstat (file, &file_stats)) { if (chopt->force_silent == 0) error (0, errno, _("failed to get attributes of %s"), quote (file)); return 1; } /* If it's a symlink and we're dereferencing, then use stat to get the attributes of the referent. */ if (S_ISLNK (file_stats.st_mode)) { if (chopt->dereference == DEREF_ALWAYS && stat (file, &file_stats)) { if (chopt->force_silent == 0) error (0, errno, _("failed to get attributes of %s"), quote (file)); return 1; } is_symlink = 1; /* With -R, don't traverse through symlinks-to-directories. But of course, this will all change with POSIX's new -H, -L, -P options. */ is_directory = 0; } else { is_symlink = 0; is_directory = S_ISDIR (file_stats.st_mode); } if ((old_uid == (uid_t) -1 || file_stats.st_uid == old_uid) && (old_gid == (gid_t) -1 || file_stats.st_gid == old_gid)) { new_uid = (uid == (uid_t) -1 ? file_stats.st_uid : uid); new_gid = (gid == (gid_t) -1 ? file_stats.st_gid : gid); if (new_uid != file_stats.st_uid || new_gid != file_stats.st_gid) { int fail; int symlink_changed = 1; int saved_errno; int called_lchown = 0; if (is_symlink) { if (chopt->dereference == DEREF_NEVER) { called_lchown = 1; fail = lchown (file, new_uid, new_gid); /* Ignore the failure if it's due to lack of support (ENOSYS) and this is not a command line argument. */ if (!cmdline_arg && fail && errno == ENOSYS) { fail = 0; symlink_changed = 0; } } else if (chopt->dereference == DEREF_ALWAYS) { /* Applying chown to a symlink and expecting it to affect the referent is not portable. So instead, open the file and use fchown on the resulting descriptor. */ int fd = open (file, O_RDONLY | O_NONBLOCK | O_NOCTTY); fail = (fd == -1 ? 1 : fchown (fd, new_uid, new_gid)); if (fd != -1) close (fd); } else { /* FIXME */ abort (); } } else { fail = chown (file, new_uid, new_gid); } saved_errno = errno; if (chopt->verbosity == V_high || (chopt->verbosity == V_changes_only && !fail)) { enum Change_status ch_status = (! symlink_changed ? CH_NOT_APPLIED : (fail ? CH_FAILED : CH_SUCCEEDED)); describe_change (file, ch_status, chopt->user_name, chopt->group_name); } if (fail) { if (chopt->force_silent == 0) error (0, saved_errno, (uid != (uid_t) -1 ? _("changing ownership of %s") : _("changing group of %s")), quote (file)); errors = 1; } else { /* The change succeeded. 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. */ } } else if (chopt->verbosity == V_high) { describe_change (file, CH_NO_CHANGE_REQUESTED, chopt->user_name, chopt->group_name); } } if (chopt->recurse && is_directory) errors |= change_dir_owner (file, uid, gid, old_uid, old_gid, chopt); return errors; }