/* * setfiles * * AUTHOR: Stephen Smalley * This program was derived in part from the setfiles.pl script * developed by Secure Computing Corporation. * * PURPOSE: * This program reads a set of file security context specifications * based on pathname regular expressions and labels files * accordingly, traversing a set of file systems specified by * the user. The program does not cross file system boundaries. * * USAGE: * setfiles [-dnpqsvW] spec_file pathname... * * -d Show what specification matched each file. * -n Do not change any file labels. * -q Be quiet (suppress non-error output). * -r Use an alternate root path * -s Use stdin for a list of files instead of searching a partition. * -v Show changes in file labels. * -W Warn about entries that have no matching file. * * spec_file The specification file. * pathname... The file systems to label (omit if using -s). * * EXAMPLE USAGE: * ./setfiles -v file_contexts `mount | awk '/ext3/{print $3}'` * * SPECIFICATION FILE: * Each specification has the form: * regexp [ -type ] ( context | <> ) * * By default, the regexp is an anchored match on both ends (i.e. a * caret (^) is prepended and a dollar sign ($) is appended automatically). * This default may be overridden by using .* at the beginning and/or * end of the regular expression. * * The optional type field specifies the file type as shown in the mode * field by ls, e.g. use -d to match only directories or -- to match only * regular files. * * The value of < may be used to indicate that matching files * should not be relabeled. * * The last matching specification is used. * * If there are multiple hard links to a file that match * different specifications and those specifications indicate * different security contexts, then a warning is displayed * but the file is still labeled based on the last matching * specification other than <>. */ #include "system.h" #include #include #define __USE_XOPEN_EXTENDED 1 /* nftw */ #include #include #include #include "debug.h" static int add_assoc = 1; /* * Command-line options. */ static int debug = 0; static int change = 1; static int quiet = 0; #define QPRINTF(args...) do { if (!quiet) printf(args); } while (0) static int use_stdin = 0; static int verbose = 0; static int warn_no_match = 0; static char *rootpath = NULL; static int rootpathlen = 0; /* * A file security context specification. */ typedef struct spec { /*@only@*/ char *pattern; /* regular expession string for diagnostic messages */ /*@only@*/ char *type_str; /* type string for diagnostic messages */ /*@only@*/ char *context; /* context string */ /*@only@*/ regex_t * preg; /* compiled regular expression */ mode_t mode; /* mode format value */ int matches; /* number of matching pathnames */ int hasMetaChars; /* indicates whether the RE has any meta characters. 0 = no meta chars 1 = has one or more meta chars */ int stem_id; /* indicates which of the stem-compression items it matches */ } * spec_t; typedef struct stem { char *buf; int len; } * stem_t; stem_t stem_arr = NULL; int num_stems = 0; int alloc_stems = 0; const char * const regex_chars = ".^$?*+|[({"; /* Return the length of the text that can be considered the stem, returns 0 * if there is no identifiable stem */ static int get_stem_from_spec(const char * const buf) { const char *tmp = strchr(buf + 1, '/'); const char *ind; if (!tmp) return 0; for (ind = buf + 1; ind < tmp; ind++) { if (strchr(regex_chars, (int)*ind)) return 0; } return tmp - buf; } /* return the length of the text that is the stem of a file name */ static int get_stem_from_file_name(const char * const buf) { const char *tmp = strchr(buf + 1, '/'); if (!tmp) return 0; return tmp - buf; } /* find the stem of a file spec, returns the index into stem_arr for a new * or existing stem, (or -1 if there is no possible stem - IE for a file in * the root directory or a regex that is too complex for us). Makes buf * point to the text AFTER the stem. */ static int find_stem_from_spec(const char **buf) { int stem_len = get_stem_from_spec(*buf); int i; if (!stem_len) return -1; for (i = 0; i < num_stems; i++) { if (stem_len != stem_arr[i].len) continue; if (strncmp(*buf, stem_arr[i].buf, stem_len)) continue; *buf += stem_len; return i; } if (num_stems == alloc_stems) { alloc_stems = alloc_stems * 2 + 16; stem_arr = xrealloc(stem_arr, sizeof(*stem_arr) * alloc_stems); } stem_arr[num_stems].len = stem_len; stem_arr[num_stems].buf = xmalloc(stem_len + 1); memcpy(stem_arr[num_stems].buf, *buf, stem_len); stem_arr[num_stems].buf[stem_len] = '\0'; num_stems++; *buf += stem_len; return num_stems - 1; } /* find the stem of a file name, returns the index into stem_arr (or -1 if * there is no match - IE for a file in the root directory or a regex that is * too complex for us). Makes buf point to the text AFTER the stem. */ static int find_stem_from_file(const char **buf) { int stem_len = get_stem_from_file_name(*buf); int i; if (stem_len) for (i = 0; i < num_stems; i++) { if (stem_len != stem_arr[i].len) continue; if (strncmp(*buf, stem_arr[i].buf, stem_len)) continue; *buf += stem_len; return i; } return -1; } /* * The array of specifications, initially in the * same order as in the specification file. * Sorting occurs based on hasMetaChars */ static spec_t spec_arr; static int nspec; /* * An association between an inode and a * specification. */ typedef struct file_spec * file_spec_t; struct file_spec { ino_t ino; /* inode number */ int specind; /* index of specification in spec */ char *file; /* full pathname for diagnostic messages about conflicts */ file_spec_t next; /* next association in hash bucket chain */ }; /* * The hash table of associations, hashed by inode number. * Chaining is used for collisions, with elements ordered * by inode number in each bucket. Each hash bucket has a dummy * header. */ #define HASH_BITS 16 #define HASH_BUCKETS (1 << HASH_BITS) #define HASH_MASK (HASH_BUCKETS-1) static struct file_spec fl_head[HASH_BUCKETS]; /* * Try to add an association between an inode and * a specification. If there is already an association * for the inode and it conflicts with this specification, * then use the specification that occurs later in the * specification array. */ static file_spec_t file_spec_add(ino_t ino, int specind, const char *file) { file_spec_t prevfl; file_spec_t fl; int h, no_conflict, ret; struct stat sb; h = (ino + (ino >> HASH_BITS)) & HASH_MASK; for (prevfl = &fl_head[h], fl = fl_head[h].next; fl != NULL; prevfl = fl, fl = fl->next) { if (ino == fl->ino) { ret = lstat(fl->file, &sb); if (ret < 0 || sb.st_ino != ino) { fl->specind = specind; free(fl->file); fl->file = xstrdup(file); return fl; } no_conflict = (strcmp(spec_arr[fl->specind].context,spec_arr[specind].context) == 0); if (no_conflict) return fl; fprintf(stderr, "%s: conflicting specifications for %s and %s, using %s.\n", __progname, file, fl->file, ((specind > fl->specind) ? spec_arr[specind]. context : spec_arr[fl->specind].context)); fl->specind = (specind > fl->specind) ? specind : fl->specind; free(fl->file); fl->file = xstrdup(file); return fl; } if (ino > fl->ino) break; } fl = xmalloc(sizeof(*fl)); fl->ino = ino; fl->specind = specind; fl->file = xstrdup(file); fl->next = prevfl->next; prevfl->next = fl; return fl; } /* * Evaluate the association hash table distribution. */ static void file_spec_eval(void) { file_spec_t fl; int h, used, nel, len, longest; used = 0; longest = 0; nel = 0; for (h = 0; h < HASH_BUCKETS; h++) { len = 0; for (fl = fl_head[h].next; fl; fl = fl->next) { len++; } if (len) used++; if (len > longest) longest = len; nel += len; } QPRINTF ("%s: hash table stats: %d elements, %d/%d buckets used, longest chain length %d\n", __progname, nel, used, HASH_BUCKETS, longest); } /* * Destroy the association hash table. */ static void file_spec_destroy(void) { file_spec_t fl; file_spec_t tmp; int h; for (h = 0; h < HASH_BUCKETS; h++) { fl = fl_head[h].next; while (fl) { tmp = fl; fl = fl->next; free(tmp->file); free(tmp); } fl_head[h].next = NULL; } } static int match(const char *name, struct stat *sb) { static char errbuf[255 + 1]; const char *fullname = name; const char *buf = name; int i, ret, file_stem; /* fullname will be the real file that gets labeled * name will be what is matched in the policy */ if (rootpath != NULL) { if (strncmp(rootpath, name, rootpathlen) != 0) { fprintf(stderr, "%s: %s is not located in %s\n", __progname, name, rootpath); return -1; } name += rootpathlen; buf += rootpathlen; } ret = lstat(fullname, sb); if (ret) { fprintf(stderr, "%s: unable to stat file %s\n", __progname, fullname); return -1; } file_stem = find_stem_from_file(&buf); /* * Check for matching specifications in reverse order, so that * the last matching specification is used. */ for (i = nspec - 1; i >= 0; i--) { spec_t sp; sp = spec_arr + i; /* if the spec in question matches no stem or has the same * stem as the file AND if the spec in question has no mode * specified or if the mode matches the file mode then we do * a regex check */ if (sp->stem_id != -1 && sp->stem_id != file_stem) continue; if (sp->mode && (sb->st_mode & S_IFMT) != sp->mode) continue; if (sp->stem_id == -1) ret = regexec(sp->preg, name, 0, NULL, 0); else ret = regexec(sp->preg, buf, 0, NULL, 0); if (ret == 0) break; if (ret == REG_NOMATCH) continue; /* else it's an error */ regerror(ret, sp->preg, errbuf, sizeof errbuf); fprintf(stderr, "%s: unable to match %s against %s: %s\n", __progname, name, sp->pattern, errbuf); return -1; } /* Cound matches found. */ if (i >= 0) spec_arr[i].matches++; return i; } /* Used with qsort to sort specs from lowest to highest hasMetaChars value */ static int spec_compare(const void* specA, const void* specB) { return ( ((const spec_t)specB)->hasMetaChars - ((const spec_t)specA)->hasMetaChars ); } /* * Check for duplicate specifications. If a duplicate specification is found * and the context is the same, give a warning to the user. If a duplicate * specification is found and the context is different, give a warning * to the user (This could be changed to error). Return of non-zero is an error. */ static int nodups_specs(void) { int i, j; for (i = 0; i < nspec; i++) { spec_t sip = spec_arr + i; for (j = i + 1; j < nspec; j++) { spec_t sjp = spec_arr + j; /* Check if same RE string */ if (strcmp(sjp->pattern, sip->pattern)) continue; if (sjp->mode && sip->mode && sjp->mode != sip->mode) continue; /* Same RE string found */ if (strcmp(sjp->context, sip->context)) { /* If different contexts, give warning */ fprintf(stderr, "ERROR: Multiple different specifications for %s (%s and %s).\n", sip->pattern, sjp->context, sip->context); } else { /* If same contexts give warning */ fprintf(stderr, "WARNING: Multiple same specifications for %s.\n", sip->pattern); } } } return 0; } static void usage(const char * const name, poptContext optCon) { fprintf(stderr, "usage: %s [-dnqvW] spec_file pathname...\n" "usage: %s -s [-dnqvW] spec_file\n", name, name); poptPrintUsage(optCon, stderr, 0); exit(1); } /* Determine if the regular expression specification has any meta characters. */ static void spec_hasMetaChars(spec_t sp) { char * c = sp->pattern; int len = strlen(c); char * end = c + len; sp->hasMetaChars = 0; /* Look at each character in the RE specification string for a * meta character. Return when any meta character reached. */ while (c != end) { switch(*c) { case '.': case '^': case '$': case '?': case '*': case '+': case '|': case '[': case '(': case '{': sp->hasMetaChars = 1; return; break; case '\\': /* skip the next character */ c++; break; default: break; } c++; } return; } /* * Apply the last matching specification to a file. * This function is called by nftw on each file during * the directory traversal. */ static int apply_spec(const char *file, const struct stat *sb_unused, int flag, struct FTW *s_unused) { const char * my_file; file_spec_t fl; struct stat my_sb; int i, ret; char * context; spec_t sp; /* Skip the extra slash at the beginning, if present. */ if (file[0] == '/' && file[1] == '/') my_file = &file[1]; else my_file = file; if (flag == FTW_DNR) { fprintf(stderr, "%s: unable to read directory %s\n", __progname, my_file); return 0; } i = match(my_file, &my_sb); if (i < 0) /* No matching specification. */ return 0; sp = spec_arr + i; /* * Try to add an association between this inode and * this specification. If there is already an association * for this inode and it conflicts with this specification, * then use the last matching specification. */ if (add_assoc) { fl = file_spec_add(my_sb.st_ino, i, my_file); if (!fl) /* Insufficient memory to proceed. */ return 1; if (fl->specind != i) /* There was already an association and it took precedence. */ return 0; } if (debug) { if (sp->type_str) { printf("%s: %s matched by (%s,%s,%s)\n", __progname, my_file, sp->pattern, sp->type_str, sp->context); } else { printf("%s: %s matched by (%s,%s)\n", __progname, my_file, sp->pattern, sp->context); } } /* Get the current context of the file. */ ret = lgetfilecon(my_file, &context); if (ret < 0) { if (errno == ENODATA) { context = xstrdup("<>"); } else { perror(my_file); fprintf(stderr, "%s: unable to obtain attribute for file %s\n", __progname, my_file); return -1; } } /* * Do not relabel the file if the matching specification is * <> or the file is already labeled according to the * specification. */ if ((strcmp(sp->context, "<>") == 0) || (strcmp(sp->context, context) == 0)) { freecon(context); return 0; } if (verbose) { printf("%s: relabeling %s from %s to %s\n", __progname, my_file, context, sp->context); } freecon(context); /* * Do not relabel the file if -n was used. */ if (!change) return 0; /* * Relabel the file to the specified context. */ ret = lsetfilecon(my_file, sp->context); if (ret) { perror(my_file); fprintf(stderr, "%s: unable to relabel %s to %s\n", __progname, my_file, sp->context); return 1; } return 0; } static int nerr = 0; static void inc_err(void) { nerr++; if (nerr > 9 && !debug) { fprintf(stderr, "Exiting after 10 errors.\n"); exit(1); } } static int parseREContexts(const char *fn) { FILE * fp; char errbuf[255 + 1]; char buf[255 + 1]; char * bp; char * regex; char * type; char * context; char * anchored_regex; int items; int len; int lineno; int pass; int regerr; spec_t sp; if ((fp = fopen(fn, "r")) == NULL) { perror(fn); return -1; } /* * Perform two passes over the specification file. * The first pass counts the number of specifications and * performs simple validation of the input. At the end * of the first pass, the spec array is allocated. * The second pass performs detailed validation of the input * and fills in the spec array. */ for (pass = 0; pass < 2; pass++) { lineno = 0; nspec = 0; sp = spec_arr; while (fgets(buf, sizeof buf, fp)) { lineno++; len = strlen(buf); if (buf[len - 1] != '\n') { fprintf(stderr, "%s: no newline on line number %d (only read %s)\n", fn, lineno, buf); inc_err(); continue; } buf[len - 1] = 0; bp = buf; while (isspace(*bp)) bp++; /* Skip comment lines and empty lines. */ if (*bp == '#' || *bp == 0) continue; items = sscanf(buf, "%as %as %as", ®ex, &type, &context); if (items < 2) { fprintf(stderr, "%s: line number %d is missing fields (only read %s)\n", fn, lineno, buf); inc_err(); if (items == 1) free(regex); continue; } else if (items == 2) { /* The type field is optional. */ free(context); context = type; type = 0; } if (pass == 1) { /* On the second pass, compile and store the specification in spec. */ const char *reg_buf = regex; sp->stem_id = find_stem_from_spec(®_buf); sp->pattern = regex; /* Anchor the regular expression. */ len = strlen(reg_buf); anchored_regex = xmalloc(len + 3); sprintf(anchored_regex, "^%s$", reg_buf); /* Compile the regular expression. */ sp->preg = xcalloc(1, sizeof(*sp->preg)); regerr = regcomp(sp->preg, anchored_regex, REG_EXTENDED | REG_NOSUB); if (regerr < 0) { regerror(regerr, sp->preg, errbuf, sizeof errbuf); fprintf(stderr, "%s: unable to compile regular expression %s on line number %d: %s\n", fn, regex, lineno, errbuf); inc_err(); } free(anchored_regex); /* Convert the type string to a mode format */ sp->type_str = type; sp->mode = 0; if (!type) goto skip_type; len = strlen(type); if (type[0] != '-' || len != 2) { fprintf(stderr, "%s: invalid type specifier %s on line number %d\n", fn, type, lineno); inc_err(); goto skip_type; } switch (type[1]) { case 'b': sp->mode = S_IFBLK; break; case 'c': sp->mode = S_IFCHR; break; case 'd': sp->mode = S_IFDIR; break; case 'p': sp->mode = S_IFIFO; break; case 'l': sp->mode = S_IFLNK; break; case 's': sp->mode = S_IFSOCK; break; case '-': sp->mode = S_IFREG; break; default: fprintf(stderr, "%s: invalid type specifier %s on line number %d\n", fn, type, lineno); inc_err(); } skip_type: sp->context = context; if (strcmp(context, "<>")) { if (security_check_context(context) < 0 && errno != ENOENT) { fprintf(stderr, "%s: invalid context %s on line number %d\n", fn, context, lineno); inc_err(); } } /* Determine if specification has * any meta characters in the RE */ spec_hasMetaChars(sp); sp++; } nspec++; if (pass == 0) { free(regex); if (type) free(type); free(context); } } if (nerr) return -1; if (pass == 0) { QPRINTF("%s: read %d specifications\n", fn, nspec); if (nspec == 0) return 0; spec_arr = xcalloc(nspec, sizeof(*spec_arr)); rewind(fp); } } fclose(fp); /* Sort the specifications with most general first */ qsort(spec_arr, nspec, sizeof(*spec_arr), spec_compare); /* Verify no exact duplicates */ if (nodups_specs() != 0) return -1; return 0; } static struct poptOption optionsTable[] = { { "debug", 'd', POPT_ARG_VAL, &debug, 1, N_("show what specification matched each file"), NULL }, { "nochange", 'n', POPT_ARG_VAL, &change, 0, N_("do not change any file labels"), NULL }, { "quiet", 'q', POPT_ARG_VAL, &quiet, 1, N_("be quiet (suppress non-error output)"), NULL }, { "root", 'r', POPT_ARG_STRING|POPT_ARGFLAG_SHOW_DEFAULT, &rootpath, 0, N_("use an alternate root path"), N_("ROOT") }, { "stdin", 's', POPT_ARG_VAL, &use_stdin, 1, N_("use stdin for a list of files instead of searching a partition"), NULL }, { "verbose", 'v', POPT_ARG_VAL, &warn_no_match, 1, N_("show changes in file labels"), NULL }, { "warn", 'W', POPT_ARG_VAL, &warn_no_match, 1, N_("warn about entries that have no matching file"), NULL }, POPT_AUTOHELP POPT_TABLEEND }; int main(int argc, char **argv) { poptContext optCon; const char ** av; const char * arg; int ec = EXIT_FAILURE; /* assume failure. */ int rc; int i; #if HAVE_MCHECK_H && HAVE_MTRACE /*@-noeffect@*/ mtrace(); /* Trace malloc only if MALLOC_TRACE=mtrace-output-file. */ /*@=noeffect@*/ #endif setprogname(argv[0]); /* Retrofit glibc __progname */ /* XXX glibc churn sanity */ if (__progname == NULL) { if ((__progname = strrchr(argv[0], '/')) != NULL) __progname++; else __progname = argv[0]; } (void) setlocale(LC_ALL, "" ); (void) bindtextdomain(PACKAGE, LOCALEDIR); (void) textdomain(PACKAGE); optCon = poptGetContext(__progname, argc, (const char **)argv, optionsTable, 0); /* Process all options, whine if unknown. */ while ((rc = poptGetNextOpt(optCon)) > 0) { switch (rc) { default: /*@-nullpass@*/ fprintf(stderr, _("%s: option table misconfigured (%d)\n"), __progname, rc); /*@=nullpass@*/ goto exit; /*@notreached@*/ /*@switchbreak@*/ break; } } if (rc < -1) { /*@-nullpass@*/ fprintf(stderr, "%s: %s: %s\n", __progname, poptBadOption(optCon, POPT_BADOPTION_NOALIAS), poptStrerror(rc)); /*@=nullpass@*/ goto exit; } /* trim trailing /, if present */ if (rootpath != NULL) { rootpathlen = strlen(rootpath); while (rootpath[rootpathlen - 1] == '/') rootpath[--rootpathlen] = 0; } av = poptGetArgs(optCon); if (use_stdin) { add_assoc = 0; /* Check exactly 1 arg */ if (av == NULL || !(av[0] != NULL && av[1] == NULL)) { usage(__progname, optCon); } } else { /* Check at least 2 args */ if (av == NULL || !(av[0] != NULL && av[1] != NULL)) usage(__progname, optCon); } /* Parse the specification file. */ if (parseREContexts(*av) != 0) { perror(*av); goto exit; } av++; /* * Apply the specifications to the file systems. */ if (use_stdin) { char buf[PATH_MAX]; struct stat sb; int flag; while(fgets(buf, sizeof(buf), stdin)) { strtok(buf, "\n"); if (buf[0] == '\n') continue; if (stat(buf, &sb)) { fprintf(stderr, "File \"%s\" not found.\n", buf); continue; } switch(sb.st_mode) { case S_IFDIR: flag = FTW_D; break; case S_IFLNK: flag = FTW_SL; break; default: flag = FTW_F; break; } apply_spec(buf, &sb, flag, NULL); } } else while ((arg = *av++) != NULL) { if (rootpath != NULL) { QPRINTF("%s: labeling files, pretending %s is /\n", __progname, rootpath); } QPRINTF("%s: labeling files under %s\n", __progname, arg); /* Walk the file tree, calling apply_spec on each file. */ if (nftw(arg, apply_spec, 1024, FTW_PHYS | FTW_MOUNT)) { fprintf(stderr, "%s: error while labeling files under %s\n", __progname, arg); goto exit; } /* * Evaluate the association hash table distribution for the * directory tree just traversed. */ file_spec_eval(); /* Reset the association hash table for the next directory tree. */ file_spec_destroy(); } if (warn_no_match) { for (i = 0; i < nspec; i++) { spec_t sp; sp = spec_arr + i; if (sp->matches > 0) continue; if (sp->type_str) { printf("%s: Warning! No matches for (%s, %s, %s)\n", __progname, sp->pattern, sp->type_str, sp->context); } else { printf("%s: Warning! No matches for (%s, %s)\n", __progname, sp->pattern, sp->context); } } } QPRINTF("%s: Done.\n", __progname); ec = 0; exit: optCon = poptFreeContext(optCon); #if HAVE_MCHECK_H && HAVE_MTRACE /*@-noeffect@*/ muntrace(); /* Trace malloc only if MALLOC_TRACE=mtrace-output-file. */ /*@=noeffect@*/ #endif return ec; }