[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,