---
id: CVE-2026-115912
aliases:
  - GHSA-6757-hggm-hp2w
  - PYSEC-2026-4345
title: >-
  vLLM: Public multimodal SHM handle forgery reaches worker-side
  `pickle.loads()` with insecure serialization disabled
summary: >-
  vLLM: Public multimodal SHM handle forgery reaches worker-side
  `pickle.loads()` with insecure serialization disabled
severity: high
cvss: 7.5
cvssVector: 'CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H'
vendor: vllm
product: vllm
ecosystem: pip
affected:
  - vllm < 0.32.0
patched:
  - vllm 0.32.0
published: '2026-10-28'
updated: '2026-10-29'
sourceUpdated: '2026-10-29T11:10:54.932940889Z'
source: OSV
sourceUrl: 'https://osv.dev/vulnerability/GHSA-6757-hggm-hp2w'
references:
  - url: >-
      https://github.com/vllm-project/vllm/security/advisories/GHSA-6757-hggm-hp2w
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-115912'
  - url: 'https://github.com/vllm-project/vllm/pull/59357'
  - url: >-
      https://github.com/vllm-project/vllm/commit/3d5625cc3c2474994107047d7a6ecb8e4008f9b0
  - url: 'https://github.com/vllm-project/vllm'
  - url: 'https://github.com/vllm-project/vllm/releases/tag/v0.32.0'
  - url: 'https://github.com/advisories/GHSA-6757-hggm-hp2w'
tags:
  - osv
  - pip
  - ghsa
  - cve.org
epss: 0.0051
epssPercentile: 0.43431
ssvc:
  exploitation: none
  automatable: 'no'
  technicalImpact: total
  timestamp: '2026-10-26T19:31:18.550495Z'
cwe:
  - CWE-502
ingestedAt: '2026-10-26T17:33:33.063Z'
---

## Overview

## Affected

- **Ecosystem / package:** pip / `vllm`
- **Affected versions:** vLLM < 0.32.0.

## Summary

When `--mm-processor-cache-type shm` is enabled, vLLM's multimodal SHM cache represents a cached item with an `address` and `monotonic_id` pair and treats that pair as authority to dereference shared memory in the worker. A public scale-out request can supply its own multimodal `features.kwargs_data`, so an authenticated caller can forge an inner SHM carrier that points at attacker-controlled bytes already staged in the ring buffer. The worker then reaches `pickle.loads()` while deserializing the forged object.

This is reachable through ordinary public HTTP routes and does not require `VLLM_ALLOW_INSECURE_SERIALIZATION=1`. The live chain used `/v1/chat/completions/render` to obtain a valid feature envelope, `/v1/chat/completions` to stage attacker-chosen bytes into SHM, and `/inference/v1/generate` to submit the forged inner carrier.

## Affected code

