[PATCH v2] libselinux: prevent ReDoS in file context regex matching
Petr Matyas <[email protected]> Thu, 30 Jul 2026 10:57:13 +0200
| Newsgroups | org.kernel.vger.selinux |
|---|---|
| Message-ID | <[email protected]> |
The PCRE2 interpreter is susceptible to catastrophic backtracking on crafted file context patterns or lookup keys. Since libselinux never called pcre2_jit_compile(3) or pcre2_jit_match(3), the JIT was available but unused, leaving the interpreter exposed on all platforms. Fix with two complementary mitigations: 1. Call pcre2_jit_compile() after every pattern compilation (including mmap deserialization) to engage JIT for both complete and partial matching. regex_match() now calls pcre2_jit_match() and falls back to pcre2_match() only on PCRE2_ERROR_JIT_BADOPTION (JIT unavailable or not compiled for the requested mode). 2. Create a shared pcre2_match_context with a backtrack step limit of 10 000 000 and pass it to both pcre2_jit_match() and the fallback pcre2_match(). PCRE2_ERROR_MATCHLIMIT is treated as no-match to keep the lookup safe rather than failing noisily. This bounds worst-case matching time to milliseconds for any pattern/subject combination, regardless of JIT availability. The vulnerability was reproduced and confirmed fixed on: - RHIVOS 2.0 / aarch64 - RHEL 10.3 / x86_64 - Fedora 44 / x86_64 Reproducer (149 bytes, base64): AwovMi0GLy8GCygKeAYGKwYLKAoKCgAAAPpXKgsoCi8vBgs8PG5vbmU+PgovMi0GLy8GCygKeAYG KwYLKAoKCiNXLy8wCwsoCgojV1dXClcqCygKLy8vV1cueHhXV1d4eHgveCsGCygKCgonVy9XV3cy eHgveAcGJAsoCi8tLwYvMi0GLy8GCzw8bm9uZT4+3q2+7y8= Signed-off-by: Petr Matyas <[email protected]> libsepol/cil: Return SEPOL_ERR on failure to set bit The functions cil_typepermissive_to_policydb() and cil_typeneveraudit_to_policydb() return rc when ebitmap_set_bit() fails, but the rc value at the point is SEPOL_OK. --- libselinux/src/regex.c | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/libselinux/src/regex.c b/libselinux/src/regex.c index 7a4c5ecc..af4c4328 100644 --- a/libselinux/src/regex.c +++ b/libselinux/src/regex.c @@ -41,8 +41,17 @@ static pthread_once_t match_data_key_once = PTHREAD_ONCE_INIT; static int match_data_key_alloc_failed = 0; static int match_data_key_created = 0; static pthread_once_t once = PTHREAD_ONCE_INIT; +static pcre2_match_context *match_context; static char arch_string_buffer[32]; +/* + * Limit the number of backtrack steps to prevent catastrophic backtracking + * (ReDoS) from crafted file context patterns or lookup keys. 10 million steps + * is generous for legitimate file context patterns while bounding worst-case + * matching time to milliseconds. + */ +#define REGEX_MATCH_LIMIT 10000000U + static void regex_arch_string_init(void) { char const *endianness; @@ -94,6 +103,12 @@ int regex_prepare_data(struct regex_data **regex, char const *pattern_string, goto err; } + /* JIT-compile for complete and partial matching to avoid catastrophic + * backtracking in the PCRE2 interpreter. Failures are non-fatal: + * regex_match() falls back to pcre2_match() on PCRE2_ERROR_JIT_BADOPTION. */ + (void)pcre2_jit_compile((*regex)->regex, + PCRE2_JIT_COMPLETE | PCRE2_JIT_PARTIAL_SOFT); + return 0; err: @@ -148,6 +163,10 @@ int regex_load_mmap(struct mmap_area *mmap_area, struct regex_data **regex, if (rc != 1) goto err; + (void)pcre2_jit_compile((*regex)->regex, + PCRE2_JIT_COMPLETE | + PCRE2_JIT_PARTIAL_SOFT); + *regex_compiled = true; } @@ -212,10 +231,18 @@ static void match_data_thread_free(void *ptr) static void match_data_key_init(void) { + pcre2_match_context *mctx; + if (__selinux_key_create(&match_data_key, match_data_thread_free) == 0) match_data_key_created = 1; else match_data_key_alloc_failed = 1; + + mctx = pcre2_match_context_create(NULL); + if (mctx) { + pcre2_set_match_limit(mctx, REGEX_MATCH_LIMIT); + match_context = mctx; + } } static void __attribute__((destructor)) match_data_key_destroy(void) @@ -262,9 +289,15 @@ int regex_match(struct regex_data *regex, char const *subject, int partial) return REGEX_ERROR; } - rc = pcre2_match(regex->regex, (PCRE2_SPTR)subject, - PCRE2_ZERO_TERMINATED, 0, - partial ? PCRE2_PARTIAL_SOFT : 0, match_data, NULL); + rc = pcre2_jit_match(regex->regex, (PCRE2_SPTR)subject, + PCRE2_ZERO_TERMINATED, 0, + partial ? PCRE2_PARTIAL_SOFT : 0, match_data, + match_context); + if (rc == PCRE2_ERROR_JIT_BADOPTION || rc == PCRE2_ERROR_JIT_STACKLIMIT) + rc = pcre2_match(regex->regex, (PCRE2_SPTR)subject, + PCRE2_ZERO_TERMINATED, 0, + partial ? PCRE2_PARTIAL_SOFT : 0, match_data, + match_context); if (slow) pcre2_match_data_free(match_data); @@ -275,6 +308,7 @@ int regex_match(struct regex_data *regex, char const *subject, int partial) case PCRE2_ERROR_PARTIAL: return REGEX_MATCH_PARTIAL; case PCRE2_ERROR_NOMATCH: + case PCRE2_ERROR_MATCHLIMIT: return REGEX_NO_MATCH; default: return REGEX_ERROR; -- 2.55.0