1 /* chown-core.c -- core functions for changing ownership.
2    Copyright (C) 2000-2023 Free Software Foundation, Inc.
3 
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8 
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13 
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
16 
17 /* Extracted from chown.c/chgrp.c and librarified by Jim Meyering.  */
18 
19 #include <config.h>
20 #include <stdio.h>
21 #include <sys/types.h>
22 #include <pwd.h>
23 #include <grp.h>
24 
25 #include "system.h"
26 #include "assure.h"
27 #include "chown-core.h"
28 #include "ignore-value.h"
29 #include "root-dev-ino.h"
30 #include "xfts.h"
31 
32 #define FTSENT_IS_DIRECTORY(E)	\
33   ((E)->fts_info == FTS_D	\
34    || (E)->fts_info == FTS_DC	\
35    || (E)->fts_info == FTS_DP	\
36    || (E)->fts_info == FTS_DNR)
37 
38 enum RCH_status
39   {
40     /* we called fchown and close, and both succeeded */
41     RC_ok = 2,
42 
43     /* required_uid and/or required_gid are specified, but don't match */
44     RC_excluded,
45 
46     /* The file was replaced by another file during the requested change.  */
47     RC_inode_changed,
48 
49     /* open/fchown isn't needed, isn't safe, or doesn't work due to
50        permissions problems; fall back on chown */
51     RC_do_ordinary_chown,
52 
53     /* open, fstat, fchown, or close failed */
54     RC_error
55   };
56 
57 extern void
chopt_init(struct Chown_option * chopt)58 chopt_init (struct Chown_option *chopt)
59 {
60   chopt->verbosity = V_off;
61   chopt->root_dev_ino = nullptr;
62   chopt->affect_symlink_referent = true;
63   chopt->recurse = false;
64   chopt->force_silent = false;
65   chopt->user_name = nullptr;
66   chopt->group_name = nullptr;
67 }
68 
69 extern void
chopt_free(struct Chown_option * chopt)70 chopt_free (struct Chown_option *chopt)
71 {
72   free (chopt->user_name);
73   free (chopt->group_name);
74 }
75 
76 /* Convert the numeric user-id, UID, to a string stored in xmalloc'd memory,
77    and return it.  Use the decimal representation of the ID.  */
78 
79 static char *
uid_to_str(uid_t uid)80 uid_to_str (uid_t uid)
81 {
82   char buf[INT_BUFSIZE_BOUND (intmax_t)];
83   return xstrdup (TYPE_SIGNED (uid_t) ? imaxtostr (uid, buf)
84                   : umaxtostr (uid, buf));
85 }
86 
87 /* Convert the numeric group-id, GID, to a string stored in xmalloc'd memory,
88    and return it.  Use the decimal representation of the ID.  */
89 
90 static char *
gid_to_str(gid_t gid)91 gid_to_str (gid_t gid)
92 {
93   char buf[INT_BUFSIZE_BOUND (intmax_t)];
94   return xstrdup (TYPE_SIGNED (gid_t) ? imaxtostr (gid, buf)
95                   : umaxtostr (gid, buf));
96 }
97 
98 /* Convert the numeric group-id, GID, to a string stored in xmalloc'd memory,
99    and return it.  If there's no corresponding group name, use the decimal
100    representation of the ID.  */
101 
102 extern char *
gid_to_name(gid_t gid)103 gid_to_name (gid_t gid)
104 {
105   struct group *grp = getgrgid (gid);
106   return grp ? xstrdup (grp->gr_name) : gid_to_str (gid);
107 }
108 
109 /* Convert the numeric user-id, UID, to a string stored in xmalloc'd memory,
110    and return it.  If there's no corresponding user name, use the decimal
111    representation of the ID.  */
112 
113 extern char *
uid_to_name(uid_t uid)114 uid_to_name (uid_t uid)
115 {
116   struct passwd *pwd = getpwuid (uid);
117   return pwd ? xstrdup (pwd->pw_name) : uid_to_str (uid);
118 }
119 
120 /* Allocate a string representing USER and GROUP.  */
121 
122 static char *
user_group_str(char const * user,char const * group)123 user_group_str (char const *user, char const *group)
124 {
125   char *spec = nullptr;
126 
127   if (user)
128     {
129       if (group)
130         {
131           spec = xmalloc (strlen (user) + 1 + strlen (group) + 1);
132           stpcpy (stpcpy (stpcpy (spec, user), ":"), group);
133         }
134       else
135         {
136           spec = xstrdup (user);
137         }
138     }
139   else if (group)
140     {
141       spec = xstrdup (group);
142     }
143 
144   return spec;
145 }
146 
147 /* Tell the user how/if the user and group of FILE have been changed.
148    If USER is null, give the group-oriented messages.
149    CHANGED describes what (if anything) has happened. */
150 
151 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)152 describe_change (char const *file, enum Change_status changed,
153                  char const *old_user, char const *old_group,
154                  char const *user, char const *group)
155 {
156   char const *fmt;
157   char *old_spec;
158   char *spec;
159 
160   if (changed == CH_NOT_APPLIED)
161     {
162       printf (_("neither symbolic link %s nor referent has been changed\n"),
163               quoteaf (file));
164       return;
165     }
166 
167   spec = user_group_str (user, group);
168   old_spec = user_group_str (user ? old_user : nullptr,
169                              group ? old_group : nullptr);
170 
171   switch (changed)
172     {
173     case CH_SUCCEEDED:
174       fmt = (user ? _("changed ownership of %s from %s to %s\n")
175              : group ? _("changed group of %s from %s to %s\n")
176              : _("no change to ownership of %s\n"));
177       break;
178     case CH_FAILED:
179       if (old_spec)
180         {
181           fmt = (user ? _("failed to change ownership of %s from %s to %s\n")
182                  : group ? _("failed to change group of %s from %s to %s\n")
183                  : _("failed to change ownership of %s\n"));
184         }
185       else
186         {
187           fmt = (user ? _("failed to change ownership of %s to %s\n")
188                  : group ? _("failed to change group of %s to %s\n")
189                  : _("failed to change ownership of %s\n"));
190           free (old_spec);
191           old_spec = spec;
192           spec = nullptr;
193         }
194       break;
195     case CH_NO_CHANGE_REQUESTED:
196       fmt = (user ? _("ownership of %s retained as %s\n")
197              : group ? _("group of %s retained as %s\n")
198              : _("ownership of %s retained\n"));
199       break;
200     default:
201       affirm (false);
202     }
203 
204   printf (fmt, quoteaf (file), old_spec, spec);
205 
206   free (old_spec);
207   free (spec);
208 }
209 
210 /* Change the owner and/or group of the FILE to UID and/or GID (safely)
211    only if REQUIRED_UID and REQUIRED_GID match the owner and group IDs
212    of FILE.  ORIG_ST must be the result of 'stat'ing FILE.
213 
214    The 'safely' part above means that we can't simply use chown(2),
215    since FILE might be replaced with some other file between the time
216    of the preceding stat/lstat and this chown call.  So here we open
217    FILE and do everything else via the resulting file descriptor.
218    We first call fstat and verify that the dev/inode match those from
219    the preceding stat call, and only then, if appropriate (given the
220    required_uid and required_gid constraints) do we call fchown.
221 
222    Return RC_do_ordinary_chown if we can't open FILE, or if FILE is a
223    special file that might have undesirable side effects when opening.
224    In this case the caller can use the less-safe ordinary chown.
225 
226    Return one of the RCH_status values.  */
227 
228 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)229 restricted_chown (int cwd_fd, char const *file,
230                   struct stat const *orig_st,
231                   uid_t uid, gid_t gid,
232                   uid_t required_uid, gid_t required_gid)
233 {
234   enum RCH_status status = RC_ok;
235   struct stat st;
236   int open_flags = O_NONBLOCK | O_NOCTTY;
237   int fd;
238 
239   if (required_uid == (uid_t) -1 && required_gid == (gid_t) -1)
240     return RC_do_ordinary_chown;
241 
242   if (! S_ISREG (orig_st->st_mode))
243     {
244       if (S_ISDIR (orig_st->st_mode))
245         open_flags |= O_DIRECTORY;
246       else
247         return RC_do_ordinary_chown;
248     }
249 
250   fd = openat (cwd_fd, file, O_RDONLY | open_flags);
251   if (! (0 <= fd
252          || (errno == EACCES && S_ISREG (orig_st->st_mode)
253              && 0 <= (fd = openat (cwd_fd, file, O_WRONLY | open_flags)))))
254     return (errno == EACCES ? RC_do_ordinary_chown : RC_error);
255 
256   if (fstat (fd, &st) != 0)
257     status = RC_error;
258   else if (! psame_inode (orig_st, &st))
259     status = RC_inode_changed;
260   else if ((required_uid == (uid_t) -1 || required_uid == st.st_uid)
261            && (required_gid == (gid_t) -1 || required_gid == st.st_gid))
262     {
263 #if HAVE_FCHOWN
264       if (fchown (fd, uid, gid) == 0)
265         return close (fd) < 0 ? RC_error : RC_ok;
266 #endif
267       status = RC_error;
268     }
269 
270   int saved_errno = errno;
271   close (fd);
272   errno = saved_errno;
273   return status;
274 }
275 
276 /* Change the owner and/or group of the file specified by FTS and ENT
277    to UID and/or GID as appropriate.
278    If REQUIRED_UID is not -1, then skip files with any other user ID.
279    If REQUIRED_GID is not -1, then skip files with any other group ID.
280    CHOPT specifies additional options.
281    Return true if successful.  */
282 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)283 change_file_owner (FTS *fts, FTSENT *ent,
284                    uid_t uid, gid_t gid,
285                    uid_t required_uid, gid_t required_gid,
286                    struct Chown_option const *chopt)
287 {
288   char const *file_full_name = ent->fts_path;
289   char const *file = ent->fts_accpath;
290   struct stat const *file_stats;
291   struct stat stat_buf;
292   bool ok = true;
293   bool do_chown;
294   bool symlink_changed = true;
295 
296   switch (ent->fts_info)
297     {
298     case FTS_D:
299       if (chopt->recurse)
300         {
301           if (ROOT_DEV_INO_CHECK (chopt->root_dev_ino, ent->fts_statp))
302             {
303               /* This happens e.g., with "chown -R --preserve-root 0 /"
304                  and with "chown -RH --preserve-root 0 symlink-to-root".  */
305               ROOT_DEV_INO_WARN (file_full_name);
306               /* Tell fts not to traverse into this hierarchy.  */
307               fts_set (fts, ent, FTS_SKIP);
308               /* Ensure that we do not process "/" on the second visit.  */
309               ignore_value (fts_read (fts));
310               return false;
311             }
312           return true;
313         }
314       break;
315 
316     case FTS_DP:
317       if (! chopt->recurse)
318         return true;
319       break;
320 
321     case FTS_NS:
322       /* For a top-level file or directory, this FTS_NS (stat failed)
323          indicator is determined at the time of the initial fts_open call.
324          With programs like chmod, chown, and chgrp, that modify
325          permissions, it is possible that the file in question is
326          accessible when control reaches this point.  So, if this is
327          the first time we've seen the FTS_NS for this file, tell
328          fts_read to stat it "again".  */
329       if (ent->fts_level == 0 && ent->fts_number == 0)
330         {
331           ent->fts_number = 1;
332           fts_set (fts, ent, FTS_AGAIN);
333           return true;
334         }
335       if (! chopt->force_silent)
336         error (0, ent->fts_errno, _("cannot access %s"),
337                quoteaf (file_full_name));
338       ok = false;
339       break;
340 
341     case FTS_ERR:
342       if (! chopt->force_silent)
343         error (0, ent->fts_errno, "%s", quotef (file_full_name));
344       ok = false;
345       break;
346 
347     case FTS_DNR:
348       if (! chopt->force_silent)
349         error (0, ent->fts_errno, _("cannot read directory %s"),
350                quoteaf (file_full_name));
351       ok = false;
352       break;
353 
354     case FTS_DC:		/* directory that causes cycles */
355       if (cycle_warning_required (fts, ent))
356         {
357           emit_cycle_warning (file_full_name);
358           return false;
359         }
360       break;
361 
362     default:
363       break;
364     }
365 
366   if (!ok)
367     {
368       do_chown = false;
369       file_stats = nullptr;
370     }
371   else if (required_uid == (uid_t) -1 && required_gid == (gid_t) -1
372            && chopt->verbosity == V_off
373            && ! chopt->root_dev_ino
374            && ! chopt->affect_symlink_referent)
375     {
376       do_chown = true;
377       file_stats = ent->fts_statp;
378     }
379   else
380     {
381       file_stats = ent->fts_statp;
382 
383       /* If this is a symlink and we're dereferencing them,
384          stat it to get info on the referent.  */
385       if (chopt->affect_symlink_referent && S_ISLNK (file_stats->st_mode))
386         {
387           if (fstatat (fts->fts_cwd_fd, file, &stat_buf, 0) != 0)
388             {
389               if (! chopt->force_silent)
390                 error (0, errno, _("cannot dereference %s"),
391                        quoteaf (file_full_name));
392               ok = false;
393             }
394 
395           file_stats = &stat_buf;
396         }
397 
398       do_chown = (ok
399                   && (required_uid == (uid_t) -1
400                       || required_uid == file_stats->st_uid)
401                   && (required_gid == (gid_t) -1
402                       || required_gid == file_stats->st_gid));
403     }
404 
405   /* This happens when chown -LR --preserve-root encounters a symlink-to-/.  */
406   if (ok
407       && FTSENT_IS_DIRECTORY (ent)
408       && ROOT_DEV_INO_CHECK (chopt->root_dev_ino, file_stats))
409     {
410       ROOT_DEV_INO_WARN (file_full_name);
411       return false;
412     }
413 
414   if (do_chown)
415     {
416       if ( ! chopt->affect_symlink_referent)
417         {
418           ok = (lchownat (fts->fts_cwd_fd, file, uid, gid) == 0);
419 
420           /* Ignore any error due to lack of support; POSIX requires
421              this behavior for top-level symbolic links with -h, and
422              implies that it's required for all symbolic links.  */
423           if (!ok && errno == EOPNOTSUPP)
424             {
425               ok = true;
426               symlink_changed = false;
427             }
428         }
429       else
430         {
431           /* If possible, avoid a race condition with --from=O:G and without the
432              (-h) --no-dereference option.  If fts's stat call determined
433              that the uid/gid of FILE matched the --from=O:G-selected
434              owner and group IDs, blindly using chown(2) here could lead
435              chown(1) or chgrp(1) mistakenly to dereference a *symlink*
436              to an arbitrary file that an attacker had moved into the
437              place of FILE during the window between the stat and
438              chown(2) calls.  If FILE is a regular file or a directory
439              that can be opened, this race condition can be avoided safely.  */
440 
441           enum RCH_status err
442             = restricted_chown (fts->fts_cwd_fd, file, file_stats, uid, gid,
443                                 required_uid, required_gid);
444           switch (err)
445             {
446             case RC_ok:
447               break;
448 
449             case RC_do_ordinary_chown:
450               ok = (chownat (fts->fts_cwd_fd, file, uid, gid) == 0);
451               break;
452 
453             case RC_error:
454               ok = false;
455               break;
456 
457             case RC_inode_changed:
458               /* FIXME: give a diagnostic in this case?  */
459             case RC_excluded:
460               do_chown = false;
461               ok = false;
462               break;
463 
464             default:
465               unreachable ();
466             }
467         }
468 
469       /* On some systems (e.g., GNU/Linux 2.4.x),
470          the chown function resets the 'special' permission bits.
471          Do *not* restore those bits;  doing so would open a window in
472          which a malicious user, M, could subvert a chown command run
473          by some other user and operating on files in a directory
474          where M has write access.  */
475 
476       if (do_chown && !ok && ! chopt->force_silent)
477         error (0, errno, (uid != (uid_t) -1
478                           ? _("changing ownership of %s")
479                           : _("changing group of %s")),
480                quoteaf (file_full_name));
481     }
482 
483   if (chopt->verbosity != V_off)
484     {
485       bool changed =
486         ((do_chown && ok && symlink_changed)
487          && ! ((uid == (uid_t) -1 || uid == file_stats->st_uid)
488                && (gid == (gid_t) -1 || gid == file_stats->st_gid)));
489 
490       if (changed || chopt->verbosity == V_high)
491         {
492           enum Change_status ch_status =
493             (!ok ? CH_FAILED
494              : !symlink_changed ? CH_NOT_APPLIED
495              : !changed ? CH_NO_CHANGE_REQUESTED
496              : CH_SUCCEEDED);
497           char *old_usr = (file_stats
498                            ? uid_to_name (file_stats->st_uid) : nullptr);
499           char *old_grp = (file_stats
500                            ? gid_to_name (file_stats->st_gid) : nullptr);
501           char *new_usr = chopt->user_name
502                           ? chopt->user_name : uid != -1
503                                                ? uid_to_str (uid) : nullptr;
504           char *new_grp = chopt->group_name
505                           ? chopt->group_name : gid != -1
506                                                ? gid_to_str (gid) : nullptr;
507           describe_change (file_full_name, ch_status,
508                            old_usr, old_grp,
509                            new_usr, new_grp);
510           free (old_usr);
511           free (old_grp);
512           if (new_usr != chopt->user_name)
513             free (new_usr);
514           if (new_grp != chopt->group_name)
515             free (new_grp);
516         }
517     }
518 
519   if ( ! chopt->recurse)
520     fts_set (fts, ent, FTS_SKIP);
521 
522   return ok;
523 }
524 
525 /* Change the owner and/or group of the specified FILES.
526    BIT_FLAGS specifies how to treat each symlink-to-directory
527    that is encountered during a recursive traversal.
528    CHOPT specifies additional options.
529    If UID is not -1, then change the owner id of each file to UID.
530    If GID is not -1, then change the group id of each file to GID.
531    If REQUIRED_UID and/or REQUIRED_GID is not -1, then change only
532    files with user ID and group ID that match the non-(-1) value(s).
533    Return true if successful.  */
534 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)535 chown_files (char **files, int bit_flags,
536              uid_t uid, gid_t gid,
537              uid_t required_uid, gid_t required_gid,
538              struct Chown_option const *chopt)
539 {
540   bool ok = true;
541 
542   /* Use lstat and stat only if they're needed.  */
543   int stat_flags = ((required_uid != (uid_t) -1 || required_gid != (gid_t) -1
544                      || chopt->affect_symlink_referent
545                      || chopt->verbosity != V_off)
546                     ? 0
547                     : FTS_NOSTAT);
548 
549   FTS *fts = xfts_open (files, bit_flags | stat_flags, nullptr);
550 
551   while (true)
552     {
553       FTSENT *ent;
554 
555       ent = fts_read (fts);
556       if (ent == nullptr)
557         {
558           if (errno != 0)
559             {
560               /* FIXME: try to give a better message  */
561               if (! chopt->force_silent)
562                 error (0, errno, _("fts_read failed"));
563               ok = false;
564             }
565           break;
566         }
567 
568       ok &= change_file_owner (fts, ent, uid, gid,
569                                required_uid, required_gid, chopt);
570     }
571 
572   if (fts_close (fts) != 0)
573     {
574       error (0, errno, _("fts_close failed"));
575       ok = false;
576     }
577 
578   return ok;
579 }
580