# Diffusers: TOCTOU Trust Remote Code Bypass

Diffusers' trust_remote_code check reads model_index.json via one Hub call, then snapshot_download fetches a newer commit, letting a repo swap in a malicious pipeline.py for silent RCE.

- Payout: Research
- Severity: high
- Program: Diffusers
- Found by: zafido, gal-zafran
- Disclosed: 2026-05-20
- CVE: CVE-2026-45804
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-7wx4-6vff-v64p
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-7wx4-6vff-v64p
- Tags: area:ai domain:ai domain:oss impact:rce kind:research platform:ghsa sev:high tech:python technique:race-window technique:source-review vuln:misconfiguration vuln:race-condition vuln:rce

**Target:** DiffusionPipeline.from_pretrained / HuggingFace Hub repo loading
**Root cause:** The trust_remote_code check parses model_index.json from an independent hf_hub_download call, while snapshot_download separately resolves the branch HEAD; a repo update between the two calls yields different commits, so the checked config differs from the loaded one.
**Steps:**
1. Create Hub repo with commit A model_index.json using plain string `_class_name` (e.g. `"FluxPipeline"`) so load_pipe_from_hub is False and trust check passes.
2. Call `DiffusionPipeline.from_pretrained("attacker/repo")` with a breakpoint at pipeline_utils.py:1778 (snapshot_download).
3. While paused, push commit B changing `_class_name` to `["pipeline", "FluxPipeline"]` and adding `pipeline.py`.
4. Resume; snapshot_download fetches commit B, _resolve_custom_pipeline_and_cls resolves pipeline.py, _get_pipeline_class imports it without a trust check.
5. Arbitrary code executes (e.g. writes /tmp/pwned) and from_pretrained returns a functional pipeline.
**How found:** Noticed download() makes two independent Hub calls (hf_hub_download for config, then snapshot_download) with no atomicity, and that the trust check runs on the first while _resolve_custom_pipeline_and_cls re-reads config from the second — hypothesizing a repo update between them desyncs check from load.
**Exploitation:** Attacker controls a Hub repo and repeatedly flips its state between a clean config and a malicious one (list _class_name + pipeline.py) every few seconds. Victims calling from_pretrained without revision pinning and without a warm cache occasionally land in the ~0.5s window between the two Hub calls, silently importing attacker code.
**Escalation:** Statistical success: on a popular repo with many downloads/day, repeatedly toggling repo state catches a fraction of downloaders in the race window.
**Insight:** When a security check and the actual load each fetch remote content separately, look for a second resolution of the same mutable ref (branch HEAD); any re-read of config after the check is a TOCTOU candidate, and pinning revision is the only real fix.

## Proof of concept (copied exactly from the original report)

**Commit A clean model_index.json**

```
{
  "_class_name": "FluxPipeline",
  "_diffusers_version": "0.31.0"
}
```

**Commit B malicious model_index.json**

```
{
  "_class_name": ["pipeline", "FluxPipeline"],
  "_diffusers_version": "0.31.0"
}
```

**Race window between hf_hub_download and snapshot_download**

```
config_file = hf_hub_download(...)   # ← sees commit A, trust check passes

# ... filenames processing, pattern building, pipeline_is_cached check ...
# ~~~ ATTACKER PUSHES COMMIT B HERE ~~~

# pipeline_utils.py:1778
cached_folder = snapshot_download(...)  # ← sees commit B, downloads pipeline.py
```

