Looking at a Model Ran Its Code: Unsloth Studio’s Picker Was Arbitrary Code Execution, and No CVE Was Issued

On 29 September 2026, Pillar Security researcher Ariel Fogel published a finding whose most interesting property is how little the victim has to do. In the affected versions of Unsloth Studio — the browser front end that ships inside the widely installed unsloth fine-tuning package — selecting a model in the picker caused the backend to download and execute Python shipped inside that model’s Hugging Face repository. Not loading it. Not training on it. Not running inference. Reading the repository’s config.json was enough.

The maintainers shipped a fix, which Pillar independently re-tested against 2026.6.9 and confirmed closed. But they declined to publish an advisory on the grounds that Studio was labelled beta, and no CVE was assigned. That second half is the part worth your attention: this is the fourth well-documented instance of the same defect pattern in 2026, the previous three all carry CVE identifiers, and this one does not — which means no scanner in your pipeline will ever tell you about it.

The mechanism: auto_map turns configuration into code

The underlying capability is not a bug and is not Unsloth’s. Hugging Face’s transformers library deliberately lets a model repository ship its own Python. A repo’s config.json can declare an auto_map block pointing AutoConfig, AutoModel, or the tokenizer at custom modules stored next to the weights; when a caller passes trust_remote_code=True to from_pretrained, those modules are imported and run. Pillar is careful to note this is load-bearing for the ecosystem — IBM’s Granite Speech and Granite Vision models, DeepSeek-OCR, ChatGLM, and the original Qwen releases all need it — so a blanket ban on the feature is not a viable defence.

What converts that capability into a vulnerability is who decides. Pillar’s framing is the right one: does the tool run a repository’s code because the operator knowingly asked, or as an invisible side effect of something that reads as harmless? Unsloth Studio did the latter, through three compounding details documented against commit d91183d (pinned to the 2026.5.10 release) in studio/backend/utils/models/model_config.py:

  • The sink defaulted to dangerous. load_model_config(...) declared trust_remote_code: bool = True — the unsafe value was the default, not an opt-in.
  • The calling path never overrode it. The vision and capability probe called load_model_config(model_name, use_auth=True, token=hf_token) with no trust_remote_code argument, inheriting True. A separate transformers-5 subprocess branch hardcoded kwargs = {"trust_remote_code": True} outright.
  • The probe was reachable over HTTP. GET /api/models/config/{model_name:path} and GET /api/models/check-vision/{model_name:path} expose the capability inspection directly.

The sequencing is what makes this sharper than its predecessors. AutoConfig.from_pretrained exists to parse declarative metadata, so execution happened during capability inspection — before the backend fetched weights, long before any forward pass, and before the user’s own trust_remote_code control, which defaults to False, was ever consulted. The operator’s stated intent lost to an internal convenience path they could not see. Pillar’s proof of concept is deliberately inert: a tainted repo whose module opens the OS calculator and writes a marker file to the home directory.

Why “it’s beta” and “Hugging Face scans” do not close this

Unsloth disputed the assessment on two grounds, and both are worth examining because anyone building tooling on transformers will reach for the same arguments.

The first is distribution. Studio was described as beta — but per Pillar, the affected code shipped in the standard unsloth package, reachable through an ordinary pip install unsloth with no prerelease flag and no opt-in. “Beta” described the UI’s maturity, not its distribution channel. A label on a feature does not narrow the population that received it.

The second is that Hugging Face’s malware scanning would catch a hostile repo. Pillar’s rebuttal is the strongest technical passage in the writeup: Hub scanning is, by its own documentation, best-effort and explicitly not a guarantee, with responsibility placed on the user. The scanners in play (PickleScan, ModelScan) are largely blocklists aimed at unsafe serialisation and known-bad functions, and the bypass literature is long — 7z containers instead of ZIP, bdb.Bdb.run instead of exec, payloads positioned before the parse breaks. More to the point, Pillar’s own PoC repository was not flagged, because its code is not malicious at rest. It opens a calculator. An auto_map module can pass every scan cleanly and fetch its real payload at execution time. Outsourcing your execution-safety boundary to someone else’s best-effort blocklist is a reasonable layer and an unreasonable perimeter.

