/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. * * $Id: pmodes.c,v 1.23 1999/03/22 14:51:16 casper Exp $ * * * Program to list files from packages with modes that are to * permissive. Usage: * * pmodes [options] pkgdir ... * * Pmodes currently has 4 types of modes that are changed: * * m remove group/other write permissions of all files, * except those in the exceptions list. * w remove user write permission for executables that * are not root owned. * s remove g/o read permission for set-uid/set-gid executables * o change the owner of files/directories that can be safely * chowned to root. * * Any combination of changes can be switched of by specifying -X * * The -n option will create a "FILE.new" file for all changed * pkgmap/prototype files. * The -D option will limit changes to directories only. * * output: * * d m oldmode -> newmode pathname * | ^ whether the file/dir is group writable or even world writable * > type of file. * d o owner -> newowner pathname [mode] * * * Casper Dik (Casper.Dik@Holland.Sun.COM) */ #pragma ident "%Z%%M% %I% %E% SMI" #include #include #include #include #include #include #include #include #include #include "binsearch.h" static char *exceptions[] = { #include "exceptions.h" }; static char *exempt_pkgs[] = { "SUNWSMSdf", /* "data files" package for SMS */ "SUNWSMSr", /* "root" package for SMS */ "SUNWSMSsu", /* "user" package for SMS */ }; #define NEXEMPT (sizeof (exempt_pkgs) / sizeof (char *)) #define PROTO "prototype_" #define DEFAULT_SU 0 #define DEFAULT_OWNER 1 #define DEFAULT_MODES 1 #define DEFAULT_USERWRITE 1 #define DEFAULT_DIRSONLY 0 #define DEFAULT_EDITABLE 1 static int nexceptions = sizeof (exceptions)/sizeof (char *); static int dosu = DEFAULT_SU; static int doowner = DEFAULT_OWNER; static int domodes = DEFAULT_MODES; static int douserwrite = DEFAULT_USERWRITE; static int dirsonly = DEFAULT_DIRSONLY; static int editable = DEFAULT_EDITABLE; static int makenew = 0; static int installnew = 0; static int diffout = 0; static int proto = 0; static int verbose = 0; static int quiet = 0; static int errors = 0; static void update_map(char *, char *, int); static char *program; itemlist restrictto = NULL; static void usage(void) { (void) fprintf(stderr, "Usage: %s [-DowsnNmdePvq] [-r file] pkgdir ...\n", program); exit(1); } int main(int argc, char **argv) { char buf[8192]; int c; extern int optind, opterr; opterr = 0; program = argv[0]; while ((c = getopt(argc, argv, "eDowsnNmdPvqr:")) != EOF) { switch (c) { case 's': dosu = !DEFAULT_SU; break; case 'o': doowner = !DEFAULT_OWNER; break; case 'm': domodes = !DEFAULT_MODES; break; case 'w': douserwrite = !DEFAULT_USERWRITE; break; case 'D': dirsonly = !DEFAULT_DIRSONLY; break; case 'e': editable = !DEFAULT_EDITABLE; break; case 'N': installnew = 1; /* FALLTHROUGH */ case 'n': makenew = 1; break; case 'd': diffout = 1; break; case 'P': proto = 1; break; case 'v': verbose = 1; break; case 'q': quiet = 1; break; case 'r': if (restrictto == NULL) restrictto = new_itemlist(); if (item_addfile(restrictto, optarg) != 0) { perror(optarg); exit(1); } break; default: case '?': usage(); break; } } argc -= optind; argv += optind; if (argc < 1) usage(); for (; *argv; argv++) { FILE *info; char name[MAXPATHLEN]; char basedir[MAXPATHLEN] = "/"; int basedir_len; struct stat stb; int isfile = 0; boolean_t exempt = B_FALSE; /* * If a plain file is passed on the command line, we assume * it's a prototype or pkgmap file and try to find the matching * pkginfo file */ if (lstat(*argv, &stb) == 0 && S_ISREG(stb.st_mode)) { char *lastslash = strrchr(*argv, '/'); if (lastslash != NULL) *lastslash = '\0'; (void) sprintf(name, "%s/pkginfo", *argv); if (lastslash != NULL) *lastslash = '/'; isfile = 1; } else (void) sprintf(name, "%s/pkginfo", *argv); /* if there's no pkginfo file, it could be a prototype area */ if (access(name, R_OK) != 0) (void) strcat(name, ".tmpl"); info = fopen(name, "r"); if (info == 0) { if (!quiet) (void) fprintf(stderr, "Can't open pkginfo file %s\n", name); continue; } while (fgets(buf, sizeof (buf), info) != NULL && !exempt) { if (strncmp(buf, "BASEDIR=", 8) == 0) { (void) strcpy(basedir, buf+8); basedir[strlen(basedir)-1] = '\0'; } else if (strncmp(buf, "PKG=", 4) == 0) { int i; char *str; str = buf + sizeof ("PKG=") - 1; str[strlen(str)-1] = '\0'; for (i = 0; i < NEXEMPT; i++) { if (strcmp(exempt_pkgs[i], str) == 0) { exempt = B_TRUE; break; } } } } (void) fclose(info); /* exempt package */ if (exempt) continue; basedir_len = strlen(basedir); if (basedir_len != 1) basedir[basedir_len++] = '/'; (void) sprintf(name, "%s/pkgmap", *argv); if (isfile) update_map(*argv, basedir, basedir_len); else if (!proto && access(name, R_OK) == 0) update_map(name, basedir, basedir_len); else { DIR *d = opendir(*argv); struct dirent *de; if (d == NULL) { (void) fprintf(stderr, "Can't read directory \"%s\"\n", *argv); continue; } while (de = readdir(d)) { /* Skip files with .old or .new suffix */ if (strstr(de->d_name, PROTO) != NULL && strncmp(de->d_name, ".del-", 5) != 0 && strstr(de->d_name, ".old") == NULL && strstr(de->d_name, ".new") == NULL) { (void) sprintf(name, "%s/%s", *argv, de->d_name); update_map(name, basedir, basedir_len); } } (void) closedir(d); } } return (errors != 0); } #define NEXTWORD(tmp, end, warnme) \ do { \ tmp = strpbrk(tmp, "\t ");\ if (!tmp) {\ if (warnme)\ warn(name, lineno);\ return (LINE_IGNORE);\ }\ end = tmp++;\ while (*tmp && isspace(*tmp)) tmp++;\ } while (0) static void warn(const char *file, int line) { (void) fprintf(stderr, "pmodes: %s, line %d: unexpected format\n", file, line); } struct parsed_line { char *start; /* buffer start */ char *rest; /* buffer after owner */ char *owner; /* same size as ut_user */ char *old_owner; /* same size as ut_user */ char group[16]; /* whatever */ int modelen; /* number of mode bytes (3 or 4); */ int mode; /* the complete file mode */ char path[MAXPATHLEN]; /* NUL terminated pathname */ char type; /* */ char realtype; /* */ }; #define LINE_OK 0 #define LINE_IGNORE 1 #define LINE_ERROR 2 static void put_line(FILE *f, struct parsed_line *line) { if (f != NULL) if (line->rest) (void) fprintf(f, "%s%.*o %s %s", line->start, line->modelen, line->mode, line->owner, line->rest); else (void) fputs(line->start, f); } /* * the first field is the path, the second the type, the * third the class, the fourth the mode, when appropriate. * We're interested in * f (file) * e (edited file) * v (volatile file) * d (directory) * c (character devices) * b (block devices) */ static int parse_line(struct parsed_line *parse, char *buf, const char *name, int lineno) { char *tmp; char *p = buf; char *end, *q; parse->start = buf; parse->rest = 0; /* makes put_line work */ /* Trim trailing spaces */ end = buf + strlen(buf); while (end > buf+1 && isspace(end[-2])) { end -= 1; end[-1] = end[0]; end[0] = '\0'; } while (*p && isspace(*p)) p++; if (*p == '#' || *p == ':' || *p == '\0') return (LINE_IGNORE); /* * Special directives; we really should follow the include * directives but we certainly need to look at default */ if (*p == '!') { p++; while (*p && isspace(*p)) p++; if (!*p || *p == '\n') return (LINE_IGNORE); if (strncmp(p, "default", 7) == 0) { NEXTWORD(p, end, 1); parse->type = 'f'; parse->realtype = 'D'; strcpy(parse->path, "(default)"); tmp = p; NEXTWORD(p, end, 1); goto domode; } else if (strncmp(p, "include", 7) == 0) { NEXTWORD(p, end, 1); if (strstr(p, PROTO) == NULL) fprintf(stderr, "including file %s", p); } return (LINE_IGNORE); } /* * Parse the pkgmap line: * [] [ ] * [ .... ] */ /* Skip first column for non-prototype (i.e., pkgmap) files */ if (isdigit(*p)) NEXTWORD(p, end, 1); parse->realtype = parse->type = *p; switch (parse->type) { case 'i': case 's': case 'l': return (LINE_IGNORE); } NEXTWORD(p, end, 1); /* skip class */ NEXTWORD(p, end, 1); /* * p now points to pathname * At this point, we could have no mode because we are * using a default. */ tmp = p; NEXTWORD(p, end, 0); /* end points to space after name */ (void) strncpy(parse->path, tmp, end - tmp); parse->path[end - tmp] = '\0'; switch (parse->type) { case 'e': case 'v': /* type 'e' and 'v' are files, just like 'f', use 'f' in out */ parse->type = 'f'; /* FALLTHROUGH */ case 'f': case 'd': case 'p': /* FIFO - assume mode is sensible, don't treat as file */ break; case 'x': /* Exclusive directory */ parse->type = 'd'; break; /* device files have class major minor, skip */ case 'c': case 'b': NEXTWORD(p, end, 1); NEXTWORD(p, end, 1); break; default: (void) fprintf(stderr, "Unknown type '%c', %s:%d\n", parse->type, name, lineno); return (LINE_ERROR); } tmp = p; NEXTWORD(p, end, 1); domode: /* * the mode is either a 4 digit number (file is sticky/set-uid or * set-gid or the mode has a leading 0) or a three digit number * mode has all the mode bits, mode points to the three least * significant bit so fthe mode */ parse->mode = 0; for (q = tmp; q < end; q++) { if (!isdigit(*q) || *q > '7') { (void) fprintf(stderr, "Warning: Unparseble mode \"%.*s\" at %s:%d\n", end-tmp, tmp, name, lineno); return (LINE_IGNORE); } parse->mode <<= 3; parse->mode += *q - '0'; } parse->modelen = end - tmp; tmp[0] = '\0'; parse->old_owner = parse->owner = p; NEXTWORD(p, end, 1); parse->rest = end+1; *end = '\0'; (void) memset(parse->group, 0, sizeof (parse->group)); (void) strncpy(parse->group, end+1, strcspn(end+1, " \t\n")); return (LINE_OK); } static void update_map(char *name, char *basedir, int basedir_len) { char buf[8192]; int i; FILE *map, *newmap; char newname[MAXPATHLEN]; int nchanges = 0; unsigned int lineno = 0; struct parsed_line line; char *fname; map = fopen(name, "r"); if (map == 0) { (void) fprintf(stderr, "Can't open \"%s\"\n", name); return; } (void) strcpy(newname, name); (void) strcat(newname, ".new"); if (makenew) { newmap = fopen(newname, "w"); if (newmap == 0) (void) fprintf(stderr, "Can't open %s for writing\n", name); } else newmap = 0; /* Get last one or two components non-trivial of pathname */ if (verbose) { char *tmp = name + strlen(name); int cnt = 0, first = 0; while (--tmp > name && cnt < 2) { if (*tmp == '/') { if (++cnt == 1) first = tmp - name; else { fname = tmp + 1; /* Triviality check */ if (tmp - name > first - 4) cnt--; } } } if (cnt < 2) fname = name; } nchanges = 0; for (; fgets(buf, sizeof (buf), map) != 0; put_line(newmap, &line)) { int root_owner, mode_diff = 0; int changed = 0; lineno ++; switch (parse_line(&line, buf, name, lineno)) { case LINE_IGNORE: continue; case LINE_ERROR: errors++; continue; } if (restrictto) { char nbuf[MAXPATHLEN]; snprintf(nbuf, sizeof (nbuf), "%.*s%s", basedir_len, basedir, line.path); if (item_search(restrictto, nbuf) == -1) continue; } if (dirsonly && line.type != 'd') continue; root_owner = strcmp(line.owner, "root") == 0; if (dosu && line.type == 'f' && (line.mode & (S_ISUID|S_ISGID))) mode_diff = line.mode & (S_IRGRP|S_IROTH); /* * The following heuristics are used to determine whether a file * can be safely chown'ed to root: * - it's not set-uid. * and one of the following applies: * - it's not writable by the current owner and is * group/world readable * - it's world executable and a file * - owner, group and world permissions are identical * - it's a bin owned directory or a "non-volatile" * file (any owner) for which group and other r-x * permissions are identical, or it's a bin owned * executable or it's a /etc/security/dev/ device */ if (doowner && !(line.mode & S_ISUID) && !root_owner && ((!(line.mode & S_IWUSR) && (line.mode&(S_IRGRP|S_IROTH)) == (S_IRGRP|S_IROTH)) || (line.type == 'f' && (line.mode & S_IXOTH)) || ((line.mode & 07) == ((line.mode>>3) & 07) && (line.mode & 07) == ((line.mode>>6) & 07) && strcmp(line.owner, "uucp") != 0) || ((line.type == 'd' && strcmp(line.owner, "bin") == 0 || (editable && strcmp(line.owner, "bin") == 0 ? line.type : line.realtype) == 'f') && ((line.mode & 05) == ((line.mode>>3) & 05) || (line.mode & 0100) && strcmp(line.owner, "bin") == 0) && ((line.mode & 0105) != 0 || basedir_len < 18 && strncmp(basedir, "/etc/security/dev/", basedir_len) == 0 && strncmp(line.path, "/etc/security/dev/" + basedir_len, 18 - basedir_len) == 0)))) { if (!diffout) { if (!changed && verbose && !nchanges) (void) printf("%s:\n", fname); (void) printf("%c o %s -> root %s%s [%.*o]\n", line.realtype, line.owner, basedir, line.path, line.modelen, line.mode); } line.owner = "root"; root_owner = 1; changed = 1; } /* * Strip user write bit if owner != root and executable by user. * root can write even if no write bits set * Could prevent executables from being overwritten. */ if (douserwrite && line.type == 'f' && !root_owner && (line.mode & (S_IWUSR|S_IXUSR)) == (S_IWUSR|S_IXUSR)) mode_diff |= S_IWUSR; if (domodes && (line.mode & (S_IWGRP|S_IWOTH)) != 0 && (line.mode & S_ISVTX) == 0) { if (basedir_len <= 1) { /* root dir */ for (i = 0; i < nexceptions; i++) { if (strcmp(line.path, exceptions[i]+basedir_len) == 0) break; } } else { for (i = 0; i < nexceptions; i++) { if (strncmp(basedir, exceptions[i], basedir_len) == 0 && strcmp(line.path, exceptions[i]+basedir_len) == 0) break; } } if (i == nexceptions) mode_diff |= line.mode & (S_IWGRP|S_IWOTH); } if (mode_diff) { int oldmode = line.mode; line.mode &= ~mode_diff; if (line.mode != oldmode) { if (!diffout) { if (!changed && verbose && !nchanges) (void) printf("%s:\n", fname); printf("%c %c %04o -> %04o %s%s\n", line.realtype, (mode_diff & (S_IRGRP|S_IROTH)) ? 's' : 'm', oldmode, line.mode, basedir, line.path); } changed = 1; } } nchanges += changed; if (diffout && changed) { if (nchanges == 1 && verbose) (void) printf("%s:\n", fname); (void) printf("< %c %04o %s %s %s%s\n", line.realtype, line.mode | mode_diff, line.old_owner, line.group, basedir, line.path); (void) printf("> %c %04o %s %s %s%s\n", line.realtype, line.mode, line.owner, line.group, basedir, line.path); } } (void) fclose(map); if (newmap != NULL) { (void) fflush(newmap); if (ferror(newmap)) { (void) fprintf(stderr, "Error writing %s\n", name); return; } (void) fclose(newmap); if (nchanges == 0) (void) unlink(newname); else if (installnew) { char oldname[MAXPATHLEN]; (void) strcpy(oldname, name); (void) strcat(oldname, ".old"); if (rename(name, oldname) == -1 || rename(newname, name) == -1) (void) fprintf(stderr, "Couldn't install %s: %s\n", newname, strerror(errno)); } } }