A high-throughput and memory-efficient inference and serving engine for LLMs
[Security] Add timeout guard for regex compilation in structured outp… (#45118)
Signed-off-by: jperezde <jperezde@redhat.com> Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com> Co-authored-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Juan Pérez de Algaba committed
Jun 13, 2026 at 11:52 UTC
2b3006076c5e9bc4cda9e03e3641388de3c5c286
5 files changed
+126
-4
tests/v1/structured_output/test_regex_compilation_timeout.py
new
+61
@@ -0,0 +1,61 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Tests for regex compilation timeout guard.
4
+
5
+Verifies that adversarial regex patterns that would cause exponential
6
+DFA state-space explosion are rejected with a timeout rather than
7
+hanging indefinitely.
8
+
9
+Addresses advisory GHSA-rwxx-mrjm-wc2m.
10
+"""
11
+
12
+import time
13
+from unittest.mock import patch
14
+
15
+import pytest
16
+
17
+from vllm.v1.structured_output.utils import compile_regex_with_timeout
18
+
19
+
20
+class TestCompileRegexWithTimeout:
21
+ """Unit tests for the compile_regex_with_timeout utility."""
22
+
23
+ def test_normal_regex_compiles_successfully(self):
24
+ result = compile_regex_with_timeout(lambda pat: "compiled", r"[a-z]+")
25
+ assert result == "compiled"
26
+
27
+ def test_timeout_raises_value_error(self):
28
+ def slow_compile(pattern: str):
29
+ time.sleep(10)
30
+ return "never"
31
+
32
+ with (
33
+ patch("vllm.envs.VLLM_REGEX_COMPILATION_TIMEOUT_S", 1),
34
+ pytest.raises(ValueError, match="timed out"),
35
+ ):
36
+ compile_regex_with_timeout(slow_compile, r"(a+)+b")
37
+
38
+ def test_timeout_disabled_when_zero(self):
39
+ result = None
40
+ with patch("vllm.envs.VLLM_REGEX_COMPILATION_TIMEOUT_S", 0):
41
+ result = compile_regex_with_timeout(lambda pat: "no_timeout", r"(a+)+b")
42
+ assert result == "no_timeout"
43
+
44
+ def test_compilation_error_propagates(self):
45
+ def failing_compile(pattern: str):
46
+ raise RuntimeError("compilation failed")
47
+
48
+ with pytest.raises(RuntimeError, match="compilation failed"):
49
+ compile_regex_with_timeout(failing_compile, r"bad")
50
+
51
+ def test_pattern_included_in_error_message(self):
52
+ def slow_compile(pattern: str):
53
+ time.sleep(10)
54
+ return "never"
55
+
56
+ pattern = r"(a+)+b"
57
+ with (
58
+ patch("vllm.envs.VLLM_REGEX_COMPILATION_TIMEOUT_S", 1),
59
+ pytest.raises(ValueError, match=r"\(a\+\)\+b"),
60
+ ):
61
+ compile_regex_with_timeout(slow_compile, pattern)
vllm/envs.py
+8
@@ -188,6 +188,7 @@ if TYPE_CHECKING:
188
VLLM_FLASHINFER_ALLREDUCE_BACKEND: Literal["auto", "trtllm", "mnnvl"] = "auto"
189
VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE: int = 394 * 1024 * 1024
190
VLLM_XGRAMMAR_CACHE_MB: int = 0
191
+ VLLM_REGEX_COMPILATION_TIMEOUT_S: int = 5
192
VLLM_MSGPACK_ZERO_COPY_THRESHOLD: int = 256
193
VLLM_ALLOW_INSECURE_SERIALIZATION: bool = False
194
VLLM_DISABLE_REQUEST_ID_RANDOMIZATION: bool = False
@@ -1447,6 +1448,13 @@ environment_variables: dict[str, Callable[[], Any]] = {
1448
# of 512 MB should be enough for roughly 1000 JSON schemas.
1449
# It can be changed with this variable if needed for some reason.
1450
"VLLM_XGRAMMAR_CACHE_MB": lambda: int(os.getenv("VLLM_XGRAMMAR_CACHE_MB", "512")),
1451
+ # Maximum time in seconds allowed for regex compilation in structured
1452
+ # output backends (xgrammar, outlines). Prevents ReDoS attacks where
1453
+ # adversarial patterns cause exponential DFA state-space explosion.
1454
+ # Set to 0 to disable the timeout (not recommended in production).
1455
+ "VLLM_REGEX_COMPILATION_TIMEOUT_S": lambda: int(
1456
+ os.getenv("VLLM_REGEX_COMPILATION_TIMEOUT_S", "5")
1457
+ ),
1458
# Control the threshold for msgspec to use 'zero copy' for
1459
# serialization/deserialization of tensors. Tensors below
1460
# this limit will be encoded into the msgpack buffer, and
vllm/v1/structured_output/backend_outlines.py
+5
-1
@@ -23,6 +23,7 @@ from vllm.v1.structured_output.backend_types import (
23
)
24
from vllm.v1.structured_output.utils import (
25
OutlinesVocabulary,
26
+ compile_regex_with_timeout,
27
get_outlines_cache,
28
get_outlines_vocabulary,
29
)
@@ -61,7 +62,10 @@ class OutlinesBackend(StructuredOutputBackend):
62
if cache_key in self.cache:
63
return self.cache[cache_key]
64
64
- index = oc.Index(regex_string, vocabulary.inner)
65
+ index = compile_regex_with_timeout(
66
+ lambda pat: oc.Index(pat, vocabulary.inner),
67
+ regex_string,
68
+ )
69
self.cache[cache_key] = index
70
71
return index
vllm/v1/structured_output/backend_xgrammar.py
+9
-2
@@ -19,6 +19,7 @@ from vllm.v1.structured_output.backend_types import (
19
)
20
from vllm.v1.structured_output.utils import (
21
choice_as_grammar,
22
+ compile_regex_with_timeout,
23
convert_lark_to_ebnf,
24
grammar_is_likely_lark,
25
)
@@ -88,7 +89,10 @@ class XgrammarBackend(StructuredOutputBackend):
89
elif request_type == StructuredOutputOptions.GRAMMAR:
90
ctx = self.compiler.compile_grammar(grammar_spec)
91
elif request_type == StructuredOutputOptions.REGEX:
91
- ctx = self.compiler.compile_regex(grammar_spec)
92
+ ctx = compile_regex_with_timeout(
93
+ self.compiler.compile_regex,
94
+ grammar_spec,
95
+ )
96
elif request_type == StructuredOutputOptions.STRUCTURAL_TAG:
97
s_tag = json.loads(grammar_spec)
98
if "structures" in s_tag:
@@ -277,7 +281,10 @@ def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:
281
282
if so_params.regex:
283
try:
280
- xgr.Grammar.from_regex(so_params.regex)
284
+ compile_regex_with_timeout(
285
+ xgr.Grammar.from_regex,
286
+ so_params.regex,
287
+ )
288
except Exception as err:
289
raise ValueError(
290
f"Failed to transform regex into a grammar: {err}"
vllm/v1/structured_output/utils.py
+43
-1
@@ -6,7 +6,9 @@ import hashlib
6
import importlib.metadata
7
import os
8
import tempfile
9
-from typing import TYPE_CHECKING
9
+from collections.abc import Callable
10
+from concurrent.futures import ThreadPoolExecutor, TimeoutError
11
+from typing import TYPE_CHECKING, TypeVar
12
13
import numpy as np
14
import regex as re
@@ -38,9 +40,49 @@ else:
40
41
logger = init_logger(__name__)
42
43
+_T = TypeVar("_T")
44
+
45
CACHE = None
46
47
48
+def compile_regex_with_timeout(fn: Callable[[str], _T], pattern: str) -> _T:
49
+ """Run a regex compilation callable with a timeout.
50
+
51
+ Prevents ReDoS attacks where adversarial regex patterns (e.g. nested
52
+ quantifiers like ``(a+)+b``) cause exponential DFA state-space explosion,
53
+ hanging the inference worker indefinitely.
54
+
55
+ Args:
56
+ fn: Single-argument callable that takes the pattern and performs
57
+ the regex compilation.
58
+ pattern: The regex pattern string, passed to *fn* and included in
59
+ timeout error messages.
60
+
61
+ Raises:
62
+ ValueError: If compilation exceeds the configured timeout.
63
+ """
64
+ timeout = envs.VLLM_REGEX_COMPILATION_TIMEOUT_S
65
+ if timeout <= 0:
66
+ return fn(pattern)
67
+
68
+ executor = ThreadPoolExecutor(max_workers=1)
69
+ future = executor.submit(fn, pattern)
70
+ try:
71
+ result = future.result(timeout=timeout)
72
+ except TimeoutError:
73
+ future.cancel()
74
+ executor.shutdown(wait=False, cancel_futures=True)
75
+ raise ValueError(
76
+ f"Regex compilation timed out after {timeout}s. "
77
+ "The pattern may be too complex or contain constructs that "
78
+ "cause exponential state-space explosion (e.g. nested "
79
+ f"quantifiers). Pattern: {pattern[:200]}"
80
+ ) from None
81
+ else:
82
+ executor.shutdown(wait=False)
83
+ return result
84
+
85
+
86
def apply_grammar_bitmask(
87
scheduler_output: SchedulerOutput,
88
grammar_output: GrammarOutput,