Prhub

#34679 fix(constrained): reject NUL bytes in grammar specs to stop an xgrammar segfault

原始 PR 作者 ECMGit 合并时间 2026-08-20 06:31 文件变更 2 提交数 4 评论 6 代码增减 +94 / -1

执行摘要

拒绝 grammar 中的 NUL 字节,修复 xgrammar 段错误

PR 描述说明了触发条件:以 NUL 字节开头的 regex 会使 xgrammar 的 regex 转换器追加自身的 NUL 终止符并索引越界,引发 SIGSEGV,导致调度器以 exit code -11 崩溃,后续请求全部被拒。由于是 OS 信号,异常无法被捕获。此外,JSON schema 通过 pattern 也可能导致同样的崩溃,但请求体中可能没有 NUL 字节(如 \u0000 转义)。

此 PR 值得精读,尤其是 _grammar_key_contains_nul 的实现细节,展示了如何在底层第三方库存在潜在崩溃风险时,通过上层校验来规避。也展示了好的防御性编程实践。

讨论亮点
  • kpham-sgl: 要求移除 AI 生成的注释,并添加备注提醒在 xgrammar 修复后移除该补丁。作者已在后续提交中完成。
  • 维护者建议并鼓励作者到 xgrammar 仓库提交 issue 和 PR(作者已提交 https://github.com/mlc-ai/xgrammar/pull/850)。
  • CI 运行通过,/rerun-test 成功。

实现拆解

  1. 新增 _grammar_key_contains_nul 函数(位于 base_grammar_backend.py):
    • 先检查原始字符串是否包含 NUL 字节,若命中直接返回 True。
    • jsonstructural_tag 类型,进一步解析 JSON 并递归遍历所有字符串值,检查是否包含 \u0000 转义形式。
    • 对于格式错误的 JSON,则返回 False,让后端正常报错。
  2. _init_value_dispatch 张入口调用该检查,若命中则记录错误并返回 InvalidGrammarObject,替代原本可能触发崩溃的后端调用。
  3. 测试文件 test_base_grammar_backend.py 新增 TestNulByteGrammarRejection 测试类,覆盖含 NUL 的 regex、ebnf、structural_tag、JSON pattern(原始和转义形式),以及非 NUL 控制字符、转义形式的非 NUL、格式错误 JSON 等正常请求。
  4. 后续提交精简了注释,并指示待 xgrammar 上游修复版本合并后移除该 guard。
文件 模块 状态 重要度
python/sglang/srt/constrained/base_grammar_backend.py 约束解码 modified 7.01
test/registered/unit/constrained/test_base_grammar_backend.py 约束测试 modified 6.34

关键符号

_grammar_key_contains_nul

关键源码片段

python/sglang/srt/constrained/base_grammar_backend.py dependency-wiring

核心变更文件,新增 NUL 字节检查函数并在调度入口调用,防止崩溃。

import jsondef _grammar_key_contains_nul(key_type: str, key_string: str) -> bool:
    """A NUL in a spec segfaults xgrammar's regex converter, which a JSON schema
    also reaches through `pattern` (possibly escaped). Drop once the upstream fix
    https://github.com/mlc-ai/xgrammar/pull/850 is in our pinned version."""
    # 先检查原始字符串中是否有 NUL 字节
    if "\x00" in key_string:
        return True
    # 对 json 和 structural_tag 类型,需要递归检查转义后的 \u0000
    if key_type not in ("json", "structural_tag"):
        return False
    try:
        decoded = json.loads(key_string)
    except ValueError:
        # 格式错误的 JSON,交给后端正常报错
        return False
    # 深度优先遍历 JSON 结构,检查所有字符串值
    stack = [decoded]
    while stack:
        node = stack.pop()
        if isinstance(node, str):
            if "\x00" in node:
                return True
        elif isinstance(node, dict):
            stack.extend(node.keys())
            stack.extend(node.values())
        elif isinstance(node, list):
            stack.extend(node)
    return False# 在 _init_value_dispatch 中调用
if _grammar_key_contains_nul(key_type, key_string):
    logger.error(f"Rejecting {key_type} grammar containing a NUL byte")
    return InvalidGrammarObject(
        f"Invalid {key_type}: NUL bytes (\\u0000) are not allowed"
    )
test/registered/unit/constrained/test_base_grammar_backend.py test-coverage

新增测试类覆盖 NUL 拒绝和正常分派,验证修复效果且无回归。

import jsonclass TestNulByteGrammarRejection(unittest.TestCase):
    def setUp(self):
        self.backend = BaseGrammarBackend()
​
    def tearDown(self):
        self.backend.executor.shutdown(wait=True)
​
    def test_nul_payload_never_reaches_backend(self):
        # 各种含 NUL 的 grammar 都应该被拒绝,且不会调用底层后端
        cases = [
            ("regex", "\x00\x01\x02\x1f"),
            ("regex", "\x00"),
            ("regex", "a\x00b"), # 非开头的 NUL 也会崩溃,固定该行为
            ("ebnf", "root ::= \x00"),
            ("structural_tag", '{"triggers": ["\x00"]}'),
            ("json", '{"type":"string","pattern":"\\u0000"}'), # 转义形式
            ("json", '{"type":"object","properties":{"f":{"type":"string","pattern":"\\u0000x"}}}'),
            ("structural_tag", '{"format":{"pattern":"\\u0000"}}'),
        ]
        for key_type, key_string in cases:
            with self.subTest(key_type=key_type, key_string=repr(key_string)):
                dispatch = MagicMock()
                setattr(self.backend, f"dispatch_{key_type}", dispatch)
                result = self.backend._init_value_dispatch((key_type, key_string), False)
                self.assertIsInstance(result, InvalidGrammarObject)
                dispatch.assert_not_called()
​
    def test_nul_free_payload_still_dispatches(self):
        # 正常的 grammar 仍然会正常分派
        cases = [
            ("regex", "[0-9]+"),
            ("regex", r"\x00"), # 字面量转义,不是 NUL 字节
            ("regex", "\x01\x02\x1f"), # 其他控制字符不受影响
            ("json", json.dumps({"type": "string", "pattern": "[0-9]+"})),
            ("json", "{not valid json"), # 格式错误 JSON 交给后端报错
        ]
        for key_type, key_string in cases:
            with self.subTest(key_type=key_type, key_string=repr(key_string)):
                dispatch = MagicMock(return_value=BaseGrammarObject())
                setattr(self.backend, f"dispatch_{key_type}", dispatch)
                self.backend._init_value_dispatch((key_type, key_string), False)
                dispatch.assert_called_once_with(key_string)

评论区精华

xgrammar 上游修复跟进 设计

维护者 kpham-sgl 要求移除 AI 生成注释,并请作者到 xgrammar 仓库提交 issue 和 PR,以便将来在 xgrammar 修复后移除本 guard。

结论:作者已提交 xgrammar PR #850,并在注释中保留指针,计划在升级 xgrammar 版本后移除 guard。 · 已解决

风险与影响

  • 该补丁只是防御性检查,未改变正常 grammar 的处理路径,对合法请求无影响。
  • 若 xgrammar 上游修复版本发布,若未及时移除 guard,可能对含 NUL 的合法请求(极少见)造成误拒,建议跟踪上游版本并按要求移除。
  • 对非常规的 encoding 或大 JSON 可能带来轻微性能开销,但仅在初次编译 grammar 时执行。

影响所有使用 xgrammar 后端的 constrained decoding 用户,防止恶意或意外输入的 grammar 导致服务器崩溃,提升了服务的稳定性和安全性。整体影响为正,变更极小。

依赖第三方库未修复 防御性校验可能误拒极少见合法请求

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论