The benchThe off-the-shelf shelf

We ran the tools the industry reaches for. Here is exactly what they returned.

5 named scanners, over 179 public models and a labelled cohort of 25 author-flagged backdoored adapters — artifact-only, every tool seeing weights and configs, never documentation. Each tool is measured against the question it was built to answer, not one it never claimed. Where a tool was not run, this page renders nothing.

Source of record: bench/shelf/emit_sot.py · generated 2026-07-28. Every number below reads from that file; none is typed by hand.

What each tool is for

Read this first. Every one of these is a competent tool answering its own question well. The benchmark below is not “which tool is best” — it is “which question does each one answer,” because a scanner that never fires on tampered weights may be doing exactly its job, just not the job of catching a backdoor.

Serialization-exploit scanners

They answer “does loading this file run code?” — hunting unsafe pickle opcodes that execute on load. That is a real and important question. It is not “have these weights been tampered with.”

picklescan mmaitre314
Reads
pickle opcodes
Answers
detecting code-execution payloads embedded in Python pickle serialization — it answers 'does loading this file run code', not 'have these weights been tampered with'.

the scanner the HuggingFace Hub runs on upload

ModelScan Protect AI
Reads
serialized-model opcodes (pickle, HDF5, SavedModel, NumPy)
Answers
scanning serialized model files for unsafe operators that execute code on load; like picklescan, it targets load-time code execution, not weight-level tampering.

load-time-exploit scanner, multi-format

fickling Trail of Bits
Reads
pickle opcodes (static decompilation)
Answers
static analysis/decompilation of pickle streams; it marks any import outside the Python standard library as LIKELY_UNSAFE, which is conservative-by-design and fires on benign framework globals as well as malicious ones.

flags non-stdlib imports as LIKELY_UNSAFE, including benign ones

Reads
manifests, configs, embedded templates & safetensors
Answers
heuristic auditing of a model repo for suspicious files, chat-template SSTI, non-allowlisted pickle globals, and license/provenance smells; a broad repo linter, not a weight-tampering detector.

artifact-only here: README/prose withheld (see policy)

Provenance / signature verification

Verifies a cryptographic signature a publisher attached. It can only attest what someone signed — and attests nothing when no signature was published.

model-signing OpenSSF / Sigstore
Reads
Sigstore signature bundles
Answers
verifying a cryptographic provenance signature over model files; it can only attest what a publisher signed.

requires a published Sigstore bundle; no scanned model ships one

Why documentation is withheld from every tool

Documentation is withheld from every tool because a single-variable control showed a scanner's verdict tracking the prose, not the weights: modelaudit scored an identical backdoored adapter CRITICAL with its README present and clean with it removed, and scored clean weights CRITICAL when handed a card that merely mentions the word 'backdoor'. Its verdict moves with the text in both directions and is uncorrelated with the weights. Configs stay in scope — config.json auto_map remote-code is a real attack surface.

Single-variable controlsubject: modelaudit
Same tool, same scan — only the prose changes.
ArmWeightsCardVerdictWhat it cited
ABackdoorLLM/Jailbreak_Llama2-7B_BadNets adapterpresentCRITICALC&C pattern detected: backdoor — located in README.md
Bidentical adapterremovedno findings
Cconstructed controlclean EleutherAI/pythia-160mmentions the wordCRITICALC&C pattern detected: backdoor — located in README.md

What this shows: modelaudit's CRITICAL verdict is driven by the README text, not the weights: add prose and it fires, remove prose and it goes silent, on the very same tensors.

Arm C is a CONSTRUCTED control — a statement about the scanner, never about pythia-160m.

Withheld from every tool: README.md, LICENSE, .gitattributes, images, other prose. In scope: weights, configs, tokenizer files — config.json auto_map remote-code is a real attack surface and stays in.

On adapters their own authors labelled backdoored

The one cohort with ground truth: 25 adapters published and flagged backdoored by their authors (BackdoorLLM — published, author-labelled backdoored adapters), read beside named clean controls. This is the confusion matrix, denominators on the surface.

