A high-throughput and memory-efficient inference and serving engine for LLMs
[Security] Fix DoS via prompt_embeds on M-RoPE models (#45252)
Signed-off-by: jperezde <jperezde@redhat.com>
Juan Pérez de Algaba committed
Jun 13, 2026 at 12:17 UTC
470229c37efaf69c86e8bc97482b0b1ff7551c65
2 files changed
+94
-4
tests/v1/worker/test_mrope_prompt_embeds.py
new
+79
@@ -0,0 +1,79 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Test that M-RoPE position initialization handles prompt_embeds-only inputs.
4
+
5
+Regression test for GHSA-33cg-gxv8-3p8g: sending /v1/completions with
6
+prompt_embeds and no prompt_token_ids on M-RoPE models crashed the
7
+EngineCore via an assertion failure.
8
+"""
9
+
10
+from unittest.mock import Mock
11
+
12
+import pytest
13
+import torch
14
+
15
+from vllm.model_executor.models.interfaces import SupportsMRoPE
16
+from vllm.v1.worker.gpu_input_batch import CachedRequestState
17
+from vllm.v1.worker.gpu_model_runner import GPUModelRunner
18
+
19
+
20
+class FakeMRoPEModel(SupportsMRoPE):
21
+ """Minimal model that passes supports_mrope() check."""
22
+
23
+ def get_mrope_input_positions(self, input_tokens, mm_features):
24
+ seq_len = len(input_tokens)
25
+ positions = torch.arange(seq_len).unsqueeze(0).expand(3, -1)
26
+ return positions.clone(), 0
27
+
28
+
29
+def _make_runner_and_req(prompt_token_ids, prompt_embeds):
30
+ """Create a minimal GPUModelRunner instance and request state."""
31
+ model = FakeMRoPEModel()
32
+ instance = object.__new__(GPUModelRunner)
33
+ instance.get_model = lambda: model
34
+
35
+ req_state = Mock(spec=CachedRequestState)
36
+ req_state.prompt_token_ids = prompt_token_ids
37
+ req_state.prompt_embeds = prompt_embeds
38
+ req_state.mm_features = []
39
+ req_state.mrope_positions = None
40
+ req_state.mrope_position_delta = None
41
+ return instance, req_state
42
+
43
+
44
+class TestMRopePromptEmbeds:
45
+ """Verify _init_mrope_positions handles prompt_embeds-only inputs."""
46
+
47
+ def test_prompt_embeds_only_does_not_crash(self):
48
+ """Prompt-embeds-only request must not raise AssertionError."""
49
+ instance, req_state = _make_runner_and_req(
50
+ prompt_token_ids=None,
51
+ prompt_embeds=torch.randn(15, 896),
52
+ )
53
+
54
+ instance._init_mrope_positions(req_state)
55
+
56
+ assert req_state.mrope_positions is not None
57
+ assert req_state.mrope_positions.shape == (3, 15)
58
+
59
+ def test_prompt_token_ids_still_works(self):
60
+ """Normal path with prompt_token_ids continues working."""
61
+ instance, req_state = _make_runner_and_req(
62
+ prompt_token_ids=[1, 2, 3, 4, 5],
63
+ prompt_embeds=None,
64
+ )
65
+
66
+ instance._init_mrope_positions(req_state)
67
+
68
+ assert req_state.mrope_positions is not None
69
+ assert req_state.mrope_positions.shape == (3, 5)
70
+
71
+ def test_neither_token_ids_nor_embeds_raises(self):
72
+ """When both are None, a ValueError should be raised."""
73
+ instance, req_state = _make_runner_and_req(
74
+ prompt_token_ids=None,
75
+ prompt_embeds=None,
76
+ )
77
+
78
+ with pytest.raises(ValueError, match="prompt_token_ids or prompt_embeds"):
79
+ instance._init_mrope_positions(req_state)
vllm/v1/worker/gpu_model_runner.py
+15
-4
@@ -1588,9 +1588,6 @@ class GPUModelRunner(
1588
def _init_mrope_positions(self, req_state: CachedRequestState):
1589
model = self.get_model()
1590
assert supports_mrope(model), "M-RoPE support is not implemented."
1591
- assert req_state.prompt_token_ids is not None, (
1592
- "M-RoPE requires prompt_token_ids to be available."
1593
- )
1591
mrope_model = cast(SupportsMRoPE, model)
1592
1593
# `prompt_embeds` is a passthrough modality (no grid_thw), models'
@@ -1599,9 +1596,23 @@ class GPUModelRunner(
1596
mrope_features = [
1597
f for f in req_state.mm_features if f.modality != "prompt_embeds"
1598
]
1599
+
1600
+ if req_state.prompt_token_ids is not None:
1601
+ input_tokens = req_state.prompt_token_ids
1602
+ elif req_state.prompt_embeds is not None:
1603
+ # For embeddings-only inputs, get_mrope_input_positions only
1604
+ # needs the sequence length when mm_features is empty (which is
1605
+ # the case here since prompt_embeds are filtered out above).
1606
+ seq_len = req_state.prompt_embeds.shape[0]
1607
+ input_tokens = list(range(seq_len))
1608
+ else:
1609
+ raise ValueError(
1610
+ "M-RoPE requires either prompt_token_ids or prompt_embeds."
1611
+ )
1612
+
1613
req_state.mrope_positions, req_state.mrope_position_delta = (
1614
mrope_model.get_mrope_input_positions(
1604
- req_state.prompt_token_ids,
1615
+ input_tokens,
1616
mrope_features,
1617
)
1618
)