xref: /illumos-gate/usr/src/cmd/chmod/chmod.c (revision db874c57ae335a07060499f1492b0d0e2593e26c)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License, Version 1.0 only
6  * (the "License").  You may not use this file except in compliance
7  * with the License.
8  *
9  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10  * or http://www.opensolaris.org/os/licensing.
11  * See the License for the specific language governing permissions
12  * and limitations under the License.
13  *
14  * When distributing Covered Code, include this CDDL HEADER in each
15  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16  * If applicable, add the following below this CDDL HEADER, with the
17  * fields enclosed by brackets "[]" replaced with your own identifying
18  * information: Portions Copyright [yyyy] [name of copyright owner]
19  *
20  * CDDL HEADER END
21  */
22 /*
23  * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
24  * Use is subject to license terms.
25  */
26 
27 /*	Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T			*/
28 /*	  All Rights Reserved						*/
29 /*									*/
30 
31 /*
32  * University Copyright- Copyright (c) 1982, 1986, 1988
33  * The Regents of the University of California
34  * All Rights Reserved
35  *
36  * University Acknowledgment- Portions of this document are derived from
37  * software developed by the University of California, Berkeley, and its
38  * contributors.
39  */
40 
41 #pragma ident	"%Z%%M%	%I%	%E% SMI"
42 
43 /*
44  * chmod option mode files
45  * where
46  *	mode is [ugoa][+-=][rwxXlstugo] or an octal number
47  *	mode is [<+|->A[# <number] ]<aclspec>
48  *	option is -R and -f
49  */
50 
51 /*
52  *  Note that many convolutions are necessary
53  *  due to the re-use of bits between locking
54  *  and setgid
55  */
56 
57 #include <unistd.h>
58 #include <stdlib.h>
59 #include <stdio.h>
60 #include <sys/types.h>
61 #include <sys/stat.h>
62 #include <dirent.h>
63 #include <locale.h>
64 #include <string.h>	/* strerror() */
65 #include <stdarg.h>
66 #include <limits.h>
67 #include <ctype.h>
68 #include <errno.h>
69 #include <sys/acl.h>
70 #include <aclutils.h>
71 
72 static int	rflag;
73 static int	fflag;
74 
75 extern int	optind;
76 extern int	errno;
77 
78 static int	mac;		/* Alternate to argc (for parseargs) */
79 static char	**mav;		/* Alternate to argv (for parseargs) */
80 
81 static char	*ms;		/* Points to the mode argument */
82 
83 #define	ACL_ADD		1
84 #define	ACL_DELETE	2
85 #define	ACL_SLOT_DELETE 3
86 #define	ACL_REPLACE	4
87 #define	ACL_STRIP	5
88 
89 typedef struct acl_args {
90 	acl_t	*acl_aclp;
91 	int	acl_slot;
92 	int	acl_action;
93 } acl_args_t;
94 
95 extern mode_t
96 newmode_common(char *ms, mode_t new_mode, mode_t umsk, char *file, char *path,
97 	o_mode_t *group_clear_bits, o_mode_t *group_set_bits);
98 
99 static int
100 dochmod(char *name, char *path, mode_t umsk, acl_args_t *aclp),
101 chmodr(char *dir, char *path, mode_t mode, mode_t umsk, acl_args_t *aclp);
102 static int doacl(char *file, struct stat *st, acl_args_t *aclp);
103 
104 static void handle_acl(char *name, o_mode_t group_clear_bits,
105     o_mode_t group_set_bits);
106 
107 static void usage(void);
108 
109 void errmsg(int severity, int code, char *format, ...);
110 
111 static void parseargs(int ac, char *av[]);
112 
113 int
114 parse_acl_args(char *arg, acl_args_t **acl_args);
115 
116 int
117 main(int argc, char *argv[])
118 {
119 	int i, c;
120 	int status = 0;
121 	mode_t umsk;
122 	acl_args_t *acl_args = NULL;
123 
124 	(void) setlocale(LC_ALL, "");
125 #if !defined(TEXT_DOMAIN)	/* Should be defined by cc -D */
126 #define	TEXT_DOMAIN "SYS_TEST"	/* Use this only if it weren't */
127 #endif
128 	(void) textdomain(TEXT_DOMAIN);
129 
130 	parseargs(argc, argv);
131 
132 	while ((c = getopt(mac, mav, "Rf")) != EOF) {
133 		switch (c) {
134 		case 'R':
135 			rflag++;
136 			break;
137 		case 'f':
138 			fflag++;
139 			break;
140 		case '?':
141 			usage();
142 			exit(2);
143 		}
144 	}
145 
146 	/*
147 	 * Check for sufficient arguments
148 	 * or a usage error.
149 	 */
150 
151 	mac -= optind;
152 	mav += optind;
153 
154 	if (mac >= 2 && (mav[0][0] == 'A')) {
155 		if (parse_acl_args(*mav, &acl_args)) {
156 			usage();
157 			exit(2);
158 		}
159 	} else {
160 		if (mac < 2) {
161 			usage();
162 			exit(2);
163 		}
164 	}
165 
166 	ms = mav[0];
167 
168 	umsk = umask(0);
169 	(void) umask(umsk);
170 
171 	for (i = 1; i < mac; i++) {
172 		status += dochmod(mav[i], mav[i], umsk, acl_args);
173 	}
174 
175 	return (fflag ? 0 : status);
176 }
177 
178 static int
179 dochmod(char *name, char *path, mode_t umsk, acl_args_t *aclp)
180 {
181 	static struct stat st;
182 	int linkflg = 0;
183 	o_mode_t	group_clear_bits, group_set_bits;
184 
185 	if (lstat(name, &st) < 0) {
186 		errmsg(2, 0, gettext("can't access %s\n"), path);
187 		return (1);
188 	}
189 
190 	if ((st.st_mode & S_IFMT) == S_IFLNK) {
191 		linkflg = 1;
192 		if (stat(name, &st) < 0) {
193 			errmsg(2, 0, gettext("can't access %s\n"), path);
194 			return (1);
195 		}
196 	}
197 
198 	/* Do not recurse if directory is object of symbolic link */
199 	if (rflag && ((st.st_mode & S_IFMT) == S_IFDIR) && !linkflg)
200 		return (chmodr(name, path, st.st_mode, umsk, aclp));
201 
202 	if (aclp) {
203 		return (doacl(name, &st, aclp));
204 	} else if (chmod(name, newmode_common(ms, st.st_mode, umsk, name, path,
205 	    &group_clear_bits, &group_set_bits)) == -1) {
206 		errmsg(2, 0, gettext("can't change %s\n"), path);
207 		return (1);
208 	}
209 
210 	/*
211 	 * If the group permissions of the file are being modified,
212 	 * make sure that the file's ACL (if it has one) is
213 	 * modified also, since chmod is supposed to apply group
214 	 * permissions changes to both the acl mask and the
215 	 * general group permissions.
216 	 */
217 	if (group_clear_bits || group_set_bits)
218 		handle_acl(name, group_clear_bits, group_set_bits);
219 
220 	return (0);
221 }
222 
223 
224 static int
225 chmodr(char *dir, char *path,  mode_t mode, mode_t umsk, acl_args_t *aclp)
226 {
227 
228 	DIR *dirp;
229 	struct dirent *dp;
230 	char savedir[PATH_MAX];			/* dir name to restore */
231 	char currdir[PATH_MAX+1];		/* current dir name + '/' */
232 	char parentdir[PATH_MAX+1];		/* parent dir name  + '/' */
233 	int ecode;
234 	struct stat st;
235 	o_mode_t	group_clear_bits, group_set_bits;
236 
237 	if (getcwd(savedir, PATH_MAX) == 0)
238 		errmsg(2, 255, gettext("chmod: could not getcwd %s\n"),
239 		    savedir);
240 
241 	/*
242 	 * Change what we are given before doing it's contents
243 	 */
244 	if (aclp) {
245 		if (lstat(dir, &st) < 0) {
246 			errmsg(2, 0, gettext("can't access %s\n"), path);
247 			return (1);
248 		}
249 		if (doacl(dir, &st, aclp) != 0)
250 			return (1);
251 	} else if (chmod(dir, newmode_common(ms, mode, umsk, dir, path,
252 	    &group_clear_bits, &group_set_bits)) < 0) {
253 		errmsg(2, 0, gettext("can't change %s\n"), path);
254 		return (1);
255 	}
256 
257 	/*
258 	 * If the group permissions of the file are being modified,
259 	 * make sure that the file's ACL (if it has one) is
260 	 * modified also, since chmod is supposed to apply group
261 	 * permissions changes to both the acl mask and the
262 	 * general group permissions.
263 	 */
264 
265 	if (aclp == NULL) { /* only necessary when not setting ACL */
266 		if (group_clear_bits || group_set_bits)
267 			handle_acl(dir, group_clear_bits, group_set_bits);
268 	}
269 
270 	if (chdir(dir) < 0) {
271 		errmsg(2, 0, "%s/%s: %s\n", savedir, dir, strerror(errno));
272 		return (1);
273 	}
274 	if ((dirp = opendir(".")) == NULL) {
275 		errmsg(2, 0, "%s\n", strerror(errno));
276 		return (1);
277 	}
278 	dp = readdir(dirp);
279 	dp = readdir(dirp); /* read "." and ".." */
280 	ecode = 0;
281 
282 	/*
283 	 * Save parent directory path before recursive chmod.
284 	 * We'll need this for error printing purposes. Add
285 	 * a trailing '/' to the path except in the case where
286 	 * the path is just '/'
287 	 */
288 
289 	(void) strcpy(parentdir, path);
290 	if (strcmp(path, "/") != 0)
291 		(void) strcat(parentdir, "/");
292 
293 	for (dp = readdir(dirp); dp != NULL; dp = readdir(dirp))  {
294 		(void) strcpy(currdir, parentdir);
295 		(void) strcat(currdir, dp->d_name);
296 		ecode += dochmod(dp->d_name, currdir, umsk, aclp);
297 	}
298 	(void) closedir(dirp);
299 	if (chdir(savedir) < 0) {
300 		errmsg(2, 255, gettext("can't change back to %s\n"), savedir);
301 	}
302 	return (ecode ? 1 : 0);
303 }
304 
305 /* PRINTFLIKE3 */
306 void
307 errmsg(int severity, int code, char *format, ...)
308 {
309 	va_list ap;
310 	static char *msg[] = {
311 	"",
312 	"ERROR",
313 	"WARNING",
314 	""
315 	};
316 
317 	va_start(ap, format);
318 
319 	/*
320 	 * Always print error message if this is a fatal error (code == 0);
321 	 * otherwise, print message if fflag == 0 (no -f option specified)
322 	 */
323 	if (!fflag || (code != 0)) {
324 		(void) fprintf(stderr,
325 			"chmod: %s: ", gettext(msg[severity]));
326 		(void) vfprintf(stderr, format, ap);
327 	}
328 
329 	va_end(ap);
330 
331 	if (code != 0)
332 		exit(fflag ? 0 : code);
333 }
334 
335 static void
336 usage(void)
337 {
338 	(void) fprintf(stderr, gettext(
339 	    "usage:\tchmod [-fR] <absolute-mode> file ...\n"));
340 
341 	(void) fprintf(stderr, gettext(
342 	    "\tchmod [-fR] <ACL-operation> file ...\n"));
343 
344 	(void) fprintf(stderr, gettext(
345 	    "\tchmod [-fR] <symbolic-mode-list> file ...\n"));
346 
347 
348 	(void) fprintf(stderr, gettext(
349 	    "where \t<symbolic-mode-list> is a comma-separated list of\n"));
350 
351 	(void) fprintf(stderr, gettext(
352 	    "\t[ugoa]{+|-|=}[rwxXlstugo]\n"));
353 
354 	(void) fprintf(stderr, gettext(
355 	    "where \t<ACL-operation> is one of the following\n"));
356 	(void) fprintf(stderr, gettext("\tA-<acl_specification>\n"));
357 	(void) fprintf(stderr, gettext("\tA[number]-\n"));
358 	(void) fprintf(stderr, gettext(
359 	    "\tA[number]{+|=}<acl_specification>\n"));
360 	(void) fprintf(stderr, gettext(
361 	    "where \t<acl-specification> is a comma-separated list of ACEs\n"));
362 }
363 
364 /*
365  *  parseargs - generate getopt-friendly argument list for backwards
366  *		compatibility with earlier Solaris usage (eg, chmod -w
367  *		foo).
368  *
369  *  assumes the existence of a static set of alternates to argc and argv,
370  *  (namely, mac, and mav[]).
371  *
372  */
373 
374 static void
375 parseargs(int ac, char *av[])
376 {
377 	int i;			/* current argument			*/
378 	int fflag;		/* arg list contains "--"		*/
379 	size_t mav_num;		/* number of entries in mav[]		*/
380 
381 	/*
382 	 * We add an extra argument slot, in case we need to jam a "--"
383 	 * argument into the list.
384 	 */
385 
386 	mav_num = (size_t)ac+2;
387 
388 	if ((mav = calloc(mav_num, sizeof (char *))) == NULL) {
389 		perror("chmod");
390 		exit(2);
391 	}
392 
393 	/* scan for the use of "--" in the argument list */
394 
395 	for (fflag = i = 0; i < ac; i ++) {
396 		if (strcmp(av[i], "--") == 0)
397 		    fflag = 1;
398 	}
399 
400 	/* process the arguments */
401 
402 	for (i = mac = 0;
403 	    (av[i] != (char *)NULL) && (av[i][0] != (char)NULL);
404 	    i++) {
405 		if (!fflag && av[i][0] == '-') {
406 			/*
407 			 *  If there is not already a "--" argument specified,
408 			 *  and the argument starts with '-' but does not
409 			 *  contain any of the official option letters, then it
410 			 *  is probably a mode argument beginning with '-'.
411 			 *  Force a "--" into the argument stream in front of
412 			 *  it.
413 			 */
414 
415 			if ((strchr(av[i], 'R') == NULL &&
416 			    strchr(av[i], 'f') == NULL)) {
417 				mav[mac++] = strdup("--");
418 			}
419 		}
420 
421 		mav[mac++] = strdup(av[i]);
422 	}
423 
424 	mav[mac] = (char *)NULL;
425 }
426 
427 int
428 parse_acl_args(char *arg, acl_args_t **acl_args)
429 {
430 	acl_t *new_acl = NULL;
431 	int slot;
432 	int error;
433 	int len;
434 	int action;
435 	acl_args_t *new_acl_args;
436 	char *acl_spec = NULL;
437 	char *end;
438 
439 	if (arg[0] != 'A')
440 		return (1);
441 
442 	slot = strtol(&arg[1], &end, 10);
443 
444 	len = strlen(arg);
445 	switch (*end) {
446 	case '+':
447 		action = ACL_ADD;
448 		acl_spec = ++end;
449 		break;
450 	case '-':
451 		if (len == 2 && arg[0] == 'A' && arg[1] == '-')
452 			action = ACL_STRIP;
453 		else
454 			action = ACL_DELETE;
455 		if (action != ACL_STRIP) {
456 			acl_spec = ++end;
457 			if (acl_spec[0] == '\0') {
458 				action = ACL_SLOT_DELETE;
459 				acl_spec = NULL;
460 			} else if (arg[1] != '-')
461 				return (1);
462 		}
463 		break;
464 	case '=':
465 		/*
466 		 * Was slot specified?
467 		 */
468 		if (arg[1] == '=')
469 			slot = -1;
470 		action = ACL_REPLACE;
471 		acl_spec = ++end;
472 		break;
473 	default:
474 		return (1);
475 	}
476 
477 	if ((action == ACL_REPLACE || action == ACL_ADD) && acl_spec[0] == '\0')
478 		return (1);
479 
480 	if (acl_spec) {
481 		if (error = acl_fromtext(acl_spec, &new_acl)) {
482 			errmsg(1, 1, "%s\n", acl_strerror(error));
483 			return (1);
484 		}
485 	}
486 
487 	new_acl_args = malloc(sizeof (acl_args_t));
488 	if (new_acl_args == NULL)
489 		return (1);
490 
491 	new_acl_args->acl_aclp = new_acl;
492 	new_acl_args->acl_slot = slot;
493 	new_acl_args->acl_action = action;
494 
495 	*acl_args = new_acl_args;
496 
497 	return (0);
498 }
499 
500 /*
501  * This function is called whenever the group permissions of a file
502  * is being modified.  According to the chmod(1) manpage, any
503  * change made to the group permissions must be applied to both
504  * the acl mask and the acl's GROUP_OBJ.  The chmod(2) already
505  * set the mask, so this routine needs to make the same change
506  * to the GROUP_OBJ.
507  */
508 static void
509 handle_acl(char *name, o_mode_t group_clear_bits, o_mode_t group_set_bits)
510 {
511 	int aclcnt, n;
512 	aclent_t *aclp, *tp;
513 	o_mode_t newperm;
514 
515 	/*
516 	 * if this file system support ace_t acl's
517 	 * then simply return since we don't have an
518 	 * acl mask to deal with
519 	 */
520 	if (pathconf(name, _PC_ACL_ENABLED) == _ACL_ACE_ENABLED)
521 		return;
522 
523 	if ((aclcnt = acl(name, GETACLCNT, 0, NULL)) <= MIN_ACL_ENTRIES)
524 		return;	/* it's just a trivial acl; no need to change it */
525 
526 	if ((aclp = (aclent_t *)malloc((sizeof (aclent_t)) * aclcnt))
527 	    == NULL) {
528 		perror("chmod");
529 		exit(2);
530 	}
531 
532 	if (acl(name, GETACL, aclcnt, aclp) < 0) {
533 		free(aclp);
534 		(void) fprintf(stderr, "chmod: ");
535 		perror(name);
536 		return;
537 	}
538 
539 	for (tp = aclp, n = aclcnt; n--; tp++) {
540 		if (tp->a_type == GROUP_OBJ) {
541 			newperm = tp->a_perm;
542 			if (group_clear_bits != 0)
543 				newperm &= ~group_clear_bits;
544 			if (group_set_bits != 0)
545 				newperm |= group_set_bits;
546 			if (newperm != tp->a_perm) {
547 				tp->a_perm = newperm;
548 				if (acl(name, SETACL, aclcnt, aclp)
549 				    < 0) {
550 					(void) fprintf(stderr, "chmod: ");
551 					perror(name);
552 				}
553 			}
554 			break;
555 		}
556 	}
557 	free(aclp);
558 }
559 
560 static int
561 doacl(char *file, struct stat *st, acl_args_t *acl_args)
562 {
563 	acl_t *aclp;
564 	acl_t *set_aclp;
565 	int error = 0;
566 	void *to, *from;
567 	int len;
568 	int isdir;
569 
570 	isdir = S_ISDIR(st->st_mode);
571 
572 	error = acl_get(file, 0, &aclp);
573 
574 	if (error != 0) {
575 		errmsg(1, 1, "%s\n", acl_strerror(error));
576 		return (1);
577 	}
578 
579 	switch (acl_args->acl_action) {
580 	case ACL_ADD:
581 		if ((error = acl_addentries(aclp,
582 			acl_args->acl_aclp, acl_args->acl_slot)) != 0) {
583 				errmsg(1, 1, "%s\n", acl_strerror(error));
584 				acl_free(aclp);
585 				return (1);
586 		}
587 		set_aclp = aclp;
588 		break;
589 	case ACL_SLOT_DELETE:
590 
591 		if (acl_args->acl_slot + 1 > aclp->acl_cnt) {
592 			errmsg(1, 1,
593 			    gettext("Invalid slot specified for removal\n"));
594 			acl_free(aclp);
595 			return (1);
596 		}
597 
598 		if (acl_args->acl_slot == 0 && aclp->acl_cnt == 1) {
599 			errmsg(1, 1,
600 			    gettext("Can't remove all ACL "
601 			    "entries from a file\n"));
602 			acl_free(aclp);
603 			return (1);
604 		}
605 
606 		/*
607 		 * remove a single entry
608 		 *
609 		 * if last entry just adjust acl_cnt
610 		 */
611 
612 		if ((acl_args->acl_slot + 1) == aclp->acl_cnt)
613 			aclp->acl_cnt--;
614 		else {
615 			to = (char *)aclp->acl_aclp +
616 			    (acl_args->acl_slot * aclp->acl_entry_size);
617 			from = (char *)to + aclp->acl_entry_size;
618 			len = (aclp->acl_cnt - acl_args->acl_slot - 1) *
619 			    aclp->acl_entry_size;
620 			(void) memmove(to, from, len);
621 			aclp->acl_cnt--;
622 		}
623 		set_aclp = aclp;
624 		break;
625 
626 	case ACL_DELETE:
627 		if ((error = acl_removeentries(aclp, acl_args->acl_aclp,
628 		    acl_args->acl_slot, ACL_REMOVE_ALL)) != 0) {
629 			errmsg(1, 1, "%s\n", acl_strerror(error));
630 			acl_free(aclp);
631 			return (1);
632 		}
633 
634 		if (aclp->acl_cnt == 0) {
635 			errmsg(1, 1,
636 			    gettext("Can't remove all ACL "
637 			    "entries from a file\n"));
638 			acl_free(aclp);
639 			return (1);
640 		}
641 
642 		set_aclp = aclp;
643 		break;
644 	case ACL_REPLACE:
645 		if (acl_args->acl_slot >= 0)  {
646 			error = acl_modifyentries(aclp, acl_args->acl_aclp,
647 			    acl_args->acl_slot);
648 			if (error) {
649 				errmsg(1, 1, "%s\n", acl_strerror(error));
650 				acl_free(aclp);
651 				return (1);
652 			}
653 			set_aclp = aclp;
654 		} else {
655 			set_aclp = acl_args->acl_aclp;
656 		}
657 		break;
658 	case ACL_STRIP:
659 		error = acl_strip(file, st->st_uid, st->st_gid, st->st_mode);
660 		if (error) {
661 			errmsg(1, 1, "%s\n", acl_strerror(error));
662 			return (1);
663 		}
664 		acl_free(aclp);
665 		return (0);
666 		/*NOTREACHED*/
667 	default:
668 		errmsg(1, 0, gettext("Unknown ACL action requested\n"));
669 		return (1);
670 		break;
671 	}
672 
673 	error = acl_check(set_aclp, isdir);
674 
675 	if (error) {
676 		errmsg(1, 0, "%s\n%s", acl_strerror(error),
677 		    gettext("See chmod(1) for more information on "
678 		    "valid ACL syntax\n"));
679 		return (1);
680 	}
681 	if ((error = acl_set(file, set_aclp)) != 0) {
682 			errmsg(1, 0, gettext("Failed to set ACL: %s\n"),
683 			    acl_strerror(error));
684 			acl_free(aclp);
685 			return (1);
686 	}
687 	acl_free(aclp);
688 	return (0);
689 }
690