GHSA-6757-hggm-hp2w

vLLM: Public multimodal SHM handle forgery reaches worker-side `pickle.loads()` with insecure serialization disabled

En bref

Publiée
Mise à jour
CVE
CVE-2026-115912
CVSS_V3
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

Description

## 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).

Paquets affectés

PaquetVersions
PyPI · vllmintroduced 0, fixed 0.32.0

Références

Sources

SourceLicence
OSV.devCC-BY 4.0 — GitHub Advisory Database