GHSA-P92P-RXJ5-7P2X

Vulnerability from github – Published: 2026-10-23 20:43 – Updated: 2026-10-23 20:43
VLAI
Summary
vLLM: Forged multimodal UUID + P0/P1 processor-cache eviction drift: unauthenticated remote crash of the whole vLLM engine
Details

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:

# 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_features, which only replaces feature.data (vllm/multimodal/cache.py:673-690) and keeps the request's own mm_position (placeholder layout). On a cache hit the receiver cache returns the stale item wholesale:

# vllm/multimodal/cache.py:739-740  (MultiModalReceiverCache.get_and_update_item)
if (cached_item := self._cache.get(mm_hash)) is not None:
    return cached_item              # fresh data silently discarded; no size/modality check

Only the opposite drift (P0 hit / P1 miss, data=None) is handled, via the retryable MultiModalCacheMissError. Nothing validates that the cached item's modality, shape, or token count matches the current request's placeholders.

4. The mismatch is fatal to the engine. The scheduler plans encoder work from the request's (fresh) placeholder count N_B, while the worker gathers embeddings from the stale N_A-row tensor (encoder-cache hit under the same forged hash, or re-encoding the stale data — both yield N_A rows). _merge_multimodal_embeddings then raises:

# vllm/model_executor/models/utils.py:703-714
inputs_embeds[is_multimodal] = mm_embeds_flat.to(dtype=input_dtype)   # RuntimeError
...
raise ValueError(
    f"Attempted to assign {expr} = {num_actual_tokens} "
    f"multimodal tokens to {num_expected_tokens} placeholders") from e

This exception is raised inside the worker's input preparation with no per-request containment, propagates to the EngineCore busy loop's top-level handler (vllm/v1/engine/core.py:1372), and triggers _send_engine_dead(). The API server's output_handler receives EngineDeadError (vllm/v1/engine/async_llm.py:819) and the whole process tree shuts down. (Issue #55546 independently reported this sink — "one bad multimodal item kills the entire V1 engine tree" — from an organically occurring stale cache entry; this report provides the missing deterministic, remote, single-attacker trigger.)

Cost of the attack. With the default mm_processor_cache_gb=4, one max-size Qwen2.5-VL image (12845056 pixels) caches as ~77 MB, so ~56 flood requests suffice; we used 70. Flood requests are CPU-only image decodes (~0.3 s each here) and can be issued sequentially or concurrently; the total wall-clock cost of the kill was under one minute. Models with larger max_pixels fill the cache in even fewer requests. Note that issuing the flood with successful requests does not work — they would populate P1 as well; rejection after P0 insertion is essential, and the over-long-prompt 400 is one always-available way to get it (no size limits depend on non-default flags).

Impact

