xref: /illumos-gate/usr/src/cmd/chmod/chmod.c (revision 35b1ab9964f57b69ba8f03d2962f94036aa78c57)
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 2007 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 	if (mac >= 2 && (mav[0][0] == 'A')) {
153 		if (parse_acl_args(*mav, &acl_args)) {
154 			usage();
155 			exit(2);
156 		}
157 	} else {
158 		if (mac < 2) {
159 			usage();
160 			exit(2);
161 		}
162 	}
163 
164 	ms = mav[0];
165 
166 	umsk = umask(0);
167 	(void) umask(umsk);
168 
169 	for (i = 1; i < mac; i++) {
170 		status += dochmod(mav[i], mav[i], umsk, acl_args);
171 	}
172 
173 	return (fflag ? 0 : status);
174 }
175 
176 static int
177 dochmod(char *name, char *path, mode_t umsk, acl_args_t *aclp)
178 {
179 	static struct stat st;
180 	int linkflg = 0;
181 	o_mode_t	group_clear_bits, group_set_bits;
182 
183 	if (lstat(name, &st) < 0) {
184 		errmsg(2, 0, gettext("can't access %s\n"), path);
185 		return (1);
186 	}
187 
188 	if ((st.st_mode & S_IFMT) == S_IFLNK) {
189 		linkflg = 1;
190 		if (stat(name, &st) < 0) {
191 			errmsg(2, 0, gettext("can't access %s\n"), path);
192 			return (1);
193 		}
194 	}
195 
196 	/* Do not recurse if directory is object of symbolic link */
197 	if (rflag && ((st.st_mode & S_IFMT) == S_IFDIR) && !linkflg)
198 		return (chmodr(name, path, st.st_mode, umsk, aclp));
199 
200 	if (aclp) {
201 		return (doacl(name, &st, aclp));
202 	} else if (chmod(name, newmode_common(ms, st.st_mode, umsk, name, path,
203 	    &group_clear_bits, &group_set_bits)) == -1) {
204 		errmsg(2, 0, gettext("can't change %s\n"), path);
205 		return (1);
206 	}
207 
208 	/*
209 	 * If the group permissions of the file are being modified,
210 	 * make sure that the file's ACL (if it has one) is
211 	 * modified also, since chmod is supposed to apply group
212 	 * permissions changes to both the acl mask and the
213 	 * general group permissions.
214 	 */
215 	if (group_clear_bits || group_set_bits)
216 		handle_acl(name, group_clear_bits, group_set_bits);
217 
218 	return (0);
219 }
220 
221 
222 static int
223 chmodr(char *dir, char *path,  mode_t mode, mode_t umsk, acl_args_t *aclp)
224 {
225 
226 	DIR *dirp;
227 	struct dirent *dp;
228 	char savedir[PATH_MAX];			/* dir name to restore */
229 	char currdir[PATH_MAX+1];		/* current dir name + '/' */
230 	char parentdir[PATH_MAX+1];		/* parent dir name  + '/' */
231 	int ecode;
232 	struct stat st;
233 	o_mode_t	group_clear_bits, group_set_bits;
234 
235 	if (getcwd(savedir, PATH_MAX) == 0)
236 		errmsg(2, 255, gettext("chmod: could not getcwd %s\n"),
237 		    savedir);
238 
239 	/*
240 	 * Change what we are given before doing it's contents
241 	 */
242 	if (aclp) {
243 		if (lstat(dir, &st) < 0) {
244 			errmsg(2, 0, gettext("can't access %s\n"), path);
245 			return (1);
246 		}
247 		if (doacl(dir, &st, aclp) != 0)
248 			return (1);
249 	} else if (chmod(dir, newmode_common(ms, mode, umsk, dir, path,
250 	    &group_clear_bits, &group_set_bits)) < 0) {
251 		errmsg(2, 0, gettext("can't change %s\n"), path);
252 		return (1);
253 	}
254 
255 	/*
256 	 * If the group permissions of the file are being modified,
257 	 * make sure that the file's ACL (if it has one) is
258 	 * modified also, since chmod is supposed to apply group
259 	 * permissions changes to both the acl mask and the
260 	 * general group permissions.
261 	 */
262 
263 	if (aclp == NULL) { /* only necessary when not setting ACL */
264 		if (group_clear_bits || group_set_bits)
265 			handle_acl(dir, group_clear_bits, group_set_bits);
266 	}
267 
268 	if (chdir(dir) < 0) {
269 		errmsg(2, 0, "%s/%s: %s\n", savedir, dir, strerror(errno));
270 		return (1);
271 	}
272 	if ((dirp = opendir(".")) == NULL) {
273 		errmsg(2, 0, "%s\n", strerror(errno));
274 		return (1);
275 	}
276 	ecode = 0;
277 
278 	/*
279 	 * Save parent directory path before recursive chmod.
280 	 * We'll need this for error printing purposes. Add
281 	 * a trailing '/' to the path except in the case where
282 	 * the path is just '/'
283 	 */
284 
285 	if (strlcpy(parentdir, path, PATH_MAX + 1) >= PATH_MAX + 1) {
286 		errmsg(2, 0, gettext("directory path name too long: %s\n"),
287 		    path);
288 		return (1);
289 	}
290 	if (strcmp(path, "/") != 0)
291 		if (strlcat(parentdir, "/", PATH_MAX + 1) >= PATH_MAX + 1) {
292 			errmsg(2, 0,
293 			    gettext("directory path name too long: %s/\n"),
294 			    parentdir);
295 			return (1);
296 		}
297 
298 
299 	for (dp = readdir(dirp); dp != NULL; dp = readdir(dirp))  {
300 
301 		if (strcmp(dp->d_name, ".") == 0 ||	/* skip . and .. */
302 		    strcmp(dp->d_name, "..") == 0) {
303 			continue;
304 		}
305 		if (strlcpy(currdir, parentdir, PATH_MAX + 1) >= PATH_MAX + 1) {
306 			errmsg(2, 0,
307 			    gettext("directory path name too long: %s\n"),
308 			    parentdir);
309 			return (1);
310 		}
311 		if (strlcat(currdir, dp->d_name, PATH_MAX + 1)
312 		    >= PATH_MAX + 1) {
313 			errmsg(2, 0,
314 			    gettext("directory path name too long: %s%s\n"),
315 			    currdir, dp->d_name);
316 			return (1);
317 		}
318 		ecode += dochmod(dp->d_name, currdir, umsk, aclp);
319 	}
320 	(void) closedir(dirp);
321 	if (chdir(savedir) < 0) {
322 		errmsg(2, 255, gettext("can't change back to %s\n"), savedir);
323 	}
324 	return (ecode ? 1 : 0);
325 }
326 
327 /* PRINTFLIKE3 */
328 void
329 errmsg(int severity, int code, char *format, ...)
330 {
331 	va_list ap;
332 	static char *msg[] = {
333 	"",
334 	"ERROR",
335 	"WARNING",
336 	""
337 	};
338 
339 	va_start(ap, format);
340 
341 	/*
342 	 * Always print error message if this is a fatal error (code == 0);
343 	 * otherwise, print message if fflag == 0 (no -f option specified)
344 	 */
345 	if (!fflag || (code != 0)) {
346 		(void) fprintf(stderr,
347 			"chmod: %s: ", gettext(msg[severity]));
348 		(void) vfprintf(stderr, format, ap);
349 	}
350 
351 	va_end(ap);
352 
353 	if (code != 0)
354 		exit(fflag ? 0 : code);
355 }
356 
357 static void
358 usage(void)
359 {
360 	(void) fprintf(stderr, gettext(
361 	    "usage:\tchmod [-fR] <absolute-mode> file ...\n"));
362 
363 	(void) fprintf(stderr, gettext(
364 	    "\tchmod [-fR] <ACL-operation> file ...\n"));
365 
366 	(void) fprintf(stderr, gettext(
367 	    "\tchmod [-fR] <symbolic-mode-list> file ...\n"));
368 
369 
370 	(void) fprintf(stderr, gettext(
371 	    "where \t<symbolic-mode-list> is a comma-separated list of\n"));
372 
373 	(void) fprintf(stderr, gettext(
374 	    "\t[ugoa]{+|-|=}[rwxXlstugo]\n"));
375 
376 	(void) fprintf(stderr, gettext(
377 	    "where \t<ACL-operation> is one of the following\n"));
378 	(void) fprintf(stderr, gettext("\tA-<acl_specification>\n"));
379 	(void) fprintf(stderr, gettext("\tA[number]-\n"));
380 	(void) fprintf(stderr, gettext(
381 	    "\tA[number]{+|=}<acl_specification>\n"));
382 	(void) fprintf(stderr, gettext(
383 	    "where \t<acl-specification> is a comma-separated list of ACEs\n"));
384 }
385 
386 /*
387  *  parseargs - generate getopt-friendly argument list for backwards
388  *		compatibility with earlier Solaris usage (eg, chmod -w
389  *		foo).
390  *
391  *  assumes the existence of a static set of alternates to argc and argv,
392  *  (namely, mac, and mav[]).
393  *
394  */
395 
396 static void
397 parseargs(int ac, char *av[])
398 {
399 	int i;			/* current argument			*/
400 	int fflag;		/* arg list contains "--"		*/
401 	size_t mav_num;		/* number of entries in mav[]		*/
402 
403 	/*
404 	 * We add an extra argument slot, in case we need to jam a "--"
405 	 * argument into the list.
406 	 */
407 
408 	mav_num = (size_t)ac+2;
409 
410 	if ((mav = calloc(mav_num, sizeof (char *))) == NULL) {
411 		perror("chmod");
412 		exit(2);
413 	}
414 
415 	/* scan for the use of "--" in the argument list */
416 
417 	for (fflag = i = 0; i < ac; i ++) {
418 		if (strcmp(av[i], "--") == 0)
419 		    fflag = 1;
420 	}
421 
422 	/* process the arguments */
423 
424 	for (i = mac = 0;
425 	    (av[i] != (char *)NULL) && (av[i][0] != (char)NULL);
426 	    i++) {
427 		if (!fflag && av[i][0] == '-') {
428 			/*
429 			 *  If there is not already a "--" argument specified,
430 			 *  and the argument starts with '-' but does not
431 			 *  contain any of the official option letters, then it
432 			 *  is probably a mode argument beginning with '-'.
433 			 *  Force a "--" into the argument stream in front of
434 			 *  it.
435 			 */
436 
437 			if ((strchr(av[i], 'R') == NULL &&
438 			    strchr(av[i], 'f') == NULL)) {
439 				mav[mac++] = strdup("--");
440 			}
441 		}
442 
443 		mav[mac++] = strdup(av[i]);
444 	}
445 
446 	mav[mac] = (char *)NULL;
447 }
448 
449 int
450 parse_acl_args(char *arg, acl_args_t **acl_args)
451 {
452 	acl_t *new_acl = NULL;
453 	int slot;
454 	int len;
455 	int action;
456 	acl_args_t *new_acl_args;
457 	char *acl_spec = NULL;
458 	char *end;
459 
460 	if (arg[0] != 'A')
461 		return (1);
462 
463 	slot = strtol(&arg[1], &end, 10);
464 
465 	len = strlen(arg);
466 	switch (*end) {
467 	case '+':
468 		action = ACL_ADD;
469 		acl_spec = ++end;
470 		break;
471 	case '-':
472 		if (len == 2 && arg[0] == 'A' && arg[1] == '-')
473 			action = ACL_STRIP;
474 		else
475 			action = ACL_DELETE;
476 		if (action != ACL_STRIP) {
477 			acl_spec = ++end;
478 			if (acl_spec[0] == '\0') {
479 				action = ACL_SLOT_DELETE;
480 				acl_spec = NULL;
481 			} else if (arg[1] != '-')
482 				return (1);
483 		}
484 		break;
485 	case '=':
486 		/*
487 		 * Was slot specified?
488 		 */
489 		if (arg[1] == '=')
490 			slot = -1;
491 		action = ACL_REPLACE;
492 		acl_spec = ++end;
493 		break;
494 	default:
495 		return (1);
496 	}
497 
498 	if ((action == ACL_REPLACE || action == ACL_ADD) && acl_spec[0] == '\0')
499 		return (1);
500 
501 	if (acl_spec) {
502 		if (acl_parse(acl_spec, &new_acl)) {
503 			exit(1);
504 		}
505 	}
506 
507 	new_acl_args = malloc(sizeof (acl_args_t));
508 	if (new_acl_args == NULL)
509 		return (1);
510 
511 	new_acl_args->acl_aclp = new_acl;
512 	new_acl_args->acl_slot = slot;
513 	new_acl_args->acl_action = action;
514 
515 	*acl_args = new_acl_args;
516 
517 	return (0);
518 }
519 
520 /*
521  * This function is called whenever the group permissions of a file
522  * is being modified.  According to the chmod(1) manpage, any
523  * change made to the group permissions must be applied to both
524  * the acl mask and the acl's GROUP_OBJ.  The chmod(2) already
525  * set the mask, so this routine needs to make the same change
526  * to the GROUP_OBJ.
527  */
528 static void
529 handle_acl(char *name, o_mode_t group_clear_bits, o_mode_t group_set_bits)
530 {
531 	int aclcnt, n;
532 	aclent_t *aclp, *tp;
533 	o_mode_t newperm;
534 	/*
535 	 * if this file system support ace_t acl's
536 	 * then simply return since we don't have an
537 	 * acl mask to deal with
538 	 */
539 	if (pathconf(name, _PC_ACL_ENABLED) == _ACL_ACE_ENABLED)
540 		return;
541 	if ((aclcnt = acl(name, GETACLCNT, 0, NULL)) <= MIN_ACL_ENTRIES)
542 		return;	/* it's just a trivial acl; no need to change it */
543 	if ((aclp = (aclent_t *)malloc((sizeof (aclent_t)) * aclcnt))
544 	    == NULL) {
545 		perror("chmod");
546 		exit(2);
547 	}
548 
549 	if (acl(name, GETACL, aclcnt, aclp) < 0) {
550 		free(aclp);
551 		(void) fprintf(stderr, "chmod: ");
552 		perror(name);
553 		return;
554 	}
555 	for (tp = aclp, n = aclcnt; n--; tp++) {
556 		if (tp->a_type == GROUP_OBJ) {
557 			newperm = tp->a_perm;
558 			if (group_clear_bits != 0)
559 				newperm &= ~group_clear_bits;
560 			if (group_set_bits != 0)
561 				newperm |= group_set_bits;
562 			if (newperm != tp->a_perm) {
563 				tp->a_perm = newperm;
564 				if (acl(name, SETACL, aclcnt, aclp)
565 				    < 0) {
566 					(void) fprintf(stderr, "chmod: ");
567 					perror(name);
568 				}
569 			}
570 			break;
571 		}
572 	}
573 	free(aclp);
574 }
575 
576 static int
577 doacl(char *file, struct stat *st, acl_args_t *acl_args)
578 {
579 	acl_t *aclp;
580 	acl_t *set_aclp;
581 	int error = 0;
582 	void *to, *from;
583 	int len;
584 	int isdir;
585 	isdir = S_ISDIR(st->st_mode);
586 
587 	error = acl_get(file, 0, &aclp);
588 
589 	if (error != 0) {
590 		errmsg(1, 1, "%s\n", acl_strerror(error));
591 		return (1);
592 	}
593 	switch (acl_args->acl_action) {
594 	case ACL_ADD:
595 		if ((error = acl_addentries(aclp,
596 			acl_args->acl_aclp, acl_args->acl_slot)) != 0) {
597 				errmsg(1, 1, "%s\n", acl_strerror(error));
598 				acl_free(aclp);
599 				return (1);
600 		}
601 		set_aclp = aclp;
602 		break;
603 	case ACL_SLOT_DELETE:
604 		if (acl_args->acl_slot + 1 > aclp->acl_cnt) {
605 			errmsg(1, 1,
606 			    gettext("Invalid slot specified for removal\n"));
607 			acl_free(aclp);
608 			return (1);
609 		}
610 
611 		if (acl_args->acl_slot == 0 && aclp->acl_cnt == 1) {
612 			errmsg(1, 1,
613 			    gettext("Can't remove all ACL "
614 			    "entries from a file\n"));
615 			acl_free(aclp);
616 			return (1);
617 		}
618 
619 		/*
620 		 * remove a single entry
621 		 *
622 		 * if last entry just adjust acl_cnt
623 		 */
624 
625 		if ((acl_args->acl_slot + 1) == aclp->acl_cnt)
626 			aclp->acl_cnt--;
627 		else {
628 			to = (char *)aclp->acl_aclp +
629 			    (acl_args->acl_slot * aclp->acl_entry_size);
630 			from = (char *)to + aclp->acl_entry_size;
631 			len = (aclp->acl_cnt - acl_args->acl_slot - 1) *
632 			    aclp->acl_entry_size;
633 			(void) memmove(to, from, len);
634 			aclp->acl_cnt--;
635 		}
636 		set_aclp = aclp;
637 		break;
638 
639 	case ACL_DELETE:
640 		if ((error = acl_removeentries(aclp, acl_args->acl_aclp,
641 		    acl_args->acl_slot, ACL_REMOVE_ALL)) != 0) {
642 			errmsg(1, 1, "%s\n", acl_strerror(error));
643 			acl_free(aclp);
644 			return (1);
645 		}
646 
647 		if (aclp->acl_cnt == 0) {
648 			errmsg(1, 1,
649 			    gettext("Can't remove all ACL "
650 			    "entries from a file\n"));
651 			acl_free(aclp);
652 			return (1);
653 		}
654 
655 		set_aclp = aclp;
656 		break;
657 	case ACL_REPLACE:
658 		if (acl_args->acl_slot >= 0)  {
659 			error = acl_modifyentries(aclp, acl_args->acl_aclp,
660 			    acl_args->acl_slot);
661 			if (error) {
662 				errmsg(1, 1, "%s\n", acl_strerror(error));
663 				acl_free(aclp);
664 				return (1);
665 			}
666 			set_aclp = aclp;
667 		} else {
668 			set_aclp = acl_args->acl_aclp;
669 		}
670 		break;
671 	case ACL_STRIP:
672 		error = acl_strip(file, st->st_uid, st->st_gid, st->st_mode);
673 		if (error) {
674 			errmsg(1, 1, "%s\n", acl_strerror(error));
675 			return (1);
676 		}
677 		acl_free(aclp);
678 		return (0);
679 		/*NOTREACHED*/
680 	default:
681 		errmsg(1, 0, gettext("Unknown ACL action requested\n"));
682 		return (1);
683 		break;
684 	}
685 	error = acl_check(set_aclp, isdir);
686 
687 	if (error) {
688 		errmsg(1, 0, "%s\n%s", acl_strerror(error),
689 		    gettext("See chmod(1) for more information on "
690 		    "valid ACL syntax\n"));
691 		return (1);
692 	}
693 	if ((error = acl_set(file, set_aclp)) != 0) {
694 			errmsg(1, 0, gettext("Failed to set ACL: %s\n"),
695 			    acl_strerror(error));
696 			acl_free(aclp);
697 			return (1);
698 	}
699 	acl_free(aclp);
700 	return (0);
701 }
702