/*
* File contexts backend for labeling system
*
* Author : Eamon Walsh <ewalsh@tycho.nsa.gov>
* Author : Stephen Smalley <sds@tycho.nsa.gov>
*/
#include <assert.h>
#include <fcntl.h>
#include <stdarg.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <pcre.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.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)) {
selinux_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 {
selinux_log
(SELINUX_ERROR,
"%s: Multiple same specifications for %s.\n",
path, curr_spec->regex_str);
}
}
}
}
return rc;
}
static int load_mmap(struct selabel_handle *rec, const char *path,
struct stat *sb, bool isbinary)
{
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, *str_buf;
size_t len;
int *stem_map;
struct mmap_area *mmap_area;
uint32_t i, magic, version;
uint32_t entry_len, stem_map_len, regex_array_len;
if (isbinary) {
len = strlen(path);
if (len >= sizeof(mmap_path))
return -1;
strcpy(mmap_path, path);
} else {
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;
}
/* 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 = mmap_area->next_addr = addr;
mmap_area->len = mmap_area->next_len = len;
mmap_area->next = data->mmap_areas;
data->mmap_areas = mmap_area;
/* check if this looks like an fcontext file */
rc = next_entry(&magic, mmap_area, sizeof(uint32_t));
if (rc < 0 || magic != SELINUX_MAGIC_COMPILED_FCONTEXT)
return -1;
/* check if this version is higher than we understand */
rc = next_entry(&version, mmap_area, sizeof(uint32_t));
if (rc < 0 || version > SELINUX_COMPILED_FCONTEXT_MAX_VERS)
return -1;
if (version >= SELINUX_COMPILED_FCONTEXT_PCRE_VERS) {
len = strlen(pcre_version());
rc = next_entry(&entry_len, mmap_area, sizeof(uint32_t));
if (rc < 0)
return -1;
/* Check version lengths */
if (len != entry_len)
return -1;
/* Check if pcre version mismatch */
str_buf = malloc(entry_len + 1);
if (!str_buf)
return -1;
rc = next_entry(str_buf, mmap_area, entry_len);
if (rc < 0) {
free(str_buf);
return -1;
}
str_buf[entry_len] = '\0';
if ((strcmp(str_buf, pcre_version()) != 0)) {
free(str_buf);
return -1;
}
free(str_buf);
}
/* allocate the stems_data array */
rc = next_entry(&stem_map_len, mmap_area, sizeof(uint32_t));
if (rc < 0 || !stem_map_len)
return -1;
/*
* map indexed by the stem # in the mmap file and contains the stem
* number in the data stem_arr
*/
stem_map = calloc(stem_map_len, sizeof(*stem_map));
if (!stem_map)
return -1;
for (i = 0; i < stem_map_len; i++) {
char *buf;
uint32_t stem_len;
int newid;
/* the length does not inlude the nul */
rc = next_entry(&stem_len, mmap_area, sizeof(uint32_t));
if (rc < 0 || !stem_len) {
rc = -1;
goto err;
}
/* Check for stem_len wrap around. */
if (stem_len < UINT32_MAX) {
buf = (char *)mmap_area->next_addr;
/* Check if over-run before null check. */
rc = next_entry(NULL, mmap_area, (stem_len + 1));
if (rc < 0)
goto err;
if (buf[stem_len] != '\0') {
rc = -1;
goto err;
}
} else {
rc = -1;
goto err;
}
/* 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 */
rc = next_entry(®ex_array_len, mmap_area, sizeof(uint32_t));
if (rc < 0 || !regex_array_len) {
rc = -1;
goto err;
}
for (i = 0; i < regex_array_len; i++) {
struct spec *spec;
int32_t stem_id, meta_chars;
uint32_t mode = 0, prefix_len = 0;
rc = grow_specs(data);
if (rc < 0)
goto err;
spec = &data->spec_arr[data->nspec];
spec->from_mmap = 1;
spec->regcomp = 1;
/* Process context */
rc = next_entry(&entry_len, mmap_area, sizeof(uint32_t));
if (rc < 0 || !entry_len) {
rc = -1;
goto err;
}
str_buf = malloc(entry_len);
if (!str_buf) {
rc = -1;
goto err;
}
rc = next_entry(str_buf, mmap_area, entry_len);
if (rc < 0)
goto err;
if (str_buf[entry_len - 1] != '\0') {
free(str_buf);
rc = -1;
goto err;
}
spec->lr.ctx_raw = str_buf;
if (strcmp(spec->lr.ctx_raw, "<<none>>") && rec->validating) {
if (selabel_validate(rec, &spec->lr) < 0) {
selinux_log(SELINUX_ERROR,
"%s: context %s is invalid\n", mmap_path, spec->lr.ctx_raw);
goto err;
}
}
/* Process regex string */
rc = next_entry(&entry_len, mmap_area, sizeof(uint32_t));
if (rc < 0 || !entry_len) {
rc = -1;
goto err;
}
spec->regex_str = (char *)mmap_area->next_addr;
rc = next_entry(NULL, mmap_area, entry_len);
if (rc < 0)
goto err;
if (spec->regex_str[entry_len - 1] != '\0') {
rc = -1;
goto err;
}
/* Process mode */
if (version >= SELINUX_COMPILED_FCONTEXT_MODE)
rc = next_entry(&mode, mmap_area, sizeof(uint32_t));
else
rc = next_entry(&mode, mmap_area, sizeof(mode_t));
if (rc < 0)
goto err;
spec->mode = mode;
/* map the stem id from the mmap file to the data->stem_arr */
rc = next_entry(&stem_id, mmap_area, sizeof(int32_t));
if (rc < 0)
goto err;
if (stem_id < 0 || stem_id >= (int32_t)stem_map_len)
spec->stem_id = -1;
else
spec->stem_id = stem_map[stem_id];
/* retrieve the hasMetaChars bit */
rc = next_entry(&meta_chars, mmap_area, sizeof(uint32_t));
if (rc < 0)
goto err;
spec->hasMetaChars = meta_chars;
/* and prefix length for use by selabel_lookup_best_match */
if (version >= SELINUX_COMPILED_FCONTEXT_PREFIX_LEN) {
rc = next_entry(&prefix_len, mmap_area,
sizeof(uint32_t));
if (rc < 0)
goto err;
spec->prefix_len = prefix_len;
}
/* Process regex and study_data entries */
rc = next_entry(&entry_len, mmap_area, sizeof(uint32_t));
if (rc < 0 || !entry_len) {
rc = -1;
goto err;
}
spec->regex = (pcre *)mmap_area->next_addr;
rc = next_entry(NULL, mmap_area, entry_len);
if (rc < 0)
goto err;
/* Check that regex lengths match. pcre_fullinfo()
* also validates its magic number. */
rc = pcre_fullinfo(spec->regex, NULL, PCRE_INFO_SIZE, &len);
if (rc < 0 || len != entry_len) {
rc = -1;
goto err;
}
rc = next_entry(&entry_len, mmap_area, sizeof(uint32_t));
if (rc < 0 || !entry_len) {
rc = -1;
goto err;
}
spec->lsd.study_data = (void *)mmap_area->next_addr;
spec->lsd.flags |= PCRE_EXTRA_STUDY_DATA;
rc = next_entry(NULL, mmap_area, entry_len);
if (rc < 0)
goto err;
/* Check that study data lengths match. */
rc = pcre_fullinfo(spec->regex, &spec->lsd,
PCRE_INFO_STUDYSIZE, &len);
if (rc < 0 || len != entry_len) {
rc = -1;
goto err;
}
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 = 0;
char *line_buf = NULL;
int rc;
char stack_path[PATH_MAX + 1];
bool isbinary = false;
uint32_t magic;
/* 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. */
fp = fopen(path, "r");
if (fp) {
if (fstat(fileno(fp), &sb) < 0)
return -1;
if (!S_ISREG(sb.st_mode)) {
errno = EINVAL;
return -1;
}
if (fread(&magic, sizeof magic, 1, fp) != 1) {
errno = EINVAL;
fclose(fp);
return -1;
}
if (magic == SELINUX_MAGIC_COMPILED_FCONTEXT) {
/* file_contexts.bin format */
fclose(fp);
fp = NULL;
isbinary = true;
} else {
rewind(fp);
}
} else {
/*
* Text file does not exist, so clear the timestamp
* so that we will always pass the timestamp comparison
* with the bin file in load_mmap().
*/
sb.st_mtime = 0;
}
rc = load_mmap(rec, path, &sb, isbinary);
if (rc == 0)
goto out;
if (!fp)
return -1; /* no text or bin file */
/*
* Then do detailed validation of the input and fill the spec array
*/
lineno = 0;
rc = 0;
while (getline(&line_buf, &line_len, fp) > 0) {
rc = process_line(rec, path, prefix, line_buf, ++lineno);
if (rc)
goto out;
}
out:
free(line_buf);
if (fp)
fclose(fp);
return rc;
}
static void closef(struct selabel_handle *rec);
static int init(struct selabel_handle *rec, const struct selinux_opt *opts,
unsigned n)
{
struct saved_data *data = (struct saved_data *)rec->data;
const char *path = NULL;
const char *prefix = NULL;
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;
}
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);
finish:
if (status)
closef(rec);
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 spec *lookup_common(struct selabel_handle *rec,
const char *key,
int type,
bool partial)
{
struct saved_data *data = (struct saved_data *)rec->data;
struct spec *spec_arr = data->spec_arr;
int i, rc, file_stem, pcre_options = 0;
mode_t mode = (mode_t)type;
const char *buf;
struct spec *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 = (char *) 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;
if (partial)
pcre_options |= PCRE_PARTIAL_SOFT;
/*
* 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,
pcre_options, NULL, 0);
else
rc = pcre_exec(spec->regex,
get_pcre_extra(spec),
buf, strlen(buf), 0,
pcre_options, NULL, 0);
if (rc == 0) {
spec->matches++;
break;
} else if (partial && rc == PCRE_ERROR_PARTIAL)
break;
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];
finish:
free(clean_key);
return ret;
}
static struct selabel_lookup_rec *lookup(struct selabel_handle *rec,
const char *key, int type)
{
struct spec *spec;
spec = lookup_common(rec, key, type, false);
if (spec)
return &spec->lr;
return NULL;
}
static bool partial_match(struct selabel_handle *rec, const char *key)
{
return lookup_common(rec, key, 0, true) ? true : false;
}
static struct selabel_lookup_rec *lookup_best_match(struct selabel_handle *rec,
const char *key,
const char **aliases,
int type)
{
size_t n, i;
int best = -1;
struct spec **specs;
size_t prefix_len = 0;
struct selabel_lookup_rec *lr = NULL;
if (!aliases || !aliases[0])
return lookup(rec, key, type);
for (n = 0; aliases[n]; n++)
;
specs = calloc(n+1, sizeof(struct spec *));
if (!specs)
return NULL;
specs[0] = lookup_common(rec, key, type, false);
if (specs[0]) {
if (!specs[0]->hasMetaChars) {
/* exact match on key */
lr = &specs[0]->lr;
goto out;
}
best = 0;
prefix_len = specs[0]->prefix_len;
}
for (i = 1; i <= n; i++) {
specs[i] = lookup_common(rec, aliases[i-1], type, false);
if (specs[i]) {
if (!specs[i]->hasMetaChars) {
/* exact match on alias */
lr = &specs[i]->lr;
goto out;
}
if (specs[i]->prefix_len > prefix_len) {
best = i;
prefix_len = specs[i]->prefix_len;
}
}
}
if (best >= 0) {
/* longest fixed prefix match on key or alias */
lr = &specs[best]->lr;
} else {
errno = ENOENT;
}
out:
free(specs);
return lr;
}
static enum selabel_cmp_result incomp(struct spec *spec1, struct spec *spec2, const char *reason, int i, int j)
{
selinux_log(SELINUX_INFO,
"selabel_cmp: mismatched %s on entry %d: (%s, %x, %s) vs entry %d: (%s, %x, %s)\n",
reason,
i, spec1->regex_str, spec1->mode, spec1->lr.ctx_raw,
j, spec2->regex_str, spec2->mode, spec2->lr.ctx_raw);
return SELABEL_INCOMPARABLE;
}
static enum selabel_cmp_result cmp(struct selabel_handle *h1,
struct selabel_handle *h2)
{
struct saved_data *data1 = (struct saved_data *)h1->data;
struct saved_data *data2 = (struct saved_data *)h2->data;
unsigned int i, nspec1 = data1->nspec, j, nspec2 = data2->nspec;
struct spec *spec_arr1 = data1->spec_arr, *spec_arr2 = data2->spec_arr;
struct stem *stem_arr1 = data1->stem_arr, *stem_arr2 = data2->stem_arr;
bool skipped1 = false, skipped2 = false;
i = 0;
j = 0;
while (i < nspec1 && j < nspec2) {
struct spec *spec1 = &spec_arr1[i];
struct spec *spec2 = &spec_arr2[j];
/*
* Because sort_specs() moves exact pathnames to the
* end, we might need to skip over additional regex
* entries that only exist in one of the configurations.
*/
if (!spec1->hasMetaChars && spec2->hasMetaChars) {
j++;
skipped2 = true;
continue;
}
if (spec1->hasMetaChars && !spec2->hasMetaChars) {
i++;
skipped1 = true;
continue;
}
if (spec1->regcomp && spec2->regcomp) {
size_t len1, len2;
int rc;
rc = pcre_fullinfo(spec1->regex, NULL, PCRE_INFO_SIZE, &len1);
assert(rc == 0);
rc = pcre_fullinfo(spec2->regex, NULL, PCRE_INFO_SIZE, &len2);
assert(rc == 0);
if (len1 != len2 ||
memcmp(spec1->regex, spec2->regex, len1))
return incomp(spec1, spec2, "regex", i, j);
} else {
if (strcmp(spec1->regex_str, spec2->regex_str))
return incomp(spec1, spec2, "regex_str", i, j);
}
if (spec1->mode != spec2->mode)
return incomp(spec1, spec2, "mode", i, j);
if (spec1->stem_id == -1 && spec2->stem_id != -1)
return incomp(spec1, spec2, "stem_id", i, j);
if (spec2->stem_id == -1 && spec1->stem_id != -1)
return incomp(spec1, spec2, "stem_id", i, j);
if (spec1->stem_id != -1 && spec2->stem_id != -1) {
struct stem *stem1 = &stem_arr1[spec1->stem_id];
struct stem *stem2 = &stem_arr2[spec2->stem_id];
if (stem1->len != stem2->len ||
strncmp(stem1->buf, stem2->buf, stem1->len))
return incomp(spec1, spec2, "stem", i, j);
}
if (strcmp(spec1->lr.ctx_raw, spec2->lr.ctx_raw))
return incomp(spec1, spec2, "ctx_raw", i, j);
i++;
j++;
}
if ((skipped1 || i < nspec1) && !skipped2)
return SELABEL_SUPERSET;
if ((skipped2 || j < nspec2) && !skipped1)
return SELABEL_SUBSET;
if (skipped1 && skipped2)
return SELABEL_INCOMPARABLE;
return SELABEL_EQUAL;
}
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) {
selinux_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 {
selinux_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, const 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;
rec->func_partial_match = &partial_match;
rec->func_lookup_best_match = &lookup_best_match;
rec->func_cmp = &cmp;
return init(rec, opts, nopts);
}