vLLM: 公開されているマルチモーダルSHMのハンドル偽装により、ワーカー側の`pickle.loads()`が脆弱なシリアル化が無効な状態で呼び出される
## 対象
- **エコシステム/パッケージ:** pip / `vllm`
- **影響を受けるバージョン:** vLLM < 0.32.0
## 概要
`--mm-processor-cache-type shm`が有効になっている場合、vLLMのマルチモーダルSHMキャッシュは、`address`と`monotonic_id`のペアでキャッシュされたアイテムを表し、そのペアをワーカー側で共有メモリへの参照の権限として扱います。 公開されているスケールアウトリクエストは、独自のマルチモーダル`features.kwargs_data`を提供できるため、認証されたユーザーは、攻撃者が制御するバイトが既にリングバッファに配置されている、内部SHMキャリアを偽造することができます。 その後、ワーカーは偽造されたオブジェクトをデシリアライズする際に`pickle.loads()`を呼び出します。
これは、通常の公開HTTPルートを通じて到達可能であり、`VLLM_ALLOW_INSECURE_SERIALIZATION=1`を必要としません。 実際に使用されたのは、有効な特徴エンベロープを取得するために`/v1/chat/completions/render`、攻撃者が選択したバイトをSHMに配置するために`/v1/chat/completions`、そして偽造された内部キャリアを送信するために`/inference/v1/generate`です。
## 影響を受けるコード
確認されたコミットへのリンク:[`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オブジェクトのデシリアライズ処理において、バイトオブジェクトに対して`pickle.loads(serialized_data)`が実行される。
- [`vllm/multimodal/cache.py#L567-L581`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/multimodal/cache.py#L567-L581) — 送信側は、`address`と`monotonic_id`のみを含むSHMキャリアを送信する。
- [`vllm/multimodal/cache.py#L711-L737`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/multimodal/cache.py#L711-L737) — 処理側は、信頼できないペアを参照し、SHMオブジェクトを読み込む。
- [`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) — 公開されている`features.kwargs_data`が、SHMハンドルを認証することなく、エンジンへの入力としてデコードされる。
公開されている分散処理の経路では、攻撃者が制御する`features.kwargs_data`が`MultiModalKwargsItem`にデコードされ、認証なしでエンジンに直接渡される。
```python
# vllm/entrypoints/scale_out/token_in_token_out/serving.py 155-165行目
# テンソルデータが存在する場合はデシリアライズする。Noneの場合、キャッシュヒット。
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)
```
ワーカープロセスにおいて、`address` を含むデコードされたアイテムは、共有メモリへのアクセス権として扱われます。`(address, monotonic_id)` のペアだけで読み込みが行われ、キャッシュキーやハンドル発行者との関連付けはありません。
```python
# vllm/multimodal/cache.py 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
```
このアクセス処理は、SHMデシリアライザーに到達し、`pickle.loads()` を使用して `bytes` としてタグ付けされたデータチャンクを読み込みます。したがって、リングバッファに配置された攻撃者のデータも、ワーカープロセスでデシリアライズされます。
```python
# vllm/distributed/device_communicators/shm_object_storage.py 375-400行目
def deserialize(self, data_view: memoryview) -> Any:
# pickle.loads は、大きなバッファ内のピクル化されたオブジェクトの末尾を超えて読み込まないため、メタデータのサイズを保存する必要はありません。
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"メタデータ内のサポートされていないオブジェクトタイプ '{type_name}' が検出されました")
return obj
```
## 影響
認証された推論の呼び出し元は、SHM(共有メモリ)のマルチモーダルキャッシュを使用する環境において、ワーカープロセスで任意のPythonコードを実行できる可能性があります。 実証実験では、無害なマーカーが使用され、また、偽装されたリクエストによって、EngineCoreのヘルスエンドポイントが200から503に変化しました。 この脆弱性は、`VLLM_ALLOW_INSECURE_SERIALIZATION=0` の設定で利用可能です。 これは、オプションのmsgpack拡張機能ではなく、組み込みのSHMオブジェクトデシリアライザを悪用しています。
## 推奨される対策
すべてのSHMデータに対して、キャッシュキー、アドレス、および単調増加IDにバインドされた、サーバーが発行するcapability(認証情報)を使用します。 キャッシュがクリアされる際に、capabilityのシークレットをローテーションします。 また、エンジンへの引き渡し前に、パブリックなスケールアウト境界で、不正または偽装されたSHMデータを拒否します。 これにより、偽装されたデータがワーカープロセスに到達し、`pickle.loads()` などのデシリアライゼーション処理が行われるのを防ぎます。
送信側は、リングバッファに格納された32バイトのシークレットキーでキーを生成したHMAC capabilityを、`(cache_key, address, monotonic_id)` と共に送信します。 このシークレットは、`clear()` 関数でローテーションされます。 パブリックなスケールアウトパスは、エンジンへの引き渡し前に、このcapabilityを検証します。
```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:
# 有効なキャッシュキーに対する、サーバーが発行した適切な権限を持たない、
# 偽造された(アドレス、モノトニックID)を拒否します。
cache.validate_input_item(decoded_item, mm_hash)
decoded_items.append(decoded_item)
```
ワーカーの `get()` メソッドは、呼び出し元から提供されたアドレスを参照する前に、リーダー側で権限を再確認します。
```python
# vllm/distributed/device_communicators/shm_object_storage.py
def get(self, address, monotonic_id, capability=None, key=None):
if not self.is_writer:
# アドレスを参照する前に、偽造されたハンドルを拒否します。
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 :])
...
```
ここで、`validate_capability` は `hmac.compare_digest` を使用して HMAC を再計算し、不一致がある場合は `Missing server-issued SHM capability for cache key.` または `Invalid server-issued SHM capability for cache key.` というエラーを発生させます。この修正は `v0.25.1` に問題なく適用でき、生成パスが不正なハンドルを拒否すること、および受信側がキャッシュキーに対して、送信側からの対応するハンドル(および現在の `clear()` の後に発行された権限)を必要とすることを検証する回帰テストが含まれています。
## 貢献者
**報告者:** Patch the Planet (Trail of Bits + OpenAI の共同プロジェクト)
この脆弱性は、Patch the Planet セキュリティイニシアチブの一環として、GPT-5.5-Cyber を使用して発見されました。
**提案された修正:** この問題に対する修正案は、このアドバイザリの非公開のフォークでレビュー中です: https://github.com/vllm-project/vllm-ghsa-6757-hggm-hp2w/pull/1
このアドバイザリは、[MR59357](https://github.com/vllm-project/vllm/pull/59357) で修正されました。
影響を受けるパッケージ:
- pip (Python) vllm: 影響を受けるバージョンは 0.32.0 未満、修正されたバージョンは 0.32.0。