25 author-labelled backdoored adapters + named clean controls from the catalog = 50 specimens per tool. A tool with nothing of the kind it reads renders nothing to read — never a pass.
ToolBackdoor caughtBackdoor missedClean flaggedClean clearedNothing to read
picklescanmmaitre3140 / 00 / 00 / 77 / 743 / 50
No cohort adapter ships a pickle-format file (safetensors/config only), so this load-time-exploit scanner had nothing to open on any of the poisoned adapters or the clean controls. It neither confirms nor denies weight tampering here.
ModelScanProtect AI0 / 00 / 00 / 77 / 743 / 50
Same as picklescan: the cohort ships no scannable serialized file, so ModelScan had nothing to open. Its silence is the absence of a pickle payload, not a judgement on the weights.
ficklingTrail of Bits0 / 00 / 06 / 71 / 743 / 50
fickling could only open the 7 clean controls that carry pickle files and flagged 6 of them LIKELY_UNSAFE, while the poisoned adapters (safetensors-only) gave it nothing to read. Its firing tracks benign pickle framework-imports, not the labelled backdoor.
modelauditpromptfoo0 / 2525 / 252 / 2523 / 250 / 50
modelaudit opened all 25 poisoned adapters and 25 clean controls, detecting 0 of the poisoned (fn=25) while firing on 2 clean control(s). On the author-labelled backdoors it is inverted with respect to ground truth — it fires more readily on clean weights than on the backdoored ones.
model-signingOpenSSF / Sigstore0 / 00 / 00 / 00 / 050 / 50
No cohort model carries a Sigstore bundle, so there is no provenance signature to verify for any of the 50 models. The absence is itself the measurement (unattested provenance) — neither a pass nor a fail.
The limit on this cohortPoison-only publication (no clean siblings in the org); clean controls are drawn separately from the catalog and named. Supports recall, NOT a defensible AUC. It supports a read on recall — did the tool catch the backdoor it was handed — and is explicitly not a defensible AUC. Clean-sibling count in the publishing org: 0; controls are drawn separately from the catalog and named.

Across the 179-model catalog

The broader read: how often each tool could open a model at all, how often it fired, and — the number that decides whether a positive means anything — how often it fired on a model we independently call clean.

picklescandoes not separate tampered weights from benign
34 / 179could open
0 / 34fired
0 / 11fired on clean

picklescan opened the 34 catalog models that ship a pickle-format file and reported no unsafe opcodes in any. It answers 'does loading this file execute code', not 'have these weights been tampered with', so a null result here is the tool working correctly on its own question — not a clean bill of health for the weights.

ModelScandoes not separate tampered weights from benign
34 / 179could open
0 / 34fired
0 / 11fired on clean

ModelScan opened the 34 catalog models carrying a scannable serialized file and found no unsafe operators. Like picklescan it targets load-time code execution rather than weight tampering; the empty result is an accurate answer to that narrower question.

ficklingtoo few readable specimens to judge
34 / 179could open
28 / 34fired
10 / 11fired on clean

fickling flagged 28 of the 34 openable catalog models as LIKELY_UNSAFE — including 10 of 11 we independently call clean — because it marks any non-standard-library import in a pickle as unsafe by design. On this catalog that fires on ordinary framework serialization (e.g. torch._utils._rebuild_tensor_v2), so its positives do not separate tampered weights from benign ones.

modelauditdoes not separate tampered weights from benign
96 / 179could open · 83 not scanned (over size cap)
11 / 96fired
3 / 34fired on clean

modelaudit scanned 96 catalog models artifact-only (83 exceeded the size cap and were not scanned) and raised a critical finding on 11 — 3 of 34 we call clean and 1 of 15 we flag ourselves. On this catalog its critical verdict does not track weight tampering; the criticals are chiefly standard-serialization patterns (10 of 11), with 1 chat-template heuristic finding.

What this benchmark did not measure

A benchmark that hides its gaps is not world-class. Every place the coverage is partial is listed here, in the same plain type as the results above.

Not run on the catalog · model_signing

model-signing was not run over the 180-model catalog; it appears only in the labelled cohort. No catalog model carries a model_signing row.

model_absent_from_scanned_models · modelaudit

measured but not present in scanned-models.json; omitted from per_model board.

Skipped over the size cap · modelaudit

83 catalog model(s) exceeded the artifact-size cap and were not scanned by modelaudit; they carry no modelaudit row (rendered nothing).

