1 /* chmod -- change permission modes of files
2 Copyright (C) 1989-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 /* Written by David MacKenzie <djm@gnu.ai.mit.edu> */
18
19 #include <config.h>
20 #include <stdio.h>
21 #include <getopt.h>
22 #include <sys/types.h>
23
24 #include "system.h"
25 #include "assure.h"
26 #include "dev-ino.h"
27 #include "filemode.h"
28 #include "ignore-value.h"
29 #include "modechange.h"
30 #include "quote.h"
31 #include "root-dev-ino.h"
32 #include "xfts.h"
33
34 /* The official name of this program (e.g., no 'g' prefix). */
35 #define PROGRAM_NAME "chmod"
36
37 #define AUTHORS \
38 proper_name ("David MacKenzie"), \
39 proper_name ("Jim Meyering")
40
41 struct change_status
42 {
43 enum
44 {
45 CH_NO_STAT,
46 CH_FAILED,
47 CH_NOT_APPLIED,
48 CH_NO_CHANGE_REQUESTED,
49 CH_SUCCEEDED
50 }
51 status;
52 mode_t old_mode;
53 mode_t new_mode;
54 };
55
56 enum Verbosity
57 {
58 /* Print a message for each file that is processed. */
59 V_high,
60
61 /* Print a message for each file whose attributes we change. */
62 V_changes_only,
63
64 /* Do not be verbose. This is the default. */
65 V_off
66 };
67
68 /* The desired change to the mode. */
69 static struct mode_change *change;
70
71 /* The initial umask value, if it might be needed. */
72 static mode_t umask_value;
73
74 /* If true, change the modes of directories recursively. */
75 static bool recurse;
76
77 /* If true, force silence (suppress most of error messages). */
78 static bool force_silent;
79
80 /* If true, diagnose surprises from naive misuses like "chmod -r file".
81 POSIX allows diagnostics here, as portable code is supposed to use
82 "chmod -- -r file". */
83 static bool diagnose_surprises;
84
85 /* Level of verbosity. */
86 static enum Verbosity verbosity = V_off;
87
88 /* Pointer to the device and inode numbers of '/', when --recursive.
89 Otherwise nullptr. */
90 static struct dev_ino *root_dev_ino;
91
92 /* For long options that have no equivalent short option, use a
93 non-character as a pseudo short option, starting with CHAR_MAX + 1. */
94 enum
95 {
96 NO_PRESERVE_ROOT = CHAR_MAX + 1,
97 PRESERVE_ROOT,
98 REFERENCE_FILE_OPTION
99 };
100
101 static struct option const long_options[] =
102 {
103 {"changes", no_argument, nullptr, 'c'},
104 {"recursive", no_argument, nullptr, 'R'},
105 {"no-preserve-root", no_argument, nullptr, NO_PRESERVE_ROOT},
106 {"preserve-root", no_argument, nullptr, PRESERVE_ROOT},
107 {"quiet", no_argument, nullptr, 'f'},
108 {"reference", required_argument, nullptr, REFERENCE_FILE_OPTION},
109 {"silent", no_argument, nullptr, 'f'},
110 {"verbose", no_argument, nullptr, 'v'},
111 {GETOPT_HELP_OPTION_DECL},
112 {GETOPT_VERSION_OPTION_DECL},
113 {nullptr, 0, nullptr, 0}
114 };
115
116 /* Return true if the chmodable permission bits of FILE changed.
117 The old mode was OLD_MODE, but it was changed to NEW_MODE. */
118
119 static bool
mode_changed(int dir_fd,char const * file,char const * file_full_name,mode_t old_mode,mode_t new_mode)120 mode_changed (int dir_fd, char const *file, char const *file_full_name,
121 mode_t old_mode, mode_t new_mode)
122 {
123 if (new_mode & (S_ISUID | S_ISGID | S_ISVTX))
124 {
125 /* The new mode contains unusual bits that the call to chmod may
126 have silently cleared. Check whether they actually changed. */
127
128 struct stat new_stats;
129
130 if (fstatat (dir_fd, file, &new_stats, 0) != 0)
131 {
132 if (! force_silent)
133 error (0, errno, _("getting new attributes of %s"),
134 quoteaf (file_full_name));
135 return false;
136 }
137
138 new_mode = new_stats.st_mode;
139 }
140
141 return ((old_mode ^ new_mode) & CHMOD_MODE_BITS) != 0;
142 }
143
144 /* Tell the user how/if the MODE of FILE has been changed.
145 CH describes what (if anything) has happened. */
146
147 static void
describe_change(char const * file,struct change_status const * ch)148 describe_change (char const *file, struct change_status const *ch)
149 {
150 char perms[12]; /* "-rwxrwxrwx" ls-style modes. */
151 char old_perms[12];
152 char const *fmt;
153 char const *quoted_file = quoteaf (file);
154
155 switch (ch->status)
156 {
157 case CH_NOT_APPLIED:
158 printf (_("neither symbolic link %s nor referent has been changed\n"),
159 quoted_file);
160 return;
161
162 case CH_NO_STAT:
163 printf (_("%s could not be accessed\n"), quoted_file);
164 return;
165
166 default:
167 break;
168 }
169
170 unsigned long int
171 old_m = ch->old_mode & CHMOD_MODE_BITS,
172 m = ch->new_mode & CHMOD_MODE_BITS;
173
174 strmode (ch->new_mode, perms);
175 perms[10] = '\0'; /* Remove trailing space. */
176
177 strmode (ch->old_mode, old_perms);
178 old_perms[10] = '\0'; /* Remove trailing space. */
179
180 switch (ch->status)
181 {
182 case CH_SUCCEEDED:
183 fmt = _("mode of %s changed from %04lo (%s) to %04lo (%s)\n");
184 break;
185 case CH_FAILED:
186 fmt = _("failed to change mode of %s from %04lo (%s) to %04lo (%s)\n");
187 break;
188 case CH_NO_CHANGE_REQUESTED:
189 fmt = _("mode of %s retained as %04lo (%s)\n");
190 printf (fmt, quoted_file, m, &perms[1]);
191 return;
192 default:
193 affirm (false);
194 }
195 printf (fmt, quoted_file, old_m, &old_perms[1], m, &perms[1]);
196 }
197
198 /* Change the mode of FILE.
199 Return true if successful. This function is called
200 once for every file system object that fts encounters. */
201
202 static bool
process_file(FTS * fts,FTSENT * ent)203 process_file (FTS *fts, FTSENT *ent)
204 {
205 char const *file_full_name = ent->fts_path;
206 char const *file = ent->fts_accpath;
207 const struct stat *file_stats = ent->fts_statp;
208 struct change_status ch = {0};
209 ch.status = CH_NO_STAT;
210
211 switch (ent->fts_info)
212 {
213 case FTS_DP:
214 return true;
215
216 case FTS_NS:
217 /* For a top-level file or directory, this FTS_NS (stat failed)
218 indicator is determined at the time of the initial fts_open call.
219 With programs like chmod, chown, and chgrp, that modify
220 permissions, it is possible that the file in question is
221 accessible when control reaches this point. So, if this is
222 the first time we've seen the FTS_NS for this file, tell
223 fts_read to stat it "again". */
224 if (ent->fts_level == 0 && ent->fts_number == 0)
225 {
226 ent->fts_number = 1;
227 fts_set (fts, ent, FTS_AGAIN);
228 return true;
229 }
230 if (! force_silent)
231 error (0, ent->fts_errno, _("cannot access %s"),
232 quoteaf (file_full_name));
233 break;
234
235 case FTS_ERR:
236 if (! force_silent)
237 error (0, ent->fts_errno, "%s", quotef (file_full_name));
238 break;
239
240 case FTS_DNR:
241 if (! force_silent)
242 error (0, ent->fts_errno, _("cannot read directory %s"),
243 quoteaf (file_full_name));
244 break;
245
246 case FTS_SLNONE:
247 if (! force_silent)
248 error (0, 0, _("cannot operate on dangling symlink %s"),
249 quoteaf (file_full_name));
250 break;
251
252 case FTS_DC: /* directory that causes cycles */
253 if (cycle_warning_required (fts, ent))
254 {
255 emit_cycle_warning (file_full_name);
256 return false;
257 }
258 FALLTHROUGH;
259 default:
260 ch.status = CH_NOT_APPLIED;
261 break;
262 }
263
264 if (ch.status == CH_NOT_APPLIED
265 && ROOT_DEV_INO_CHECK (root_dev_ino, file_stats))
266 {
267 ROOT_DEV_INO_WARN (file_full_name);
268 /* Tell fts not to traverse into this hierarchy. */
269 fts_set (fts, ent, FTS_SKIP);
270 /* Ensure that we do not process "/" on the second visit. */
271 ignore_value (fts_read (fts));
272 return false;
273 }
274
275 if (ch.status == CH_NOT_APPLIED && ! S_ISLNK (file_stats->st_mode))
276 {
277 ch.old_mode = file_stats->st_mode;
278 ch.new_mode = mode_adjust (ch.old_mode, S_ISDIR (ch.old_mode) != 0,
279 umask_value, change, nullptr);
280 if (chmodat (fts->fts_cwd_fd, file, ch.new_mode) == 0)
281 ch.status = CH_SUCCEEDED;
282 else
283 {
284 if (! force_silent)
285 error (0, errno, _("changing permissions of %s"),
286 quoteaf (file_full_name));
287 ch.status = CH_FAILED;
288 }
289 }
290
291 if (verbosity != V_off)
292 {
293 if (ch.status == CH_SUCCEEDED
294 && !mode_changed (fts->fts_cwd_fd, file, file_full_name,
295 ch.old_mode, ch.new_mode))
296 ch.status = CH_NO_CHANGE_REQUESTED;
297
298 if (ch.status == CH_SUCCEEDED || verbosity == V_high)
299 describe_change (file_full_name, &ch);
300 }
301
302 if (CH_NO_CHANGE_REQUESTED <= ch.status && diagnose_surprises)
303 {
304 mode_t naively_expected_mode =
305 mode_adjust (ch.old_mode, S_ISDIR (ch.old_mode) != 0,
306 0, change, nullptr);
307 if (ch.new_mode & ~naively_expected_mode)
308 {
309 char new_perms[12];
310 char naively_expected_perms[12];
311 strmode (ch.new_mode, new_perms);
312 strmode (naively_expected_mode, naively_expected_perms);
313 new_perms[10] = naively_expected_perms[10] = '\0';
314 error (0, 0,
315 _("%s: new permissions are %s, not %s"),
316 quotef (file_full_name),
317 new_perms + 1, naively_expected_perms + 1);
318 ch.status = CH_FAILED;
319 }
320 }
321
322 if ( ! recurse)
323 fts_set (fts, ent, FTS_SKIP);
324
325 return CH_NOT_APPLIED <= ch.status;
326 }
327
328 /* Recursively change the modes of the specified FILES (the last entry
329 of which is null). BIT_FLAGS controls how fts works.
330 Return true if successful. */
331
332 static bool
process_files(char ** files,int bit_flags)333 process_files (char **files, int bit_flags)
334 {
335 bool ok = true;
336
337 FTS *fts = xfts_open (files, bit_flags, nullptr);
338
339 while (true)
340 {
341 FTSENT *ent;
342
343 ent = fts_read (fts);
344 if (ent == nullptr)
345 {
346 if (errno != 0)
347 {
348 /* FIXME: try to give a better message */
349 if (! force_silent)
350 error (0, errno, _("fts_read failed"));
351 ok = false;
352 }
353 break;
354 }
355
356 ok &= process_file (fts, ent);
357 }
358
359 if (fts_close (fts) != 0)
360 {
361 error (0, errno, _("fts_close failed"));
362 ok = false;
363 }
364
365 return ok;
366 }
367
368 void
usage(int status)369 usage (int status)
370 {
371 if (status != EXIT_SUCCESS)
372 emit_try_help ();
373 else
374 {
375 printf (_("\
376 Usage: %s [OPTION]... MODE[,MODE]... FILE...\n\
377 or: %s [OPTION]... OCTAL-MODE FILE...\n\
378 or: %s [OPTION]... --reference=RFILE FILE...\n\
379 "),
380 program_name, program_name, program_name);
381 fputs (_("\
382 Change the mode of each FILE to MODE.\n\
383 With --reference, change the mode of each FILE to that of RFILE.\n\
384 \n\
385 "), stdout);
386 fputs (_("\
387 -c, --changes like verbose but report only when a change is made\n\
388 -f, --silent, --quiet suppress most error messages\n\
389 -v, --verbose output a diagnostic for every file processed\n\
390 "), stdout);
391 fputs (_("\
392 --no-preserve-root do not treat '/' specially (the default)\n\
393 --preserve-root fail to operate recursively on '/'\n\
394 "), stdout);
395 fputs (_("\
396 --reference=RFILE use RFILE's mode instead of specifying MODE values.\n\
397 RFILE is always dereferenced if a symbolic link.\n\
398 "), stdout);
399 fputs (_("\
400 -R, --recursive change files and directories recursively\n\
401 "), stdout);
402 fputs (HELP_OPTION_DESCRIPTION, stdout);
403 fputs (VERSION_OPTION_DESCRIPTION, stdout);
404 fputs (_("\
405 \n\
406 Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+'.\n\
407 "), stdout);
408 emit_ancillary_info (PROGRAM_NAME);
409 }
410 exit (status);
411 }
412
413 /* Parse the ASCII mode given on the command line into a linked list
414 of 'struct mode_change' and apply that to each file argument. */
415
416 int
main(int argc,char ** argv)417 main (int argc, char **argv)
418 {
419 char *mode = nullptr;
420 idx_t mode_len = 0;
421 idx_t mode_alloc = 0;
422 bool ok;
423 bool preserve_root = false;
424 char const *reference_file = nullptr;
425 int c;
426
427 initialize_main (&argc, &argv);
428 set_program_name (argv[0]);
429 setlocale (LC_ALL, "");
430 bindtextdomain (PACKAGE, LOCALEDIR);
431 textdomain (PACKAGE);
432
433 atexit (close_stdout);
434
435 recurse = force_silent = diagnose_surprises = false;
436
437 while ((c = getopt_long (argc, argv,
438 ("Rcfvr::w::x::X::s::t::u::g::o::a::,::+::=::"
439 "0::1::2::3::4::5::6::7::"),
440 long_options, nullptr))
441 != -1)
442 {
443 switch (c)
444 {
445 case 'r':
446 case 'w':
447 case 'x':
448 case 'X':
449 case 's':
450 case 't':
451 case 'u':
452 case 'g':
453 case 'o':
454 case 'a':
455 case ',':
456 case '+':
457 case '=':
458 case '0': case '1': case '2': case '3':
459 case '4': case '5': case '6': case '7':
460 /* Support non-portable uses like "chmod -w", but diagnose
461 surprises due to umask confusion. Even though "--", "--r",
462 etc., are valid modes, there is no "case '-'" here since
463 getopt_long reserves leading "--" for long options. */
464 {
465 /* Allocate a mode string (e.g., "-rwx") by concatenating
466 the argument containing this option. If a previous mode
467 string was given, concatenate the previous string, a
468 comma, and the new string (e.g., "-s,-rwx"). */
469
470 char const *arg = argv[optind - 1];
471 idx_t arg_len = strlen (arg);
472 idx_t mode_comma_len = mode_len + !!mode_len;
473 idx_t new_mode_len = mode_comma_len + arg_len;
474 assume (0 <= new_mode_len); /* Pacify GCC bug #109613. */
475 if (mode_alloc <= new_mode_len)
476 mode = xpalloc (mode, &mode_alloc,
477 new_mode_len + 1 - mode_alloc, -1, 1);
478 mode[mode_len] = ',';
479 memcpy (mode + mode_comma_len, arg, arg_len + 1);
480 mode_len = new_mode_len;
481
482 diagnose_surprises = true;
483 }
484 break;
485 case NO_PRESERVE_ROOT:
486 preserve_root = false;
487 break;
488 case PRESERVE_ROOT:
489 preserve_root = true;
490 break;
491 case REFERENCE_FILE_OPTION:
492 reference_file = optarg;
493 break;
494 case 'R':
495 recurse = true;
496 break;
497 case 'c':
498 verbosity = V_changes_only;
499 break;
500 case 'f':
501 force_silent = true;
502 break;
503 case 'v':
504 verbosity = V_high;
505 break;
506 case_GETOPT_HELP_CHAR;
507 case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
508 default:
509 usage (EXIT_FAILURE);
510 }
511 }
512
513 if (reference_file)
514 {
515 if (mode)
516 {
517 error (0, 0, _("cannot combine mode and --reference options"));
518 usage (EXIT_FAILURE);
519 }
520 }
521 else
522 {
523 if (!mode)
524 mode = argv[optind++];
525 }
526
527 if (optind >= argc)
528 {
529 if (!mode || mode != argv[optind - 1])
530 error (0, 0, _("missing operand"));
531 else
532 error (0, 0, _("missing operand after %s"), quote (argv[argc - 1]));
533 usage (EXIT_FAILURE);
534 }
535
536 if (reference_file)
537 {
538 change = mode_create_from_ref (reference_file);
539 if (!change)
540 error (EXIT_FAILURE, errno, _("failed to get attributes of %s"),
541 quoteaf (reference_file));
542 }
543 else
544 {
545 change = mode_compile (mode);
546 if (!change)
547 {
548 error (0, 0, _("invalid mode: %s"), quote (mode));
549 usage (EXIT_FAILURE);
550 }
551 umask_value = umask (0);
552 }
553
554 if (recurse && preserve_root)
555 {
556 static struct dev_ino dev_ino_buf;
557 root_dev_ino = get_root_dev_ino (&dev_ino_buf);
558 if (root_dev_ino == nullptr)
559 error (EXIT_FAILURE, errno, _("failed to get attributes of %s"),
560 quoteaf ("/"));
561 }
562 else
563 {
564 root_dev_ino = nullptr;
565 }
566
567 ok = process_files (argv + optind,
568 FTS_COMFOLLOW | FTS_PHYSICAL | FTS_DEFER_STAT);
569
570 main_exit (ok ? EXIT_SUCCESS : EXIT_FAILURE);
571 }
572