[Bugfix] Reject NUL byte in structured_outputs.regex (#51796)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Junhao Shen committed
Aug 14, 2026 at 08:08 UTC
38f097fab8f6d58e3b3f57bded1d98f5b48d3f6d
3 files changed
+50
tests/v1/structured_output/test_validation.py
+33
@@ -70,3 +70,36 @@ def test_degenerate_structured_outputs_rejected(structured_outputs, match):
70
StructuredOutputsConfig(),
71
tokenizer=object(),
72
)
73
+
74
+
75
+@pytest.mark.parametrize(
76
+ "regex",
77
+ [
78
+ "\x00", # a lone leading NUL
79
+ "\x00\x01\x02\x1f", # a NUL followed by other control chars
80
+ "[0-9]\x00", # an embedded NUL
81
+ ],
82
+)
83
+def test_regex_with_nul_byte_rejected(regex):
84
+ """A NUL byte is never meaningful in a structured-outputs regex and is not
85
+ handled by xgrammar's native regex converter. It must be rejected at request
86
+ validation in every backend mode (a clean 400), instead of reaching that
87
+ native code or silently falling back to another backend in the default
88
+ 'auto' mode."""
89
+ params = SamplingParams(structured_outputs=StructuredOutputsParams(regex=regex))
90
+
91
+ # Rejected before backend selection, so it is a 400 even in 'auto' mode
92
+ # (which would otherwise catch the error and fall back to another backend).
93
+ with pytest.raises(VLLMValidationError, match="NUL"):
94
+ params._validate_structured_outputs(
95
+ _StubModelConfig(is_diffusion=False),
96
+ StructuredOutputsConfig(),
97
+ tokenizer=object(),
98
+ )
99
+
100
+ # The xgrammar backend also rejects it directly (defense in depth), before
101
+ # the pattern reaches the native from_regex call.
102
+ from vllm.v1.structured_output.backend_xgrammar import validate_xgrammar_grammar
103
+
104
+ with pytest.raises(ValueError, match="NUL"):
105
+ validate_xgrammar_grammar(params)
vllm/sampling_params.py
+9
@@ -1029,6 +1029,15 @@ class SamplingParams(
1029
"structured_outputs.json_object must be True if set; omit "
1030
"structured_outputs to disable structured outputs"
1031
)
1032
+ # Reject a regex containing a NUL byte early, in every backend mode. A
1033
+ # NUL is never meaningful in a regex pattern and is not handled by the
1034
+ # regex-to-grammar conversion. Checked here, before backend selection,
1035
+ # so it is a clean 400 rather than a silent fallback in the default
1036
+ # "auto" mode.
1037
+ if self.structured_outputs.regex and "\x00" in self.structured_outputs.regex:
1038
+ raise VLLMValidationError(
1039
+ "structured_outputs.regex must not contain a NUL character ('\\x00')"
1040
+ )
1041
1042
from vllm.v1.structured_output.backend_guidance import (
1043
has_guidance_unsupported_json_features,
vllm/v1/structured_output/backend_xgrammar.py
+8
@@ -284,6 +284,14 @@ def validate_xgrammar_grammar(sampling_params: SamplingParams) -> None:
284
so_params = sampling_params.structured_outputs
285
286
if so_params.regex:
287
+ # A NUL byte is never meaningful in a regex pattern and is not handled
288
+ # by xgrammar's native regex converter. Reject it here, before the
289
+ # pattern reaches that native code; the try/except below does not cover
290
+ # this case.
291
+ if "\x00" in so_params.regex:
292
+ raise ValueError(
293
+ "structured_outputs.regex must not contain a NUL character ('\\x00')"
294
+ )
295
try:
296
compile_regex_with_timeout(
297
xgr.Grammar.from_regex,