A third objection — that the backend binds to loopback by default rather than 0.0.0.0 — misreads the threat model. The primary victim is the authenticated, trusted operator choosing a model on their own machine. That is local arbitrary code execution and it works perfectly on 127.0.0.1. Network binding changes the blast radius, not the existence of the bug. The same applies to Studio’s password: authentication governs who reaches the backend, not what the backend does with a repository the logged-in operator selects.

What an attacker gets, if they land it, is the machine that by definition holds the valuable things: Hugging Face tokens, SSH keys and cloud credentials reachable by the Studio process; the ability to tamper with weights and with training or inference outputs; and a foothold on a host chosen for its GPUs. An internal experimentation box serves no production traffic and still holds production secrets.

The pattern now has four members and three identifiers

This site has covered each earlier instance of the same defect as it landed, and the NVD records line up precisely:

  • CVE-2026-46432 (LMDeploy) — hardcoded trust_remote_code=True across multiple model-loading call sites in 0.12.3 and prior. CVSS 3.1 7.8 High, CWE-94, published 10 June 2026, with no public patch at publication. Same sink.
  • CVE-2026-4944 (vLLM) — hardcoded trust_remote_code=True in two model implementation files, bypassing the operator’s explicit --trust-remote-code=False. CVSS 3.0 8.8 High, published 28 May 2026, and itself recorded as an incomplete fix for two earlier CVEs. Same aggravator.
  • CVE-2026-6859 (InstructLab) — hardcoded trust_remote_code=True in a training script, giving code execution from a crafted Hub model. CVSS 3.1 8.8 High, CWE-829. Same class.

Unsloth Studio combined the LMDeploy sink with the vLLM aggravator and then moved the trigger earlier, to a step no operator reads as dangerous. By severity logic it belongs in the same band as its siblings. By catalogue logic it does not exist. Nothing in an SCA scan, a dependency alert, or an OSV query will surface it, because there is no record to match — the only signal is a vendor blog post, and the only remediation instruction is a version number buried in it.

One timing detail is worth stating precisely, since the two dates in circulation differ: Pillar reports the maintainers shipped the fix on 18 June 2026, and the version they re-tested, 2026.6.9, was uploaded to PyPI on 22 June 2026. If you are pinning, pin to 2026.6.9 or later rather than to a date.

What to do with this now

  • Upgrade Unsloth to 2026.6.9 or later if you ran Studio. There is no advisory and no CVE, so no tool will raise this for you. Check installed versions by hand across notebooks, Colab instances, shared GPU boxes, and container images — anywhere pip install unsloth happened without a version pin.
  • Treat an unpatched Studio host as a credential exposure, not a hygiene item. If an unpatched instance selected models from repositories you do not control, rotate the Hugging Face tokens, SSH keys, and cloud credentials reachable from that process, and review training and export artifacts produced on it.
  • Audit your own code for the sink. Grep your ML tooling for trust_remote_code=True as a default parameter value or a hardcoded literal. The pattern that keeps producing CVEs is specifically an internal path setting it where the user-facing control says otherwise — the user’s setting should be the only one that can turn it on.
  • Draw the boundary at inspection, not at loading. The lesson that distinguishes this case from its three predecessors is that metadata reads can be execution. Any operation your tooling presents as “just looking” — listing capabilities, probing vision support, rendering a model card — must not reach a path that can import repository code.
  • Do not count Hub scanning as a control. It is best-effort by design and blind to staged loaders that are benign at rest. Treat model repositories as untrusted code, and assume a clean scan means nothing about what runs at execution time.
  • Note the gap, not just the bug. “Beta” components distributed inside GA packages are a recurring blind spot in vulnerability management. If a maintainer declines an advisory for something that shipped to everyone via the standard install, your inventory is the only place that fact can be recorded.

Sources