libselinux: rework selabel_file(5) database

Currently the database for file backend of selabel stores the file
context specifications in a single long array.  This array is sorted by
special precedence rules, e.g. regular expressions without meta
character first, ordered by length, and the remaining regular
expressions ordered by stem (the prefix part of the regular expressions
without meta characters) length.

This results in suboptimal lookup performance for two reasons;
File context specifications without any meta characters (e.g.
'/etc/passwd') are still matched via an expensive regular expression
match operation.
All such trivial regular expressions are matched against before any non-
trivial regular expression, resulting in thousands of regex match
operations for lookups for paths not matching any of the trivial ones.

Rework the internal representation of the database in two ways:
Convert regular expressions without any meta characters and containing
only supported escaped characters (e.g. '/etc/rc\.d/init\.d') into
literal strings, which get compared via strcmp(3) later on.
Store the specifications in a tree structure to reduce the to number of
specifications that need to be checked.

Since the internal representation is completely rewritten introduce a
new compiled file context file format mirroring the tree structure.
The new format also stores all multi-byte data in network byte-order, so
that such compiled files can be cross-compiled, e.g. for embedded
devices with read-only filesystems (except for the regular expressions,
which are still architecture-dependent, but ignored on architecture mis-
match).

The improved lookup performance will also benefit SELinux aware daemons,
which create files with their default context, e.g. systemd.

Fedora 41 (pre-compiled regular expressions are omitted on Fedora):
    file_contexts.bin:           567248  ->   413191  (bytes)
    file_contexts.homedirs.bin:   20677  ->    13107  (bytes)

Debian Sid (pre-compiled regular expressions are included):
    file_contexts.bin:          7790690  ->  3646256  (bytes)
    file_contexts.homedirs.bin:  835950  ->   708793  (bytes)

(selabel_lookup -b file -k /bin/bash)

Fedora 41 in VM:
    text:      time:       7.2 ms  ->   3.5 ms
               peak heap:   2.33M  ->    1.81M
               peak rss:    6.64M  ->    6.37M
    compiled:  time:       5.9 ms  ->   1.6 ms
               peak heap:   2.14M  ->    1.23M
               peak rss:    6.76M  ->    5.91M

Debian Sid on Raspberry Pi 3:
    text:      time:      33.4 ms  ->  21.2 ms
               peak heap:  10.59M  ->  607.32K
               peak rss:    6.55M  ->    4.46M
    compiled:  time:      38.3 ms  ->  23.5 ms
               peak heap:  13.28M  ->    2.00M
               peak rss:   12.21M  ->    7.60M

(restorecon -vRn /)

Fedora 41 in VM:
       9.6 s  ->   1.3 s
Debian Sid on Raspberry Pi 3:
      94.6 s  ->  12.1 s

(restorecon -vRn -T0 /)

Fedora 39 in VM (8 cores):
      10.9 s  ->   1.0 s
Debian Sid on Raspberry Pi 3 (4 cores):
      58.9 s  ->  12.6 s

(note: I am unsure why the parallel runs on Fedora are slower)

There might be subtle differences in lookup results which evaded my
testing, because some precedence rules are oblique.  For example
`/usr/(.*/)?lib(/.*)?` has to have a higher precedence than
`/usr/(.*/)?bin(/.*)?` to match the current Fedora behavior.  Please
report any behavior changes.

The maximum node depth in the database is set to 3, which seems to give
the best performance to memory usage ratio.  Might be tweaked for
systems with different filesystem hierarchies (Android?).

I am not that familiar with the selabel_partial_match(3),
selabel_get_digests_all_partial_matches(3) and
selabel_hash_all_partial_matches(3) related interfaces, so I only did
some rudimentary tests for them.

CC: Petr Lautrbach <plautrba@redhat.com>
CC: James Carter <jwcart2@gmail.com>
CC: Stephen Smalley <stephen.smalley.work@gmail.com>
Signed-off-by: Christian Göttsche <cgzones@googlemail.com>
Acked-by: James Carter <jwcart2@gmail.com>
This commit is contained in:
Christian Göttsche 2024-11-05 19:33:16 +01:00 committed by James Carter
parent 90b1c237a5
commit 92306daf52
7 changed files with 2779 additions and 1185 deletions

View File

@ -91,7 +91,7 @@ static int process_line(struct selabel_handle *rec,
unsigned int nspec = data->nspec; unsigned int nspec = data->nspec;
const char *errbuf = NULL; const char *errbuf = NULL;
items = read_spec_entries(line_buf, &errbuf, 2, &prop, &context); items = read_spec_entries(line_buf, strlen(line_buf), &errbuf, 2, &prop, &context);
if (items < 0) { if (items < 0) {
if (errbuf) { if (errbuf) {
selinux_log(SELINUX_ERROR, selinux_log(SELINUX_ERROR,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -71,8 +71,8 @@ extern void digest_gen_hash(struct selabel_digest *digest);
struct selabel_lookup_rec { struct selabel_lookup_rec {
char * ctx_raw; char * ctx_raw;
char * ctx_trans; char * ctx_trans;
int validated; unsigned int lineno;
unsigned lineno; bool validated;
}; };
struct selabel_handle { struct selabel_handle {
@ -143,6 +143,6 @@ compat_validate(const struct selabel_handle *rec,
* The read_spec_entries function may be used to * The read_spec_entries function may be used to
* replace sscanf to read entries from spec files. * replace sscanf to read entries from spec files.
*/ */
extern int read_spec_entries(char *line_buf, const char **errbuf, int num_args, ...); extern int read_spec_entries(char *line_buf, size_t nread, const char **errbuf, int num_args, ...);
#endif /* _SELABEL_INTERNAL_H_ */ #endif /* _SELABEL_INTERNAL_H_ */

View File

@ -4,6 +4,7 @@
* Author : Richard Haines <richard_c_haines@btinternet.com> * Author : Richard Haines <richard_c_haines@btinternet.com>
*/ */
#include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdarg.h> #include <stdarg.h>
#include <ctype.h> #include <ctype.h>
@ -21,10 +22,11 @@
* errno will be set. * errno will be set.
* *
*/ */
static inline int read_spec_entry(char **entry, char **ptr, int *len, const char **errbuf) static inline int read_spec_entry(char **entry, const char **ptr, size_t *len, const char **errbuf)
{ {
const char *tmp_buf;
*entry = NULL; *entry = NULL;
char *tmp_buf = NULL;
while (isspace((unsigned char)**ptr) && **ptr != '\0') while (isspace((unsigned char)**ptr) && **ptr != '\0')
(*ptr)++; (*ptr)++;
@ -43,6 +45,9 @@ static inline int read_spec_entry(char **entry, char **ptr, int *len, const char
} }
if (*len) { if (*len) {
if (*len >= UINT16_MAX)
return -1;
*entry = strndup(tmp_buf, *len); *entry = strndup(tmp_buf, *len);
if (!*entry) if (!*entry)
return -1; return -1;
@ -62,22 +67,23 @@ static inline int read_spec_entry(char **entry, char **ptr, int *len, const char
* This function calls read_spec_entry() to do the actual string processing. * This function calls read_spec_entry() to do the actual string processing.
* As such, can return anything from that function as well. * As such, can return anything from that function as well.
*/ */
int read_spec_entries(char *line_buf, const char **errbuf, int num_args, ...) int read_spec_entries(char *line_buf, size_t nread, const char **errbuf, int num_args, ...)
{ {
char **spec_entry, *buf_p; char **spec_entry;
int len, rc, items, entry_len = 0; const char *buf_p;
size_t entry_len = 0;
int rc, items;
va_list ap; va_list ap;
*errbuf = NULL; *errbuf = NULL;
len = strlen(line_buf); if (line_buf[nread - 1] == '\n')
if (line_buf[len - 1] == '\n') line_buf[nread - 1] = '\0';
line_buf[len - 1] = '\0';
else else
/* Handle case if line not \n terminated by bumping /* Handle case if line not \n terminated by bumping
* the len for the check below (as the line is NUL * the len for the check below (as the line is NUL
* terminated by getline(3)) */ * terminated by getline(3)) */
len++; nread++;
buf_p = line_buf; buf_p = line_buf;
while (isspace((unsigned char)*buf_p)) while (isspace((unsigned char)*buf_p))
@ -94,7 +100,7 @@ int read_spec_entries(char *line_buf, const char **errbuf, int num_args, ...)
while (items < num_args) { while (items < num_args) {
spec_entry = va_arg(ap, char **); spec_entry = va_arg(ap, char **);
if (len - 1 == buf_p - line_buf) { if (buf_p[0] == '\0' || nread - 1 == (size_t)(buf_p - line_buf)) {
va_end(ap); va_end(ap);
return items; return items;
} }

View File

@ -1,4 +1,5 @@
#include <assert.h> #include <assert.h>
#include <endian.h>
#include <pthread.h> #include <pthread.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
@ -18,7 +19,6 @@
/* If the compiler doesn't define __BYTE_ORDER__, try to use the C /* If the compiler doesn't define __BYTE_ORDER__, try to use the C
* library <endian.h> header definitions. */ * library <endian.h> header definitions. */
#include <endian.h>
#ifndef __BYTE_ORDER #ifndef __BYTE_ORDER
#error Neither __BYTE_ORDER__ nor __BYTE_ORDER defined. Unable to determine endianness. #error Neither __BYTE_ORDER__ nor __BYTE_ORDER defined. Unable to determine endianness.
#endif #endif
@ -116,13 +116,15 @@ int regex_load_mmap(struct mmap_area *mmap_area, struct regex_data **regex,
int do_load_precompregex, bool *regex_compiled) int do_load_precompregex, bool *regex_compiled)
{ {
int rc; int rc;
uint32_t entry_len; uint32_t data_u32, entry_len;
*regex_compiled = false; *regex_compiled = false;
rc = next_entry(&entry_len, mmap_area, sizeof(uint32_t)); rc = next_entry(&data_u32, mmap_area, sizeof(uint32_t));
if (rc < 0) if (rc < 0)
return -1; return -1;
entry_len = be32toh(data_u32);
if (entry_len && do_load_precompregex) { if (entry_len && do_load_precompregex) {
/* /*
* this should yield exactly one because we store one pattern at * this should yield exactly one because we store one pattern at
@ -169,7 +171,7 @@ int regex_writef(struct regex_data *regex, FILE *fp, int do_write_precompregex)
int rc = 0; int rc = 0;
size_t len; size_t len;
PCRE2_SIZE serialized_size; PCRE2_SIZE serialized_size;
uint32_t to_write = 0; uint32_t to_write = 0, data_u32;
PCRE2_UCHAR *bytes = NULL; PCRE2_UCHAR *bytes = NULL;
if (do_write_precompregex) { if (do_write_precompregex) {
@ -177,14 +179,15 @@ int regex_writef(struct regex_data *regex, FILE *fp, int do_write_precompregex)
rc = pcre2_serialize_encode((const pcre2_code **)&regex->regex, rc = pcre2_serialize_encode((const pcre2_code **)&regex->regex,
1, &bytes, &serialized_size, NULL); 1, &bytes, &serialized_size, NULL);
if (rc != 1 || serialized_size >= UINT32_MAX) { if (rc != 1 || serialized_size >= UINT32_MAX) {
rc = -1; rc = -3;
goto out; goto out;
} }
to_write = serialized_size; to_write = serialized_size;
} }
/* write serialized pattern's size */ /* write serialized pattern's size */
len = fwrite(&to_write, sizeof(uint32_t), 1, fp); data_u32 = htobe32(to_write);
len = fwrite(&data_u32, sizeof(uint32_t), 1, fp);
if (len != 1) { if (len != 1) {
rc = -1; rc = -1;
goto out; goto out;
@ -355,11 +358,15 @@ int regex_load_mmap(struct mmap_area *mmap_area, struct regex_data **regex,
int do_load_precompregex __attribute__((unused)), bool *regex_compiled) int do_load_precompregex __attribute__((unused)), bool *regex_compiled)
{ {
int rc; int rc;
uint32_t entry_len; uint32_t data_u32, entry_len;
size_t info_len; size_t info_len;
rc = next_entry(&entry_len, mmap_area, sizeof(uint32_t)); rc = next_entry(&data_u32, mmap_area, sizeof(uint32_t));
if (rc < 0 || !entry_len) if (rc < 0)
return -1;
entry_len = be32toh(data_u32);
if (!entry_len)
return -1; return -1;
*regex = regex_data_create(); *regex = regex_data_create();
@ -380,10 +387,12 @@ int regex_load_mmap(struct mmap_area *mmap_area, struct regex_data **regex,
if (rc < 0 || info_len != entry_len) if (rc < 0 || info_len != entry_len)
goto err; goto err;
rc = next_entry(&entry_len, mmap_area, sizeof(uint32_t)); rc = next_entry(&data_u32, mmap_area, sizeof(uint32_t));
if (rc < 0) if (rc < 0)
goto err; goto err;
entry_len = be32toh(data_u32);
if (entry_len) { if (entry_len) {
(*regex)->lsd.study_data = (void *)mmap_area->next_addr; (*regex)->lsd.study_data = (void *)mmap_area->next_addr;
(*regex)->lsd.flags |= PCRE_EXTRA_STUDY_DATA; (*regex)->lsd.flags |= PCRE_EXTRA_STUDY_DATA;
@ -424,45 +433,45 @@ int regex_writef(struct regex_data *regex, FILE *fp,
{ {
int rc; int rc;
size_t len; size_t len;
uint32_t to_write; uint32_t data_u32;
size_t size; size_t size;
pcre_extra *sd = get_pcre_extra(regex); pcre_extra *sd = get_pcre_extra(regex);
/* determine the size of the pcre data in bytes */ /* determine the size of the pcre data in bytes */
rc = pcre_fullinfo(regex->regex, NULL, PCRE_INFO_SIZE, &size); rc = pcre_fullinfo(regex->regex, NULL, PCRE_INFO_SIZE, &size);
if (rc < 0) if (rc < 0 || size >= UINT32_MAX)
return -1; return -3;
/* write the number of bytes in the pcre data */ /* write the number of bytes in the pcre data */
to_write = size; data_u32 = htobe32(size);
len = fwrite(&to_write, sizeof(uint32_t), 1, fp); len = fwrite(&data_u32, sizeof(uint32_t), 1, fp);
if (len != 1) if (len != 1)
return -1; return -1;
/* write the actual pcre data as a char array */ /* write the actual pcre data as a char array */
len = fwrite(regex->regex, 1, to_write, fp); len = fwrite(regex->regex, 1, size, fp);
if (len != to_write) if (len != size)
return -1; return -1;
if (sd) { if (sd) {
/* determine the size of the pcre study info */ /* determine the size of the pcre study info */
rc = rc =
pcre_fullinfo(regex->regex, sd, PCRE_INFO_STUDYSIZE, &size); pcre_fullinfo(regex->regex, sd, PCRE_INFO_STUDYSIZE, &size);
if (rc < 0) if (rc < 0 || size >= UINT32_MAX)
return -1; return -3;
} else } else
size = 0; size = 0;
/* write the number of bytes in the pcre study data */ /* write the number of bytes in the pcre study data */
to_write = size; data_u32 = htobe32(size);
len = fwrite(&to_write, sizeof(uint32_t), 1, fp); len = fwrite(&data_u32, sizeof(uint32_t), 1, fp);
if (len != 1) if (len != 1)
return -1; return -1;
if (sd) { if (sd) {
/* write the actual pcre study data as a char array */ /* write the actual pcre study data as a char array */
len = fwrite(sd->study_data, 1, to_write, fp); len = fwrite(sd->study_data, 1, size, fp);
if (len != to_write) if (len != size)
return -1; return -1;
} }

View File

@ -1,25 +1,25 @@
#include <ctype.h> #include <endian.h>
#include <errno.h> #include <errno.h>
#include <getopt.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <getopt.h>
#include <limits.h>
#include <selinux/selinux.h> #include <selinux/selinux.h>
#include <sepol/sepol.h> #include <sepol/sepol.h>
#include "../src/avc_sidtab.h"
#include "../src/label_file.h" #include "../src/label_file.h"
#include "../src/regex.h" #include "../src/regex.h"
static const char *policy_file; static const char *policy_file;
static int ctx_err; static int ctx_err;
static int validate_context(char **ctxp) static int validate_context(char **ctxp)
{ {
char *ctx = *ctxp; const char *ctx = *ctxp;
if (policy_file && sepol_check_context(ctx) < 0) { if (policy_file && sepol_check_context(ctx) < 0) {
ctx_err = -1; ctx_err = -1;
@ -35,20 +35,20 @@ static int process_file(struct selabel_handle *rec, const char *filename)
int rc; int rc;
char *line_buf = NULL; char *line_buf = NULL;
size_t line_len = 0; size_t line_len = 0;
ssize_t nread;
FILE *context_file; FILE *context_file;
const char *prefix = NULL; const char *prefix = NULL;
context_file = fopen(filename, "r"); context_file = fopen(filename, "re");
if (!context_file) { if (!context_file) {
fprintf(stderr, "Error opening %s: %s\n", fprintf(stderr, "Error opening %s: %m\n", filename);
filename, strerror(errno));
return -1; return -1;
} }
line_num = 0; line_num = 0;
rc = 0; rc = 0;
while (getline(&line_buf, &line_len, context_file) > 0) { while ((nread = getline(&line_buf, &line_len, context_file)) > 0) {
rc = process_line(rec, filename, prefix, line_buf, ++line_num); rc = process_line(rec, filename, prefix, line_buf, nread, ++line_num);
if (rc || ctx_err) { if (rc || ctx_err) {
/* With -p option need to check and fail if ctx err as /* With -p option need to check and fail if ctx err as
* process_line() context validation on Linux does not * process_line() context validation on Linux does not
@ -65,211 +65,445 @@ out:
return rc; return rc;
} }
static int literal_spec_to_sidtab(const struct literal_spec *lspec, struct sidtab *stab)
{
security_id_t dummy;
return sidtab_context_to_sid(stab, lspec->lr.ctx_raw, &dummy);
}
static int regex_spec_to_sidtab(const struct regex_spec *rspec, struct sidtab *stab)
{
security_id_t dummy;
return sidtab_context_to_sid(stab, rspec->lr.ctx_raw, &dummy);
}
static int spec_node_to_sidtab(const struct spec_node *node, struct sidtab *stab)
{
int rc;
for (uint32_t i = 0; i < node->literal_specs_num; i++) {
rc = literal_spec_to_sidtab(&node->literal_specs[i], stab);
if (rc)
return rc;
}
for (uint32_t i = 0; i < node->regex_specs_num; i++) {
rc = regex_spec_to_sidtab(&node->regex_specs[i], stab);
if (rc)
return rc;
}
for (uint32_t i = 0; i < node->children_num; i++) {
rc = spec_node_to_sidtab(&node->children[i], stab);
if (rc)
return rc;
}
return 0;
}
static int create_sidtab(const struct saved_data *data, struct sidtab *stab)
{
int rc;
rc = sidtab_init(stab);
if (rc < 0)
return rc;
return spec_node_to_sidtab(data->root, stab);
}
/* /*
* File Format * File Format
* *
* The format uses network byte-order.
*
* u32 - magic number * u32 - magic number
* u32 - version * u32 - version
* u32 - length of pcre version EXCLUDING nul * u32 - length of upcoming pcre version EXCLUDING nul
* char - pcre version string EXCLUDING nul * [char] - pcre version string EXCLUDING nul
* u32 - number of stems * u32 - length of upcoming pcre architecture EXCLUDING nul
* ** Stems * [char] - pcre architecture string EXCLUDING nul
* u32 - length of stem EXCLUDING nul * u64 - number of total specifications
* char - stem char array INCLUDING nul * u32 - number of upcoming context definitions
* u32 - number of regexs * [Ctx] - array of context definitions
* ** Regexes * Node - root node
* u32 - length of upcoming context INCLUDING nul *
* char - char array of the raw context * Context Definition Format (Ctx)
* u32 - length of the upcoming regex_str *
* char - char array of the original regex string including the stem. * u16 - length of upcoming raw context EXCLUDING nul
* u32 - mode bits for >= SELINUX_COMPILED_FCONTEXT_MODE * [char] - char array of the raw context EXCLUDING nul
* mode_t for <= SELINUX_COMPILED_FCONTEXT_PCRE_VERS *
* s32 - stemid associated with the regex * Node Format
* u32 - spec has meta characters *
* u32 - The specs prefix_len if >= SELINUX_COMPILED_FCONTEXT_PREFIX_LEN * u16 - length of upcoming stem INCLUDING nul
* u32 - data length of the pcre regex * [char] - stem char array INCLUDING nul
* char - a buffer holding the raw pcre regex info * u32 - number of upcoming literal specifications
* u32 - data length of the pcre regex study daya * [LSpec] - array of literal specifications
* char - a buffer holding the raw pcre regex study data * u32 - number of upcoming regular expression specifications
* [RSpec] - array of regular expression specifications
* u32 - number of upcoming child nodes
* [Node] - array of child nodes
*
* Literal Specification Format (LSpec)
*
* u32 - context table index for raw context (1-based)
* u16 - length of upcoming regex_str INCLUDING nul
* [char] - char array of the original regex string including the stem INCLUDING nul
* u16 - length of upcoming literal match INCLUDING nul
* [char] - char array of the simplified literal match INCLUDING nul
* u8 - file kind (LABEL_FILE_KIND_*)
*
* Regular Expression Specification Format (RSpec)
*
* u32 - context table index for raw context (1-based)
* u16 - length of upcoming regex_str INCLUDING nul
* [char] - char array of the original regex string including the stem INCLUDING nul
* u16 - length of the fixed path prefix
* u8 - file kind (LABEL_FILE_KIND_*)
* [Regex] - serialized pattern of regex, subject to underlying regex library
*/ */
static int write_binary_file(struct saved_data *data, int fd,
int do_write_precompregex)
{
struct spec *specs = data->spec_arr;
FILE *bin_file;
size_t len;
uint32_t magic = SELINUX_MAGIC_COMPILED_FCONTEXT;
uint32_t section_len;
uint32_t i;
int rc;
const char *reg_version;
const char *reg_arch;
bin_file = fdopen(fd, "w");
static int security_id_compare(const void *a, const void *b)
{
const struct security_id *sid_a = a, *sid_b = b;
return (sid_a->id > sid_b->id) - (sid_a->id < sid_b->id);
}
static int write_sidtab(FILE *bin_file, const struct sidtab *stab)
{
struct security_id *sids;
uint32_t data_u32, index;
uint16_t data_u16;
size_t len;
/* write number of entries */
data_u32 = htobe32(stab->nel);
len = fwrite(&data_u32, sizeof(uint32_t), 1, bin_file);
if (len != 1)
return -1;
/* sort entries by id */
sids = calloc(stab->nel, sizeof(*sids));
if (!sids)
return -1;
index = 0;
for (unsigned i = 0; i < SIDTAB_SIZE; i++) {
const struct sidtab_node *cur = stab->htable[i];
while (cur) {
sids[index++] = cur->sid_s;
cur = cur->next;
}
}
assert(index == stab->nel);
qsort(sids, stab->nel, sizeof(struct security_id), security_id_compare);
assert(sids[0].id == 1);
assert(sids[stab->nel - 1].id == stab->nel);
/* write raw contexts sorted by id */
for (uint32_t i = 0; i < stab->nel; i++) {
const char *ctx = sids[i].ctx;
size_t ctx_len = strlen(ctx);
if (ctx_len == 0 || ctx_len >= UINT16_MAX) {
free(sids);
return -2;
}
data_u16 = htobe16(ctx_len);
len = fwrite(&data_u16, sizeof(uint16_t), 1, bin_file);
if (len != 1) {
free(sids);
return -1;
}
len = fwrite(ctx, sizeof(char), ctx_len, bin_file);
if (len != ctx_len) {
free(sids);
return -1;
}
}
free(sids);
return 0;
}
static int write_literal_spec(FILE *bin_file, const struct literal_spec *lspec, const struct sidtab *stab)
{
const struct security_id *sid;
const char *orig_regex, *literal_match;
size_t orig_regex_len, literal_match_len;
uint32_t data_u32;
uint16_t data_u16;
uint8_t data_u8;
size_t len;
/* write raw context sid */
sid = sidtab_context_lookup(stab, lspec->lr.ctx_raw);
assert(sid); /* should be set via create_sidtab() */
data_u32 = htobe32(sid->id);
len = fwrite(&data_u32, sizeof(uint32_t), 1, bin_file);
if (len != 1)
return -1;
/* write original regex string */
orig_regex = lspec->regex_str;
orig_regex_len = strlen(orig_regex);
if (orig_regex_len == 0 || orig_regex_len >= UINT16_MAX)
return -2;
orig_regex_len += 1;
data_u16 = htobe16(orig_regex_len);
len = fwrite(&data_u16, sizeof(uint16_t), 1, bin_file);
if (len != 1)
return -1;
len = fwrite(orig_regex, sizeof(char), orig_regex_len, bin_file);
if (len != orig_regex_len)
return -1;
/* write literal match string */
literal_match = lspec->literal_match;
literal_match_len = strlen(literal_match);
if (literal_match_len == 0 || literal_match_len >= UINT16_MAX)
return -2;
literal_match_len += 1;
data_u16 = htobe16(literal_match_len);
len = fwrite(&data_u16, sizeof(uint16_t), 1, bin_file);
if (len != 1)
return -1;
len = fwrite(literal_match, sizeof(char), literal_match_len, bin_file);
if (len != literal_match_len)
return -1;
/* write file kind */
data_u8 = lspec->file_kind;
len = fwrite(&data_u8, sizeof(uint8_t), 1, bin_file);
if (len != 1)
return -1;
return 0;
}
static int write_regex_spec(FILE *bin_file, bool do_write_precompregex, const struct regex_spec *rspec, const struct sidtab *stab)
{
const struct security_id *sid;
const char *regex;
size_t regex_len;
uint32_t data_u32;
uint16_t data_u16;
uint8_t data_u8;
size_t len;
int rc;
/* write raw context sid */
sid = sidtab_context_lookup(stab, rspec->lr.ctx_raw);
assert(sid); /* should be set via create_sidtab() */
data_u32 = htobe32(sid->id);
len = fwrite(&data_u32, sizeof(uint32_t), 1, bin_file);
if (len != 1)
return -1;
/* write regex string */
regex = rspec->regex_str;
regex_len = strlen(regex);
if (regex_len == 0 || regex_len >= UINT16_MAX)
return -2;
regex_len += 1;
data_u16 = htobe16(regex_len);
len = fwrite(&data_u16, sizeof(uint16_t), 1, bin_file);
if (len != 1)
return -1;
len = fwrite(regex, sizeof(char), regex_len, bin_file);
if (len != regex_len)
return -1;
/* write prefix length */
data_u16 = htobe16(rspec->prefix_len);
len = fwrite(&data_u16, sizeof(uint16_t), 1, bin_file);
if (len != 1)
return -1;
/* write file kind */
data_u8 = rspec->file_kind;
len = fwrite(&data_u8, sizeof(uint8_t), 1, bin_file);
if (len != 1)
return -1;
/* Write serialized regex */
rc = regex_writef(rspec->regex, bin_file, do_write_precompregex);
if (rc < 0)
return rc;
return 0;
}
static int write_spec_node(FILE *bin_file, bool do_write_precompregex, const struct spec_node *node, const struct sidtab *stab)
{
size_t stem_len;
uint32_t data_u32;
uint16_t data_u16;
size_t len;
int rc;
stem_len = node->stem_len;
if ((stem_len == 0 && node->parent) || stem_len >= UINT16_MAX)
return -2;
stem_len += 1;
data_u16 = htobe16(stem_len);
len = fwrite(&data_u16, sizeof(uint16_t), 1, bin_file);
if (len != 1)
return -1;
len = fwrite(node->stem ?: "", sizeof(char), stem_len, bin_file);
if (len != stem_len)
return -1;
/* write number of literal specs */
data_u32 = htobe32(node->literal_specs_num);
len = fwrite(&data_u32, sizeof(uint32_t), 1, bin_file);
if (len != 1)
return -1;
/* write literal specs */
for (uint32_t i = 0; i < node->literal_specs_num; i++) {
rc = write_literal_spec(bin_file, &node->literal_specs[i], stab);
if (rc)
return rc;
}
/* write number of regex specs */
data_u32 = htobe32(node->regex_specs_num);
len = fwrite(&data_u32, sizeof(uint32_t), 1, bin_file);
if (len != 1)
return -1;
/* write regex specs */
for (uint32_t i = 0; i < node->regex_specs_num; i++) {
rc = write_regex_spec(bin_file, do_write_precompregex, &node->regex_specs[i], stab);
if (rc)
return rc;
}
/* write number of child nodes */
data_u32 = htobe32(node->children_num);
len = fwrite(&data_u32, sizeof(uint32_t), 1, bin_file);
if (len != 1)
return -1;
/* write child nodes */
for (uint32_t i = 0; i < node->children_num; i++) {
rc = write_spec_node(bin_file, do_write_precompregex, &node->children[i], stab);
if (rc)
return rc;
}
return 0;
}
static int write_binary_file(const struct saved_data *data, const struct sidtab *stab,
int fd, const char *path, bool do_write_precompregex,
const char *progname)
{
FILE *bin_file;
const char *reg_arch, *reg_version;
size_t len, reg_arch_len, reg_version_len;
uint64_t data_u64;
uint32_t data_u32;
int rc;
bin_file = fdopen(fd, "we");
if (!bin_file) { if (!bin_file) {
perror("fopen output_file"); fprintf(stderr, "%s: failed to open %s: %m\n", progname, path);
exit(EXIT_FAILURE); close(fd);
return -1;
} }
/* write some magic number */ /* write some magic number */
len = fwrite(&magic, sizeof(uint32_t), 1, bin_file); data_u32 = htobe32(SELINUX_MAGIC_COMPILED_FCONTEXT);
len = fwrite(&data_u32, sizeof(uint32_t), 1, bin_file);
if (len != 1) if (len != 1)
goto err; goto err_write;
/* write the version */ /* write the version */
section_len = SELINUX_COMPILED_FCONTEXT_MAX_VERS; data_u32 = htobe32(SELINUX_COMPILED_FCONTEXT_MAX_VERS);
len = fwrite(&section_len, sizeof(uint32_t), 1, bin_file); len = fwrite(&data_u32, sizeof(uint32_t), 1, bin_file);
if (len != 1) if (len != 1)
goto err; goto err_write;
/* write version of the regex back-end */ /* write version of the regex back-end */
reg_version = regex_version(); reg_version = regex_version();
if (!reg_version) if (!reg_version)
goto err; goto err_check;
section_len = strlen(reg_version); reg_version_len = strlen(reg_version);
len = fwrite(&section_len, sizeof(uint32_t), 1, bin_file); if (reg_version_len == 0 || reg_version_len >= UINT32_MAX)
goto err_check;
data_u32 = htobe32(reg_version_len);
len = fwrite(&data_u32, sizeof(uint32_t), 1, bin_file);
if (len != 1) if (len != 1)
goto err; goto err_write;
len = fwrite(reg_version, sizeof(char), section_len, bin_file); len = fwrite(reg_version, sizeof(char), reg_version_len, bin_file);
if (len != section_len) if (len != reg_version_len)
goto err; goto err_write;
/* write regex arch string */ /* write regex arch string */
reg_arch = regex_arch_string(); reg_arch = regex_arch_string();
if (!reg_arch) if (!reg_arch)
goto err; goto err_check;
section_len = strlen(reg_arch); reg_arch_len = strlen(reg_arch);
len = fwrite(&section_len, sizeof(uint32_t), 1, bin_file); if (reg_arch_len == 0 || reg_arch_len >= UINT32_MAX)
goto err_check;
data_u32 = htobe32(reg_arch_len);
len = fwrite(&data_u32, sizeof(uint32_t), 1, bin_file);
if (len != 1) if (len != 1)
goto err; goto err_write;
len = fwrite(reg_arch, sizeof(char), section_len, bin_file); len = fwrite(reg_arch, sizeof(char), reg_arch_len, bin_file);
if (len != section_len) if (len != reg_arch_len)
goto err; goto err_write;
/* write the number of stems coming */ /* write number of total specifications */
section_len = data->num_stems; data_u64 = htobe64(data->num_specs);
len = fwrite(&section_len, sizeof(uint32_t), 1, bin_file); len = fwrite(&data_u64, sizeof(uint64_t), 1, bin_file);
if (len != 1) if (len != 1)
goto err; goto err_write;
for (i = 0; i < section_len; i++) { /* write context table */
char *stem = data->stem_arr[i].buf; rc = write_sidtab(bin_file, stab);
uint32_t stem_len = data->stem_arr[i].len; if (rc)
/* write the strlen (aka no nul) */
len = fwrite(&stem_len, sizeof(uint32_t), 1, bin_file);
if (len != 1)
goto err;
/* include the nul in the file */
stem_len += 1;
len = fwrite(stem, sizeof(char), stem_len, bin_file);
if (len != stem_len)
goto err;
}
/* write the number of regexes coming */
section_len = data->nspec;
len = fwrite(&section_len, sizeof(uint32_t), 1, bin_file);
if (len != 1)
goto err;
for (i = 0; i < section_len; i++) {
char *context = specs[i].lr.ctx_raw;
char *regex_str = specs[i].regex_str;
mode_t mode = specs[i].mode;
size_t prefix_len = specs[i].prefix_len;
int32_t stem_id = specs[i].stem_id;
struct regex_data *re = specs[i].regex;
uint32_t to_write;
/* length of the context string (including nul) */
to_write = strlen(context) + 1;
len = fwrite(&to_write, sizeof(uint32_t), 1, bin_file);
if (len != 1)
goto err;
/* original context string (including nul) */
len = fwrite(context, sizeof(char), to_write, bin_file);
if (len != to_write)
goto err;
/* length of the original regex string (including nul) */
to_write = strlen(regex_str) + 1;
len = fwrite(&to_write, sizeof(uint32_t), 1, bin_file);
if (len != 1)
goto err;
/* original regex string */
len = fwrite(regex_str, sizeof(char), to_write, bin_file);
if (len != to_write)
goto err;
/* binary F_MODE bits */
to_write = mode;
len = fwrite(&to_write, sizeof(uint32_t), 1, bin_file);
if (len != 1)
goto err;
/* stem for this regex (could be -1) */
len = fwrite(&stem_id, sizeof(stem_id), 1, bin_file);
if (len != 1)
goto err;
/* does this spec have a metaChar? */
to_write = specs[i].hasMetaChars;
len = fwrite(&to_write, sizeof(to_write), 1, bin_file);
if (len != 1)
goto err;
/* For SELINUX_COMPILED_FCONTEXT_PREFIX_LEN */
to_write = prefix_len;
len = fwrite(&to_write, sizeof(to_write), 1, bin_file);
if (len != 1)
goto err;
/* Write regex related data */
rc = regex_writef(re, bin_file, do_write_precompregex);
if (rc < 0)
goto err;
}
rc = 0;
out:
fclose(bin_file);
return rc; return rc;
err:
rc = write_spec_node(bin_file, do_write_precompregex, data->root, stab);
if (rc)
goto err;
out:
if (fclose(bin_file) && rc == 0) {
fprintf(stderr, "%s: failed to close %s: %m\n", progname, path);
rc = -1; rc = -1;
}
return rc;
err_check:
rc = -2;
goto err;
err_write:
rc = -1;
goto err;
err:
fprintf(stderr, "%s: failed to compile file context specifications: %s\n",
progname,
(rc == -3) ? "regex serialization failure" :
((rc == -2) ? "invalid fcontext specification" : "write failure"));
goto out; goto out;
} }
static void free_specs(struct saved_data *data)
{
struct spec *specs = data->spec_arr;
unsigned int num_entries = data->nspec;
unsigned int i;
for (i = 0; i < num_entries; i++) {
free(specs[i].lr.ctx_raw);
free(specs[i].lr.ctx_trans);
free(specs[i].regex_str);
free(specs[i].type_str);
regex_data_free(specs[i].regex);
}
free(specs);
num_entries = data->num_stems;
for (i = 0; i < num_entries; i++)
free(data->stem_arr[i].buf);
free(data->stem_arr);
memset(data, 0, sizeof(*data));
}
static __attribute__ ((__noreturn__)) void usage(const char *progname) static __attribute__ ((__noreturn__)) void usage(const char *progname)
{ {
fprintf(stderr, fprintf(stderr,
"usage: %s [-o out_file] [-p policy_file] fc_file\n" "usage: %s [-iV] [-o out_file] [-p policy_file] fc_file\n"
"Where:\n\t" "Where:\n\t"
"-o Optional file name of the PCRE formatted binary\n\t" "-o Optional file name of the PCRE formatted binary\n\t"
" file to be output. If not specified the default\n\t" " file to be output. If not specified the default\n\t"
@ -287,6 +521,7 @@ static __attribute__ ((__noreturn__)) void usage(const char *progname)
" Arch identifier format (PCRE2):\n\t" " Arch identifier format (PCRE2):\n\t"
" <pointer width>-<size type width>-<endianness>, e.g.,\n\t" " <pointer width>-<size type width>-<endianness>, e.g.,\n\t"
" \"8-8-el\" for x86_64.\n\t" " \"8-8-el\" for x86_64.\n\t"
"-V Print binary output format version and exit.\n\t"
"fc_file The text based file contexts file to be processed.\n", "fc_file The text based file contexts file to be processed.\n",
progname); progname);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
@ -294,21 +529,24 @@ static __attribute__ ((__noreturn__)) void usage(const char *progname)
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
const char *path = NULL; const char *path;
const char *out_file = NULL; const char *out_file = NULL;
int do_write_precompregex = 1; bool do_write_precompregex = true;
char stack_path[PATH_MAX + 1]; char stack_path[PATH_MAX + 1];
char *tmp = NULL; char *tmp = NULL;
size_t len;
int fd, rc, opt; int fd, rc, opt;
FILE *policy_fp = NULL; FILE *policy_fp = NULL;
struct stat buf; struct stat buf;
struct selabel_handle *rec = NULL; struct selabel_handle *rec = NULL;
struct saved_data *data = NULL; struct saved_data *data = NULL;
struct spec_node *root = NULL;
struct sidtab stab = {};
if (argc < 2) if (argc < 2)
usage(argv[0]); usage(argv[0]);
while ((opt = getopt(argc, argv, "io:p:r")) > 0) { while ((opt = getopt(argc, argv, "io:p:rV")) > 0) {
switch (opt) { switch (opt) {
case 'o': case 'o':
out_file = optarg; out_file = optarg;
@ -317,18 +555,20 @@ int main(int argc, char *argv[])
policy_file = optarg; policy_file = optarg;
break; break;
case 'r': case 'r':
do_write_precompregex = 0; do_write_precompregex = false;
break; break;
case 'i': case 'i':
printf("%s (%s)\n", regex_version(), printf("%s (%s)\n", regex_version(), regex_arch_string());
regex_arch_string()); return 0;
case 'V':
printf("Compiled fcontext format version %d\n", SELINUX_COMPILED_FCONTEXT_MAX_VERS);
return 0; return 0;
default: default:
usage(argv[0]); usage(argv[0]);
} }
} }
if (optind >= argc) if (optind + 1 != argc)
usage(argv[0]); usage(argv[0]);
path = argv[optind]; path = argv[optind];
@ -339,7 +579,7 @@ int main(int argc, char *argv[])
/* Open binary policy if supplied. */ /* Open binary policy if supplied. */
if (policy_file) { if (policy_file) {
policy_fp = fopen(policy_file, "r"); policy_fp = fopen(policy_file, "re");
if (!policy_fp) { if (!policy_fp) {
fprintf(stderr, "%s: failed to open %s: %s\n", fprintf(stderr, "%s: failed to open %s: %s\n",
@ -373,7 +613,7 @@ int main(int argc, char *argv[])
* error is detected, the process will be aborted. */ * error is detected, the process will be aborted. */
rec->validating = 1; rec->validating = 1;
selinux_set_callback(SELINUX_CB_VALIDATE, selinux_set_callback(SELINUX_CB_VALIDATE,
(union selinux_callback)&validate_context); (union selinux_callback) { .func_validate = &validate_context });
data = (struct saved_data *)calloc(1, sizeof(*data)); data = (struct saved_data *)calloc(1, sizeof(*data));
if (!data) { if (!data) {
@ -384,6 +624,17 @@ int main(int argc, char *argv[])
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
root = calloc(1, sizeof(*root));
if (!root) {
fprintf(stderr, "%s: calloc failed: %s\n", argv[0], strerror(errno));
free(data);
free(rec);
if (policy_fp)
fclose(policy_fp);
exit(EXIT_FAILURE);
}
data->root = root;
rec->data = data; rec->data = data;
rc = process_file(rec, path); rc = process_file(rec, path);
@ -392,9 +643,11 @@ int main(int argc, char *argv[])
goto err; goto err;
} }
rc = sort_specs(data); sort_specs(data);
if (rc) {
fprintf(stderr, "%s: sort_specs failed\n", argv[0]); rc = create_sidtab(data, &stab);
if (rc < 0) {
fprintf(stderr, "%s: failed to generate sidtab: %s\n", argv[0], strerror(errno));
goto err; goto err;
} }
@ -403,40 +656,41 @@ int main(int argc, char *argv[])
else else
rc = snprintf(stack_path, sizeof(stack_path), "%s.bin", path); rc = snprintf(stack_path, sizeof(stack_path), "%s.bin", path);
if (rc < 0 || rc >= (int)sizeof(stack_path)) { if (rc < 0 || (size_t)rc >= sizeof(stack_path)) {
fprintf(stderr, "%s: snprintf failed\n", argv[0]); fprintf(stderr, "%s: snprintf failed\n", argv[0]);
goto err; goto err;
} }
len = rc;
tmp = malloc(strlen(stack_path) + 7); tmp = malloc(len + 7);
if (!tmp) { if (!tmp) {
fprintf(stderr, "%s: malloc failed: %s\n", argv[0], strerror(errno)); fprintf(stderr, "%s: malloc failed: %s\n", argv[0], strerror(errno));
goto err; goto err;
} }
rc = sprintf(tmp, "%sXXXXXX", stack_path); rc = snprintf(tmp, len + 7, "%sXXXXXX", stack_path);
if (rc < 0) { if (rc < 0 || (size_t)rc >= len + 7) {
fprintf(stderr, "%s: sprintf failed\n", argv[0]); fprintf(stderr, "%s: snprintf failed\n", argv[0]);
goto err; goto err;
} }
fd = mkstemp(tmp); fd = mkstemp(tmp);
if (fd < 0) { if (fd < 0) {
fprintf(stderr, "%s: mkstemp %s failed: %s\n", argv[0], tmp, strerror(errno)); fprintf(stderr, "%s: mkstemp %s failed: %s\n", argv[0], tmp, strerror(errno));
close(fd);
goto err; goto err;
} }
rc = fchmod(fd, buf.st_mode); rc = fchmod(fd, buf.st_mode);
if (rc < 0) { if (rc < 0) {
fprintf(stderr, "%s: fchmod %s failed: %s\n", argv[0], tmp, strerror(errno)); fprintf(stderr, "%s: fchmod %s failed: %s\n", argv[0], tmp, strerror(errno));
close(fd);
goto err_unlink; goto err_unlink;
} }
rc = write_binary_file(data, fd, do_write_precompregex); rc = write_binary_file(data, &stab, fd, tmp, do_write_precompregex, argv[0]);
if (rc < 0) { if (rc < 0)
fprintf(stderr, "%s: write_binary_file %s failed\n", argv[0], tmp);
goto err_unlink; goto err_unlink;
}
rc = rename(tmp, stack_path); rc = rename(tmp, stack_path);
if (rc < 0) { if (rc < 0) {
@ -449,9 +703,11 @@ out:
if (policy_fp) if (policy_fp)
fclose(policy_fp); fclose(policy_fp);
free_specs(data); sidtab_destroy(&stab);
free(rec); free_spec_node(data->root);
free(data->root);
free(data); free(data);
free(rec);
free(tmp); free(tmp);
return rc; return rc;