Blog// 15 MIN READ

The Model File Is a Program: Loading a Model Runs Code

Loading a model runs code, and three advisories published this week show the controls you configure are only honoured where the code chooses to read them.

EVIDENCE → INTERPRETATION → DECISION

Three advisories landed on 12 and 13 September 2026 against two of the most widely deployed open-source model stacks. Two are remote code execution from a model artefact. The third is a configured resource limit that one code path never applies.

Read together, they support a claim that is worth stating plainly and then defending: a model file is not data. It is a program, and the flags you set are honoured only where the code chooses to read them.

That changes which question is useful. Asking “is trust_remote_code disabled?” is asking a loader to describe itself. Asking “what executes when this artefact is loaded, and what contains it when that execution is hostile?” is asking about a boundary you actually control.

What landed

Advisory Project What went wrong Severity
CVE-2026-90553
GHSA-3c86-2m5g-59q7
vLLM before 0.28.0 The LlavaOnevision2 processor loader loaded remote processor classes and ignored the trust_remote_code parameter, so code in a model repository executed even with the flag set to False. CWE-94. 7.8 high
CVE-2026-90777 ESPnet before v.202609 Pretrained checkpoints were deserialised with torch.load and weights_only=False, so an attacker-supplied checkpoint executed code during deserialisation through the initialisation and fine-tuning path. CWE-502. 8.8 high
CVE-2026-90554
GHSA-936p-m5pv-vvjf
vLLM before 0.28.0 The NanoNemotronVL audio-from-video path called the audio loader without the duration and size limits, so the configured decode limits were not enforced, unlike the direct audio upload path. A small compressed video could force gigabytes of allocation. CWE-400. 6.2 medium

Version facts, stated precisely because they are the actionable part. Both vLLM issues were fixed in 0.28.0. The ESPnet issue was fixed in release v.202609, commit 91ca045. vLLM is the default serving stack for a large share of self-hosted inference, and ESPnet is long-established in speech processing, so both are the kind of dependency that arrives through a colleague’s working example rather than through a formal procurement decision.

One more precision, because it is the crux of the third advisory. The vLLM decode limits exist and are documented as environment settings. The direct audio upload path applies them. The audio-from-video path did not pass them to the loader call. The limit was configured, the configuration was respected in one place, and a sibling path simply did not read it.

The claim: loading a model is running code

Most teams carry a mental model in which a model is an asset: a large file of numbers that a runtime reads, interprets and uses to produce output. That model is correct about the numbers and wrong about the file. What a runtime actually does with a model artefact is execute a load path, and a load path is software that reads attacker-influenceable input and makes decisions with it.

Three things follow immediately, and none of them are exotic. The file format determines whether reading the file can call functions. The loader determines which code it is willing to import from whatever repository the artefact came from. And the process determines what any of that code can reach once it is running.

Diagram of the model load path from checkpoint artefact through loader into process authority, with three gates marked: the serialisation format, the trust flag and process isolation, showing where each of the three advisories defeats a gate
Original model. Artefact in, execution out, and three gates in between.

The diagram is deliberately unflattering to the flag. Gates one and two are properties of the loader, which means they are promises the loader makes about itself. Gate three, isolation, is a property of your design, which means it is the only one that still holds when the loader is wrong. That asymmetry is the whole argument of this article.

Failure one: the format is a program

The ESPnet advisory is the cleaner of the two code-execution cases, because no flag is involved at all. Pretrained checkpoints were loaded with torch.load and weights_only=False.

The relevant background is not obscure. PyTorch’s serialisation notes are explicit that the default format is pickle-based, that unpickling runs arbitrary code, and that weights_only=True restricts the unpickler to a safe subset of types. The flag exists precisely because the format is dangerous. Setting it to False is not a subtle configuration error; it is instructing the runtime to trust the file’s contents to decide what happens next.

What makes this a design failure rather than a typo is where execution lands in the sequence. Deserialisation happens while the file is being read. Nothing about the model has been evaluated yet. There is no point at which a policy about this artefact could have run first, because the loading is the policy. If the checkpoint is hostile, the code has already run by the time a runtime could form an opinion about the model it just read.

The memory anchor here is CWE-502, deserialisation of untrusted data, and the reason it keeps returning in AI tooling is structural. Checkpoints are shared artefacts. They are published to hubs, attached to papers, passed between teams and downloaded as the fastest way to get a baseline. Each of those is a distribution channel for executable content wearing the appearance of a weights file.

Failure two: the flag existed and one path did not honour it

The vLLM processor advisory is the more instructive of the two, because the control was present, documented and configured, and it still did not apply.

trust_remote_code is a well-understood control. Disabled, it is a statement that the runtime will not import and execute code shipped inside a model repository, and will stick to code that ships with the library. Teams running self-hosted inference routinely turn it off and treat that decision as a settled question in their threat model.

In vLLM before 0.28.0, the LlavaOnevision2 processor loader loaded remote processor classes and ignored that parameter. A model repository containing code in processing_llava_onevision2.py executed with the authority of the vLLM process even when the flag was set to False. The class is CWE-94, code injection, and the fix landed in 0.28.0.

