root/usr.sbin/setfmac/setfmac.c
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2002, 2004 Networks Associates Technology, Inc.
 * All rights reserved.
 *
 * This software was developed for the FreeBSD Project by NAI Labs, the
 * Security Research Division of Network Associates, Inc. under
 * DARPA/SPAWAR contract N66001-01-C-8035 ("CBOSS"), as part of the DARPA
 * CHATS research program.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/types.h>
#include <sys/mac.h>
#include <sys/queue.h>
#include <sys/stat.h>

#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <fts.h>
#include <libgen.h>
#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

struct label_spec {
        struct label_spec_entry {
                regex_t regex;  /* compiled regular expression to match */
                char *regexstr; /* uncompiled regular expression */
                mode_t mode;    /* mode to possibly match */
                const char *modestr;    /* print-worthy ",-?" mode string */
                char *mactext;  /* MAC label to apply */
                int flags;      /* miscellaneous flags */
#define         F_DONTLABEL     0x01
#define         F_ALWAYSMATCH   0x02
        } *entries,             /* entries[0..nentries] */
          *match;               /* cached decision for MAC label to apply */
        size_t nentries;        /* size of entries list */
        STAILQ_ENTRY(label_spec) link;
};

struct label_specs {
        STAILQ_HEAD(label_specs_head, label_spec) head;
};

void usage(int) __dead2;
struct label_specs *new_specs(void);
void add_specs(struct label_specs *, const char *, int);
void add_setfmac_specs(struct label_specs *, char *);
void add_spec_line(const char *, int, struct label_spec_entry *, char *);
int apply_specs(struct label_specs *, FTSENT *, int, int);
int specs_empty(struct label_specs *);

static int qflag;

int
main(int argc, char **argv)
{
        FTSENT *ftsent;
        FTS *fts;
        struct label_specs *specs;
        int eflag = 0, xflag = 0, vflag = 0, Rflag = 0, hflag;
        int ch, is_setfmac;
        char *bn;

        bn = basename(argv[0]);
        if (bn == NULL)
                err(1, "basename");
        is_setfmac = strcmp(bn, "setfmac") == 0;
        hflag = is_setfmac ? FTS_LOGICAL : FTS_PHYSICAL;
        specs = new_specs();
        while ((ch = getopt(argc, argv, is_setfmac ? "Rhq" : "ef:qs:vx")) !=
            -1) {
                switch (ch) {
                case 'R':
                        Rflag = 1;
                        break;
                case 'e':
                        eflag = 1;
                        break;
                case 'f':
                        add_specs(specs, optarg, 0);
                        break;
                case 'h':
                        hflag = FTS_PHYSICAL;
                        break;
                case 'q':
                        qflag = 1;
                        break;
                case 's':
                        add_specs(specs, optarg, 1);
                        break;
                case 'v':
                        vflag++;
                        break;
                case 'x':
                        xflag = FTS_XDEV;
                        break;
                default:
                        usage(is_setfmac);
                }
        }
        argc -= optind;
        argv += optind;

        if (is_setfmac) {
                if (argc <= 1)  
                        usage(is_setfmac);
                add_setfmac_specs(specs, *argv);
                argc--;
                argv++;
        } else {
                if (argc == 0 || specs_empty(specs))
                        usage(is_setfmac);
        }
        fts = fts_open(argv, hflag | xflag, NULL);
        if (fts == NULL)
                err(1, "cannot traverse filesystem%s", argc ? "s" : "");
        while (errno = 0, (ftsent = fts_read(fts)) != NULL) {
                switch (ftsent->fts_info) {
                case FTS_DP:            /* skip post-order */
                        break;
                case FTS_D:             /* do pre-order */
                case FTS_DC:            /* do cyclic? */
                        /* don't ever recurse directories as setfmac(8) */
                        if (is_setfmac && !Rflag)
                                fts_set(fts, ftsent, FTS_SKIP);
                case FTS_DEFAULT:       /* do default */
                case FTS_F:             /* do regular */
                case FTS_SL:            /* do symlink */
                case FTS_SLNONE:        /* do symlink */
                case FTS_W:             /* do whiteout */
                        if (apply_specs(specs, ftsent, hflag, vflag)) {
                                if (eflag) {
                                        errx(1, "labeling not supported in %s",
                                            ftsent->fts_path);
                                }
                                if (!qflag)
                                        warnx("labeling not supported in %s",
                                            ftsent->fts_path);
                                fts_set(fts, ftsent, FTS_SKIP);
                        }
                        break;
                case FTS_DNR:           /* die on all errors */
                case FTS_ERR:
                case FTS_NS:
                        err(1, "traversing %s", ftsent->fts_path);
                default:
                        errx(1, "CANNOT HAPPEN (%d) traversing %s",
                            ftsent->fts_info, ftsent->fts_path);
                }
        }
        if (errno != 0)
                err(1, "fts_read");
        fts_close(fts);
        exit(0);
}

