vLLM: insufficient authenticity check
Summary
Multimodal content parts (image_url, video_url, input_audio, ...) accept a user-supplied uuid field that vLLM uses verbatim as the item's cache identity (mm_hash). The two frontend/engine processor caches (P0 "sender" cache in the API process, P1 "receiver" cache in the EngineCore process) are independent LRU caches whose evictions are not synchronized, and the P1 hit path performs zero consistency validation , it silently substitutes the cached item's tensor data for whatever the current request actually contains.
A single unauthenticated client can exploit this to kill the entire engine, deterministically, with default server flags:
1. Seed: send a small image with uuid="U". Both caches now map U to an item with N_A visual tokens (e.g. 4).
2. Drift: send ~70 requests, each carrying a *unique* uuid, a max-size image, and an over-long text prompt. Each request is rejected with 400 , but after its image has been inserted into the P0 cache (insertion happens during chat rendering, before prompt-length validation), so these requests fill P0 without ever reaching the engine/P1. U is evicted from P0 only.
3. Kill: send a *large* image with uuid="U". P0 misses → the image is freshly processed and the prompt is spliced with N_B (e.g. 2048+) placeholder tokens; the request carries full data to the engine, but P1...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| vllm PyPI | < 0.31.0 | 0.31.0 |
Details and references
## Summary Multimodal content parts (`image_url`, `video_url`, `input_audio`, ...) accept a user-supplied `uuid` field that vLLM uses **verbatim as the item's cache identity** (`mm_hash`). The two frontend/engine processor caches (P0 "sender" cache in the API process, P1 "receiver" cache in the EngineCore process) are independent LRU caches whose evictions are **not synchronized**, and the P1 hit path performs **zero consistency validation** , it silently substitutes the cached item's tensor data for whatever the current request actually contains. A single unauthenticated client can exploit this to kill the entire engine, deterministically, with default server flags: 1. **Seed**: send a small image with `uuid="U"`. Both caches now map `U` to an item with **N_A** visual tokens (e.g. 4). 2. **Drift**: send ~70 requests, each carrying a *unique* uuid, a max-size image, and an over-long text prompt. Each request is rejected with 400 , but **after** its image has been inserted into the P0 cache (insertion happens during chat rendering, before prompt-length validation), so these requests fill P0 **without ever reaching the engine/P1**. `U` is evicted from P0 only. 3. **Kill**: send a *large* image with `uuid="U"`. P0 misses → the image is freshly processed and the prompt is spliced with **N_B** (e.g. 2048+) placeholder tokens; the request carries full data to the engine, but P1 still has `U` → the engine **discards the fresh data and substitutes the stale N_A-row tensor**. The scheduler plans for N_B encoder slots while the worker merges N_A embeddings into N_B placeholders → `ValueError: Attempted to assign 4 = 4 multimodal tokens to 2048 placeholders` at `vllm/model_executor/models/utils.py:711` → uncaught in the EngineCore busy loop → `_send_engine_dead()` → the EngineCore process dies, every API server reports `EngineDeadError` and shuts down. `/health` never recovers; the deployment needs a full restart. Verified end-to-end on the official `vllm/vllm-openai:v0.29.0` image with Qwen2.5-VL-3B-Instruct, default configuration (no flags beyond `--served-model-name`/`--gpu-memory-utilization`): after 70 flood requests the kill request returned `500 EngineCore encountered an issue` and `/health` went dead permanently. The vulnerable code is byte-identical on `main` (post-0.29.0). ## Details **1. The client-controlled `uuid` becomes the cache hash verbatim.** `vllm/entrypoints/chat_utils.py:1677` / `:1884-1886` read `part["uuid"]` per content part and pass it through as `mm_uuids`. In `vllm/multimodal/processing/inputs.py:62-63`, when no `hf_processor_mm_kwargs` are present (the default for chat requests), the uuid string **is** the item hash , no content binding at all: ```python # vllm/multimodal/processing/inputs.py:62-63 else: hashes.append(uuid_item) # client string used verbatim as mm_hash ``` **2. P0 insertion happens before request validation.** In the serving path, `render_chat_request` (`vllm/entrypoints/openai/chat_completion/serving.py:275`) runs full mm processing , `mm_processor.apply` (`vllm/renderers/base.py:759`) → `_cached_apply_hf_processor` → `_merge_mm_kwargs` → `cache.get_and_update_item` (`vllm/multimodal/processing/processor.py:1338`), which inserts the processed item into the P0 sender cache (default `mm_processor_cache_type="lru"`, 4 GiB). Only afterwards does `get_max_tokens` (`vllm/entrypoints/serve/utils/api_utils.py:183-186`) reject over-long prompts with 400. **A request can therefore write to P0 while never reaching the engine** , the primitive that desynchronizes P0 from P1. (Public issue #43941 documented this same asymmetry for the opposite drift direction, which was later handled via `MultiModalCacheMissError`; the P0-miss/P1-hit direction below was never addressed.) **3. P1 substitutes cached data on hash hit with no consistency check.** In the EngineCore input thread, `preprocess_add_request` (`vllm/v1/engine/core.py:997-1000`) calls `mm_receiver_cache.get_and_update_featur
- CVSS 3.1
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
- Severity from
- GitHub (reviewed advisory)
- Weakness
- CWE-345
- Also known as
- CVE-2026-114050
- github.com/vllm-project/vllm/security/advisories/GHSA-p92p-rxj5-7p2x
- github.com/vllm-project/vllm/pull/57833
- github.com/vllm-project/vllm/commit/7268f6e38ebbb3d61700f16c638c6eba0c88bb0c
- github.com/vllm-project/vllm
- github.com/vllm-project/vllm/releases/tag/v0.31.0
- nvd.nist.gov/vuln/detail/cve-2026-114050
- www.vulncheck.com/advisories/vllm-before-0.31.0-remote-engine-crash-via-forged-multimodal-uuid