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

Forged SHM (address, monotonic_id) carrier in vLLM's public scale-out features.kwargs_data reaches worker-side pickle.loads(), giving authenticated callers arbitrary Python execution.

- Payout: Research
- Severity: high
- Program: vLLM
- Found by: KernelClint
- Disclosed: 2026-10-28
- CVE: CVE-2026-115912
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-6757-hggm-hp2w
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-6757-hggm-hp2w
- Tags: area:ai domain:ai domain:api impact:rce kind:research platform:ghsa sev:high tech:python technique:chained technique:source-review vuln:deserialization vuln:rce

**Target:** vLLM scale-out inference HTTP routes with multimodal SHM processor cache
**Root cause:** The multimodal SHM cache treats a caller-supplied (address, monotonic_id) pair as authority to dereference shared memory, with no capability binding it to the cache key or issuer; the SHM deserializer then pickle.loads() any chunk tagged as bytes.
**Steps:**
1. Enable `--mm-processor-cache-type shm` on the deployment.
2. Call `/v1/chat/completions/render` to obtain a valid feature envelope.
3. Call `/v1/chat/completions` to stage attacker-chosen bytes into the SHM ring buffer.
4. Submit a forged inner SHM carrier via `/inference/v1/generate` in `features.kwargs_data` with attacker-controlled `address` and `monotonic_id`.
5. Worker dereferences the pair in `get_and_update_item` and `pickle.loads()` the staged bytes.
**How found:** Code reading of the scale-out serving path showed public `features.kwargs_data` decoded into engine input without authenticating SHM handles; tracing the (address, monotonic_id) pair into the worker deref and the bytes branch of the SHM deserializer revealed the pickle.loads() sink.
**Exploitation:** Authenticated caller chains three public routes: render to get a valid envelope, chat/completions to stage bytes in SHM, then /inference/v1/generate with a forged carrier pointing at those bytes. Worker unpickles them, executing arbitrary Python; live PoC used a benign marker gadget and drove EngineCore health from 200 to 503.
**Escalation:** Works with VLLM_ALLOW_INSECURE_SERIALIZATION=0, abusing the built-in SHM deserializer rather than the optional msgpack ext-hook flag.
**Insight:** When a cache handle is just an (address, id) pair, check whether the reader validates it against the cache key or issuer — a public field that carries such a handle into a worker is a deserialization sink even with insecure-serialization flags off.

## Proof of concept (copied exactly from the original report)

**Worker dereferences forged SHM address pair**

```
@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
```

**SHM deserializer pickle.loads on bytes chunk**

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

**Public scale-out decodes unauthenticated kwargs_data**

```
# 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
                    ]
```

