Model Registry Artifact Pin Decay
A deployment fetches one model artifact using an immutable commit SHA, giving the appearance of a locked, reproducible setup. Shortly after, in the same session, it loads another artifact that affects runtime behavior — like a custom Python module, weights, or config — from a mutable reference such as 'main' or a tag, which can change without notice.
How the attack works
The deployment first downloads an artifact pinned to a 40 or 64-character commit hash, which is treated as proof the operator intended to run a fixed, reproducible model. Within a 30-minute window in the same process session, the system then fetches a behavior-affecting file — a dynamic auto_map module, a .gguf file, an image/feature processor config, side weights, or config.json — using an unpinned reference like main, master, a branch, a tag, or no revision at all. Because that second fetch is not locked, whoever controls the upstream repository can change what gets served at any time, and the next load could pull different code or weights without the operator changing anything. The detection reads the registry URL itself, since the revision or ref is embedded in the path or a query parameter, and only fires on this pinned-then-unpinned transition, not on downloads alone.
Netzilo detection
Netzilo reports this behaviour when it is observed.
- Signature ID
- 7c3f1ad2-59be-4de1-a1f0-2b6c98d4e5a7
- Severity
- High
Why it matters
An attacker or a compromised upstream maintainer can alter the unpinned artifact later, causing the deployment to silently run different code, weights, or configuration than what was reviewed and approved — undermining any assumption of reproducibility or supply-chain control.
What you can do
- →Pin every behavior-affecting artifact (custom modules, processor configs, side weights, config.json) to a commit SHA, not a branch, tag, or default ref.
- →Review deployments flagged for mixing pinned weights with unpinned tokenizer/processor/config files, since this is a common but risky shortcut during evaluation.
- →Treat pinning by release tag as equivalent to unpinned, since tags can be moved or reassigned on most model hubs.
- →Audit CI and benchmark pipelines that intentionally load both a pinned production model and an unpinned comparison model in the same session, and separate these into distinct sessions where possible.
Known benign look-alikes
- Deployments that intentionally pin only primary weights with --revision <sha> while deliberately tracking main for the tokenizer, chat template or processor config during evaluation. Mitigated by report-only action and by requiring the artifact to be code/weights/processor rather than documentation or tokenizer inventory.
- Teams whose policy is to pin by release tag (for example v1.0) rather than by commit SHA. A tag ref is mutable on the Hub and is deliberately classed as unpinned, so these sessions will be reported. Treat as a policy finding, not a bug.
- CI or benchmark jobs that, in one shell lineage, download a pinned production model and then separately pull a nightly/unpinned model for comparison. Mitigated by the 30-minute window, instance-scoped session attribution and per-artifact dedupe.
- Internal mirror or caching proxies that rewrite /resolve/<sha>/ to /resolve/main/ on egress, making a genuinely pinned fetch look unpinned at the network layer.
- Interactive developer sessions where an engineer manually curls an artifact from main while a pinned server is warming up in the same process ancestry.
- First-run cold caches that fetch config.json from the default branch before the operator's pin is applied by the launcher wrapper.