xref: /titanic_52/usr/src/tools/pmodes/pmodes.c (revision 3f7d54a6b84904c8f4d8daa4c7b577bede7df8b9)
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 /*
23  * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
24  * Use is subject to license terms.
25  *
26  * $Id: pmodes.c,v 1.23 1999/03/22 14:51:16 casper Exp $
27  *
28  *
29  * Program to list files from packages with modes that are to
30  * permissive.  Usage:
31  *
32  *	pmodes [options] pkgdir ...
33  *
34  * Pmodes currently has 4 types of modes that are changed:
35  *
36  *	m	remove group/other write permissions of all files,
37  *		except those in the exceptions list.
38  *	w	remove user write permission for executables that
39  *		are not root owned.
40  *	s	remove g/o read permission for set-uid/set-gid executables
41  *	o	change the owner of files/directories that can be safely
42  *		chowned to root.
43  *
44  *	Any combination of changes can be switched of by specifying -X
45  *
46  *	The -n option will create a "FILE.new" file for all changed
47  *	pkgmap/prototype files.
48  *	The -D option will limit changes to directories only.
49  *
50  * output:
51  *
52  * d m oldmode -> newmode pathname
53  * | ^ whether the file/dir is group writable or even world writable
54  * > type of file.
55  * d o owner -> newowner pathname [mode]
56  *
57  *
58  * Casper Dik (Casper.Dik@Holland.Sun.COM)
59  */
60 
61 #include <stdio.h>
62 #include <unistd.h>
63 #include <string.h>
64 #include <ctype.h>
65 #include <dirent.h>
66 #include <stdlib.h>
67 #include <errno.h>
68 #include <sys/param.h>
69 #include <sys/stat.h>
70 #include "binsearch.h"
71 
72 static char *exceptions[] = {
73 	"/etc/lp",
74 	"/var/cache/cups",
75 };
76 
77 static char *exempt_pkgs[] = {
78 	"SUNWSMSdf",	/* "data files" package for SMS */
79 	"SUNWSMSr",	/* "root" package for SMS */
80 	"SUNWSMSsu",	/* "user" package for SMS */
81 	"SUNWnethackr",	/* "root" package for nethack */
82 };
83 
84 #define	NEXEMPT	(sizeof (exempt_pkgs) / sizeof (char *))
85 
86 #define	PROTO "prototype_"
87 
88 #define	DEFAULT_SU 0
89 #define	DEFAULT_OWNER 1
90 #define	DEFAULT_MODES 1
91 #define	DEFAULT_USERWRITE 1
92 #define	DEFAULT_DIRSONLY 0
93 #define	DEFAULT_EDITABLE 1
94 
95 static int nexceptions = sizeof (exceptions)/sizeof (char *);
96 static int dosu = DEFAULT_SU;
97 static int doowner = DEFAULT_OWNER;
98 static int domodes = DEFAULT_MODES;
99 static int douserwrite = DEFAULT_USERWRITE;
100 static int dirsonly = DEFAULT_DIRSONLY;
101 static int editable = DEFAULT_EDITABLE;
102 static int makenew = 0;
103 static int installnew = 0;
104 static int diffout = 0;
105 static int proto = 0;
106 static int verbose = 0;
107 static int quiet = 0;
108 static int errors = 0;
109 
110 static void update_map(char *, char *, int);
111 
112 static char *program;
113 
114 itemlist restrictto = NULL;
115 
116 static void
117 usage(void) {
118 	(void) fprintf(stderr,
119 	    "Usage: %s [-DowsnNmdePvq] [-r file] pkgdir ...\n", program);
120 	exit(1);
121 }
122 
123 int
124 main(int argc, char **argv)
125 {
126 	char buf[8192];
127 	int c;
128 	extern int optind, opterr;
129 
130 	opterr = 0;
131 
132 	program = argv[0];
133 
134 	while ((c = getopt(argc, argv, "eDowsnNmdPvqr:")) != EOF) {
135 		switch (c) {
136 		case 's': dosu = !DEFAULT_SU; break;
137 		case 'o': doowner = !DEFAULT_OWNER; break;
138 		case 'm': domodes = !DEFAULT_MODES; break;
139 		case 'w': douserwrite = !DEFAULT_USERWRITE; break;
140 		case 'D': dirsonly = !DEFAULT_DIRSONLY; break;
141 		case 'e': editable = !DEFAULT_EDITABLE; break;
142 		case 'N': installnew = 1; /* FALLTHROUGH */
143 		case 'n': makenew = 1; break;
144 		case 'd': diffout = 1; break;
145 		case 'P': proto = 1; break;
146 		case 'v': verbose = 1; break;
147 		case 'q': quiet = 1; break;
148 		case 'r':
149 			if (restrictto == NULL)
150 				restrictto = new_itemlist();
151 			if (item_addfile(restrictto, optarg) != 0) {
152 				perror(optarg);
153 				exit(1);
154 			}
155 			break;
156 		default:
157 		case '?': usage(); break;
158 		}
159 	}
160 	argc -= optind;
161 	argv += optind;
162 
163 	if (argc < 1)
164 		usage();
165 
166 	for (; *argv; argv++) {
167 		FILE *info;
168 		char name[MAXPATHLEN];
169 		char basedir[MAXPATHLEN] = "/";
170 		int basedir_len;
171 		struct stat stb;
172 		int isfile = 0;
173 		boolean_t exempt = B_FALSE;
174 
175 		/*
176 		 * If a plain file is passed on the command line, we assume
177 		 * it's a prototype or pkgmap file and try to find the matching
178 		 * pkginfo file
179 		 */
180 		if (lstat(*argv, &stb) == 0 && S_ISREG(stb.st_mode)) {
181 			char *lastslash = strrchr(*argv, '/');
182 
183 			if (lastslash != NULL)
184 				*lastslash = '\0';
185 			(void) sprintf(name, "%s/pkginfo", *argv);
186 			if (lastslash != NULL)
187 				*lastslash = '/';
188 			isfile = 1;
189 		} else
190 			(void) sprintf(name, "%s/pkginfo", *argv);
191 
192 		/* if there's no pkginfo file, it could be a prototype area */
193 
194 		if (access(name, R_OK) != 0)
195 			(void) strcat(name, ".tmpl");
196 
197 		info = fopen(name, "r");
198 		if (info == 0) {
199 			if (!quiet)
200 				(void) fprintf(stderr,
201 				    "Can't open pkginfo file %s\n", name);
202 			continue;
203 		}
204 
205 		while (fgets(buf, sizeof (buf), info) != NULL && !exempt) {
206 			if (strncmp(buf, "BASEDIR=", 8) == 0) {
207 				(void) strcpy(basedir, buf+8);
208 				basedir[strlen(basedir)-1] = '\0';
209 			} else if (strncmp(buf, "PKG=", 4) == 0) {
210 				int i;
211 				char *str;
212 
213 				str = buf + sizeof ("PKG=") - 1;
214 				str[strlen(str)-1] = '\0';
215 				if (str[0] == '"')
216 					str++;
217 				if (str[strlen(str)-1] == '"')
218 					str[strlen(str)-1] = '\0';
219 				for (i = 0; i < NEXEMPT; i++) {
220 					if (strcmp(exempt_pkgs[i], str) == 0) {
221 						exempt = B_TRUE;
222 						break;
223 					}
224 				}
225 			}
226 		}
227 
228 		(void) fclose(info);
229 
230 		/* exempt package */
231 		if (exempt)
232 			continue;
233 
234 		basedir_len = strlen(basedir);
235 		if (basedir_len != 1)
236 			basedir[basedir_len++] = '/';
237 
238 		(void) sprintf(name, "%s/pkgmap", *argv);
239 		if (isfile)
240 			update_map(*argv, basedir, basedir_len);
241 		else if (!proto && access(name, R_OK) == 0)
242 			update_map(name, basedir, basedir_len);
243 		else {
244 			DIR *d = opendir(*argv);
245 			struct dirent *de;
246 
247 			if (d == NULL) {
248 				(void) fprintf(stderr,
249 					"Can't read directory \"%s\"\n", *argv);
250 				continue;
251 			}
252 			while (de = readdir(d)) {
253 				/* Skip files with .old or .new suffix */
254 				if (strstr(de->d_name, PROTO) != NULL &&
255 				    strncmp(de->d_name, ".del-", 5) != 0 &&
256 				    strstr(de->d_name, ".old") == NULL &&
257 				    strstr(de->d_name, ".new") == NULL) {
258 					(void) sprintf(name, "%s/%s", *argv,
259 					    de->d_name);
260 					update_map(name, basedir, basedir_len);
261 				}
262 			}
263 			(void) closedir(d);
264 		}
265 	}
266 	return (errors != 0);
267 }
268 
269 #define	NEXTWORD(tmp, end, warnme) \
270 	do { \
271 		tmp = strpbrk(tmp, "\t ");\
272 		if (!tmp) {\
273 			if (warnme)\
274 				warn(name, lineno);\
275 			return (LINE_IGNORE);\
276 		}\
277 		end = tmp++;\
278 		while (*tmp && isspace(*tmp)) tmp++;\
279 	} while (0)
280 
281 static void
282 warn(const char *file, int line)
283 {
284 	(void) fprintf(stderr, "pmodes: %s, line %d: unexpected format\n",
285 		file, line);
286 }
287 
288 struct parsed_line {
289 	char *start;		/* buffer start */
290 	char *rest;		/* buffer after owner */
291 	char *owner;		/* same size as ut_user */
292 	char *old_owner;	/* same size as ut_user */
293 	char group[16];		/* whatever */
294 	int  modelen;		/* number of mode bytes (3 or 4); */
295 	int mode;		/* the complete file mode */
296 	char path[MAXPATHLEN];	/* NUL terminated pathname */
297 	char type;		/* */
298 	char realtype;		/* */
299 };
300 
301 #define	LINE_OK		0
302 #define	LINE_IGNORE	1
303 #define	LINE_ERROR	2
304 
305 static void
306 put_line(FILE *f, struct parsed_line *line)
307 {
308 	if (f != NULL)
309 		if (line->rest)
310 			(void) fprintf(f, "%s%.*o %s %s", line->start,
311 			    line->modelen, line->mode, line->owner, line->rest);
312 		else
313 			(void) fputs(line->start, f);
314 }
315 
316 /*
317  * the first field is the path, the second the type, the
318  * third the class, the fourth the mode, when appropriate.
319  * We're interested in
320  *		f (file)
321  *		e (edited file)
322  *		v (volatile file)
323  *		d (directory)
324  *		c (character devices)
325  *		b (block devices)
326  */
327 
328 static int
329 parse_line(struct parsed_line *parse, char *buf, const char *name, int lineno)
330 {
331 	char *tmp;
332 	char *p = buf;
333 	char *end, *q;
334 
335 	parse->start = buf;
336 	parse->rest = 0;		/* makes put_line work */
337 
338 	/* Trim trailing spaces */
339 	end = buf + strlen(buf);
340 	while (end > buf+1 && isspace(end[-2])) {
341 		end -= 1;
342 		end[-1] = end[0];
343 		end[0] = '\0';
344 	}
345 
346 	while (*p && isspace(*p))
347 		p++;
348 
349 	if (*p == '#' || *p == ':' || *p == '\0')
350 		return (LINE_IGNORE);
351 
352 	/*
353 	 * Special directives; we really should follow the include
354 	 * directives but we certainly need to look at default
355 	 */
356 	if (*p == '!') {
357 		p++;
358 		while (*p && isspace(*p))
359 			p++;
360 
361 		if (!*p || *p == '\n')
362 			return (LINE_IGNORE);
363 
364 		if (strncmp(p, "default", 7) == 0) {
365 			NEXTWORD(p, end, 1);
366 			parse->type = 'f';
367 			parse->realtype = 'D';
368 			strcpy(parse->path, "(default)");
369 			tmp = p;
370 			NEXTWORD(p, end, 1);
371 			goto domode;
372 		} else if (strncmp(p, "include", 7) == 0) {
373 			NEXTWORD(p, end, 1);
374 			if (strstr(p, PROTO) == NULL)
375 				fprintf(stderr, "including file %s", p);
376 		}
377 		return (LINE_IGNORE);
378 	}
379 
380 	/*
381 	 * Parse the pkgmap line:
382 	 * [<number>] <type> <class> <path> [<major> <minor>]
383 	 * [ <mode> <owner> <group> .... ]
384 	 */
385 
386 	/* Skip first column for non-prototype (i.e., pkgmap) files */
387 	if (isdigit(*p))
388 		NEXTWORD(p, end, 1);
389 
390 	parse->realtype = parse->type = *p;
391 
392 	switch (parse->type) {
393 	case 'i': case 's': case 'l':
394 		return (LINE_IGNORE);
395 	}
396 
397 	NEXTWORD(p, end, 1);
398 
399 	/* skip class */
400 	NEXTWORD(p, end, 1);
401 
402 	/*
403 	 * p now points to pathname
404 	 * At this point, we could have no mode because we are
405 	 * using a default.
406 	 */
407 	tmp = p;
408 	NEXTWORD(p, end, 0);
409 
410 	/* end points to space after name */
411 	(void) strncpy(parse->path, tmp, end - tmp);
412 	parse->path[end - tmp] = '\0';
413 
414 	switch (parse->type) {
415 	case 'e':
416 	case 'v':
417 		/* type 'e' and 'v' are files, just like 'f', use 'f' in out */
418 		parse->type = 'f';
419 		/* FALLTHROUGH */
420 	case 'f':
421 	case 'd':
422 	case 'p': /* FIFO - assume mode is sensible, don't treat as file */
423 		break;
424 
425 	case 'x': /* Exclusive directory */
426 		parse->type = 'd';
427 		break;
428 
429 	/* device files have class major minor, skip */
430 	case 'c':
431 	case 'b':
432 		NEXTWORD(p, end, 1); NEXTWORD(p, end, 1);
433 		break;
434 
435 	default:
436 		(void) fprintf(stderr, "Unknown type '%c', %s:%d\n",
437 			    parse->type, name, lineno);
438 		return (LINE_ERROR);
439 	}
440 	tmp = p;
441 	NEXTWORD(p, end, 1);
442 
443 domode:
444 	/*
445 	 * the mode is either a 4 digit number (file is sticky/set-uid or
446 	 * set-gid or the mode has a leading 0) or a three digit number
447 	 * mode has all the mode bits, mode points to the three least
448 	 * significant bit so fthe mode
449 	 */
450 	parse->mode = 0;
451 	for (q = tmp; q < end; q++) {
452 		if (!isdigit(*q) || *q > '7') {
453 			(void) fprintf(stderr,
454 			    "Warning: Unparseble mode \"%.*s\" at %s:%d\n",
455 			    end-tmp, tmp, name, lineno);
456 			return (LINE_IGNORE);
457 		}
458 		parse->mode <<= 3;
459 		parse->mode += *q - '0';
460 	}
461 	parse->modelen = end - tmp;
462 	tmp[0] = '\0';
463 
464 	parse->old_owner = parse->owner = p;
465 
466 	NEXTWORD(p, end, 1);
467 
468 	parse->rest = end+1;
469 	*end = '\0';
470 
471 	(void) memset(parse->group, 0, sizeof (parse->group));
472 	(void) strncpy(parse->group, end+1, strcspn(end+1, " \t\n"));
473 
474 	return (LINE_OK);
475 }
476 
477 static void
478 update_map(char *name, char *basedir, int basedir_len)
479 {
480 	char buf[8192];
481 	int i;
482 	FILE *map, *newmap;
483 	char newname[MAXPATHLEN];
484 	int nchanges = 0;
485 	unsigned int lineno = 0;
486 	struct parsed_line line;
487 	char *fname;
488 
489 	map = fopen(name, "r");
490 	if (map == 0) {
491 		(void) fprintf(stderr, "Can't open \"%s\"\n", name);
492 		return;
493 	}
494 	(void) strcpy(newname, name);
495 	(void) strcat(newname, ".new");
496 	if (makenew) {
497 		newmap = fopen(newname, "w");
498 		if (newmap == 0)
499 			(void) fprintf(stderr, "Can't open %s for writing\n",
500 			    name);
501 	} else
502 		newmap = 0;
503 
504 	/* Get last one or two components non-trivial of pathname */
505 	if (verbose) {
506 		char *tmp = name + strlen(name);
507 		int cnt = 0, first = 0;
508 
509 		while (--tmp > name && cnt < 2) {
510 			if (*tmp == '/') {
511 				if (++cnt == 1)
512 					first = tmp - name;
513 				else  {
514 					fname = tmp + 1;
515 					/* Triviality check */
516 					if (tmp - name > first - 4)
517 						cnt--;
518 				}
519 			}
520 		}
521 		if (cnt < 2)
522 			fname = name;
523 	}
524 
525 	nchanges = 0;
526 
527 	for (; fgets(buf, sizeof (buf), map) != 0; put_line(newmap, &line)) {
528 
529 		int root_owner, mode_diff = 0;
530 		int changed = 0;
531 
532 		lineno ++;
533 
534 		switch (parse_line(&line, buf, name, lineno)) {
535 		case LINE_IGNORE:
536 			continue;
537 		case LINE_ERROR:
538 			errors++;
539 			continue;
540 		}
541 
542 		if (restrictto) {
543 			char nbuf[MAXPATHLEN];
544 			snprintf(nbuf, sizeof (nbuf), "%.*s%s", basedir_len,
545 			    basedir, line.path);
546 
547 			if (item_search(restrictto, nbuf) == -1)
548 				continue;
549 		}
550 
551 		if (dirsonly && line.type != 'd')
552 			continue;
553 
554 		root_owner = strcmp(line.owner, "root") == 0;
555 		if (dosu && line.type == 'f' && (line.mode & (S_ISUID|S_ISGID)))
556 			mode_diff = line.mode & (S_IRGRP|S_IROTH);
557 
558 		/*
559 		 * The following heuristics are used to determine whether a file
560 		 * can be safely chown'ed to root:
561 		 *	- it's not set-uid.
562 		 *	and one of the following applies:
563 		 *	    - it's not writable by the current owner and is
564 		 *	    group/world readable
565 		 *	    - it's world executable and a file
566 		 *	    - owner, group and world permissions are identical
567 		 *	    - it's a bin owned directory or a "non-volatile"
568 		 *	    file (any owner) for which group and other r-x
569 		 *	    permissions are identical, or it's a bin owned
570 		 *	    executable or it's a /etc/security/dev/ device
571 		 */
572 
573 		if (doowner && !(line.mode & S_ISUID) &&
574 		    !root_owner &&
575 		    ((!(line.mode & S_IWUSR) &&
576 			(line.mode&(S_IRGRP|S_IROTH)) == (S_IRGRP|S_IROTH)) ||
577 			(line.type == 'f' && (line.mode & S_IXOTH)) ||
578 			((line.mode & 07) == ((line.mode>>3) & 07) &&
579 			    (line.mode & 07) == ((line.mode>>6) & 07) &&
580 			    strcmp(line.owner, "uucp") != 0) ||
581 			((line.type == 'd' && strcmp(line.owner, "bin") == 0 ||
582 			    (editable && strcmp(line.owner, "bin") == 0  ?
583 				    line.type : line.realtype)  == 'f') &&
584 			    ((line.mode & 05) == ((line.mode>>3) & 05) ||
585 				(line.mode & 0100) &&
586 				strcmp(line.owner, "bin") == 0) &&
587 			    ((line.mode & 0105) != 0 ||
588 				basedir_len < 18 &&
589 				strncmp(basedir, "/etc/security/dev/",
590 				    basedir_len) == 0 &&
591 				strncmp(line.path, "/etc/security/dev/"
592 				    + basedir_len, 18 - basedir_len) == 0)))) {
593 			if (!diffout) {
594 				if (!changed && verbose && !nchanges)
595 					(void) printf("%s:\n", fname);
596 				(void) printf("%c o %s -> root %s%s [%.*o]\n",
597 					line.realtype, line.owner, basedir,
598 					line.path, line.modelen, line.mode);
599 			}
600 			line.owner = "root";
601 			root_owner = 1;
602 			changed = 1;
603 		}
604 		/*
605 		 * Strip user write bit if owner != root and executable by user.
606 		 * root can write even if no write bits set
607 		 * Could prevent  executables from being overwritten.
608 		 */
609 		if (douserwrite && line.type == 'f' && !root_owner &&
610 		    (line.mode & (S_IWUSR|S_IXUSR)) == (S_IWUSR|S_IXUSR))
611 			mode_diff |= S_IWUSR;
612 
613 
614 		if (domodes && (line.mode & (S_IWGRP|S_IWOTH)) != 0 &&
615 		    (line.mode & S_ISVTX) == 0) {
616 			if (basedir_len <= 1) { /* root dir */
617 				for (i = 0; i < nexceptions; i++) {
618 					if (strcmp(line.path,
619 					    exceptions[i]+basedir_len) == 0)
620 						break;
621 				}
622 			} else {
623 				for (i = 0; i < nexceptions; i++) {
624 					if (strncmp(basedir, exceptions[i],
625 						basedir_len) == 0 &&
626 					    strcmp(line.path,
627 						exceptions[i]+basedir_len) == 0)
628 						break;
629 				}
630 			}
631 			if (i == nexceptions)
632 				mode_diff |= line.mode & (S_IWGRP|S_IWOTH);
633 		}
634 
635 		if (mode_diff) {
636 			int oldmode = line.mode;
637 
638 			line.mode &= ~mode_diff;
639 
640 			if (line.mode != oldmode) {
641 				if (!diffout) {
642 					if (!changed && verbose && !nchanges)
643 						(void) printf("%s:\n", fname);
644 					printf("%c %c %04o -> %04o %s%s\n",
645 					    line.realtype,
646 					    (mode_diff & (S_IRGRP|S_IROTH)) ?
647 						's' : 'm',
648 						oldmode, line.mode, basedir,
649 						line.path);
650 				}
651 				changed = 1;
652 			}
653 		}
654 		nchanges += changed;
655 		if (diffout && changed) {
656 			if (nchanges == 1 && verbose)
657 				(void) printf("%s:\n", fname);
658 
659 			(void) printf("< %c %04o %s %s %s%s\n", line.realtype,
660 			    line.mode | mode_diff, line.old_owner, line.group,
661 			    basedir, line.path);
662 			(void) printf("> %c %04o %s %s %s%s\n", line.realtype,
663 			    line.mode, line.owner, line.group, basedir,
664 			    line.path);
665 		}
666 	}
667 	(void) fclose(map);
668 
669 	if (newmap != NULL) {
670 		(void) fflush(newmap);
671 		if (ferror(newmap)) {
672 			(void) fprintf(stderr, "Error writing %s\n", name);
673 			return;
674 		}
675 		(void) fclose(newmap);
676 		if (nchanges == 0)
677 			(void) unlink(newname);
678 		else if (installnew) {
679 			char oldname[MAXPATHLEN];
680 
681 			(void) strcpy(oldname, name);
682 			(void) strcat(oldname, ".old");
683 			if (rename(name, oldname) == -1 ||
684 			    rename(newname, name) == -1)
685 				(void) fprintf(stderr,
686 				    "Couldn't install %s: %s\n",
687 				    newname, strerror(errno));
688 		}
689 	}
690 }
691