@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 2b3006076c
Fork 0

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,