执行摘要
- 一句话:为正则编译添加超时防护,防止 ReDoS 攻击
- 推荐动作:该 PR 修复了安全漏洞,实现经过多轮 review,生命周期管理正确,测试覆盖完整,建议合并。值得关注的设计决策包括线程池手动生命周期管理和函数签名优化。
功能与动机
正则编译中的 ReDoS 漏洞(GHSA-rwxx-mrjm-wc2m)允许攻击者通过嵌套量词(如 (a+)+b)触发指数级 DFA 状态爆炸,导致 worker 线程永久挂起。本 PR 通过为编译添加可配置超时来防御此类攻击。
实现拆解
- 环境变量声明:在
vllm/envs.py 的 EnvVars 类和运行时解析块中分别定义 VLLM_REGEX_COMPILATION_TIMEOUT_S: int = 5,同时添加 os.getenv 读取逻辑,值为 0 表示禁用超时。
- 工具函数实现:在
vllm/v1/structured_output/utils.py 中新增 compile_regex_with_timeout。该函数接收一个编译可执行体 fn 和 pattern,创建一个 ThreadPoolExecutor(max_workers=1) 并提交任务,调用 future.result(timeout=timeout)。若超时,则取消 future 并立即关闭线程池(shutdown(wait=False, cancel_futures=True)),抛出 ValueError;否则正常关闭并返回结果。它不依赖上下文管理器,确保超时路径不会被阻塞。
- xgrammar 后端接入:修改
vllm/v1/structured_output/backend_xgrammar.py,在 compile_grammar 方法的 REGEX 分支中将 self.compiler.compile_regex(grammar_spec) 替换为 compile_regex_with_timeout(self.compiler.compile_regex, grammar_spec);在 validate_xgrammar_grammar 函数中将 xgr.Grammar.from_regex(so_params.regex) 替换为 compile_regex_with_timeout(xgr.Grammar.from_regex, so_params.regex),并移除冗余的 except ValueError: raise 分支。
- outlines 后端接入:修改
vllm/v1/structured_output/backend_outlines.py 的 _compile_index 方法,将 oc.Index(regex_string, vocabulary.inner) 放置在超时包装内:compile_regex_with_timeout(lambda pat: oc.Index(pat, vocabulary.inner), regex_string)。
- 单元测试:新增
tests/v1/structured_output/test_regex_compilation_timeout.py,包含 5 个测试用例:正常编译成功、超时引发 ValueError、超时禁用时正常返回、编译本身异常传播、错误消息中包含 pattern 前 200 字符。使用 patch 修改环境变量以控制超时时间。
关键文件:
vllm/v1/structured_output/utils.py(模块 结构化输出;类别 source;类型 core-logic;符号 compile_regex_with_timeout): 核心工具函数 compile_regex_with_timeout 的实现所在,是超时保护的核心组件。
vllm/v1/structured_output/backend_xgrammar.py(模块 结构化输出;类别 source;类型 core-logic;符号 compile_grammar, validate_xgrammar_grammar): 在 xgrammar 后端的编译入口和验证函数中接入超时,保护结构化输出的正则路径。
vllm/v1/structured_output/backend_outlines.py(模块 结构化输出;类别 source;类型 core-logic;符号 _compile_index): 在 outlines 后端的 Index 创建中接入超时,覆盖另一种结构化输出实现。
vllm/envs.py(模块 环境配置;类别 source;类型 configuration): 声明 VLLM_REGEX_COMPILATION_TIMEOUT_S 环境变量,使超时时间可配置。
tests/v1/structured_output/test_regex_compilation_timeout.py(模块 测试;类别 test;类型 test-coverage;符号 TestCompileRegexWithTimeout, test_normal_regex_compiles_successfully, test_timeout_raises_value_error, test_timeout_disabled_when_zero): 新增的测试套件,全面覆盖超时保护的各种场景,保障功能正确性。
关键符号:compile_regex_with_timeout, XgrammarBackend.compile_grammar, XgrammarBackend.validate_xgrammar_grammar, OutlinesBackend._compile_index
关键源码片段
vllm/v1/structured_output/utils.py
核心工具函数 compile_regex_with_timeout 的实现所在,是超时保护的核心组件。
# compile_regex_with_timeout 是正则编译超时保护的核心函数。
# 它接收编译函数 fn 和 pattern,通过 ThreadPoolExecutor 在子线程中执行,
# 并在超过 VLLM_REGEX_COMPILATION_TIMEOUT_S 秒后抛出 ValueError。
_T = TypeVar("_T")
def compile_regex_with_timeout(fn: Callable[[str], _T], pattern: str) -> _T:
"""Run a regex compilation callable with a 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
评论区精华
风险与影响
- 风险:性能风险:每次编译创建单线程临时线程池,开销可忽略。误杀风险:默认5秒对绝大多数合法正则足够,但复杂 pattern 可能超时,用户可设超时为0禁用。线程泄漏:实现已覆盖超时和正常完成两种路径的 shutdown,无显著泄漏。影响范围:仅影响结构化输出开启正则 regex 的请求,其他请求不受影响。
- 影响:用户透明受益于该安全保护,无需配置。系统稳定性提升,减少因恶意请求导致的 hang。团队新增少量需维护的代码,但测试覆盖完整。
- 风险标记:线程生命周期管理, 默认超时配置, 安全边界
关联脉络
参与讨论