GHSA-6757-hggm-hp2w
vLLM: Public multimodal SHM handle forgery reaches worker-side `pickle.loads()` with insecure serialization disabled
Packages
Details
As written in the advisory (Markdown source, shown as plain text).
## 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).
References
External links from the advisory. They lead to third-party sites.
- https://github.com/vllm-project/vllm/security/advisories/GHSA-6757-hggm-hp2w
- https://nvd.nist.gov/vuln/detail/CVE-2026-115912
- https://github.com/vllm-project/vllm/pull/59357
- https://github.com/vllm-project/vllm/commit/3d5625cc3c2474994107047d7a6ecb8e4008f9b0
- https://github.com/vllm-project/vllm
- https://github.com/vllm-project/vllm/releases/tag/v0.32.0