CVE-2026-105753: vLLM: Mirrored multimodal IPC caches desync after a rejected request — a later request reusing the same media hash trips a receiver assertion in the engine core
vLLM's default multimodal cache can desync between the API frontend and the engine core after a rejected request. A later request that reuses the same media hash then trips an assertion in the engine core, so a remote client with API access can make multimodal requests fail. Affected: vllm before 0.28.0. Upgrade to 0.28.0.
Published Oct 6, 2026 · Analysis Oct 22, 2026
What an attacker can do
With the default mm_processor_cache_type="lru", two processes mirror a cache. The frontend (P0) keeps only metadata in MultiModalProcessorSenderCache. The engine core (P1) keeps the payload in MultiModalReceiverCache. Both must call get_and_update() in lockstep for every request.
The invariant breaks when a request is rejected after P0 rendered and hashed the media but before P1 received it. An oversized chat prompt rejected on max_model_len is one example, because rendering happens before the length check. P0 now believes the media is cached. A later request with the same media hash gets a P0 hit, so P0 sends None instead of the payload. P1 has nothing stored and fails on assert mm_item is not None, f"Expected a cached item for {mm_hash=}".
An attacker needs only API access to a multimodal model: send a media item in a request that is rejected, then reuse it. The result is availability loss. The advisory claims no code execution, memory corruption or data disclosure. On the confirmed revision the failure is a request-level preprocessing error, though it notes public reports of the assertion cascading into engine-loop assertions on other revisions.
Who is affected
vLLM up to and including 0.25.1 was confirmed vulnerable, and the package data lists everything before 0.28.0 as affected. Only multimodal models running the default mirrored lru cache are exposed. The processor_only, disabled-cache and shm paths are not affected.
pip show vllmCheck whether you set --mm-processor-cache-type to something other than lru.
vllm
- Ecosystem
- PyPI
- Affected
- < 0.28.0
- Fixed in
- 0.28.0
- Check
pip show vllm
How to fix it
Upgrade to vLLM 0.28.0:
pip install -U vllm==0.28.0The advisory's suggested fix has two parts. Commit to the P0 sender cache only after the request passes admission checks, or roll back the insert on rejection. Also turn the P1 assert into a request-scoped error so a desync fails one request, not the engine loop. If you cannot upgrade, the advisory says the disabled-caching and shm paths are not affected, so changing the cache type is a possible stopgap. It does not state that as an official workaround.
How to tell if you were hit
Search engine logs for the message Expected a cached item for mm_hash=, which comes from the receiver assertion in vllm/multimodal/cache.py. Look for it right after requests rejected for exceeding max_model_len that carried images or other media, and for clients that repeat the same media across such requests. Repeated preprocessing errors on a multimodal endpoint, or engine-loop assertions, point to this flaw.
Original advisory
From the GitHub Advisory Database (CC BY 4.0).
Affected
- Ecosystem / package: pip /
vllm - Affected versions: vLLM ≤ 0.25.1 (confirmed on 0.25.1, commit `752a3a504485`). The lower bound predates 0.25.1; maintainers can confirm how far back the mirrored sender/receiver cache protocol reaches.
Summary
vLLM's default multimodal cache (mm_processor_cache_type="lru") mirrors state across two processes: the frontend (P0) holds only metadata (MultiModalProcessorSenderCache) while the engine core (P1) holds the real payload (MultiModalReceiverCache). The design invariant is that get_and_update() runs on P0 and P1 in lockstep for every request, so eviction order stays mirrored and P0 can answer "is this cached in P1?" without talking to P1.
That invariant breaks when a request is rejected after P0 has rendered and hashed the multimodal input (populating the P0 cache) but before P1 receives the item — for example, an oversized chat prompt rejected on max_model_len *after* rendering. P0 now believes the media is cached while P1 never got it. A later request reusing the same media hash gets a P0 hit, so P0 sends None instead of the payload, and P1 — which has nothing cached — trips assert mm_item is not None, f"Expected a cached item for {mm_hash=}".
This is a remotely reachable, request-controlled cache-mirroring desync on the standard multimodal inference path. It requires only the default cache configuration.
Affected code
Links pinned to the confirmed commit `752a3a504485` (v0.25.1):
- P0 metadata cache —
MultiModalProcessorSenderCacheat `vllm/multimodal/cache.py#L379`;get_and_update_itemat `#L410-L421`, commit assertion at `#L418`. - P1 payload cache (the actual sink) —
MultiModalReceiverCacheat `vllm/multimodal/cache.py#L630`;get_and_update_itemat `#L652-L663`, with the failingassert mm_item is not None, f"Expected a cached item for {mm_hash=}"at `#L660`. - Default `mm_processor_cache_type = "lru"` at `vllm/config/multimodal.py#L132`; dispatch in `vllm/multimodal/registry.py#L294-L307` (sender) and `#L322-L331` (receiver). The
processor_only, disabled-caching, andshmpaths are not affected. - Rejection-after-render window — rendering happens before length validation in `vllm/entrypoints/openai/chat_completion/serving.py#L206-L231` (
render_chat_request), and the length check raises after the render in `vllm/entrypoints/serve/utils/api_utils.py#L171-L189`. - Cache-commit call site — `vllm/multimodal/processing/processor.py#L1347` (
_merge_mm_kwargs) commits the P0 sender entry during render. - The separate stale-order eviction hang is already fixed via `vllm/utils/cache.py#L120-L121` (
LRUCache.touch()guardingif key in self:) — a different mechanism that does not touch the sender/receiver commit-ordering protocol and does not remediate this assertion.
The P1 receiver sink — the assertion that fires when P1 receives None for a hash it never cached:
# vllm/multimodal/cache.py Lines 651-663
@override
def get_and_update_item(
self,
mm_item: MultiModalKwargsItem | None,
mm_hash: str,
) -> MultiModalKwargsItem:
if (cached_item := self._cache.get(mm_hash)) is not None:
return cached_item
assert mm_item is not None, f"Expected a cached item for {mm_hash=}"
self._cache[mm_hash] = mm_item
return mm_itemThe P0 sender — on a hit it drops the payload (returns None) and, on a miss during render, unconditionally commits the metadata entry with no rollback tied to admission:
# vllm/multimodal/cache.py Lines 409-422
@override
def get_and_update_item(
self,
mm_item: MultiModalProcessorCacheInItem,
mm_hash: str,
) -> MultiModalProcessorCacheOutItem:
if (cached_item := self._cache.get(mm_hash)) is not None:
return None, cached_item.prompt_updates
assert mm_item is not None, f"Expected a cached item for {mm_hash=}"
self._cache[mm_hash] = MultiModalProcessorCacheItemMetadata(*mm_item)
return mm_itemImpact
A remote client submitting multimodal requests can poison a cache identity — render a media item successfully, then have that request rejected — so a later request reusing the same media hash fails on the P1 receiver assertion. This is an availability failure against a shared serving instance. No code execution, memory corruption, or data disclosure is claimed.
On this revision the failure is scoped as a request-level preprocessing error (the engine core catches around preprocess_add_request); public reports show the same assertion cascading into further engine-loop assertions on other revisions. It applies to multimodal models running the default mirrored lru cache.
Suggested Fix
Two complementary changes:
- Make the mirrored commit atomic with admission — insert into the P0 sender cache only after the request has passed all admission checks (length, limits) and P1 has acknowledged the item, or roll back the P0 insert on rejection.
- Defense in depth — convert the P1 receiver
assert mm_item is not Noneinto a checked, request-scoped error (fetch-on-miss from P0) so a desync degrades a single request rather than asserting in the engine loop.
The core of the rollback half: wrap the post-render length check so a ValueError rejection discards the P0 entries the render just committed, before re-raising. Add a discard_sender_cache_item() on the processor cache (no-op default, pop on the sender) and a Renderer.discard_mm_cache_entries() that walks a rendered request's mm_hashes:
# vllm/entrypoints/openai/chat_completion/serving.py (_create_chat_completion)
- max_tokens = get_max_tokens(
- max_model_len,
- ...,
- truncate_prompt_tokens=request.truncate_prompt_tokens,
- )
+ try:
+ max_tokens = get_max_tokens(
+ max_model_len,
+ ...,
+ truncate_prompt_tokens=request.truncate_prompt_tokens,
+ )
+ except ValueError:
+ for rendered_input in engine_inputs:
+ if mm_hashes := rendered_input.get("mm_hashes"):
+ self.renderer.discard_mm_cache_entries(mm_hashes)
+ raise# vllm/multimodal/cache.py (MultiModalProcessorSenderCache)
+ @override
+ def discard_sender_cache_item(self, mm_hash: str) -> None:
+ self._cache.pop(mm_hash, None)This closes the max_model_len rejection path; because any other rejection-after-render path reopens the same window, pairing it with the defense-in-depth change above (making the P1 assert a checked, request-scoped error) is recommended.
Credit
Reported by: Patch the Planet (Trail of Bits + OpenAI collaboration)
This vulnerability was discovered using GPT-5.5-Cyber as part of the Patch the Planet security initiative.
---
Proposed fix: a fix for this issue is proposed in a public pull request: https://github.com/vllm-project/vllm/pull/51897
Credited to KernelClint, dhalf, jperezdealgaba.
Questions
- Can this flaw lead to code execution in vLLM?
No. The advisory claims only an availability failure. It states no code execution, memory corruption or data disclosure. The CVSS vector is availability-only with low privileges.
- Does it affect text-only models?
No. The flaw lives in the multimodal processor cache mirrored between the frontend and the engine core. It applies to multimodal models with the default
lrucache.- Which vLLM version fixes it?
0.28.0 is the patched version in the package data. The advisory confirmed the bug on 0.25.1 and notes the real lower bound may be earlier.
References
Read for this analysis:
- https://github.com/vllm-project/vllm/security/advisories/GHSA-ph3r-5jfg-f84f
- https://github.com/vllm-project/vllm/pull/46747
- https://github.com/vllm-project/vllm/pull/51897
- https://github.com/vllm-project/vllm/commit/396204230423b7cc6798300926b8fa30190d26a9
- https://github.com/vllm-project/vllm/releases/tag/v0.28.0
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology
More vLLM vulnerabilities
All vLLM entriesCVE-2026-114050
vLLM: Forged multimodal UUID + P0/P1 processor-cache eviction drift: unauthenticated remote crash of the whole vLLM engine
medium6.5Oct 23, 2026CVE-2026-114055
vLLM : DoS via unvalidated mm_processor_kwargs in multimodal image processors (Isaac, InternVL, H2OVL, etc.)
high7.5Oct 23, 2026CVE-2026-105752
vLLM: Harmony tool continuations drop `cache_salt` — restoring a cross-tenant prefix-cache membership oracle
low3.1Oct 6, 2026CVE-2026-105754
vLLM: Scale-out disaggregated multimodal transport trusts caller-supplied features
medium6.5Oct 5, 2026CVE-2026-105755
vLLM: Flash late-interaction scoring caches query embeddings under a caller-controlled request id — cross-request integrity break and induced errors on `/score` and `/rerank`
medium4.2Oct 5, 2026CVE-2026-105756
vLLM: Loose `cache_salt` validation lets a single request kill EngineCore on LMCache-MP deployments — uncaught downstream `ValueError` denial of service
medium6.5Oct 5, 2026