Links pinned to the confirmed commit [`752a3a504485`](https://github.com/vllm-project/vllm/tree/752a3a504485790a2e8491cacbb35c137339ad34) (v0.25.1):

- [`vllm/distributed/device_communicators/shm_object_storage.py#L375-L400`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/distributed/device_communicators/shm_object_storage.py#L375-L400) — SHM object deserialization reaches `pickle.loads(serialized_data)` for byte objects.
- [`vllm/multimodal/cache.py#L567-L581`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/multimodal/cache.py#L567-L581) — sender emits an SHM carrier containing only `address` and `monotonic_id`.
- [`vllm/multimodal/cache.py#L711-L737`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/multimodal/cache.py#L711-L737) — worker dereferences the untrusted pair and reads the SHM object.
- [`vllm/entrypoints/scale_out/token_in_token_out/serving.py#L146-L173`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/entrypoints/scale_out/token_in_token_out/serving.py#L146-L173) — public `features.kwargs_data` is decoded into engine input without authenticating SHM handles before handoff.

The public scale-out route decodes attacker-controlled `features.kwargs_data` into `MultiModalKwargsItem`s and hands them straight to the engine — no authentication of the SHM handle:

```python
# vllm/entrypoints/scale_out/token_in_token_out/serving.py Lines 155-165
            # Deserialize tensor data when present; None → cache hit.
            mm_kwargs: dict[str, list[MultiModalKwargsItem | None]] = {}
            if features.kwargs_data is not None:
                for modality, items in features.kwargs_data.items():
                    mm_kwargs[modality] = [
                        decode_mm_kwargs_item(item) if item is not None else None
                        for item in items
                    ]
            else:
                for modality, hashes in features.mm_hashes.items():
                    mm_kwargs[modality] = [None] * len(hashes)
```

In the worker, a decoded item that carries an `address` is treated as authority to dereference shared memory: the `(address, monotonic_id)` pair alone drives the read, with no binding to the cache key or to who issued the handle:

```python
# vllm/multimodal/cache.py Lines 711-723
    @override
    def get_and_update_item(
        self,
        mm_item: MultiModalKwargsItem | None,
        mm_hash: str,
    ) -> MultiModalKwargsItem:
        assert mm_item is not None, f"Expected an address item for {mm_hash=}"
        if "address" in mm_item:
            address = cast(int, mm_item["address"].data)
            monotonic_id = cast(int, mm_item["monotonic_id"].data)
            return self._shm_cache.get(address, monotonic_id)

        return mm_item
```

The dereference lands in the SHM deserializer, which `pickle.loads()` any chunk tagged as `bytes` — so attacker bytes staged in the ring buffer are unpickled in the worker process:

```python
# vllm/distributed/device_communicators/shm_object_storage.py Lines 375-400
    def deserialize(self, data_view: memoryview) -> Any:
        # pickle.loads do not read past the end of a pickled object
        # within a large buffer, so we can skip storing the metadata size
        type_name, nbytes, len_arr = pickle.loads(data_view)
        serialized_data = data_view[-nbytes:]

        if type_name == torch.Tensor.__name__:
            obj = []
            start_idx = 0
            for length in len_arr:
                item_bytes = serialized_data[start_idx : start_idx + length]
                obj.append(item_bytes)
                start_idx += length
            obj = self.tensor_decoder.decode(obj)
        elif type_name == self._mm_kwargs_item_cls.__name__:
            obj = []
            start_idx = 0
            for length in len_arr:
                item_bytes = serialized_data[start_idx : start_idx + length]
                obj.append(item_bytes)
                start_idx += length
            obj = self.mm_decoder.decode(obj)
        elif type_name == bytes.__name__:
            obj = pickle.loads(serialized_data)
        else:
            raise ValueError(f"Unsupported object type '{type_name}' in metadata")

        return obj
```

## Impact

An ordinary authenticated inference caller can execute arbitrary Python in the worker process on a deployment that uses SHM multimodal caching. The live proof used a benign marker gadget and also caused the EngineCore health endpoint to fall from 200 to 503 after the forged request. The exploit is reachable with `VLLM_ALLOW_INSECURE_SERIALIZATION=0`; it abuses the built-in SHM object deserializer, not the separate optional msgpack ext-hook flag.


## Suggested Fix

Authenticate every SHM carrier with a server-issued capability bound to the cache key, address, and monotonic id; rotate the capability secret when the cache is cleared; and reject malformed or forged SHM carriers at the public scale-out boundary before engine handoff — so a forged pair never reaches the worker deref / `pickle.loads()`.

The sender emits an HMAC capability (keyed by a 32-byte secret stored in the ring buffer and rotated on `clear()`) over `(cache_key, address, monotonic_id)` alongside the pair. The public scale-out path validates it before engine handoff:

```python
# vllm/entrypoints/scale_out/token_in_token_out/serving.py
        cache = self.online_renderer.renderer.mm_processor_cache
        for mm_hash, item in zip(hashes, items):
            decoded_item = decode_mm_kwargs_item(item) if item is not None else None
            if decoded_item is not None and cache is not None:
                # Rejects a forged (address, monotonic_id) that lacks a valid
                # server-issued capability for this cache key.
                cache.validate_input_item(decoded_item, mm_hash)
            decoded_items.append(decoded_item)
```

The worker `get()` re-checks the capability on the reader side before it ever dereferences the caller-supplied address:

```python
# vllm/distributed/device_communicators/shm_object_storage.py
    def get(self, address, monotonic_id, capability=None, key=None):
        if not self.is_writer:
            # Reject forged handles before dereferencing the supplied address.
            self.validate_capability(key, address, monotonic_id, capability)
        with self.ring_buffer.access_buf(address) as (data_view, buf_metadata):
            self._validate_monotonic_id(address, monotonic_id, buf_metadata)
            obj = self.ser_de.deserialize(data_view[self.flag_bytes :])
            ...
```

where `validate_capability` recomputes the HMAC with `hmac.compare_digest` and raises `Missing server-issued SHM capability for cache key.` / `Invalid server-issued SHM capability for cache key.` on any mismatch. The fix applies cleanly to `v0.25.1` and ships regression tests asserting the generate path rejects an invalid handle and that the receiver requires a matching sender handle (and a capability issued after the current `clear()`) for the cache key.

## 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 proposed change for this issue is up for review in this advisory's temporary private fork: https://github.com/vllm-project/vllm-ghsa-6757-hggm-hp2w/pull/1

This advisory was fixed in [MR59357](https://github.com/vllm-project/vllm/pull/59357).

## Affected packages

- `vllm < 0.32.0`

## Remediation

Upgrade to a patched release:

- `vllm 0.32.0`
