Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

libselinux: rework selabel_file(5) database #406

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

cgzones
Copy link
Contributor

@cgzones cgzones commented Aug 11, 2023

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 (since the filesystem is a
tree) 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).

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

Performance data

Compiled file context sizes

Fedora 38 (regular expressions are omitted on Fedora):
file_contexts.bin: 596783 -> 575284 (bytes)
file_contexts.homedirs.bin: 21219 -> 18185 (bytes)

Debian Sid (regular expressions are included):
file_contexts.bin: 2580704 -> 1428354 (bytes)
file_contexts.homedirs.bin: 130946 -> 96884 (bytes)

Single lookup

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

Fedora 38 in VM:
text: time: 3.6 ms -> 4.7 ms
peak heap: 2.32M -> 1.44M
peak rss: 5.61M -> 6.03M
compiled: time: 1.5 ms -> 1.5 ms
peak heap: 2.14M -> 917.93K
peak rss: 5.33M -> 5.47M

Debian Sid on Raspberry Pi 3:
text: time: 33.9 ms -> 19.9 ms
peak heap: 10.46M -> 468.72K
peak rss: 9.44M -> 4.98M
compiled: time: 39.3 ms -> 22.8 ms
peak heap: 13.09M -> 1.86M
peak rss: 12.57M -> 7.86M

Full filesystem relabel

(restorecon -vRn /)

Fedora 38 in VM:
27.445 s -> 3.293 s
Debian Sid on Raspberry Pi 3:
86.734 s -> 10.810 s

(restorecon -vRn -T0 /)

Fedora 38 in VM (8 cores):
29.205 s -> 2.521 s
Debian Sid on Raspberry Pi 3 (4 cores):
46.974 s -> 10.728 s

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

TODO

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.

If any code section is unclear I am happy to add some inline comments.

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.

@cgzones cgzones force-pushed the label_lookup_perf branch 3 times, most recently from 5f14b69 to 90929c4 Compare August 14, 2023 13:13
@cgzones cgzones force-pushed the label_lookup_perf branch 3 times, most recently from 414c040 to 1b106eb Compare December 20, 2023 18:15
@cgzones cgzones force-pushed the label_lookup_perf branch 3 times, most recently from fa2f731 to 3d57563 Compare January 30, 2024 14:09
Introduce a helper to remove SELinux file security contexts.

Mainly for testing label operations, and only for SELinux disabled
systems, since removing file contexts is not supported by SELinux.

Signed-off-by: Christian Göttsche <[email protected]>
---
v2:
   move from libselinux/utils to policycoreutils and rename
Add a utility around selabel_cmp(3).

Can be used by users to compare a pre-compiled fcontext file to an
original text-based file context definition file.

Can be used for development to verify compilation and parsing of the
pre-compiled fcontext format works correctly.

Signed-off-by: Christian Göttsche <[email protected]>
---
v2:
   split nested block into own function
Use type unsigned for hash values, as returned by sidtab_hash().
Use size_t for buffer length and counting variables.
Constify stats parameter.

Signed-off-by: Christian Göttsche <[email protected]>
---
v2:
  add patch
Reinterpret the currently unused - and always initialized to 1 - member
refcnt of the struct security_id to hold a unique number identifying
the sidtab entry.  This identifier can be used instead of the full
context string within other data structures to minimize memory usage.

Signed-off-by: Christian Göttsche <[email protected]>
---
v2:
  add patch
Add sidtab_context_lookup() to just lookup a context, not inserting
non-existent ones.

Tweak sidtab_destroy() to accept a zero'ed struct sidtab.

Remove redundant lookup in sidtab_context_to_sid() after insertion by
returning the newly created node directly from sidtab_insert().

Drop declaration of only internal used sidtab_insert().

Signed-off-by: Christian Göttsche <[email protected]>
---
v3:
  use sidtab_context_lookup() in sidtab_context_to_sid()
v2:
  add patch
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 39 (pre-compiled regular expressions are omitted on Fedora):
    file_contexts.bin:           599034  ->   430211  (bytes)
    file_contexts.homedirs.bin:   21275  ->    13491  (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 39 in VM:
    text:      time:       3.1 ms  ->   3.6 ms
               peak heap:   2.33M  ->    1.81M
               peak rss:    6.64M  ->    6.37M
    compiled:  time:       1.8 ms  ->   1.7 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 39 in VM:
      28.3 s  ->   3.6 s
Debian Sid on Raspberry Pi 3:
      94.6 s  ->  12.1 s

(restorecon -vRn -T0 /)

Fedora 39 in VM (8 cores):
      31.1 s  ->   2.5 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 <[email protected]>
CC: James Carter <[email protected]>
CC: Stephen Smalley <[email protected]>
Signed-off-by: Christian Göttsche <[email protected]>
---
v3:
  - set errno to EINVAL on old compiled fcontext format file input
  - correctly compare regular expression specifications by considering
    their prefix-length
v2: misc issues revealed via fuzzing:
  - free root node via data pointer in sefcontext_compile
  - drop reachable assert in regex_simplify()
  - fix crash on unexpected NUL byte in read_spec_entries()
  - more strict checks when loading compiled format
  - do not return <<none>> context for literal specs
  - add missing partial support for literal specs
  - set errno on load_mmap() failure
  - support SELABEL_OPT_SUBSET
  - de-duplicate context strings into a separate array for the binary
    format
  - store an internal file kind instead of the whole mode_t
Due to the selabel_file(5) rework this code is no longer used.

Signed-off-by: Christian Göttsche <[email protected]>
Add two fuzzers reading and performing lookup on selabel_file(5)
databases.  One fuzzer takes input in form of a textual fcontext
definition, the other one takes compiled fcontexts definitions.  The
lookup key and whether to lookup any or a specific file type is also
part of the generated input.

CC: Evgeny Vereshchagin <[email protected]>
Signed-off-by: Christian Göttsche <[email protected]>
---
v2: add patch
Support the parallel usage of the translated label lookup via
selabel_lookup(3) in multi threaded applications by locking the step
of computing the translated context and the validation state.

A potential use case might can usage from a Rust application via FFI.

Signed-off-by: Christian Göttsche <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant