{"id":"GHSA-6757-hggm-hp2w","summary":"vLLM: Public multimodal SHM handle forgery reaches worker-side `pickle.loads()` with insecure serialization disabled","details":"## Affected\n\n- **Ecosystem / package:** pip / `vllm`\n- **Affected versions:** vLLM \u003c 0.32.0.\n\n## Summary\n\nWhen `--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.\n\nThis 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.\n\n## Affected code\n\nLinks pinned to the confirmed commit [`752a3a504485`](https://github.com/vllm-project/vllm/tree/752a3a504485790a2e8491cacbb35c137339ad34) (v0.25.1):\n\n- [`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.\n- [`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`.\n- [`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.\n- [`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.\n\nThe 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:\n\n```python\n# vllm/entrypoints/scale_out/token_in_token_out/serving.py Lines 155-165\n            # Deserialize tensor data when present; None → cache hit.\n            mm_kwargs: dict[str, list[MultiModalKwargsItem | None]] = {}\n            if features.kwargs_data is not None:\n                for modality, items in features.kwargs_data.items():\n                    mm_kwargs[modality] = [\n                        decode_mm_kwargs_item(item) if item is not None else None\n                        for item in items\n                    ]\n            else:\n                for modality, hashes in features.mm_hashes.items():\n                    mm_kwargs[modality] = [None] * len(hashes)\n```\n\nIn 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:\n\n```python\n# vllm/multimodal/cache.py Lines 711-723\n    @override\n    def get_and_update_item(\n        self,\n        mm_item: MultiModalKwargsItem | None,\n        mm_hash: str,\n    ) -\u003e MultiModalKwargsItem:\n        assert mm_item is not None, f\"Expected an address item for {mm_hash=}\"\n        if \"address\" in mm_item:\n            address = cast(int, mm_item[\"address\"].data)\n            monotonic_id = cast(int, mm_item[\"monotonic_id\"].data)\n            return self._shm_cache.get(address, monotonic_id)\n\n        return mm_item\n```\n\nThe 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:\n\n```python\n# vllm/distributed/device_communicators/shm_object_storage.py Lines 375-400\n    def deserialize(self, data_view: memoryview) -\u003e Any:\n        # pickle.loads do not read past the end of a pickled object\n        # within a large buffer, so we can skip storing the metadata size\n        type_name, nbytes, len_arr = pickle.loads(data_view)\n        serialized_data = data_view[-nbytes:]\n\n        if type_name == torch.Tensor.__name__:\n            obj = []\n            start_idx = 0\n            for length in len_arr:\n                item_bytes = serialized_data[start_idx : start_idx + length]\n                obj.append(item_bytes)\n                start_idx += length\n            obj = self.tensor_decoder.decode(obj)\n        elif type_name == self._mm_kwargs_item_cls.__name__:\n            obj = []\n            start_idx = 0\n            for length in len_arr:\n                item_bytes = serialized_data[start_idx : start_idx + length]\n                obj.append(item_bytes)\n                start_idx += length\n            obj = self.mm_decoder.decode(obj)\n        elif type_name == bytes.__name__:\n            obj = pickle.loads(serialized_data)\n        else:\n            raise ValueError(f\"Unsupported object type '{type_name}' in metadata\")\n\n        return obj\n```\n\n## Impact\n\nAn 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.\n\n\n## Suggested Fix\n\nAuthenticate 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()`.\n\nThe 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:\n\n```python\n# vllm/entrypoints/scale_out/token_in_token_out/serving.py\n        cache = self.online_renderer.renderer.mm_processor_cache\n        for mm_hash, item in zip(hashes, items):\n            decoded_item = decode_mm_kwargs_item(item) if item is not None else None\n            if decoded_item is not None and cache is not None:\n                # Rejects a forged (address, monotonic_id) that lacks a valid\n                # server-issued capability for this cache key.\n                cache.validate_input_item(decoded_item, mm_hash)\n            decoded_items.append(decoded_item)\n```\n\nThe worker `get()` re-checks the capability on the reader side before it ever dereferences the caller-supplied address:\n\n```python\n# vllm/distributed/device_communicators/shm_object_storage.py\n    def get(self, address, monotonic_id, capability=None, key=None):\n        if not self.is_writer:\n            # Reject forged handles before dereferencing the supplied address.\n            self.validate_capability(key, address, monotonic_id, capability)\n        with self.ring_buffer.access_buf(address) as (data_view, buf_metadata):\n            self._validate_monotonic_id(address, monotonic_id, buf_metadata)\n            obj = self.ser_de.deserialize(data_view[self.flag_bytes :])\n            ...\n```\n\nwhere `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.\n\n## Credit\n\n**Reported by:** Patch the Planet (Trail of Bits + OpenAI collaboration)\n\nThis vulnerability was discovered using GPT-5.5-Cyber as part of the Patch the Planet security initiative.\n\n---\n\n**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\n\nThis advisory was fixed in [MR59357](https://github.com/vllm-project/vllm/pull/59357).","aliases":["CVE-2026-115912","PYSEC-2026-4345"],"modified":"2026-10-29T11:10:54.932940889Z","published":"2026-10-28T21:15:15Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-10-28T21:15:15Z","nvd_published_at":"2026-10-26T17:17:18Z","cwe_ids":["CWE-502"],"severity":"HIGH"},"references":[{"type":"WEB","url":"https://github.com/vllm-project/vllm/security/advisories/GHSA-6757-hggm-hp2w"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-115912"},{"type":"WEB","url":"https://github.com/vllm-project/vllm/pull/59357"},{"type":"WEB","url":"https://github.com/vllm-project/vllm/commit/3d5625cc3c2474994107047d7a6ecb8e4008f9b0"},{"type":"PACKAGE","url":"https://github.com/vllm-project/vllm"},{"type":"WEB","url":"https://github.com/vllm-project/vllm/releases/tag/v0.32.0"}],"affected":[{"package":{"name":"vllm","ecosystem":"PyPI","purl":"pkg:pypi/vllm"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.32.0"}]}],"versions":["0.0.1","0.1.0","0.1.1","0.1.2","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.10.0","0.10.1","0.10.1.1","0.10.2","0.11.0","0.11.1","0.11.2","0.12.0","0.13.0","0.14.0","0.14.1","0.15.0","0.15.1","0.16.0","0.17.0","0.17.1","0.18.0","0.18.1","0.19.0","0.19.1","0.2.0","0.2.1","0.2.1.post1","0.2.2","0.2.3","0.2.4","0.2.5","0.2.6","0.2.7","0.20.0","0.20.1","0.20.2","0.21.0","0.22.0","0.22.1","0.23.0","0.24.0","0.25.0","0.25.1","0.26.0","0.27.0","0.27.1","0.28.0","0.29.0","0.3.0","0.3.1","0.3.2","0.3.3","0.30.0","0.31.0","0.4.0","0.4.0.post1","0.4.1","0.4.2","0.4.3","0.5.0","0.5.0.post1","0.5.1","0.5.2","0.5.3","0.5.3.post1","0.5.4","0.5.5","0.6.0","0.6.1","0.6.1.post1","0.6.1.post2","0.6.2","0.6.3","0.6.3.post1","0.6.4","0.6.4.post1","0.6.5","0.6.6","0.6.6.post1","0.7.0","0.7.1","0.7.2","0.7.3","0.8.0","0.8.1","0.8.2","0.8.3","0.8.4","0.8.5","0.8.5.post1","0.9.0","0.9.0.1","0.9.1","0.9.2"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-6757-hggm-hp2w/GHSA-6757-hggm-hp2w.json"}}],"schema_version":"1.9.2","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H"}]}