/*
* File contexts backend for labeling system
*
* Author : Eamon Walsh <ewalsh@tycho.nsa.gov>
* Author : Stephen Smalley <sds@tycho.nsa.gov>
*
* This library derived in part from setfiles and the setfiles.pl script
* developed by Secure Computing Corporation.
*/
#include <assert.h>
#include <fcntl.h>
#include <stdarg.h>
#include <string.h>
#include <stdio.h>
#include <stdio_ext.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <pcre.h>
#include <linux/limits.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "callbacks.h"
#include "label_internal.h"
#include "label_file.h"
/*
* Internals, mostly moved over from matchpathcon.c
*/
/* 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 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(struct saved_data *data, const char **buf)
{
int i;
int stem_len = get_stem_from_file_name(*buf);
if (!stem_len)
return -1;
for (i = 0; i < data->num_stems; i++) {
if (stem_len == data->stem_arr[i].len
&& !strncmp(*buf, data->stem_arr[i].buf, stem_len)) {
*buf += stem_len;
return i;
}
}
return -1;
}
/*
* Warn about duplicate specifications.
*/
static int nodups_specs(struct saved_data *data, const char *path)
{
int rc = 0;
unsigned int ii, jj;
struct spec *curr_spec, *spec_arr = data->spec_arr;
for (ii = 0; ii < data->nspec; ii++) {
curr_spec = &spec_arr[ii];
for (jj = ii + 1; jj < data->nspec; jj++) {
if ((!strcmp(spec_arr[jj].regex_str, curr_spec->regex_str))
&& (!spec_arr[jj].mode || !curr_spec->mode
|| spec_arr[jj].mode == curr_spec->mode)) {
rc = -1;
errno = EINVAL;
if (strcmp(spec_arr[jj].lr.ctx_raw, curr_spec->lr.ctx_raw)) {
COMPAT_LOG
(SELINUX_ERROR,
"%s: Multiple different specifications for %s (%s and %s).\n",
path, curr_spec->regex_str,
spec_arr[jj].lr.ctx_raw,
curr_spec->lr.ctx_raw);
} else {
COMPAT_LOG
(SELINUX_ERROR,
"%s: Multiple same specifications for %s.\n",
path, curr_spec->regex_str);
}
}
}
}
return rc;
}
static int compile_regex(struct saved_data *data, struct spec *spec, const char **errbuf)
{
const char *tmperrbuf;
char *reg_buf, *anchored_regex, *cp;
struct stem *stem_arr = data->stem_arr;
size_t len;
int erroff;
if (spec->regcomp)
return 0; /* already done */
/* Skip the fixed stem. */
reg_buf = spec->regex_str;
if (spec->stem_id >= 0)
reg_buf += stem_arr[spec->stem_id].len;
/* Anchor the regular expression. */
len = strlen(reg_buf);
cp = anchored_regex = malloc(len + 3);
if (!anchored_regex)
return -1;
/* Create ^...$ regexp. */
*cp++ = '^';
cp = mempcpy(cp, reg_buf, len);
*cp++ = '$';
*cp = '\0';
/* Compile the regular expression. */
spec->regex = pcre_compile(anchored_regex, PCRE_DOTALL, &tmperrbuf, &erroff, NULL);
free(anchored_regex);
if (!spec->regex) {
if (errbuf)
*errbuf=tmperrbuf;
return -1;
}
spec->sd = pcre_study(spec->regex, 0, &tmperrbuf);
if (!spec->sd && tmperrbuf) {
if (errbuf)
*errbuf=tmperrbuf;
return -1;
}
/* Done. */
spec->regcomp = 1;
return 0;
}
static int process_line(struct selabel_handle *rec,
const char *path, const char *prefix,
char *line_buf, unsigned lineno)
{
int items, len, rc;
char *buf_p, *regex, *type, *context;
struct saved_data *data = (struct saved_data *)rec->data;
struct spec *spec_arr;
unsigned int nspec = data->nspec;
const char *errbuf = NULL;
len = strlen(line_buf);
if (line_buf[len - 1] == '\n')
line_buf[len - 1] = 0;
buf_p = line_buf;
while (isspace(*buf_p))
buf_p++;
/* Skip comment lines and empty lines. */
if (*buf_p == '#' || *buf_p == 0)
return 0;
items = sscanf(line_buf, "%ms %ms %ms", ®ex, &type, &context);
if (items < 2) {
COMPAT_LOG(SELINUX_WARNING,
"%s: line %u is missing fields, skipping\n", path,
lineno);
if (items == 1)
free(regex);
return 0;
} else if (items == 2) {
/* The type field is optional. */
free(context);
context = type;
type = 0;
}
len = get_stem_from_spec(regex);
if (len && prefix && strncmp(prefix, regex, len)) {
/* Stem of regex does not match requested prefix, discard. */
free(regex);
free(type);
free(context);
return 0;
}
rc = grow_specs(data);
if (rc)
return rc;
spec_arr = data->spec_arr;
/* process and store the specification in spec. */
spec_arr[nspec].stem_id = find_stem_from_spec(data, regex);
spec_arr[nspec].regex_str = regex;
if (rec->validating && compile_regex(data, &spec_arr[nspec], &errbuf)) {
COMPAT_LOG(SELINUX_WARNING, "%s: line %u has invalid regex %s: %s\n",
path, lineno, regex, (errbuf ? errbuf : "out of memory"));
}
/* Convert the type string to a mode format */
spec_arr[nspec].type_str = type;
spec_arr[nspec].mode = 0;
if (type) {
mode_t mode = string_to_mode(type);
if (mode == (mode_t)-1) {
COMPAT_LOG(SELINUX_WARNING, "%s: line %u has invalid file type %s\n",
path, lineno, type);
mode = 0;
}
spec_arr[nspec].mode = mode;
}
spec_arr[nspec].lr.ctx_raw = context;
/* Determine if specification has
* any meta characters in the RE */
spec_hasMetaChars(&spec_arr[nspec]);
if (strcmp(context, "<<none>>") && rec->validating)
compat_validate(rec, &spec_arr[nspec].lr, path, lineno);
data->nspec = ++nspec;
return 0;
}
static int load_mmap(struct selabel_handle *rec, const char *path, struct stat *sb)
{
struct saved_data *data = (struct saved_data *)rec->data;
char mmap_path[PATH_MAX + 1];
int mmapfd;
int rc;
struct stat mmap_stat;
char *addr;
size_t len;
int stem_map_len, *stem_map;
struct mmap_area *mmap_area;
uint32_t i;
uint32_t *magic;
uint32_t *section_len;
uint32_t *plen;
rc = snprintf(mmap_path, sizeof(mmap_path), "%s.bin", path);
if (rc >= (int)sizeof(mmap_path))
return -1;
mmapfd = open(mmap_path, O_RDONLY | O_CLOEXEC);
if (mmapfd < 0)
return -1;
rc = fstat(mmapfd, &mmap_stat);
if (rc < 0) {
close(mmapfd);
return -1;
}
/* if mmap is old, ignore it */
if (mmap_stat.st_mtime < sb->st_mtime) {
close(mmapfd);
return -1;
}
if (mmap_stat.st_mtime == sb->st_mtime &&
mmap_stat.st_mtim.tv_nsec < sb->st_mtim.tv_nsec) {
close(mmapfd);
return -1;
}
/* ok, read it in... */
len = mmap_stat.st_size;
len += (sysconf(_SC_PAGE_SIZE) - 1);
len &= ~(sysconf(_SC_PAGE_SIZE) - 1);
mmap_area = malloc(sizeof(*mmap_area));
if (!mmap_area) {
close(mmapfd);
return -1;
}
addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, mmapfd, 0);
close(mmapfd);
if (addr == MAP_FAILED) {
free(mmap_area);
perror("mmap");
return -1;
}
/* save where we mmap'd the file to cleanup on close() */
mmap_area->addr = addr;
mmap_area->len = len;
mmap_area->next = data->mmap_areas;
data->mmap_areas = mmap_area;
/* check if this looks like an fcontext file */
magic = (uint32_t *)addr;
if (*magic != SELINUX_MAGIC_COMPILED_FCONTEXT)
return -1;
addr += sizeof(uint32_t);
/* check if this version is higher than we understand */
section_len = (uint32_t *)addr;
if (*section_len > SELINUX_COMPILED_FCONTEXT_MAX_VERS)
return -1;
addr += sizeof(uint32_t);
if (*section_len >= SELINUX_COMPILED_FCONTEXT_PCRE_VERS) {
len = strlen(pcre_version());
plen = (uint32_t *)addr;
if (*plen > mmap_area->len)
return -1; /* runs off the end of the map */
if (len != *plen)
return -1; /* pcre version length mismatch */
addr += sizeof(uint32_t);
if (memcmp((char *)addr, pcre_version(), len))
return -1; /* pcre version content mismatch */
if (addr + *plen >= (char *)mmap_area->addr + mmap_area->len)
return -1; /* Buffer over-run */
addr += *plen;
}
/* allocate the stems_data array */
section_len = (uint32_t *)addr;
addr += sizeof(uint32_t);
/*
* map indexed by the stem # in the mmap file and contains the stem
* number in the data stem_arr
*/
stem_map_len = *section_len;
stem_map = calloc(stem_map_len, sizeof(*stem_map));
if (!stem_map)
return -1;
for (i = 0; i < *section_len; i++) {
char *buf;
uint32_t stem_len;
int newid;
/* the length does not inlude the nul */
plen = (uint32_t *)addr;
addr += sizeof(uint32_t);
stem_len = *plen;
buf = (char *)addr;
addr += (stem_len + 1); // +1 is the nul
/* store the mapping between old and new */
newid = find_stem(data, buf, stem_len);
if (newid < 0) {
newid = store_stem(data, buf, stem_len);
if (newid < 0) {
rc = newid;
goto err;
}
data->stem_arr[newid].from_mmap = 1;
}
stem_map[i] = newid;
}
/* allocate the regex array */
section_len = (uint32_t *)addr;
addr += sizeof(*section_len);
for (i = 0; i < *section_len; i++) {
struct spec *spec;
int32_t stem_id;
rc = grow_specs(data);
if (rc < 0)
goto err;
spec = &data->spec_arr[data->nspec];
spec->from_mmap = 1;
spec->regcomp = 1;
plen = (uint32_t *)addr;
addr += sizeof(uint32_t);
rc = -1;
spec->lr.ctx_raw = strdup((char *)addr);
if (!spec->lr.ctx_raw)
goto err;
if (addr + *plen >= (char *)mmap_area->addr + mmap_area->len)
return -1;
addr += *plen;
plen = (uint32_t *)addr;
addr += sizeof(uint32_t);
spec->regex_str = (char *)addr;
if (addr + *plen >= (char *)mmap_area->addr + mmap_area->len)
return -1;
addr += *plen;
spec->mode = *(mode_t *)addr;
addr += sizeof(mode_t);
/* map the stem id from the mmap file to the data->stem_arr */
stem_id = *(int32_t *)addr;
if (stem_id == -1 || stem_id >= stem_map_len)
spec->stem_id = -1;
else
spec->stem_id = stem_map[stem_id];
addr += sizeof(int32_t);
/* retrieve the hasMetaChars bit */
spec->hasMetaChars = *(uint32_t *)addr;
addr += sizeof(uint32_t);
plen = (uint32_t *)addr;
addr += sizeof(uint32_t);
spec->regex = (pcre *)addr;
if (addr + *plen >= (char *)mmap_area->addr + mmap_area->len)
return -1;
addr += *plen;
plen = (uint32_t *)addr;
addr += sizeof(uint32_t);
spec->lsd.study_data = (void *)addr;
spec->lsd.flags |= PCRE_EXTRA_STUDY_DATA;
if (addr + *plen >= (char *)mmap_area->addr + mmap_area->len)
return -1;
addr += *plen;
data->nspec++;
}
/* win */
rc = 0;
err:
free(stem_map);
return rc;
}
static int process_file(const char *path, const char *suffix, struct selabel_handle *rec, const char *prefix)
{
FILE *fp;
struct stat sb;
unsigned int lineno;
size_t line_len;
char *line_buf = NULL;
int rc;
char stack_path[PATH_MAX + 1];
/* append the path suffix if we have one */
if (suffix) {
rc = snprintf(stack_path, sizeof(stack_path), "%s.%s", path, suffix);
if (rc >= (int)sizeof(stack_path)) {
errno = ENAMETOOLONG;
return -1;
}
path = stack_path;
}
/* Open the specification file. */
if ((fp = fopen(path, "r")) == NULL)
return -1;
__fsetlocking(fp, FSETLOCKING_BYCALLER);
if (fstat(fileno(fp), &sb) < 0)
return -1;
if (!S_ISREG(sb.st_mode)) {
errno = EINVAL;
return -1;
}
rc = load_mmap(rec, path, &sb);
if (rc == 0)
goto out;
/*
* The do detailed validation of the input and fill the spec array
*/
lineno = 0;
while (getline(&line_buf, &line_len, fp) > 0) {
rc = process_line(rec, path, prefix, line_buf, ++lineno);
if (rc)
return rc;
}
out:
free(line_buf);
fclose(fp);
return 0;
}
static int init(struct selabel_handle *rec, struct selinux_opt *opts,
unsigned n)
{
struct saved_data *data = (struct saved_data *)rec->data;
const char *path = NULL;
const char *prefix = NULL;
char subs_file[PATH_MAX + 1];
int status = -1, baseonly = 0;
/* Process arguments */
while (n--)
switch(opts[n].type) {
case SELABEL_OPT_PATH:
path = opts[n].value;
break;
case SELABEL_OPT_SUBSET:
prefix = opts[n].value;
break;
case SELABEL_OPT_BASEONLY:
baseonly = !!opts[n].value;
break;
}
/* Process local and distribution substitution files */
if (!path) {
rec->dist_subs = selabel_subs_init(selinux_file_context_subs_dist_path(), rec->dist_subs);
rec->subs = selabel_subs_init(selinux_file_context_subs_path(), rec->subs);
path = selinux_file_context_path();
} else {
snprintf(subs_file, sizeof(subs_file), "%s.subs_dist", path);
rec->dist_subs = selabel_subs_init(subs_file, rec->dist_subs);
snprintf(subs_file, sizeof(subs_file), "%s.subs", path);
rec->subs = selabel_subs_init(subs_file, rec->subs);
}
rec->spec_file = strdup(path);
/*
* The do detailed validation of the input and fill the spec array
*/
status = process_file(path, NULL, rec, prefix);
if (status)
goto finish;
if (rec->validating) {
status = nodups_specs(data, path);
if (status)
goto finish;
}
if (!baseonly) {
status = process_file(path, "homedirs", rec, prefix);
if (status && errno != ENOENT)
goto finish;
status = process_file(path, "local", rec, prefix);
if (status && errno != ENOENT)
goto finish;
}
status = sort_specs(data);
status = 0;
finish:
if (status)
free(data->spec_arr);
return status;
}
/*
* Backend interface routines
*/
static void closef(struct selabel_handle *rec)
{
struct saved_data *data = (struct saved_data *)rec->data;
struct mmap_area *area, *last_area;
struct spec *spec;
struct stem *stem;
unsigned int i;
for (i = 0; i < data->nspec; i++) {
spec = &data->spec_arr[i];
free(spec->lr.ctx_trans);
free(spec->lr.ctx_raw);
if (spec->from_mmap)
continue;
free(spec->regex_str);
free(spec->type_str);
if (spec->regcomp) {
pcre_free(spec->regex);
pcre_free_study(spec->sd);
}
}
for (i = 0; i < (unsigned int)data->num_stems; i++) {
stem = &data->stem_arr[i];
if (stem->from_mmap)
continue;
free(stem->buf);
}
if (data->spec_arr)
free(data->spec_arr);
if (data->stem_arr)
free(data->stem_arr);
area = data->mmap_areas;
while (area) {
munmap(area->addr, area->len);
last_area = area;
area = area->next;
free(last_area);
}
free(data);
}
static struct selabel_lookup_rec *lookup(struct selabel_handle *rec,
const char *key, int type)
{
struct saved_data *data = (struct saved_data *)rec->data;
struct spec *spec_arr = data->spec_arr;
int i, rc, file_stem;
mode_t mode = (mode_t)type;
const char *buf;
struct selabel_lookup_rec *ret = NULL;
char *clean_key = NULL;
const char *prev_slash, *next_slash;
unsigned int sofar = 0;
if (!data->nspec) {
errno = ENOENT;
goto finish;
}
/* Remove duplicate slashes */
if ((next_slash = strstr(key, "//"))) {
clean_key = malloc(strlen(key) + 1);
if (!clean_key)
goto finish;
prev_slash = key;
while (next_slash) {
memcpy(clean_key + sofar, prev_slash, next_slash - prev_slash);
sofar += next_slash - prev_slash;
prev_slash = next_slash + 1;
next_slash = strstr(prev_slash, "//");
}
strcpy(clean_key + sofar, prev_slash);
key = clean_key;
}
buf = key;
file_stem = find_stem_from_file(data, &buf);
mode &= S_IFMT;
/*
* Check for matching specifications in reverse order, so that
* the last matching specification is used.
*/
for (i = data->nspec - 1; i >= 0; i--) {
struct spec *spec = &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 ((spec->stem_id == -1 || spec->stem_id == file_stem) &&
(!mode || !spec->mode || mode == spec->mode)) {
if (compile_regex(data, spec, NULL) < 0)
goto finish;
if (spec->stem_id == -1)
rc = pcre_exec(spec->regex, get_pcre_extra(spec), key, strlen(key), 0, 0, NULL, 0);
else
rc = pcre_exec(spec->regex, get_pcre_extra(spec), buf, strlen(buf), 0, 0, NULL, 0);
if (rc == 0) {
spec->matches++;
break;
} else if (rc == PCRE_ERROR_NOMATCH)
continue;
errno = ENOENT;
/* else it's an error */
goto finish;
}
}
if (i < 0 || strcmp(spec_arr[i].lr.ctx_raw, "<<none>>") == 0) {
/* No matching specification. */
errno = ENOENT;
goto finish;
}
errno = 0;
ret = &spec_arr[i].lr;
finish:
free(clean_key);
return ret;
}
static void stats(struct selabel_handle *rec)
{
struct saved_data *data = (struct saved_data *)rec->data;
unsigned int i, nspec = data->nspec;
struct spec *spec_arr = data->spec_arr;
for (i = 0; i < nspec; i++) {
if (spec_arr[i].matches == 0) {
if (spec_arr[i].type_str) {
COMPAT_LOG(SELINUX_WARNING,
"Warning! No matches for (%s, %s, %s)\n",
spec_arr[i].regex_str,
spec_arr[i].type_str,
spec_arr[i].lr.ctx_raw);
} else {
COMPAT_LOG(SELINUX_WARNING,
"Warning! No matches for (%s, %s)\n",
spec_arr[i].regex_str,
spec_arr[i].lr.ctx_raw);
}
}
}
}
int selabel_file_init(struct selabel_handle *rec, struct selinux_opt *opts,
unsigned nopts)
{
struct saved_data *data;
data = (struct saved_data *)malloc(sizeof(*data));
if (!data)
return -1;
memset(data, 0, sizeof(*data));
rec->data = data;
rec->func_close = &closef;
rec->func_stats = &stats;
rec->func_lookup = &lookup;
return init(rec, opts, nopts);
}