Any unauthenticated client that can reach a multimodal vLLM endpoint (/v1/chat/completions, /v1/responses, etc. — all accept content-part uuid) can deterministically crash the entire serving instance: the EngineCore process dies, every in-flight request fails, and the API server exits; the deployment stays down until restarted. In data-parallel or multi-API-server deployments the blast radius extends to the whole engine group (cf. the DP8 outage in #55546). The attack requires no special privileges, no non-default server flags, no race conditions, and costs under a minute of sequential requests at default cache sizes.

This is distinct from the documented "Multimodal Media UUID Security" guidance in docs/usage/security.md, which covers only integrity/confidentiality consequences of uuid collisions (a caller silently receiving another caller's media). The guidance does not mention availability, and following it does not help: the attacker here deliberately collides with their own uuid, and the fatal element is the missing consistency check on the P1 hit path plus the unsynchronized eviction of the two caches.

Suggested remediation (any one of these breaks the kill chain):

  1. On a P1 receiver-cache hit, validate the cached item against the request's mm_position (modality and expected embedding count); on mismatch, treat it as a miss (MultiModalCacheMissError) or reject the request — never substitute silently.
  2. Make the feature/placeholder count mismatch in _merge_multimodal_embeddings a per-request error instead of a fatal engine exception (also requested by #55546).
  3. Bind server-side cache identity to content even when a uuid is provided (e.g. hash(content) + uuid), eliminating forged-hash cache hits entirely.
  4. Synchronize P0/P1 evictions (the engine already pushes invalidate to P0 on cache-miss; eviction-direction sync would close the remaining drift).

A public fix is available here: https://github.com/vllm-project/vllm/pull/57833

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "vllm"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.31.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-114050"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-345"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-23T20:43:44Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nMultimodal content parts (`image_url`, `video_url`, `input_audio`, ...) accept a user-supplied `uuid` field that vLLM uses **verbatim as the item\u0027s 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** \u2014 it silently substitutes the cached item\u0027s tensor data for whatever the current request actually contains.\n\nA single unauthenticated client can exploit this to kill the entire engine, deterministically, with default server flags:\n\n1. **Seed**: send a small image with `uuid=\"U\"`. Both caches now map `U` to an item with **N_A** visual tokens (e.g. 4).\n2. **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 \u2014 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.\n3. **Kill**: send a *large* image with `uuid=\"U\"`. P0 misses \u2192 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` \u2192 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 \u2192 `ValueError: Attempted to assign 4 = 4 multimodal tokens to 2048 placeholders` at `vllm/model_executor/models/utils.py:711` \u2192 uncaught in the EngineCore busy loop \u2192 `_send_engine_dead()` \u2192 the EngineCore process dies, every API server reports `EngineDeadError` and shuts down. `/health` never recovers; the deployment needs a full restart.\n\nVerified 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).\n\n## Details\n\n**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 \u2014 no content binding at all:\n\n```python\n# vllm/multimodal/processing/inputs.py:62-63\nelse:\n    hashes.append(uuid_item)        # client string used verbatim as mm_hash\n```\n\n**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 \u2014 `mm_processor.apply` (`vllm/renderers/base.py:759`) \u2192 `_cached_apply_hf_processor` \u2192 `_merge_mm_kwargs` \u2192 `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** \u2014 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.)\n\n**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_features`, which only replaces `feature.data` (`vllm/multimodal/cache.py:673-690`) and keeps the request\u0027s own `mm_position` (placeholder layout). On a cache hit the receiver cache returns the stale item wholesale:\n\n```python\n# vllm/multimodal/cache.py:739-740  (MultiModalReceiverCache.get_and_update_item)\nif (cached_item := self._cache.get(mm_hash)) is not None:\n    return cached_item              # fresh data silently discarded; no size/modality check\n```\n\nOnly the opposite drift (P0 hit / P1 miss, `data=None`) is handled, via the retryable `MultiModalCacheMissError`. Nothing validates that the cached item\u0027s modality, shape, or token count matches the current request\u0027s placeholders.\n\n**4. The mismatch is fatal to the engine.** The scheduler plans encoder work from the request\u0027s (fresh) placeholder count N_B, while the worker gathers embeddings from the stale N_A-row tensor (encoder-cache hit under the same forged hash, or re-encoding the stale data \u2014 both yield N_A rows). `_merge_multimodal_embeddings` then raises:\n\n```python\n# vllm/model_executor/models/utils.py:703-714\ninputs_embeds[is_multimodal] = mm_embeds_flat.to(dtype=input_dtype)   # RuntimeError\n...\nraise ValueError(\n    f\"Attempted to assign {expr} = {num_actual_tokens} \"\n    f\"multimodal tokens to {num_expected_tokens} placeholders\") from e\n```\n\nThis exception is raised inside the worker\u0027s input preparation with no per-request containment, propagates to the EngineCore busy loop\u0027s top-level handler (`vllm/v1/engine/core.py:1372`), and triggers `_send_engine_dead()`. The API server\u0027s `output_handler` receives `EngineDeadError` (`vllm/v1/engine/async_llm.py:819`) and the whole process tree shuts down. (Issue #55546 independently reported this sink \u2014 \"one bad multimodal item kills the entire V1 engine tree\" \u2014 from an organically occurring stale cache entry; this report provides the missing deterministic, remote, single-attacker trigger.)\n\n**Cost of the attack.** With the default `mm_processor_cache_gb=4`, one max-size Qwen2.5-VL image (12845056 pixels) caches as ~77 MB, so ~56 flood requests suffice; we used 70. Flood requests are CPU-only image decodes (~0.3 s each here) and can be issued sequentially or concurrently; the total wall-clock cost of the kill was under one minute. Models with larger `max_pixels` fill the cache in even fewer requests. Note that issuing the flood with *successful* requests does not work \u2014 they would populate P1 as well; rejection after P0 insertion is essential, and the over-long-prompt 400 is one always-available way to get it (no size limits depend on non-default flags).\n\n\n## Impact\n\nAny unauthenticated client that can reach a multimodal vLLM endpoint (`/v1/chat/completions`, `/v1/responses`, etc. \u2014 all accept content-part `uuid`) can **deterministically crash the entire serving instance**: the EngineCore process dies, every in-flight request fails, and the API server exits; the deployment stays down until restarted. In data-parallel or multi-API-server deployments the blast radius extends to the whole engine group (cf. the DP8 outage in #55546). The attack requires no special privileges, no non-default server flags, no race conditions, and costs under a minute of sequential requests at default cache sizes.\n\nThis is distinct from the documented \"Multimodal Media UUID Security\" guidance in `docs/usage/security.md`, which covers only **integrity/confidentiality** consequences of uuid collisions (a caller silently receiving another caller\u0027s media). The guidance does not mention availability, and following it does not help: the attacker here deliberately collides with *their own* uuid, and the fatal element is the missing consistency check on the P1 hit path plus the unsynchronized eviction of the two caches.\n\n**Suggested remediation** (any one of these breaks the kill chain):\n\n1. On a P1 receiver-cache hit, validate the cached item against the request\u0027s `mm_position` (modality and expected embedding count); on mismatch, treat it as a miss (`MultiModalCacheMissError`) or reject the request \u2014 never substitute silently.\n2. Make the feature/placeholder count mismatch in `_merge_multimodal_embeddings` a per-request error instead of a fatal engine exception (also requested by #55546).\n3. Bind server-side cache identity to content even when a uuid is provided (e.g. `hash(content) + uuid`), eliminating forged-hash cache hits entirely.\n4. Synchronize P0/P1 evictions (the engine already pushes `invalidate` to P0 on cache-miss; eviction-direction sync would close the remaining drift).\n\nA public fix is available here: https://github.com/vllm-project/vllm/pull/57833",
  "id": "GHSA-p92p-rxj5-7p2x",
  "modified": "2026-10-23T20:43:45Z",
  "published": "2026-10-23T20:43:44Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/security/advisories/GHSA-p92p-rxj5-7p2x"
    },
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/pull/57833"
    },
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/commit/7268f6e38ebbb3d61700f16c638c6eba0c88bb0c"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/vllm-project/vllm"
    },
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/releases/tag/v0.31.0"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/cve-2026-114050"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/vllm-before-0.31.0-remote-engine-crash-via-forged-multimodal-uuid"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "vLLM: Forged multimodal UUID + P0/P1 processor-cache eviction drift: unauthenticated remote crash of the whole vLLM engine"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.