● Zion Boggan
repos/Cyclops V2/vault/01-archetypes/ml-model-loaders.md
zionboggan.com ↗
201 lines · markdown
History for this file →
1
---
2
archetype: ml-model-loaders
3
headline: true
4
primary_gate_families: [1, 7]
5
---
6
 
7
# Archetype - ML Model Loaders & Inference Runtimes (HEADLINE)
8
 
9
**This is the highest-yield archetype for Cyclops v2.** The thesis: model files are *executable
10
attack surface masquerading as data*. The entire ML ecosystem ships parsers and deserializers that
11
run at **load time**, before any inference, often in C/C++ with weak bounds checking, written fast,
12
shipped weekly, and consumed by millions who treat a downloaded `.gguf` / `.safetensors` / `.pt` /
13
`.onnx` as inert. It is not inert. A model is a program.
14
 
15
Two distinct kill-chain classes live here and you must hunt both:
16
- **(A) Deserialization-as-design** - formats that *intentionally* execute code (pickle, joblib,
17
  arbitrary Python in custom nodes). The bug is the trust, not a memory error.
18
- **(B) Parser memory-corruption** - formats that *claim* to be safe data (GGUF, safetensors, ONNX
19
  protobuf, NPY) but whose C/C++ parsers have OOB read/write, integer overflow, type confusion. The
20
  bug is in the loader, and the oracle is ASan.
21
 
