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