83 models not scanned — list
  • EleutherAI/pythia-1b
  • Goekdeniz-Guelmez/Josiefied-Qwen2.5-1.5B-Instruct-abliterated-v1
  • HuggingFaceTB/SmolLM2-1.7B
  • HuggingFaceTB/SmolLM2-1.7B-Instruct
  • HuggingFaceTB/SmolLM2-360M-Instruct
  • HuggingFaceTB/SmolLM3-3B
  • HuggingFaceTB/SmolLM3-3B-Base
  • IlyaGusev/T-lite-instruct-0.1-abliterated
  • IlyaGusev/gemma-2-2b-it-abliterated
  • OhhMoo/qwen05b-gsm8k-sft-instruct
  • Qwen/Qwen2.5-3B
  • Qwen/Qwen2.5-3B-Instruct
  • Qwen/Qwen3-1.7B
  • Qwen/Qwen3-1.7B-Base
  • TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T
  • allenai/OLMo-2-0425-1B
  • anakin87/gemma-2-2b-neogenesis-ita
  • archivestudio/phi-2
  • daekeun-ml/phi-2-ko-v0.1
  • deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B
  • efficient-large-model/gemma-2-2b-it
  • failspy/meta-llama-3-8b-instruct-abliterated-v3
  • google-bert/bert-base-uncased
  • google/gemma-2-2b
  • google/gemma-2-2b-it
  • google/gemma-2-2b-jpn-it
  • huihui-ai/Llama-3.2-3B-Instruct-abliterated
  • huihui-ai/Qwen2.5-3B-Instruct-abliterated
  • huihui-ai/Qwen2.5-Coder-1.5B-Instruct-abliterated
  • huihui-ai/meta-llama-3.1-8b-instruct-abliterated
  • huihui-ai/qwen2.5-14b-instruct-1m-abliterated
  • huihui-ai/qwen2.5-14b-instruct-abliterated-v2
  • huihui-ai/qwen2.5-32b-instruct-abliterated
  • jartine/phi-2-llamafile
  • litert-community/qwen2.5-0.5b-instruct
  • lomahony/eleuther-pythia410m-hh-sft
  • lomahony/pythia-1b-helpful-sft
  • microsoft/phi-2
  • microsoft/phi-2-pytdml
  • mlabonne/NeuralDaredevil-8B-abliterated
  • mlabonne/Qwen3-1.7B-abliterated
  • mlabonne/chimerallama-3-8b
  • mlabonne/chimerallama-3-8b-v2
  • mlabonne/chimerallama-3-8b-v3
  • mlabonne/datacampllama-3.1-8b
  • mlabonne/finellama-3.1-8b
  • mlabonne/hermes-3-llama-3.1-8b-lorablated
  • mlabonne/meta-llama-3-8b
  • mlabonne/meta-llama-3.1-8b-instruct-abliterated
  • mlabonne/numinillama-3.1-8b
  • mlabonne/orpollama-3-8b
  • mlabonne/twinllama-3.1-8b
  • mlabonne/twinllama-3.1-8b-dpo
  • mlabonne/ultrallama-3.1-8b
  • mntss/clt-gemma-2-2b-2.5m
  • mntss/clt-gemma-2-2b-426k
  • mozilla-ai/gemma-2-2b-it-llamafile
  • mrm8488/phi-2-coder
  • openai-community/gpt2
  • openai-community/gpt2-medium
  • prithivMLmods/Bellatrix-Tiny-1B-v3
  • qwen/qwen2.5-14b
  • qwen/qwen2.5-14b-instruct
  • qwen/qwen2.5-14b-instruct-1m
  • qwen/qwen2.5-32b
  • qwen/qwen2.5-32b-instruct
  • susnato/phi-2
  • tiiuae/Falcon3-1B-Base
  • tiiuae/Falcon3-1B-Instruct
  • toughtty/phi-2
  • unsloth/gemma-2-2b
  • unsloth/gemma-2-2b-it
  • unsloth/gemma-2-9b-it
  • unsloth/gemma-2b
  • unsloth/gemma-2b-it
  • unsloth/llama-3-8b
  • unsloth/llama-3-8b-instruct
  • unsloth/llama-3.1-8b-instruct
  • unsloth/meta-llama-3.1-8b
  • unsloth/meta-llama-3.1-8b-instruct
  • unsloth/qwen2.5-14b-instruct
  • vagosolutions/sauerkrautlm-gemma-2-2b-it
  • venkycs/phi-2-instruct
Superseded earlier run

earlier partial cohort run (46 rows) subsumed by cohort-matrix.json (5 tools x 50); used only for cross-check, not as the cohort SoT.

Superseded earlier run

earlier partial cohort run (75 rows) subsumed by cohort-matrix.json (5 tools x 50); used only for cross-check, not as the cohort SoT.

model_absent_from_scanned_models · fickling

measured but not present in scanned-models.json; omitted from per_model board.

model_absent_from_scanned_models · modelscan

measured but not present in scanned-models.json; omitted from per_model board.

model_absent_from_scanned_models · picklescan

measured but not present in scanned-models.json; omitted from per_model board.

How to read this fairly

  • picklescan and ModelScan never firing on this catalog is correct on their own terms: they hunt unsafe pickle opcodes — load-time code execution — and report that accurately. The finding is not that they are bad tools; it is that the question they answer is not “have these weights been tampered with.”
  • Every clean-model CRITICAL we recorded traces to standard PyTorch bf16 serialization (torch.BFloat16Storage reaching importlib.import_module); fickling’s blanket LIKELY_UNSAFE traces to torch._utils._rebuild_tensor_v2. These are properties of ordinary framework files, not evidence of tampering.
  • No model on HuggingFace — including the Meta, Mistral, Google and Qwen flagships — ships a Sigstore bundle, so OpenSSF model-signing has nothing to verify. That is a fact about the ecosystem, not a defect in the tool.
  • A tool with no entry for a model was not run on it, and renders nothing. Absence of measurement is never a verdict here.
The measured board

This is the shelf, run in the open. Now read what Vulcora does instead.