22
See: [[../00-doctrine/SEVERITY-GATE#family-1-remote-code-execution-the-crown|Gate Family 1]] ·
23
[[memory-unsafe-parsers]] (sister archetype for the pure-parser fuzzing mechanics) ·
24
[[ai-agent-mcp]] (model loaders reached *over the network* via inference servers).
25
 
26
---
27
 
28
## Principals & trust boundaries
29
 
30
- **Principals:** the model *author/publisher* (untrusted - anyone on HuggingFace Hub, Civitai, an
31
  npm/PyPI package, a torrent), the *runtime/library* (the thing parsing the file), the *host user*
32
  (whose process and credentials the load runs as).
33
- **The core trust boundary:** *"downloaded model file → load function"*. Every user who does
34
  `from_pretrained(...)`, `llama_load_model_from_file(...)`, `onnxruntime.InferenceSession(...)`,
35
  `torch.load(...)`, `joblib.load(...)`, `comfyui` "drag this workflow in" - crosses it.
36
- **Why it's so wide:** model sharing is the *entire culture* of the field. The file is meant to be
37
  passed around. The trust boundary is crossed billions of times a day and almost no consumer thinks
38
  of the file as code.
39
 
40
**Boundary map (fill this per target):**
41
```
42
principals:        [model publisher (untrusted)] → [loader lib] → [host process / user creds]
43
trust inputs:      .gguf | .safetensors | .pt/.pth | .onnx | .npy/.npz | .pkl | .joblib |
44
                   .bin (HF) | workflow.json (ComfyUI) | model_index.json | tokenizer.json |
45
                   config.json | *.gradio cache | adapter_config.json (PEFT/LoRA)
46
enforcement pts:   header magic check, version gate, "weights_only" flag, allowlist of globals,
47
                   tensor-count/dim sanity checks, mmap bounds, protobuf size limits
48
cross-boundary
49
sinks:             pickle.Unpickler.find_class / __reduce__ ; numpy buffer parse ;
50
                   memcpy/alloca from file-controlled length ; ggml tensor dim multiply ;
51
                   protobuf arena alloc ; eval/exec in custom node ; os.system in workflow
52
confusion
53
hypotheses:        "weights_only=True is actually bypassable here"; "the header length field is
54
                   trusted before the file size is checked"; "n_dims * n_elements overflows size_t";
55
                   "the allowlist misses one __reduce__-able global"; "the safe format has an
56
                   unsafe metadata side-channel"
57
```
58
 
59
---
60
 
61
## Kill chains (each lands on a Gate family)
62
 
63
### KC-1 - Pickle / `__reduce__` deserialization → RCE (Class A)
64
The classic and *still* live. `pickle` calls `__reduce__` on unpickling, which can return
65
`(os.system, ('id',))`. PyTorch `.pt`/`.pth` are zip files containing a pickle. `joblib`/sklearn
66
`.pkl`, many HF `.bin` legacy weights, are pickles.
67
- **Sinks:** `pickle.load`, `pickle.Unpickler.find_class`, `torch.load(..., weights_only=False)`,
68
  `joblib.load`, `numpy.load(..., allow_pickle=True)`, `dill.load`, `cloudpickle.load`.
69
- **The fresh angle (novelty, not sibling-CVE):** everyone knows raw pickle is dangerous. The novel
70
  bugs are in the **allowlist bypasses** of the "safe" loaders: `weights_only=True` (PyTorch's
71
  defense) has had repeated bypasses (e.g. via allowed globals that themselves reach dangerous
72
  reductions; via `torch.storage` / rebuild functions; via nested types the allowlist forgot). Hunt
73
  the *allowlist*, the `_get_allowed_globals` table, the `GLOBAL`/`REDUCE` opcode handling - find one
74
  reachable dangerous global the allowlist trusts.
75
- **Oracle:** loading the crafted model fires a callback / writes `/tmp/cyclops-$RAND` → Family 1.
76
- **CVE class to cite & avoid duplicating:** CVE-2025-class PyTorch `weights_only` bypasses,
77
  `numpy.load` allow_pickle RCE, the perennial sklearn/joblib pickle RCE. Check the feed first.
78
 
79
### KC-2 - GGUF / GGML parser memory-corruption → RCE (Class B) *(prime hunting ground)*
80
`llama.cpp` and everything built on it (Ollama, LM Studio, text-generation-webui, countless wrappers)
81
parse GGUF with hand-written C. The format has a header → metadata KV array → tensor-info array →
82
tensor data, all with **file-controlled length and count fields**.
83
- **Sinks to CPG-trace:** `gguf_init_from_file`, `gguf_read_emplace`, the KV-pair reader
84
  (`gguf_kv` / `gguf_get_arr_*`), `ggml_new_tensor`, `ggml_nbytes`, `ggml_row_size`, and every
85
  `malloc`/`GGML_MALLOC`/`memcpy` whose size derives from a file field.
86
- **Bug shapes:** integer overflow in `n_dims * ne[i] * type_size` → undersized alloc → heap
87
  overflow on tensor copy; an array `n` count read before validating it against remaining file size →
88
  OOB read; a metadata string length field trusted into a `memcpy`/`malloc`; type/enum confusion in
89
  `ggml_type` indexing a per-type size table OOB.
90
- **Oracle:** build `llama.cpp` (or the wrapper) with `-fsanitize=address`, feed the crafted `.gguf`,
91
  get the ASan report with a controlled fault → Family 1 (SOURCE/REPLICA). Escalate to control of
92
  RIP/allocations for the full RCE claim.
93
- **CVE class:** the GGUF heap-overflow / OOB family (multiple 2024 llama.cpp advisories). Novelty =
94
  a *different* field or a wrapper that re-implemented the parse and reintroduced the class.
95
 
96
### KC-3 - safetensors metadata / header parse (Class B, the "safe format" trap)
97
`safetensors` was designed to *be the safe alternative* to pickle - no code execution by design. That
98
makes it the perfect place for a novel bug, because everyone assumes it can't have one.
99
- **Surface:** the 8-byte little-endian header length, then a JSON header describing each tensor's
100
  `dtype`, `shape`, and **byte offsets** `[begin, end]` into the buffer. The Rust core is memory-safe,
101
  but: (a) the **offset/shape arithmetic** - does `begin..end` get validated against the buffer and
102
  against `shape × dtype_size`? An offset overflow or a mismatch can yield OOB; (b) **language ports** -
103
  C++/Zig/Go re-implementations and the C ABI consumers don't get Rust's guarantees; (c) the JSON
104
  metadata fed to a *downstream* consumer that does something unsafe with it.
105
- **Sinks:** `SafeTensors::deserialize`, the offset-validation routine, and any C/C++ reimpl's
106
  `memcpy(dst, base+begin, end-begin)`.
107
- **Oracle:** ASan crash on a port, or a proven OOB read returning out-of-tensor bytes.
108
 
109
### KC-4 - ONNX / protobuf model graph → parse corruption or operator abuse (Class B)
110
ONNX Runtime parses a protobuf graph then *builds and runs operators*. Two sub-surfaces:
111
- **Protobuf/loader corruption:** `onnx::ModelProto` parse, external-data references (an ONNX model
112
  can point at an *external* data file path - path traversal / arbitrary file read sink), and the
113
  shape-inference pass (`onnxruntime::...ShapeInference`) doing arithmetic on attacker dims.
114
- **Operator-level:** custom ops, the `Scan`/`If`/`Loop` control-flow ops, and ops that allocate from
115
  attacker-controlled dims. ORT has a real history of heap overflows in op kernels.
116
- **External-data trap (Family 7):** the `external_data` location field can be a path → arbitrary
117
  file read at load. Cite the ONNX external-tensor path-traversal class.
118
- **Sinks:** `onnxruntime::Model::Load`, `LoadExternalTensor`, op `Compute()` kernels with dim-derived
119
  allocs. **Oracle:** ASan crash, or the external-data path reading our canary file → Family 7.
120
 
121
### KC-5 - ComfyUI custom nodes / workflow.json → RCE (Class A, social)
122
ComfyUI workflows are shared as JSON/PNG-embedded graphs that reference **custom nodes** (arbitrary
123
Python packages auto-installed from a registry). "Load this cool workflow" → pulls a custom node →
124
runs its `__init__` / node code.
125
- **Sinks:** the custom-node loader (`nodes.py` import path), any node calling `eval`/`exec`/
126
  `subprocess`/`os.system` on workflow-supplied strings, the model-manager auto-download.
127
- **This is also a [[cicd-supplychain|supply-chain]] / [[ai-agent-mcp|agent]] surface** - rotate the lens.
128
- **Oracle:** importing/running the malicious workflow fires a callback → Family 1.
129
 
130
### KC-6 - Gradio / model-server file & SSRF surface (Family 7, bridges to [[ai-agent-mcp]])
131
Gradio (the demo UI under most HF Spaces and local model demos) has a history of path traversal
132
(`/file=` arbitrary file read) and SSRF in the proxy/`/queue` paths.
133
- **Sinks:** the `/file=` route's path resolution, the URL-fetch in upload/proxy handlers.
134
- **Oracle:** read our canary secret via `/file=` (Family 7) or land SSRF→metadata.
135
- **CVE class:** Gradio `/file=` traversal (CVE-2023/2024 class), Gradio SSRF. Find the *new* route.
136
 
137
### KC-7 - Tokenizer / config side-channels (Class A/B, the overlooked input)
138
Even with "safe" weights, the *accompanying* files load too: `tokenizer.json`,
139
`tokenizer_config.json`, `config.json`, `generation_config.json`, `chat_template` (a **Jinja2
140
template** → SSTI sink!), `adapter_config.json` for PEFT/LoRA. The `chat_template` Jinja2 is a real
141
[[../00-doctrine/SEVERITY-GATE#family-1-remote-code-execution-the-crown|SSTI→RCE]] surface - HF added
142
a sandbox; hunt the sandbox bypass.
143
- **Sinks:** `jinja2.Environment` rendering the chat template, any `eval`-ish config parse, the
144
  `trust_remote_code=True` path (which literally imports model-repo Python - a known but still-fertile
145
  RCE: hunt where `trust_remote_code` defaults true or is reachable without the flag).
146
- **Oracle:** template/remote-code executes → Family 1.
147
 
148
---
149
 
150
## The dynamic oracle, concretely (this archetype's whole point)
151
 
152
For **Class B (parser memory-corruption)** the pipeline is:
153
1. `git clone` the freshest in-scope loader. Pin the commit.
154
2. Build with `-fsanitize=address,undefined` (or use the project's fuzz harness if present -
155
   `llama.cpp` ships `tests/`, ORT has fuzzers, many have OSS-Fuzz targets you can *re-run on new code*).
156
3. Write a libFuzzer/AFL++ harness whose entry is the **load function** (`gguf_init_from_file`,
157
   `safetensors deserialize`, `Model::Load`). Seed with a real valid model, mutate.
158
4. Run; collect crashes; **`triage_crash`** to confirm a *controlled* fault (write to attacker data,
159
   PC control, OOB on heap with our pattern). A null-deref / pure read-DoS is **DISPROVEN for RCE**
160
   per [[../00-doctrine/SEVERITY-GATE#family-6-mass-data-exfil-financial-impact|the gate]] - log it,
161
   do not dress a DoS as critical.
162
5. Minimize the crashing model file; that file *is* the REPLICA_CONFIRMED artifact.
163
 
164
For **Class A (deserialization)** the oracle is direct: craft the malicious model, load it in a
165
disposable container, observe the callback/file-write. No fuzzing needed - the proof is the popped
166
shell.
167
 
168
Relevant tooling in the kit: ASan/UBSan builds, libFuzzer/AFL++/`boofuzz`, `joern_query` &
169
`codeql_query` for the source→sink path, `triage_crash`, a disposable `sandbox_create` to detonate
170
Class-A models, `start_callback_server` for the RCE callback.
171
 
172
---
173
 
174
## BORING traps - do NOT spend time here
175
 
176
- **"`torch.load` runs arbitrary pickle."** Known since forever. Reporting raw-pickle RCE against a
177
  tool that *documents* "only load models you trust" is a non-finding. The finding is the
178
  **safe-path bypass** (`weights_only=True` defeated, allowlist escaped, a "safe" format corrupted).
179
- **Null-deref / assertion-abort / pure read-DoS in a parser.** Not RCE. Cleared-axes hardening note.
180
- **"This model could be malicious."** Of course it could. That is the threat model, not a bug. The
181
  bug is a *specific* loader flaw with an *artifact*.
182
- **Re-finding a public llama.cpp/ORT CVE in an old checkout.** Sibling-CVE. Hunt code days old.
183
- **Out-of-memory from a huge dim field.** Resource exhaustion ≠ corruption. Verify it's actually an
184
  OOB write before getting excited.
185
- **Gradio version banner / "old Gradio".** Version-based "findings" are not findings; prove the route.
186
 
187
---
188
 
189
## Where the novel bugs actually are (steer here)
190
 
191
1. **Allowlist / safe-mode bypasses** in the *defenses* (`weights_only`, find_class allowlists, the
192
   chat-template sandbox, safetensors offset validation). The defense is newer code than the format.
193
2. **Re-implementations** - a Go/C++/Zig/Rust-C-ABI port of a "safe" format that dropped a check.
194
3. **Integer arithmetic on attacker dims** - `n0*n1*n2*type_size` overflow before alloc. Grep/CPG for
195
   every multiply feeding a `malloc`/`memcpy` in `ggml`, ORT kernels, NPY/`safetensors` consumers.
196
4. **External-reference fields** - ONNX `external_data` path, GGUF embedded paths, anything where the
197
   model file names *another* file/URL → traversal / SSRF (Family 7).
198
5. **The accompanying files** - tokenizer/chat-template/config that load alongside "safe" weights and
199
   reach Jinja2/`trust_remote_code`/eval.
200
6. **Freshly-merged format extensions** - a new GGUF KV type, a new ONNX opset, a new quant format
201
   landed last week. New parse code = unfuzzed code. See [[../06-methods/NOVELTY-DOCTRINE]].