Comparison of the documented contract of trust_remote_code set to false against what the LlavaOnevision2 processor loader did, showing the flag was not consulted on that path, and noting that a pickle checkpoint executes before any flag is consulted at all
Original comparison. The left column is what the operator plans around. The right column is what happened.

The lesson is not that this particular parameter is unreliable. It is that a flag is a promise a codebase makes about its own behaviour, and promises are kept per code path. A loader with one well-guarded entry point and one entry point added later is the exact geometry that produced this, and it is the same geometry as a second session path skipping the checks the first one ran, which I wrote about yesterday. The subject differs, the discipline does not.

There is a further reason to prefer isolation over flags, and the ESPnet case demonstrates it directly. If the format executes during deserialisation, then no flag is consulted at all, because the execution precedes every decision your stack would have made about the model. Flags are consulted by code. Code in a pickle runs before code in your loader.

Failure three: a configured limit that one path never applied

The third advisory is a denial of service rather than code execution, and I am including it because it shows the pattern is not about severity. In vLLM before 0.28.0, the NanoNemotronVL audio-from-video path called the audio loader without the duration and size limits, so the documented decode limits were never enforced. The direct audio upload path applied them. Serving that model with audio extraction from video enabled meant a small, highly compressed video could force the allocation of gigabytes during decoding. Fixed in 0.28.0, CWE-400.

The shape is identical to the OAuth case I covered yesterday and to the flag case above: a control that exists, is configured by an operator who believes it applies, and is honoured by one code path and not a sibling. That repetition across a model server, an identity platform and a speech toolkit is the strongest evidence available that this is a category rather than a coincidence. Where a system grows a second way to do a thing it already did, the second way inherits the capability and not the discipline.

The model registry is a supply-chain surface you already trust

It is worth naming the uncomfortable implication. Almost every AI team downloads model artefacts from a public hub. That download is usually treated as acquiring data. It is acquiring code, in a format that may execute during the load, through a loader whose own flag semantics have just been shown to be imperfect, into a process that holds database credentials, API keys, network reach and accelerator access.

The analogy to package managers is close but incomplete, and the difference matters. A package install is at least recognised as installing software, which is why it attracts lockfiles, provenance checks, private mirrors and review. A model download is often recognised as fetching a file, which is why it usually attracts none of those. The advisory record shows the analogy is more literal than teams assume.

What I would take from that is a change to where the countermeasure goes. Signing and provenance help, exactly as they help for packages, and they are worth having: the 2026-09-10 article on signed builds and the UNC6780 supply-chain campaign covers why a valid signature is not the same as a safety verdict. But provenance tells you where an artefact came from. It does not tell you what the loader will do with it. Both questions need an answer, and only one of them is solvable by policy alone.

What does not follow

Three limits, stated because the advisories are narrower than a headline would make them.

First, none of this is a compromise in the default case. The vLLM issues require a specific model architecture and, for the processor case, a model repository containing malicious code, so the practical requirement is that you load an untrusted artefact. Loading only internal, reviewed models is a real mitigation, and it is the mitigation most teams already have without having decided to adopt it.

Second, CVE-2026-90553 is not a claim that trust_remote_code is generally broken. It is a claim that one loader path failed to consult it, in one version range. The parameter remains the right control where it is honoured, and the fix confirms that.

Third, I have not reproduced any of these. What I can defend is the mechanism and the architectural reading, taken from the published advisories, PyTorch’s serialisation documentation and the CWE definitions. Anyone stating otherwise would need a running instance and a purpose-built artefact, which is not something to do outside an authorised environment.

The artefact trust boundary: five questions

This is the part I would use to review a deployment, because it is ordered from cheapest to most expensive and the first two cost nothing.

Question What a bad answer looks like What to do about it
1. What format are our checkpoints in when we load them, and does the load path force the safe unpickler? A pickle-based artefact loaded with the permissive default, or a code path that sets the safe mode off for convenience. Require the restricted mode, convert artefacts to a non-executable format where possible, and treat any exception as a reviewed decision rather than a default.
2. Which loaders in our stack can import code from a model repository, and are they all honouring the flag? A flag disabled globally that one loader path never reads. The vLLM advisory is the proof this happens. Test it rather than assuming. Load a benign artefact from a repository that should be refused and confirm the refusal, then repeat for every architecture you serve.
3. Where does the load happen, and what can that process reach? Loading in the same process that serves inference and holds provider credentials and network egress. Load in a separate process or job with the minimum reach it needs. This is the only question that still helps when answers one and two are wrong.
4. Do our configured limits appear on every path that can trigger the work? Limits applied on the direct path and omitted on a sibling path, as in the audio case. Test the sibling paths. For each limit, name the code paths that must apply it and confirm each one does.
5. Do we know which artefacts we have loaded, from where, and with what provenance? No inventory, so an advisory cannot be triaged against what is actually running. Keep the inventory. Without it, the answer to every other question is an estimate.

