@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 470229c37e
Fork 0

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 )