Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions tests/v1/structured_output/test_regex_compilation_timeout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for regex compilation timeout guard.

Verifies that adversarial regex patterns that would cause exponential
DFA state-space explosion are rejected with a timeout rather than
hanging indefinitely.

Addresses advisory GHSA-rwxx-mrjm-wc2m.
"""

import time
from unittest.mock import patch

import pytest

from vllm.v1.structured_output.utils import compile_regex_with_timeout


class TestCompileRegexWithTimeout:
"""Unit tests for the compile_regex_with_timeout utility."""

def test_normal_regex_compiles_successfully(self):
result = compile_regex_with_timeout(lambda pat: "compiled", r"[a-z]+")
assert result == "compiled"

def test_timeout_raises_value_error(self):
def slow_compile(pattern: str):
time.sleep(10)
return "never"

with (
patch("vllm.envs.VLLM_REGEX_COMPILATION_TIMEOUT_S", 1),
pytest.raises(ValueError, match="timed out"),
):
compile_regex_with_timeout(slow_compile, r"(a+)+b")

def test_timeout_disabled_when_zero(self):
result = None
with patch("vllm.envs.VLLM_REGEX_COMPILATION_TIMEOUT_S", 0):
result = compile_regex_with_timeout(lambda pat: "no_timeout", r"(a+)+b")
assert result == "no_timeout"

def test_compilation_error_propagates(self):
def failing_compile(pattern: str):
raise RuntimeError("compilation failed")

with pytest.raises(RuntimeError, match="compilation failed"):
compile_regex_with_timeout(failing_compile, r"bad")

def test_pattern_included_in_error_message(self):
def slow_compile(pattern: str):
time.sleep(10)
return "never"

pattern = r"(a+)+b"
with (
patch("vllm.envs.VLLM_REGEX_COMPILATION_TIMEOUT_S", 1),
pytest.raises(ValueError, match=r"\(a\+\)\+b"),
):
compile_regex_with_timeout(slow_compile, pattern)
8 changes: 8 additions & 0 deletions vllm/envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@
VLLM_FLASHINFER_ALLREDUCE_BACKEND: Literal["auto", "trtllm", "mnnvl"] = "auto"
VLLM_FLASHINFER_WORKSPACE_BUFFER_SIZE: int = 394 * 1024 * 1024
VLLM_XGRAMMAR_CACHE_MB: int = 0
VLLM_REGEX_COMPILATION_TIMEOUT_S: int = 5
VLLM_MSGPACK_ZERO_COPY_THRESHOLD: int = 256
VLLM_ALLOW_INSECURE_SERIALIZATION: bool = False
VLLM_DISABLE_REQUEST_ID_RANDOMIZATION: bool = False
Expand Down Expand Up @@ -1446,6 +1447,13 @@ def _resolve_rust_frontend_path() -> str | None:
# of 512 MB should be enough for roughly 1000 JSON schemas.
# It can be changed with this variable if needed for some reason.
"VLLM_XGRAMMAR_CACHE_MB": lambda: int(os.getenv("VLLM_XGRAMMAR_CACHE_MB", "512")),
# Maximum time in seconds allowed for regex compilation in structured
# output backends (xgrammar, outlines). Prevents ReDoS attacks where
# adversarial patterns cause exponential DFA state-space explosion.
# Set to 0 to disable the timeout (not recommended in production).
"VLLM_REGEX_COMPILATION_TIMEOUT_S": lambda: int(
os.getenv("VLLM_REGEX_COMPILATION_TIMEOUT_S", "5")
),
# Control the threshold for msgspec to use 'zero copy' for
# serialization/deserialization of tensors. Tensors below
# this limit will be encoded into the msgpack buffer, and
Expand Down
6 changes: 5 additions & 1 deletion vllm/v1/structured_output/backend_outlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from vllm.v1.structured_output.utils import (
OutlinesVocabulary,
compile_regex_with_timeout,
get_outlines_cache,
get_outlines_vocabulary,
)
Expand Down Expand Up @@ -61,7 +62,10 @@ def _compile_index(
if cache_key in self.cache:
return self.cache[cache_key]

index = oc.Index(regex_string, vocabulary.inner)
index = compile_regex_with_timeout(
lambda pat: oc.Index(pat, vocabulary.inner),
regex_string,
)
self.cache[cache_key] = index

return index
Expand Down
11 changes: 9 additions & 2 deletions vllm/v1/structured_output/backend_xgrammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)
from vllm.v1.structured_output.utils import (
choice_as_grammar,
compile_regex_with_timeout,
convert_lark_to_ebnf,
grammar_is_likely_lark,
)
Expand Down Expand Up @@ -88,7 +89,10 @@ def compile_grammar(
elif request_type == StructuredOutputOptions.GRAMMAR:
ctx = self.compiler.compile_grammar(grammar_spec)
elif request_type == StructuredOutputOptions.REGEX:
ctx = self.compiler.compile_regex(grammar_spec)
ctx = compile_regex_with_timeout(
self.compiler.compile_regex,
grammar_spec,
)
elif request_type == StructuredOutputOptions.STRUCTURAL_TAG:
s_tag = json.loads(grammar_spec)
if "structures" in s_tag:
Expand Down Expand Up @@ -277,7 +281,10 @@ def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:

if so_params.regex:
try:
xgr.Grammar.from_regex(so_params.regex)
compile_regex_with_timeout(
xgr.Grammar.from_regex,
so_params.regex,
)
except Exception as err:
raise ValueError(
f"Failed to transform regex into a grammar: {err}"
Expand Down
44 changes: 43 additions & 1 deletion vllm/v1/structured_output/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import importlib.metadata
import os
import tempfile
from typing import TYPE_CHECKING
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from typing import TYPE_CHECKING, TypeVar

import numpy as np
import regex as re
Expand Down Expand Up @@ -38,9 +40,49 @@

logger = init_logger(__name__)

_T = TypeVar("_T")

CACHE = None


def compile_regex_with_timeout(fn: Callable[[str], _T], pattern: str) -> _T:
"""Run a regex compilation callable with a timeout.

Prevents ReDoS attacks where adversarial regex patterns (e.g. nested
quantifiers like ``(a+)+b``) cause exponential DFA state-space explosion,
hanging the inference worker indefinitely.

Args:
fn: Single-argument callable that takes the pattern and performs
the regex compilation.
pattern: The regex pattern string, passed to *fn* and included in
timeout error messages.

Raises:
ValueError: If compilation exceeds the configured timeout.
"""
timeout = envs.VLLM_REGEX_COMPILATION_TIMEOUT_S
if timeout <= 0:
return fn(pattern)

executor = ThreadPoolExecutor(max_workers=1)
future = executor.submit(fn, pattern)
try:
result = future.result(timeout=timeout)
except TimeoutError:
future.cancel()
executor.shutdown(wait=False, cancel_futures=True)
raise ValueError(
f"Regex compilation timed out after {timeout}s. "
"The pattern may be too complex or contain constructs that "
"cause exponential state-space explosion (e.g. nested "
f"quantifiers). Pattern: {pattern[:200]}"
) from None
else:
executor.shutdown(wait=False)
return result


def apply_grammar_bitmask(
scheduler_output: SchedulerOutput,
grammar_output: GrammarOutput,
Expand Down
Loading