void
usage(int is_setfmac)
{

        if (is_setfmac)
                fprintf(stderr, "usage: setfmac [-Rhq] label file ...\n");
        else
                fprintf(stderr, "usage: setfsmac [-ehqvx] [-f specfile [...]] [-s specfile [...]] file ...\n");
        exit(1);
}

static int
chomp_line(char **line, size_t *linesize)
{
        char *s;
        int freeme = 0;
        
        for (s = *line; (unsigned)(s - *line) < *linesize; s++) {
                if (!isspace(*s))
                        break;
        }
        if (*s == '#') {
                **line = '\0';
                *linesize = 0;
                return (freeme);
        }
        memmove(*line, s, *linesize - (s - *line));
        *linesize -= s - *line;
        for (s = &(*line)[*linesize - 1]; s >= *line; s--) {
                if (!isspace(*s))
                        break;
        }
        if (s != &(*line)[*linesize - 1]) {
                *linesize = s - *line + 1;
        } else {
                s = malloc(*linesize + 1);
                if (s == NULL)
                        err(1, "malloc");
                strncpy(s, *line, *linesize);
                *line = s;
                freeme = 1;
        }
        (*line)[*linesize] = '\0';
        return (freeme);
}

void
add_specs(struct label_specs *specs, const char *file, int is_sebsd)
{
        struct label_spec *spec;
        FILE *fp;
        char *line;
        size_t nlines = 0, linesize;
        int freeline;

        spec = malloc(sizeof(*spec));
        if (spec == NULL)
                err(1, "malloc");
        fp = fopen(file, "r");
        if (fp == NULL)
                err(1, "opening %s", file);
        while ((line = fgetln(fp, &linesize)) != NULL) {
                freeline = chomp_line(&line, &linesize);
                if (linesize > 0) /* only allocate space for non-comments */
                        nlines++;
                if (freeline)
                        free(line);
        }
        if (ferror(fp))
                err(1, "fgetln on %s", file);
        rewind(fp);
        spec->entries = calloc(nlines, sizeof(*spec->entries));
        if (spec->entries == NULL)
                err(1, "malloc");
        spec->nentries = nlines;
        while (nlines > 0) {
                line = fgetln(fp, &linesize);
                if (line == NULL) {
                        if (feof(fp))
                                errx(1, "%s ended prematurely", file);
                        else
                                err(1, "failure reading %s", file);
                }
                freeline = chomp_line(&line, &linesize);
                if (linesize == 0) {
                        if (freeline)
                                free(line);
                        continue;
                }
                add_spec_line(file, is_sebsd, &spec->entries[--nlines], line);
                if (freeline)
                        free(line);
        }
        fclose(fp);
        if (!qflag)
                warnx("%s: read %lu specifications", file,
                    (long)spec->nentries);
        STAILQ_INSERT_TAIL(&specs->head, spec, link);
}

void
add_setfmac_specs(struct label_specs *specs, char *label)
{
        struct label_spec *spec;

        spec = malloc(sizeof(*spec));
        if (spec == NULL)
                err(1, "malloc");
        spec->nentries = 1;
        spec->entries = calloc(spec->nentries, sizeof(*spec->entries));
        if (spec->entries == NULL)
                err(1, "malloc");
        /* The _only_ thing specified here is the mactext! */
        spec->entries->mactext = label;
        spec->entries->flags |= F_ALWAYSMATCH;
        STAILQ_INSERT_TAIL(&specs->head, spec, link);
}

void
add_spec_line(const char *file, int is_sebsd, struct label_spec_entry *entry,
    char *line)
{
        char *regexstr, *modestr, *macstr, *regerrorstr;
        size_t size;
        int error;

        regexstr = strtok(line, " \t");
        if (regexstr == NULL)
                errx(1, "%s: need regular expression", file);
        modestr = strtok(NULL, " \t");
        if (modestr == NULL)
                errx(1, "%s: need a label", file);
        macstr = strtok(NULL, " \t");
        if (macstr == NULL) {   /* the mode is just optional */
                macstr = modestr;
                modestr = NULL;
        }
        if (strtok(NULL, " \t") != NULL)
                errx(1, "%s: extraneous fields at end of line", file);
        /* assume we need to anchor this regex */
        if (asprintf(&regexstr, "^%s$", regexstr) == -1)
                err(1, "%s: processing regular expression", file);
        entry->regexstr = regexstr;
        error = regcomp(&entry->regex, regexstr, REG_EXTENDED | REG_NOSUB);
        if (error) {
                size = regerror(error, &entry->regex, NULL, 0);
                regerrorstr = malloc(size);
                if (regerrorstr == NULL)
                        err(1, "malloc");
                (void)regerror(error, &entry->regex, regerrorstr, size);
                errx(1, "%s: %s: %s", file, entry->regexstr, regerrorstr);
        }
        if (!is_sebsd) {
                entry->mactext = strdup(macstr);
                if (entry->mactext == NULL)
                        err(1, "strdup");
        } else {
                if (asprintf(&entry->mactext, "sebsd/%s", macstr) == -1)
                        err(1, "asprintf");
                if (strcmp(macstr, "<<none>>") == 0)
                        entry->flags |= F_DONTLABEL;
        }
        if (modestr != NULL) {
                if (strlen(modestr) != 2 || modestr[0] != '-')
                        errx(1, "%s: invalid mode string: %s", file, modestr);
                switch (modestr[1]) {
                case 'b':
                        entry->mode = S_IFBLK;
                        entry->modestr = ",-b";
                        break;
                case 'c':
                        entry->mode = S_IFCHR;
                        entry->modestr = ",-c";
                        break;
                case 'd':
                        entry->mode = S_IFDIR;
                        entry->modestr = ",-d";
                        break;
                case 'p':
                        entry->mode = S_IFIFO;
                        entry->modestr = ",-p";
                        break;
                case 'l':
                        entry->mode = S_IFLNK;
                        entry->modestr = ",-l";
                        break;
                case 's':
                        entry->mode = S_IFSOCK;
                        entry->modestr = ",-s";
                        break;
                case '-':
                        entry->mode = S_IFREG;
                        entry->modestr = ",--";
                        break;
                default:
                        errx(1, "%s: invalid mode string: %s", file, modestr);
                }
        } else {
                entry->modestr = "";
        }
}

