Skip to content

GHSA-ph3r-5jfg-f84f CVE-2026-105753 PYSEC-2026-4197

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

Medium PyPI
Severity
6.5 / 10
Medium · CVSS 3.1
Exploit chance (EPSS)
0.43%
Chance of exploitation in the next 30 days. Higher than 37.02% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-617 Reachable Assertion
Fix
0.28.0
Upgrade vllm to this version or later

Description

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):

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_item

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

Impact

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:

  1. 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.
  2. Defense in depth — convert the P1 receiver assert mm_item is not None into 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

Affected versions

PyPI

PackageVulnerableFixed in
vllm
< 0.28.0
0.28.0

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

vllm PyPI → 0.28.0

pip
pip install --upgrade "vllm>=0.28.0"
poetry
poetry add "vllm>=0.28.0"
uv
uv add "vllm>=0.28.0"

Severity breakdown

CVSS 3.1 6.5

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

Weakness

CWE-617: Reachable Assertion

The product contains an assert() or similar statement that can be triggered by an attacker, which leads to an application exit or other behavior that is more severe than necessary.

How to prevent it
  • Implementation: Make sensitive open/close operation non reachable by directly user-controlled data (e.g. open/close resources)
  • Implementation: Perform input validation on user data.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-8397-8qf6-f2gm
vLLM: Phi-4 multimodal audio encoder materializes and retains full attention matrices: a single max-length audio OOM-crashes the vLLM engine
Medium Also affects vllm
GHSA-x567-49g5-p4cf
vLLM: Responses API bypasses trust_request_chat_template for chat_template_kwargs
Medium Also affects vllm
GHSA-vvpg-rrww-wpmh
vLLM: Permissive default CORS origins allow cross-origin requests to the vLLM HTTP server
Medium Also affects vllm
GHSA-q97c-p368-75c4
vLLM: Out-of-vocabulary prompt token id passes input validation and triggers a CUDA device-side assert (engine crash / DoS)
Medium Also affects vllm
GHSA-h3rc-j82p-2c3h
vLLM: Untrusted EPS inputs can trigger denial of service or Ghostscript RCE
High Also affects vllm
GHSA-gq5q-89c7-vhvx
vLLM DeepSeek-OCR image processing vulnerable to denial of service
Medium Also affects vllm
GHSA-6757-hggm-hp2w
vLLM: Public multimodal SHM handle forgery reaches worker-side `pickle.loads()` with insecure serialization disabled
High Also affects vllm
GHSA-542m-9234-8gvf
vLLM: DoS via unbounded chat_template Jinja2 rendering exhausts the renderer thread pool
Medium Also affects vllm
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor