Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/check-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ own suite, suite version, and probe-set digest ([tier2.md](tier2.md)).

| Version | Rule fingerprint | Date | Changes |
|---|---|---|---|
| `tier1/0.13` | `bdd0f0d40534d12b31778da958a1d4ce4bab8dea1fcb8d3bf7327cfae73eaa9c` | 2026-10-19 | Chat template, in a model directory: each GGUF file's own chat templates are graded. Before, a directory's GGUF files were named unparsed for their metadata and their templates were not read, so the row covered only `tokenizer_config.json`, `chat_template.json`, and `.jinja` files, and a GGUF-only repository had no row. Files that carry the same templates and special tokens are graded once. When they do not all carry the same templates, each version is also compared with the others, as with a reviewed reference (`Options.Siblings`). A version that renders like another on every standard probe but adds a content-conditional branch is a LEAD (`quant-template-mismatch`, high). This is the near-miss rule of `tier1/0.8`, with no reference input. Any other difference is listed in the notes, not judged. The directory scan's engine code is outside the fingerprint, and the comparison it uses (`render.go`) is inside it. On 47 real GGUF repositories (712 GGUF files carrying a template, 49 template versions), all 47 PASS, two list a second version, and no `quant-template-mismatch` was found; see [false-positive-baseline.md](false-positive-baseline.md). |
| `tier1/0.12` | `00b784144756ac03404c549c0bc2e5f00013f9994bdcdd5bc64702a70aa64ea6` | 2026-10-19 | Remote code reads two config fields that make a loader run code without `trust_remote_code`, at any depth of a `config.json`. An attention field (`_attn_implementation_internal`, or `_attn_implementation`, whose setter writes it) naming a Hub kernel repository outside `kernels-community` FAILs as `config-kernel-injection`: transformers 4.56.0 to 5.2.x download and run that kernel in a plain `from_pretrained` once the `kernels` package is installed (CVE-2026-4372), and `save_pretrained` never writes either field. One naming a `kernels-community` kernel, which 5.3.0 and later still load from the field, is a LEAD (`config-hub-kernel`). A truthy `trust_remote_code` is a LEAD (`config-trust-remote-code`): LightGlue's config passed it to the nested `AutoConfig.from_pretrained` for its keypoint detector before transformers 5.5.0, importing another repository's code (CVE-2026-5241), and other loaders ignore it. The row FAILs for the first time. On 403 real repositories, no result changed; see [false-positive-baseline.md](false-positive-baseline.md). |
| `tier1/0.11` | `1fc34539a816d9d2bfdad5005f5991fd08e10eacf4a6960bd6e4c31f0a9dd26f` | 2026-10-15 | No rule change: the Chat template check gains `Triggers`, which returns the literals the content conditions test in each template it grades LEAD, each with the prompts the template renders with it (armed, with the branch firing, and counterfactual, the same conversation without the branch's text), for Tier 2's trigger confirmation (#284, [tier2.md](tier2.md#trigger-confirmation)). It reads the same analysis and renderer the row does and changes no row's status, notes, or findings. |
| `tier1/0.11` | `5a5d7715b241a0f1002ed4e94d442cc5dc79a748d87d03194b0483f2b8b390c7` | 2026-10-15 | Tokenizer config, on a GGUF: a special-token id of -1 written as a 32-bit integer (`0xFFFFFFFF`, signed or not) is the "no token" sentinel, not an id outside the vocabulary, so it no longer FAILs as `special-token-out-of-range`, as `tier1/0.7` did for a negative id in a directory's `config.json`. llama.cpp skips it with a warning, as it skips any id outside the vocabulary, and it names no token. Found on `ggml-org/tiny-llamas` `stories260K.gguf` (padding and separator ids), whose Tokenizer row FAILed on v0.1.0-rc.5. Any other id at or past the vocabulary's size still FAILs. The row's FAIL note no longer says llama.cpp "would misread or reject" every inconsistency: it says llama.cpp refuses a type or score table shorter than the vocabulary, skips a special-token id outside it with a warning, and reads a type it does not define as undefined. The change only removes a FAIL; the benchmark's outcomes are unchanged. See [false-positive-baseline.md](false-positive-baseline.md). |
Expand Down
33 changes: 25 additions & 8 deletions docs/detection-benchmark.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ generated corpus, on 2026-10-04; CI does not re-run them.
scanner here was given any of them. With Socair's defaults (none of the
four), the remapped tokenizer and the near-miss template pass, the known-bad
file is matched against no list (with none supplied, that check does not
run), and the edited weights have no row that compares them, so Socair detects 65 of 75. Scores with the cases that need an input left out
run), and the edited weights have no row that compares them, so Socair detects 66 of 76. Scores with the cases that need an input left out
follow the comparison table.
- **The attacks are defanged.** Each case keeps the attack's structure (the
gadget, the opcodes, the archive trick, the template construct) and swaps its
Expand All @@ -44,11 +44,11 @@ generated corpus, on 2026-10-04; CI does not re-run them.
models are measured separately, on real corpora, in
[false-positive-baseline.md](false-positive-baseline.md).

## Socair (check set tier1/0.12, 2026-10-19)
## Socair (check set tier1/0.13, 2026-10-19)

| Check row | Cases | Detected (FAIL or LEAD) | Withheld as a gap (NOT_TESTED) | Missed (PASS) |
|---|---|---|---|---|
| Chat template | 11 | 10 | 0 | 1 |
| Chat template | 12 | 11 | 0 | 1 |
| File inventory and payloads | 10 | 9 | 0 | 1 |
| Format and structure | 14 | 11 | 3 | 0 |
| Hash, provenance, lineage | 1 | 1 | 0 | 0 |
Expand All @@ -57,12 +57,12 @@ generated corpus, on 2026-10-04; CI does not re-run them.
| Remote code | 11 | 10 | 0 | 1 |
| Tensor reference | 1 | 1 | 0 | 0 |
| Tokenizer config | 3 | 3 | 0 | 0 |
| **All** | **75** | **69** | **3** | **3** |
| **All** | **76** | **70** | **3** | **3** |

Benign controls flagged (FAIL or LEAD on any row): 0 of 18.
Benign controls flagged (FAIL or LEAD on any row): 0 of 19.

Four of these detections depend on a reference input (see "Read this first");
with Socair's defaults it detects 65 of 75.
with Socair's defaults it detects 66 of 76.

Check set tier1/0.8 replaced the pickle row's import allowlist with a typed
grammar (`socair-wo/1`, see [check-set.md](check-set.md)) and added three
Expand Down Expand Up @@ -225,6 +225,18 @@ both PASSed: neither ships a `.py` file or an `auto_map`. The
`_attn_implementation` names one of the library's own implementations, as
`microsoft/Phi-4-multimodal-instruct` ships. No earlier case changed outcome.

Check set tier1/0.13 grades the chat templates a model directory's GGUF files
carry, and compares the versions with each other (#294).
`dir-gguf-quant-near-miss` is `gguf-template-reviewed-near-miss` hidden in
one quant of three, scanned as a directory with no reference input. The other
quants' template is its reference, so it is a LEAD with Socair's defaults,
where the single-file case needs `SOCAIR_TEMPLATE_REFERENCE`. Under tier1/0.12
a directory scan did not read GGUF templates, so it PASSed: no row read it.
The `control-dir-gguf-quants` control pins the shape
`bartowski/Mistral-Nemo-Instruct-2407-GGUF` ships, whose f32 file carries an
older template than its quants. That difference is listed, not judged. No
earlier case changed outcome.

| Case | Format | Technique | Row | Result | Severity |
|---|---|---|---|---|---|
| `control-pickle` | pickle | benign state-dict-shaped pickle written by CPython | (control: every row) | PASS | |
Expand All @@ -243,6 +255,7 @@ both PASSed: neither ships a `.py` file or an `auto_map`. The
| `control-dir-openvino` | model directory | benign directory with an openvino/ export as sentence-transformers ships: an f32 IR and an int8 one whose Consts share a range | (control: every row) | PASS | |
| `control-dir-training-script` | model directory | benign directory with train_script.py, as sentence-transformers ships, that no loader imports | (control: every row) | PASS | |
| `control-dir-attention-field` | model directory | benign config.json whose _attn_implementation names flash_attention_2, as Phi-4-multimodal ships | (control: every row) | PASS | |
| `control-dir-gguf-quants` | model directory | benign GGUF repository whose f32 file carries an older chat template than its quants, as bartowski/Mistral-Nemo-Instruct-2407-GGUF ships | (control: every row) | PASS | |
| `control-dir-checksums` | model directory | benign directory with an uploader's SHA256SUMS that matches every file | (control: every row) | PASS | |
| `pickle-os-system-p2` | pickle | os.system via GLOBAL + REDUCE, protocol 2 | Pickle opcode scan | FAIL | critical |
| `pickle-os-system-p4` | pickle | os.system via STACK_GLOBAL, protocol 4 (the Python 3.8+ default) | Pickle opcode scan | FAIL | critical |
Expand Down Expand Up @@ -273,6 +286,7 @@ both PASSed: neither ships a `.py` file or an `auto_map`. The
| `gguf-template-macro-conditional` | gguf | a system turn emitted from a macro only when a message carries a trigger | Chat template | LEAD | high |
| `gguf-template-tuple-trigger` | gguf | system instruction injected only when the user's message is one of a tuple of trigger phrases | Chat template | LEAD | high |
| `gguf-template-reviewed-near-miss` | gguf | a reviewed template with a branch that prefills the answer when a message carries a trigger | Chat template | LEAD | high |
| `dir-gguf-quant-near-miss` | model directory | one quant of three whose chat template renders like the others' on every standard probe but prefills the answer when a message carries a trigger | Chat template | LEAD | high |
| `gguf-metadata-base64` | gguf | base64 payload in a metadata string | File inventory and payloads | FAIL | high |
| `gguf-metadata-array-payload` | gguf | base64 payload in a metadata string array | File inventory and payloads | FAIL | high |
| `gguf-metadata-elf` | gguf | an ELF executable in a metadata string | File inventory and payloads | FAIL | critical |
Expand Down Expand Up @@ -371,8 +385,9 @@ the two added with #262 (`dir-weights-edited` and the
added with #264, the two added with #263 (`dir-checksum-swapped` and the
`control-dir-checksums` control), the seven OpenVINO cases and controls
added with #268, the ten Keras cases and controls added with #269, and the
three config-field cases and controls added with #291, so its counts cover the
cases before them; the other scanners have not been run on those fifty-one. A *suspicious* result counts as a finding, as Socair's LEAD does:
three config-field cases and controls added with #291, and the two GGUF
quant cases and controls added with #294, so its counts cover the cases before
them; the other scanners have not been run on those fifty-three. A *suspicious* result counts as a finding, as Socair's LEAD does:
picklescan's suspicious globals and Fickling's SUSPICIOUS. "Not scanned" means
the tool skipped the file, could not parse it, or does not support the format.

Expand Down Expand Up @@ -460,6 +475,7 @@ and the two formats they belong to read, from that run:
| `control-dir-torchscript` | clean | not run | not run | not run | not run |
| `control-dir-training-script` | clean | not run | not run | not run | not run |
| `control-dir-attention-field` | clean | not run | not run | not run | not run |
| `control-dir-gguf-quants` | clean | not run | not run | not run | not run |
| `control-dir-tensor-reference` | clean | not run | not run | not run | not run |
| `control-gguf` | clean | clean | clean | finding (not a pickle) | could not scan |
| `control-model-directory` | clean | clean | could not scan | could not scan | could not scan |
Expand Down Expand Up @@ -510,6 +526,7 @@ and the two formats they belong to read, from that run:
| `gguf-template-macro-conditional` | finding | not run | not run | not run | not run |
| `gguf-template-plain-guidance` | clean | clean | clean | finding (not a pickle) | could not scan |
| `gguf-template-reviewed-near-miss` | finding | not run | not run | not run | not run |
| `dir-gguf-quant-near-miss` | finding | not run | not run | not run | not run |
| `gguf-template-tuple-trigger` | finding | not run | not run | not run | not run |
| `gguf-tokenizer-remapped` | finding | clean | clean | finding (not a pickle) | could not scan |
| `keras-config-module` | finding | not run | not run | not run | not run |
Expand Down
7 changes: 6 additions & 1 deletion docs/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ artifact:
one row per check: the worst result wins, and every file that did not PASS
is named. A `*.safetensors.index.json` must agree exactly with its shards.
- The chat template comes from `tokenizer_config.json`, `chat_template.json`,
and any `.jinja` file.
any `.jinja` file, and each GGUF file's metadata (#294). GGUF files that
carry the same templates are graded once. When they differ, each version is
compared with the others as with a reviewed reference
(`chattemplate.Options.Siblings`): one that renders like another on every
standard probe but adds a content-conditional branch is a LEAD
(`quant-template-mismatch`). Any other difference is listed, not judged.
- **Remote code** is a LEAD for the Python a loader would import: a module an
`auto_map` (any config, any depth), `custom_pipelines`, a sentence-transformers
`modules.json` or Router, or a diffusers `model_index.json` names; a file a
Expand Down
66 changes: 66 additions & 0 deletions docs/false-positive-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,72 @@ published exploit, not in this corpus. The corpus shows only that real
configurations do not trip it. The `control-dir-attention-field` control
pins the Phi-4-multimodal shape.

## Chat templates of a directory's GGUF files (2026-10-19, check set tier1/0.13)

Every GGUF file carries its own chat template in its metadata, and a serving
stack reads the template of the file it loads. A repository's quants can
carry different templates, and a model page shows one file's. That is where
a backdoored template can hide (Pillar Security, 2026; arXiv 2602.04653).
Until this change a directory scan did not read GGUF templates at all: it
named each GGUF unparsed for its metadata. The Chat template row now grades
each version of the templates the directory's GGUF files carry, and compares
the versions with each other.

**The question measured first:** do the quants of real repositories carry
different templates? If many do, a LEAD on any difference would withhold
them for the ordinary reason that a quant was made before the template was
fixed.

**Method.** The metadata of every GGUF file was streamed at a pinned commit:
the key-value section only, never the tensors, and only the first shard of a
split file, since the other shards carry no template. The set: the seven
most-downloaded GGUF repositories of each of bartowski, unsloth,
lmstudio-community, ggml-org, Qwen, TheBloke, MaziyarPanahi, mradermacher,
google, and microsoft on 2026-10-19 (the listing includes some repositories
with no GGUF file), plus 16 named multi-quant repositories from unsloth,
bartowski, lmstudio-community, Qwen, Microsoft, and Google.

| Repositories with GGUF files | Read | Unreadable (gated) | No GGUF carries a template | Two or more carry one |
|---|---|---|---|---|
| 58 | 57 (884 files) | 1 (`google/gemma-3-4b-it-qat-q4_0-gguf`) | 10 | 47 (712 files) |

For each template version, a stub GGUF with no tensors was written for the
gate. It keeps the version's templates, vocabulary, token types, and special
token ids, which is everything the row reads. Quants of one model share
those, so one stub serves every file that carries the version.

```
SOCAIR_QUANT_TEMPLATE_CORPUS=<dir> go test ./internal/engine -run RealQuantTemplates -v
```

The gate prints each repository's row and the status each version has on
its own. It fails if a repository whose files all carry the same templates
is graded differently from one of them on its own.

**Result.** 45 of the 47 repositories carry one version in every file that
has a template. Files with none (`mmproj-*` projectors, imatrix files, and
some multi-token-prediction drafts) are not compared. Two carry a second
version:

| Repository | Commit | Versions | The odd one out | Why | Result |
|---|---|---|---|---|---|
| bartowski/Mistral-Nemo-Instruct-2407-GGUF | `a2dd64a` | 2 | `Mistral-Nemo-Instruct-2407-f32.gguf` (24 quants carry the other) | an older template, before tool calls and the role-alternation check were added | PASS, versions listed |
| unsloth/Qwen3.8-27B-GGUF | `4ca7207` | 2 | `mtp-Qwen3.8-27B-Q4_0.gguf` (25 files carry the other) | the multi-token-prediction draft carries an older template, without system-message merging | PASS, versions listed |

All 47 PASS. No version is a `quant-template-mismatch`: both odd ones out
differ from their siblings on a standard probe, so the comparison stops and
the difference is listed, not judged. Grading every difference as a LEAD
would have withheld 2 of 47 benign repositories (4%). That is why only the
near-miss shape, identical on every standard probe and different under a
condition it adds, is a LEAD.

**Limits.** The near-miss rule needs at least one clean sibling. A
repository whose every quant carries the same backdoored template has no
sibling to compare with, and the template's own analysis is what grades it.
A template that differs from its siblings on a standard probe as well as
under a trigger is listed, not judged. Tier 2 trigger confirmation reads
only a directory's Hugging Face templates, not its GGUF ones.

## bcomplex32, and the storage classes torch.cuda defines (2026-10-14, check set tier1/0.9)

Two leftovers from the change above (#241).
Expand Down
Loading
Loading