The ordering is deliberate. Question three is the one I would answer first if I could only answer one, because it is the gate that does not depend on any codebase keeping a promise. A loader running in an unprivileged, network-restricted, credential-free process turns both code-execution advisories into much smaller incidents, and that remains true for the loaders nobody has written an advisory about yet.

Evidence to look for

Detection here is awkward, and it is worth being honest about why. If a load path executes hostile code, the code runs with the authority of a process that was legitimately doing its job, in an environment where loading code at startup is expected. There is no malformed request to block and no signature to match.

What is available is a comparison against a known baseline, and it works better than it sounds.

Signal Why it indicates this class
Outbound network connections from a process that should only be reading a local artefact A load path has no reason to reach the network. Any egress during a load is worth explaining, and it is the most reliable single indicator available.
Child processes or shell execution parented by the serving process at startup or at model swap Loaders import and execute code. Code that spawns a process is the behaviour you are looking for, and it is visible in process accounting.
New or unexpected files in the working directory, cache directories or temporary paths after a load Persisting a foothold is the usual second step. A baseline of what the load path normally writes makes the difference obvious.
Credentials touched during a load window The value of this class is access to provider keys and data. A load path reading a credential store is anomalous by construction.
Artefacts loaded that are not in the inventory The cheapest reliable control. If you cannot say which artefacts are in use, no advisory can be triaged against your deployment.

Two of these are worth implementing this week for reasons that have nothing to do with these advisories. Record every artefact you load, with its origin and hash, because an inventory is the difference between a triage and a guess. And baseline the network and process behaviour of your load path, because a load path that reaches the network or spawns a shell is either doing something clever that deserves a comment, or it is the incident.

What I take from this

Three advisories, two projects, one week, and a single idea underneath them: the controls that protect model loading are largely promises made by loaders about themselves, and the only guarantee you hold is the boundary you place around the thing that executes.

The practical shift is small and specific. Stop treating trust_remote_code, safe unpickling and resource limits as settings that make loading safe. Treat them as hygiene that reduces the frequency of incidents, and then answer the question that survives all of them: if this artefact contained hostile code, what would that code be able to reach from where it runs? If the answer is your credentials, your network and your data, then the flags are the only thing standing between you and a bad day, and this week’s advisories are three separate demonstrations that flags are not load-bearing.

Versions are the immediate action, and they are unambiguous. Anything on vLLM before 0.28.0 should move, which addresses both the processor loader and the decode limits. Anything on ESPnet before v.202609 should move. Neither upgrade is difficult, and both are the kind of dependency that sits in a container image somebody built once and nobody has revisited.

Frequently asked questions

Is loading a model file dangerous?

It can be, because loading a model executes a load path. Two properties decide the risk: whether the artefact’s format can run code while being read (pickle-based checkpoints can), and whether the loader will import code shipped inside the model repository. An artefact from a source you do not control should be treated as executable content, not as data.

What did CVE-2026-90553 change about trust_remote_code?

Nothing about the parameter itself. In vLLM before 0.28.0, the LlavaOnevision2 processor loader loaded remote processor classes and ignored the flag, so code in a model repository ran even with trust_remote_code set to False. The fix in 0.28.0 makes that path honour it, which confirms the flag is the intended control. The lesson is that a flag is honoured per code path.

What is the difference between CWE-502 and CWE-94 here?

CWE-502 is deserialisation of untrusted data, which is the ESPnet checkpoint case: the format itself executes during the load, before any trust decision is made. CWE-94 is code injection, which is the vLLM processor case: the loader imported and ran code from the repository when the configuration said it would not. One is the file being executable, the other is the loader choosing to execute.

Does disabling trust_remote_code make model loading safe?

No. It removes one class of execution where the flag is honoured by every path in use. It does not help when the artefact format executes during deserialisation, because that happens before any flag is consulted, and this week’s advisories show a flag being ignored by one path. Isolation around the load is the control that does not depend on a codebase keeping a promise.

How should model artefacts be treated in a supply chain?

As software rather than as data. That means an inventory of what is loaded and from where, provenance and signing checks, and the same scrutiny a package dependency would receive. Provenance answers where an artefact came from; it does not answer what the loader will do with it, so both questions need an answer.

Which versions are affected?

vLLM before 0.28.0 for both the processor loader issue (CVE-2026-90553, CVSS 7.8) and the audio-from-video decode limit issue (CVE-2026-90554, CVSS 6.2). ESPnet before release v.202609 for the checkpoint deserialisation issue (CVE-2026-90777, CVSS 8.8), fixed in commit 91ca045. Upgrading resolves all three.

Sources

Related reading on this site: why a valid signature is not a safety verdict is covered in A Signed Build Can Still Be Malicious, the sibling-path pattern in session issuance is covered in The Second Door, and the same discipline applied to agent tooling is covered in The MCP Server Is the Attack Surface.

One next action

If you run an agent that executes tools, AgentCrash is an open-source harness that crash-tests it in a synthetic workplace: synthetic tools, visual evidence and regression tests. It is built for exactly this problem, turning a containment claim into something you can run against the system instead of arguing about it in review.