[PATCH v3] libselinux: prevent ReDoS in file context regex matching

Petr Matyas <[email protected]> Thu, 30 Jul 2026 15:56:01 +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), 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) for both complete and partial matching modes.
   pcre2_match() uses the JIT automatically when available, which avoids
   the interpreter's susceptibility to catastrophic backtracking.
   Note: pcre2_jit_match() is deliberately NOT used as it is known to
   produce incorrect results on some platforms (e.g. aarch64).

2. Create a shared pcre2_match_context with a backtrack step limit of
   10 000 000 and pass it to 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]>
---
 libselinux/src/regex.c | 35 ++++++++++++++++++++++++++++++++++-
 1 file changed, 34 insertions(+), 1 deletion(-)

diff --git a/libselinux/src/regex.c b/libselinux/src/regex.c
index 7a4c5ecc..3f7df4c7 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. pcre2_match() uses the
+	 * JIT automatically when available, avoiding the interpreter's
+	 * susceptibility to catastrophic backtracking. Failures are non-fatal. */
+	(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,14 @@ int regex_match(struct regex_data *regex, char const *subject, int partial)
 			return REGEX_ERROR;
 	}
 
+	/* Use pcre2_match() rather than pcre2_jit_match(): pcre2_match()
+	 * automatically uses the JIT when the code has been compiled with
+	 * pcre2_jit_compile(), while pcre2_jit_match() is known to produce
+	 * incorrect results on some platforms (e.g. aarch64). */
 	rc = pcre2_match(regex->regex, (PCRE2_SPTR)subject,
 			 PCRE2_ZERO_TERMINATED, 0,
-			 partial ? PCRE2_PARTIAL_SOFT : 0, match_data, NULL);
+			 partial ? PCRE2_PARTIAL_SOFT : 0, match_data,
+			 match_context);
 
 	if (slow)
 		pcre2_match_data_free(match_data);
@@ -275,6 +307,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