int
specs_empty(struct label_specs *specs)
{

        return (STAILQ_EMPTY(&specs->head));
}

int
apply_specs(struct label_specs *specs, FTSENT *ftsent, int hflag, int vflag)
{
        regmatch_t pmatch;
        struct label_spec *ls;
        struct label_spec_entry *ent;
        char *regerrorstr, *macstr;
        size_t size;
        mac_t mac;
        int error, matchedby;

        /*
         * Work through file context sources in order of specification
         * on the command line, and through their entries in reverse
         * order to find the "last" (hopefully "best") match.
         */
        matchedby = 0;
        STAILQ_FOREACH(ls, &specs->head, link) {
                for (ls->match = NULL, ent = ls->entries;
                    ent < &ls->entries[ls->nentries]; ent++) {
                        if (ent->flags & F_ALWAYSMATCH)
                                goto matched;
                        if (ent->mode != 0 &&
                            (ftsent->fts_statp->st_mode & S_IFMT) != ent->mode)
                                continue;
                        pmatch.rm_so = 0;
                        pmatch.rm_eo = ftsent->fts_pathlen;
                        error = regexec(&ent->regex, ftsent->fts_path, 1,
                            &pmatch, REG_STARTEND);
                        switch (error) {
                        case REG_NOMATCH:
                                continue;
                        case 0:
                                break;
                        default:
                                size = regerror(error, &ent->regex, NULL, 0);
                                regerrorstr = malloc(size);
                                if (regerrorstr == NULL)
                                        err(1, "malloc");
                                (void)regerror(error, &ent->regex, regerrorstr,
                                    size);
                                errx(1, "%s: %s", ent->regexstr, regerrorstr);
                        }
                matched:
                        ls->match = ent;
                        if (vflag) {
                                if (matchedby == 0) {
                                        printf("%s matched by ",
                                            ftsent->fts_path);
                                        matchedby = 1;
                                }
                                printf("%s(%s%s,%s)", matchedby == 2 ? "," : "",
                                    ent->regexstr, ent->modestr, ent->mactext);
                                if (matchedby == 1)
                                        matchedby = 2;
                        }
                        break;
                }
        }
        if (vflag && matchedby)
                printf("\n");
        size = 0;
        STAILQ_FOREACH(ls, &specs->head, link) {
                /* cached match decision */
                if (ls->match && (ls->match->flags & F_DONTLABEL) == 0)
                         /* add length of "x\0"/"y," */
                        size += strlen(ls->match->mactext) + 1;
        }
        if (size == 0)
                return (0);
        macstr = malloc(size);
        if (macstr == NULL)
                err(1, "malloc");
        *macstr = '\0';
        STAILQ_FOREACH(ls, &specs->head, link) {
                /* cached match decision */
                if (ls->match && (ls->match->flags & F_DONTLABEL) == 0) {
                        if (*macstr != '\0')
                                strcat(macstr, ",");
                        strcat(macstr, ls->match->mactext);
                }
        }
        if (mac_from_text(&mac, macstr))
                err(1, "mac_from_text(%s)", macstr);
        if ((hflag == FTS_PHYSICAL ? mac_set_link(ftsent->fts_accpath, mac) :
            mac_set_file(ftsent->fts_accpath, mac)) != 0) {
                if (errno == EOPNOTSUPP) {
                        mac_free(mac);
                        free(macstr);
                        return (1);
                }
                err(1, "mac_set_link(%s, %s)", ftsent->fts_path, macstr);
        }
        mac_free(mac);
        free(macstr);
        return (0);
}

struct label_specs *
new_specs(void)
{
        struct label_specs *specs;

        specs = malloc(sizeof(*specs));
        if (specs == NULL)
                err(1, "malloc");
        STAILQ_INIT(&specs->head);
        return (specs);
}