[Security] Reject min_tokens that exceeds the filled max_tokens default (#57731)
Signed-off-by: Juan Pérez de Algaba <jperezde@redhat.com>
Juan Pérez de Algaba committed
Sep 22, 2026 at 07:50 UTC
4800024b98f70c9b3022c27b41e63a5dc9556ed7
3 files changed
+64
tests/entrypoints/scale_out/token_in_token_out/test_serving_tokens.py
+23
@@ -112,6 +112,29 @@ async def test_generate_endpoint(client):
112
assert data["choices"][0].get("sampling_mask") is None
113
114
115
+@pytest.mark.asyncio
116
+async def test_generate_rejects_min_tokens_above_filled_max_tokens(client):
117
+ """Explicit null max_tokens must not skip the min_tokens bound."""
118
+ payload = {
119
+ "model": MODEL_NAME,
120
+ "token_ids": [1, 2, 3],
121
+ "sampling_params": {"max_tokens": None, "min_tokens": 2147483648},
122
+ "stream": False,
123
+ }
124
+ resp = await client.post(GEN_ENDPOINT, json=payload)
125
+ assert resp.status_code == 400
126
+ assert "min_tokens" in resp.json()["error"]["message"]
127
+
128
+ followup = {
129
+ "model": MODEL_NAME,
130
+ "token_ids": [1, 2, 3],
131
+ "sampling_params": {"max_tokens": 4},
132
+ "stream": False,
133
+ }
134
+ resp = await client.post(GEN_ENDPOINT, json=followup)
135
+ resp.raise_for_status()
136
+
137
+
138
@pytest.mark.asyncio
139
@pytest.mark.skipif(
140
envs.VLLM_USE_RUST_FRONTEND,
tests/test_request_input_bounds.py
+34
@@ -61,6 +61,40 @@ def test_routed_experts_offset_validated_before_engine_submission(offset):
61
)
62
63
64
+def _process_inputs_with_max_model_len(
65
+ params: SamplingParams, max_model_len: int = 2048
66
+):
67
+ processor = Mock(spec=InputProcessor)
68
+ processor.tokenizer = None
69
+ processor.generation_config_fields = {}
70
+ processor.renderer = Mock()
71
+ processor.renderer.get_eos_token_id.return_value = None
72
+ processor.vllm_config = Mock()
73
+ processor.model_config = Mock()
74
+ processor.model_config.max_model_len = max_model_len
75
+ prompt = {"type": "token", "prompt_token_ids": [1, 2, 3]}
76
+ return InputProcessor.process_inputs(
77
+ processor, "req", prompt, params, ("generate",)
78
+ )
79
+
80
+
81
+def test_unset_max_tokens_rejects_oversized_min_tokens():
82
+ """Reject min_tokens that only becomes invalid after max_tokens is filled."""
83
+ params = SamplingParams(max_tokens=None, min_tokens=2147483648)
84
+ with pytest.raises(
85
+ VLLMValidationError,
86
+ match="min_tokens must be less than or equal to max_tokens",
87
+ ):
88
+ _process_inputs_with_max_model_len(params)
89
+
90
+
91
+def test_unset_max_tokens_accepts_min_tokens_within_filled_max():
92
+ params = SamplingParams(max_tokens=None, min_tokens=10)
93
+ request = _process_inputs_with_max_model_len(params)
94
+ assert request.sampling_params.max_tokens == 2045
95
+ assert request.sampling_params.min_tokens == 10
96
+
97
+
98
# --- Stop strings: public requests cap the number of stop strings ---------
99
100
vllm/v1/engine/input_processor.py
+7
@@ -397,6 +397,13 @@ class InputProcessor:
397
sampling_params.max_tokens = (
398
self.model_config.max_model_len - prompt_len
399
)
400
+ # min_tokens is not checked while max_tokens is unset.
401
+ if sampling_params.min_tokens > sampling_params.max_tokens:
402
+ raise VLLMValidationError(
403
+ f"min_tokens must be less than or equal to "
404
+ f"max_tokens={sampling_params.max_tokens}, got "
405
+ f"{sampling_params.min_tokens}."
406
+ )
407
408
sampling_params.update_from_generation_config(
409
self.generation_config_fields,