[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