From 75b1e82fd688c52ace7a71fe013057d57c81cb5a Mon Sep 17 00:00:00 2001 From: Christopher Maher Date: Mon, 19 Oct 2026 08:04:12 -0700 Subject: [PATCH 1/2] Remote code: config.json fields that run code without trust_remote_code The Remote code row named the Python a loader imports when trust_remote_code is given. Two published transformers vulnerabilities run code from a field of config.json with trust_remote_code=False, and the row did not read either. - An attention field (_attn_implementation_internal, or _attn_implementation, whose setter writes it) naming a Hub kernel repository: 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). save_pretrained never writes either field, so a kernel outside kernels-community is a FAIL, the row's first. A kernels-community kernel, which 5.3.0 and later still load from _attn_implementation, is a LEAD. - A truthy trust_remote_code: LightGlue's config passed it to the nested AutoConfig.from_pretrained for its keypoint detector before 5.5.0 (CVE-2026-5241). The published LightGlue DISK checkpoint sets it on purpose and other loaders ignore it, so it is a LEAD. Both read every config.json at any depth. On 403 real repositories (the 400 most-downloaded and 100 most-liked transformers models plus 48 named), no result changed: 357 PASS and 46 LEAD before and after. Check set tier1/0.12. Signed-off-by: Christopher Maher --- docs/attestation-template.md | 2 +- docs/check-set.md | 1 + docs/detection-benchmark.md | 39 ++++-- docs/dev.md | 6 +- docs/false-positive-baseline.md | 81 ++++++++++++ internal/benchmark/corpus.go | 11 ++ internal/checks/remotecode/check.go | 63 ++++++++- internal/checks/remotecode/corpus_test.go | 6 +- internal/checks/remotecode/fields.go | 149 ++++++++++++++++++++++ internal/checks/remotecode/fields_test.go | 95 ++++++++++++++ internal/engine/scan.go | 2 +- internal/report/frameworks.go | 5 + internal/report/model.go | 1 + 13 files changed, 441 insertions(+), 20 deletions(-) create mode 100644 internal/checks/remotecode/fields.go create mode 100644 internal/checks/remotecode/fields_test.go diff --git a/docs/attestation-template.md b/docs/attestation-template.md index 62996a8..5710b0d 100644 --- a/docs/attestation-template.md +++ b/docs/attestation-template.md @@ -97,7 +97,7 @@ The rows that run depend on the format and on the inputs: every report carries s | Chat template | Code reach, hidden or obfuscated text, override or content-triggered instructions, and the text the rendered template adds to the prompt | `[result]` | `[evidence]` | `[notes]` | | Tokenizer config | Tampered tokenizer tables: special-token ids, token types, control tokens | `[result]` | `[evidence]` | `[notes]` | | Quant match (GGUF) | Declared quantization (file name) against the tensor types in the file | `[result]` | `[evidence]` | `[notes]` | -| Remote code (model directory) | Python a loader would import: modules a config names (auto_map, custom_pipelines, sentence-transformers and diffusers modules), files loaders import by their path, and the repository files those import | `[result]` | `[evidence]` | `[notes]` | +| Remote code (model directory) | Python a loader would import: modules a config names (auto_map, custom_pipelines, sentence-transformers and diffusers modules), files loaders import by their path, the repository files those import, and config.json fields that run code without trust_remote_code (a Hub kernel as the attention implementation, trust_remote_code itself) | `[result]` | `[evidence]` | `[notes]` | | Pickle opcode scan (pickle) | Imports in pickle-based model files that reach code execution, and pickles that build more than tensors and plain containers | `[result]` | `[evidence]` | `[notes]` | | License policy (with a policy) | The license the artifact states, against the operator's allowed list | `[result]` | `[evidence]` | `[notes, with the license's usage-policy obligations listed and not marked met]` | | Intake policy (with a policy) | The artifact against the organization's intake policy: allowed formats, publisher allow and deny lists, and publishers whose models need a verified signature | `[result]` | `[evidence: the rule each finding breaks]` | `[notes: the policy by name and hash, the publisher and where it was read, and each rule that broke, could not be evaluated, or held]` | diff --git a/docs/check-set.md b/docs/check-set.md index d5a8663..8a0bf2f 100644 --- a/docs/check-set.md +++ b/docs/check-set.md @@ -46,6 +46,7 @@ own suite, suite version, and probe-set digest ([tier2.md](tier2.md)). | Version | Rule fingerprint | Date | Changes | |---|---|---|---| +| `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). | | `tier1/0.10` | `b7a105c69b51c5acf6c9d23bd9b9b1e209d522a341e759bca4132ddefea38c5c` | 2026-10-14 | A check whose input was not supplied is named, not run. Two rows no longer grade NOT_TESTED for want of an input, and instead are listed in the report's `out_of_scope.not_run` with the reason, as the Tier 2 checks are: Known-bad hash match runs only when a known-bad list is supplied (`SOCAIR_DENYLIST`, or a verified feed that carries one; a list the operator named that is empty or could not be read still gives a NOT_TESTED row), and Chat template runs only when the artifact carries a template (a GGUF's `tokenizer.chat_template`; a directory's `tokenizer_config.json`, `chat_template.json`, or `.jinja` file; a template file that could not be read still gives a row). Neither withholds promotion when its input is absent, as the License policy row does not without a policy: a clean model pulled through the airlock with its provenance is authorized without an acceptance. There is no standing public list of malicious model files to default to, and a model with no template (a base, embedding, or reranker model) has none to check; a template a serving stack supplies is outside the artifact. `report.Validate` refuses a check named both as a row and as not run. The benchmark's outcomes are unchanged: its known-bad case is scanned with a list. | diff --git a/docs/detection-benchmark.md b/docs/detection-benchmark.md index 40650eb..c41c3fc 100644 --- a/docs/detection-benchmark.md +++ b/docs/detection-benchmark.md @@ -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 63 of 73. Scores with the cases that need an input left out + 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 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 @@ -44,7 +44,7 @@ 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.11, 2026-10-15) +## Socair (check set tier1/0.12, 2026-10-19) | Check row | Cases | Detected (FAIL or LEAD) | Withheld as a gap (NOT_TESTED) | Missed (PASS) | |---|---|---|---|---| @@ -54,15 +54,15 @@ generated corpus, on 2026-10-04; CI does not re-run them. | Hash, provenance, lineage | 1 | 1 | 0 | 0 | | Known-bad hash match | 1 | 1 | 0 | 0 | | Pickle opcode scan | 23 | 23 | 0 | 0 | -| Remote code | 9 | 8 | 0 | 1 | +| Remote code | 11 | 10 | 0 | 1 | | Tensor reference | 1 | 1 | 0 | 0 | | Tokenizer config | 3 | 3 | 0 | 0 | -| **All** | **73** | **67** | **3** | **3** | +| **All** | **75** | **69** | **3** | **3** | -Benign controls flagged (FAIL or LEAD on any row): 0 of 17. +Benign controls flagged (FAIL or LEAD on any row): 0 of 18. Four of these detections depend on a reference input (see "Read this first"); -with Socair's defaults it detects 63 of 73. +with Socair's defaults it detects 65 of 75. 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 @@ -210,6 +210,21 @@ special-token id of -1 (`0xFFFFFFFF`, the "no token" sentinel llama.cpp skips) from failing the Tokenizer row as an id outside the vocabulary; every case and control here writes its special-token ids inside the vocabulary. +Check set tier1/0.12 adds two Remote code cases for config fields that make +transformers run code with `trust_remote_code=False` (#NNN). +`dir-config-kernel-injection` names an attacker's Hub kernel in +`_attn_implementation_internal`, the CVE-2026-4372 shape, which transformers +4.56.0 to 5.2.x download and run in a plain `from_pretrained`. It is the +row's first FAIL: `save_pretrained` never writes the field, so the field is +the injection. `dir-config-trust-remote-code` is a LightGlue config that +grants `trust_remote_code` to the keypoint detector it loads from another +repository, the CVE-2026-5241 shape. It is a LEAD, because the published +LightGlue DISK checkpoint sets the same field on purpose. Under tier1/0.11 +both PASSed: neither ships a `.py` file or an `auto_map`. The +`control-dir-attention-field` control pins a `config.json` whose +`_attn_implementation` names one of the library's own implementations, as +`microsoft/Phi-4-multimodal-instruct` ships. 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 | | @@ -227,6 +242,7 @@ case and control here writes its special-token ids inside the vocabulary. | `control-openvino` | openvino | benign OpenVINO IR .xml on its own: standard operations, its weights in a .bin beside it | (control: every row) | PASS | | | `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-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 | @@ -285,6 +301,8 @@ case and control here writes its special-token ids inside the vocabulary. | `dir-remote-code-imported` | model directory | auto_map naming a clean module that imports repository code that runs a command | Remote code | LEAD | medium | | `dir-remote-code-other-repo` | model directory | auto_map naming code in another repository, which the loader fetches | Remote code | LEAD | medium | | `dir-remote-code-outside-path` | model directory | auto_map naming a module by an absolute path outside the directory | Remote code | LEAD | medium | +| `dir-config-kernel-injection` | model directory | config.json naming an attacker's Hub kernel in _attn_implementation_internal | Remote code | FAIL | critical | +| `dir-config-trust-remote-code` | model directory | a LightGlue config.json that grants trust_remote_code to the keypoint detector it loads from another repository | Remote code | LEAD | medium | | `dir-st-custom-module` | model directory | a sentence-transformers module in modules.json that runs a command, with no auto_map | Remote code | LEAD | medium | | `dir-custom-generate` | model directory | custom_generate/generate.py that runs a command, named by no config | Remote code | LEAD | medium | | `dir-inference-handler` | model directory | an Inference Endpoints handler.py that runs a command, imported without trust_remote_code | Remote code | LEAD | medium | @@ -352,9 +370,9 @@ the two added with #262 (`dir-weights-edited` and the `control-dir-tensor-reference` control), the six ONNX cases and controls 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, and the ten Keras cases and controls added with #269, so its -counts cover the cases before them; the other scanners have not been run on -those forty-eight. A *suspicious* result counts as a finding, as Socair's LEAD does: +added with #268, the ten Keras cases and controls added with #269, and the +three config-field cases and controls added with #NNN, 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: 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. @@ -441,6 +459,7 @@ and the two formats they belong to read, from that run: | `control-dir-checksums` | clean | not run | not run | not run | not 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-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 | @@ -467,6 +486,8 @@ and the two formats they belong to read, from that run: | `dir-remote-code-imported` | finding | not run | not run | not run | not run | | `dir-remote-code-other-repo` | finding | not run | not run | not run | not run | | `dir-remote-code-outside-path` | finding | not run | not run | not run | not run | +| `dir-config-kernel-injection` | finding | not run | not run | not run | not run | +| `dir-config-trust-remote-code` | finding | not run | not run | not run | not run | | `dir-st-custom-module` | finding | not run | not run | not run | not run | | `dir-custom-generate` | finding | not run | not run | not run | not run | | `dir-inference-handler` | finding | not run | not run | not run | not run | diff --git a/docs/dev.md b/docs/dev.md index 067fdaf..f82f75b 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -60,7 +60,11 @@ artifact: `pipeline.py`, or `code/inference.py` at the root); and every repository file those import, transitively. A reference to another repository or outside the directory is a LEAD too. Any other `.py` file is listed in the notes, not - judged (`internal/checks/remotecode`, #254). + judged (`internal/checks/remotecode`, #254). Two `config.json` fields run + code without `trust_remote_code` at all (#NNN): a Hub kernel outside + `kernels-community` named as the attention implementation FAILs + (CVE-2026-4372), and a `kernels-community` kernel there, or a truthy + `trust_remote_code` (CVE-2026-5241), is a LEAD. - The tokenizer is inspected from `tokenizer.json`, `tokenizer_config.json`, `special_tokens_map.json`, and the special-token ids in `config.json` and `generation_config.json` (`tokenizer.InspectHF`): inconsistent tables FAIL, diff --git a/docs/false-positive-baseline.md b/docs/false-positive-baseline.md index 834b319..e868386 100644 --- a/docs/false-positive-baseline.md +++ b/docs/false-positive-baseline.md @@ -1395,6 +1395,87 @@ compiled or bytecode modules are outside the row, and an environment variable (`HF_DEFAULT_PIPELINE_NAME`, `SAGEMAKER_PROGRAM`) can rename the file an inference toolkit imports. +## Remote code: config fields that run code (2026-10-19, check set tier1/0.12) + +Two published vulnerabilities make transformers run code a repository names +in `config.json` when the caller set `trust_remote_code=False`. The row now +reads both fields, with rules taken from the fixes and from the loader's +source at pinned releases: + +- **CVE-2026-4372** ([NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-4372), + [Pluto Security](https://pluto.security/blog/unauthenticated-remote-code-execution-in-huggingface-transformers-via-config-injection/), + fix [a7f8e7f](https://github.com/huggingface/transformers/commit/a7f8e7ff37d87d1a1a0c8cf607971c607741452f)). + `PreTrainedConfig.__init__` + ([v5.2.0](https://github.com/huggingface/transformers/blob/v5.2.0/src/transformers/configuration_utils.py), + [v4.57.6](https://github.com/huggingface/transformers/blob/v4.57.6/src/transformers/configuration_utils.py)) + applies every key it does not know with `setattr`. `_attn_implementation_internal` + sets the attention implementation directly, and `_attn_implementation` does + the same through its property setter. A value `is_kernel` takes for a Hub + repository (`owner/repo`, with an optional `@revision` and `:function`) is + downloaded and run as a kernel when the model loads, once the `kernels` + package is installed (4.56.0 to 5.2.x). The fix stops applying the internal + field and makes 5.3.0 and later load only `kernels-community` kernels unless + the caller passes `allow_all_kernels` + ([v5.19.0](https://github.com/huggingface/transformers/blob/v5.19.0/src/transformers/integrations/hub_kernels.py)). + It does not stop `_attn_implementation`, so a `kernels-community` kernel + named there still loads. `to_dict` drops the internal field, and the + property is not an attribute, so `save_pretrained` writes neither. + A kernel from any other owner is a FAIL (`config-kernel-injection`). The + field is the injection, whatever the kernel holds, and Socair cannot read + code that lives in another repository. A `kernels-community` kernel is a + LEAD (`config-hub-kernel`). A top-level public `attn_implementation` is + not read: `from_dict` replaces it with the caller's own value. +- **CVE-2026-5241** ([NVD](https://nvd.nist.gov/vuln/detail/CVE-2026-5241), + fix [676559d](https://github.com/huggingface/transformers/commit/676559d5022b74aaa0cee1cee0842b7f27c5320e), + transformers 5.5.0). `LightGlueConfig` declared `trust_remote_code` as a + config field. When `keypoint_detector_config` names a model type + transformers does not register, it passed that field to + `AutoConfig.from_pretrained(keypoint_detector_config["_name_or_path"])`, + which imports that repository's code. A truthy `trust_remote_code` in a + `config.json` is a LEAD (`config-trust-remote-code`), not a FAIL. The + published LightGlue DISK checkpoint sets it on purpose (below), and other + loaders ignore the key. + +Both rules read every `config.json` in the directory, at any depth, because +a composite model builds each sub-config from its dict the same way. They do +not read other files: `sentence_bert_config.json` and `tokenizer_config.json` +can also carry `trust_remote_code`, and no loader's handling of those copies +was read for this change. Before this change the row was a LEAD at most. +These are its first FAILs. + +**Method.** The same method as the Remote code baseline above: each +repository's `.json` files (up to 2 MiB, leaving out `tokenizer.json` and +`vocab.json`) at a pinned commit through the Hub API, with no weights and no +Python. The set: the 400 most-downloaded transformers models and the 100 +most-liked on 2026-10-19, plus 48 named repositories. The named set covers +both LightGlue checkpoints, `openai/gpt-oss-20b` and `-120b`, and composite and +remote-code models from DeepSeek, Moonshot, MiniMax, Microsoft, Qwen, Google, +Mistral, NVIDIA, IBM, Zhipu, and others. Of 508 repositories, 24 are gated +and 81 hold no JSON file (GGUF, ComfyUI, and detector repositories). The +other 403 were scanned with the gate built from `main` at `47c847d` and from +this change: 2,227 JSON files, 535 of them a `config.json`. + +``` +SOCAIR_REMOTECODE_CORPUS="" go test ./internal/checks/remotecode -run RealRepos -v +``` + +Both runs: 357 PASS, 46 LEAD, 0 NOT_TESTED, 0 FAIL. **No result changed.** +Every field either rule reads, across all 2,227 files: + +| Repository | Commit | File | Field | Value | Result | +|---|---|---|---|---|---| +| ETH-CVG/lightglue_disk | `2df49cd2b07eda0b72cf62a56ece8b5cf0fff02e` | `config.json` | `trust_remote_code` | `true` | adds `config-trust-remote-code` to a row that is already a LEAD through its `auto_map` | +| microsoft/Phi-4-multimodal-instruct | `93f923e1a7727d1c4f446756212d9d3e8fcc5d81` | `config.json` | `_attn_implementation` | `"flash_attention_2"` | none: the library's own implementation, not a Hub kernel | +| inclusionAI/Ling-3.1-flash | `95feb31291cba718d1789ba8db76aea516e5d624` | `tokenizer_config.json` | `trust_remote_code` | `true` | not read (not a `config.json`) | +| jinaai/jina-embeddings-v2-small-en | `44e7d1d6caec8c883c2d4b207588504d519788d0` | `sentence_bert_config.json` | `model_args.trust_remote_code` | `true` | not read (not a `config.json`) | +| 3 others (llava-hf/llava-1.5-7b-hf, openai/whisper-large-v3, openai/whisper-large-v3-turbo) | | `tokenizer_config.json` | `trust_remote_code` | `false` | not read | + +No configuration in the set names a Hub kernel in any field, and none +carries `_attn_implementation_internal`. The FAIL is grounded in the +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. + ## bcomplex32, and the storage classes torch.cuda defines (2026-10-14, check set tier1/0.9) Two leftovers from the change above (#241). diff --git a/internal/benchmark/corpus.go b/internal/benchmark/corpus.go index 940810f..adf8d62 100644 --- a/internal/benchmark/corpus.go +++ b/internal/benchmark/corpus.go @@ -378,6 +378,8 @@ const ( srcPillar = "Pillar Security, chat-template backdoors across 18 models and 4 engines (2026); arXiv 2602.04653" srcLlamaCPP = "llama.cpp GGUF parser advisories: tensor data offsets outside the data section" srcRemoteCode = "Hugging Face trust_remote_code: repository code a loader executes" + srcConfigKern = "CVE-2026-4372 (Pluto Security, 2026): config.json's _attn_implementation_internal naming a Hub kernel repository, which transformers 4.56.0 to 5.2.x download and run in a plain from_pretrained once the kernels package is installed" + srcConfigTrust = "CVE-2026-5241: a LightGlue config.json whose trust_remote_code transformers before 5.5.0 passed to the nested AutoConfig.from_pretrained for its keypoint detector, importing another repository's code" srcLoaderPath = "Files loaders import by their path: transformers' custom_generate/generate.py, the Hugging Face inference toolkit's handler.py, diffusers' /.py" srcPayloadMeta = "Model metadata used to carry encoded payloads and executables (JFrog and Protect AI model threat reports)" srcTokenizer = "Tokenizer tampering: remapped tokens and instruction-bearing special tokens" @@ -541,6 +543,10 @@ func Cases() []Case { "modules.json": []byte(`[{"idx":0,"name":"0","path":"","type":"sentence_transformers.models.Transformer"}]`), "train_script.py": []byte("from sentence_transformers import SentenceTransformer\nmodel = SentenceTransformer('nreimers/MiniLM-L6-H384-uncased')\n"), })}, + // microsoft/Phi-4-multimodal-instruct ships _attn_implementation set + // to one of the library's own implementations: not a kernel (#NNN). + {"control-dir-attention-field", "model directory", "benign config.json whose _attn_implementation names flash_attention_2, as Phi-4-multimodal ships", "control", "", Clean, + repo(map[string][]byte{"config.json": []byte(`{"_name_or_path":"org/bench","model_type":"phi4mm","_attn_implementation":"flash_attention_2"}`)})}, // An uploader's SHA256SUMS that matches its files is noted, never // flagged (#263). {"control-dir-checksums", "model directory", "benign directory with an uploader's SHA256SUMS that matches every file", "control", "", Clean, @@ -757,6 +763,11 @@ func Cases() []Case { repo(map[string][]byte{"config.json": []byte(`{"_name_or_path":"org/bench","model_type":"bench","auto_map":{"AutoModel":"attacker/code--modeling_bench.BenchModel"}}`)})}, {"dir-remote-code-outside-path", "model directory", "auto_map naming a module by an absolute path outside the directory", srcRemoteCode, "Remote code", Detect, repo(map[string][]byte{"config.json": []byte(`{"_name_or_path":"org/bench","model_type":"bench","auto_map":{"AutoModel":"/tmp/payload.BenchModel"}}`)})}, + // A config field that runs code with no trust_remote_code (#NNN). + {"dir-config-kernel-injection", "model directory", "config.json naming an attacker's Hub kernel in _attn_implementation_internal", srcConfigKern, "Remote code", Detect, + repo(map[string][]byte{"config.json": []byte(`{"_name_or_path":"org/bench","model_type":"llama","_attn_implementation_internal":"attacker/bench-attn-kernel"}`)})}, + {"dir-config-trust-remote-code", "model directory", "a LightGlue config.json that grants trust_remote_code to the keypoint detector it loads from another repository", srcConfigTrust, "Remote code", Detect, + repo(map[string][]byte{"config.json": []byte(`{"_name_or_path":"org/bench","model_type":"lightglue","trust_remote_code":true,"keypoint_detector_config":{"model_type":"bench_detector","_name_or_path":"attacker/bench-detector"}}`)})}, {"dir-st-custom-module", "model directory", "a sentence-transformers module in modules.json that runs a command, with no auto_map", srcRemoteCode, "Remote code", Detect, repo(map[string][]byte{ "modules.json": []byte(`[{"idx":0,"name":"0","path":"","type":"custom_st.Transformer"}]`), diff --git a/internal/checks/remotecode/check.go b/internal/checks/remotecode/check.go index 9f55e53..2c62e79 100644 --- a/internal/checks/remotecode/check.go +++ b/internal/checks/remotecode/check.go @@ -31,6 +31,22 @@ // not judged, because the loader then imports it from an installed package // or fails. // +// A config can also make a loader run code with no trust_remote_code at all, +// and that is judged as the field itself: +// +// - an attention field of config.json (_attn_implementation_internal, or +// _attn_implementation, whose setter writes it), at any depth, naming a +// Hub kernel repository: transformers 4.56.0 to 5.2.x download and run +// the kernel in a plain from_pretrained once the kernels package is +// installed (CVE-2026-4372). save_pretrained never writes either field, +// so a kernel outside kernels-community there is a FAIL; a +// kernels-community kernel, which 5.3.0 and later still load, is a LEAD; +// - trust_remote_code set in config.json: transformers' LightGlue read it +// before 5.5.0 and passed it to the nested AutoConfig.from_pretrained +// that loads its keypoint detector, importing another repository's code +// with trust_remote_code=False (CVE-2026-5241). Other loaders ignore it, +// so it is a LEAD. +// // Any other Python or script file is listed in the notes as present and // imported by none of these, not judged: Socair does not say a loader runs a // file no loader names. A directory with no reference PASSes. A config that @@ -40,7 +56,8 @@ // // Out of scope: loaders that import a file their caller names (SpeechBrain's // foreign_class), configs that are code by design (HyperPyYAML, Hydra's -// _target_), Hub kernels a config's attn_implementation names, PEFT's +// _target_), a public attn_implementation (from_dict replaces a top-level one +// with the caller's own; one in a sub-config is not read here), PEFT's // auto_mapping (an installed module), compiled or bytecode modules, and an // environment variable that renames a handler. package remotecode @@ -61,7 +78,8 @@ import ( const ( resultName = "Remote code" looksFor = "Python a loader would import: modules a config names (auto_map, custom_pipelines, sentence-transformers and diffusers modules), " + - "files loaders import by their path, and the repository files those import" + "files loaders import by their path, the repository files those import, " + + "and config.json fields that run code without trust_remote_code (a Hub kernel as the attention implementation, trust_remote_code itself)" ) const ( @@ -90,6 +108,7 @@ func Inspect(root string, files []modeldir.File) checks.Result { t := newTree(paths) var refs []reference + var fields []override var unread []string configs := 0 for _, f := range files { @@ -119,6 +138,9 @@ func Inspect(root string, files []modeldir.File) checks.Result { continue } configs++ + if obj, ok := v.(map[string]any); ok { + fields = append(fields, overrides(f.Path, obj)...) + } got, all := t.loaderRefs(f.Path, v) if !all { unread = append(unread, fmt.Sprintf("%s: more than %d references, the rest not read", f.Path, maxRefsPerFile)) @@ -149,6 +171,18 @@ func Inspect(root string, files []modeldir.File) checks.Result { } reached, importedBy, unreadSrc := follow(root, t, uniq(entries)) + failed := false + for i, o := range fields { + if i == maxFindings { + r.Findings = append(r.Findings, checks.Finding{Pattern: o.pattern, Span: fmt.Sprintf("and %d more field(s)", len(fields)-i), + Detail: "not listed one by one"}) + break + } + r.Findings = append(r.Findings, checks.Finding{Pattern: o.pattern, Span: clip(o.where) + " -> " + clip(o.what), Detail: o.detail}) + } + for _, o := range fields { + failed = failed || o.fail + } for i, ref := range leads { if i == maxFindings { r.Findings = append(r.Findings, checks.Finding{Pattern: ref.pattern, Span: fmt.Sprintf("and %d more reference(s)", len(leads)-i), @@ -179,7 +213,15 @@ func Inspect(root string, files []modeldir.File) checks.Result { switch { case len(r.Findings) > 0: r.Status = checks.Lead - notes = append(notes, leadNote(leads, reached)) + if failed { + r.Status = checks.Fail + } + if len(fields) > 0 { + notes = append(notes, fieldNote(fields, failed)) + } + if len(leads) > 0 || len(reached) > 0 { + notes = append(notes, leadNote(leads, reached)) + } if len(unreadSrc) > 0 { notes = append(notes, "could not read for imports, so files they import may be unnamed: "+list(unreadSrc)) } @@ -192,8 +234,8 @@ func Inspect(root string, files []modeldir.File) checks.Result { default: r.Status = checks.Pass notes = append(notes, fmt.Sprintf("no loader is told to import code: no auto_map, custom_pipelines, sentence-transformers module outside "+ - "sentence_transformers, or diffusers custom component in the %d JSON config(s) read, and no custom_generate/generate.py, "+ - "handler.py, pipeline.py, or code/inference.py", configs)) + "sentence_transformers, diffusers custom component, Hub kernel named as an attention implementation, or trust_remote_code "+ + "granted in the %d JSON config(s) read, and no custom_generate/generate.py, handler.py, pipeline.py, or code/inference.py", configs)) } for i, ref := range noted { if i == maxListed { @@ -210,6 +252,17 @@ func Inspect(root string, files []modeldir.File) checks.Result { return r } +// fieldNote says what the config fields that override the caller do. +func fieldNote(fields []override, failed bool) string { + s := fmt.Sprintf("%d config field(s) make a loader run code the caller did not allow with trust_remote_code, each named in the evidence", len(fields)) + if failed { + s += ". A Hub kernel outside kernels-community named in an attention field of config.json is the CVE-2026-4372 injection: " + + "transformers 4.56.0 to 5.2.x run it in a plain from_pretrained once the kernels package is installed, and save_pretrained " + + "never writes the field. Do not load this model" + } + return s +} + // finding is the evidence for one reference. func finding(ref reference) checks.Finding { span := clip(ref.where) diff --git a/internal/checks/remotecode/corpus_test.go b/internal/checks/remotecode/corpus_test.go index 69e9b8b..f765236 100644 --- a/internal/checks/remotecode/corpus_test.go +++ b/internal/checks/remotecode/corpus_test.go @@ -18,7 +18,7 @@ import ( // (space-separated; each holds one repository per subdirectory, such as the // configs and .py files fetched without weights) and prints each row. It // fails if a repository with a non-empty auto_map in any JSON file is not a -// LEAD: the narrowing in #254 must never clear real remote code. +// LEAD or a FAIL: the narrowing in #254 must never clear real remote code. // // SOCAIR_REMOTECODE_CORPUS="" go test ./internal/checks/remotecode -run RealRepos -v func TestRealRepos(t *testing.T) { @@ -50,8 +50,8 @@ func TestRealRepos(t *testing.T) { } sort.Strings(pats) t.Logf("%-10s %s [%s] %s", r.Status, e.Name(), strings.Join(uniq(pats), ","), r.Notes) - if hasAutoMap(dir, files) && r.Status != checks.Lead { - t.Errorf("%s has an auto_map and is %s: real remote code must stay a LEAD", e.Name(), r.Status) + if hasAutoMap(dir, files) && r.Status != checks.Lead && r.Status != checks.Fail { + t.Errorf("%s has an auto_map and is %s: real remote code must stay a finding", e.Name(), r.Status) } } } diff --git a/internal/checks/remotecode/fields.go b/internal/checks/remotecode/fields.go new file mode 100644 index 0000000..14a7584 --- /dev/null +++ b/internal/checks/remotecode/fields.go @@ -0,0 +1,149 @@ +package remotecode + +import ( + "encoding/json" + "fmt" + "path" + "regexp" + "strings" +) + +// override is a config field that makes a loader run code the caller did not +// agree to: the field is the evidence, whatever the code it names holds. +type override struct { + pattern string + where string + what string + detail string + fail bool +} + +// kernelRef is what transformers' is_kernel takes for a Hub kernel +// repository (integrations/hub_kernels.py, v4.57.6, v5.2.0, v5.19.0): +// owner/repo, with an optional @revision and :function. +var kernelRef = regexp.MustCompile(`^[^/:]+/[^/:]+(?:@[^/:]+)?(?::[^/:]+)?$`) + +// curatedKernels is the one owner transformers 5.3.0 and later load a kernel +// from unless the caller allows others (get_kernel's allow_all_kernels). +const curatedKernels = "kernels-community" + +// attnFields are the config keys that set a model's attention +// implementation from config.json. PreTrainedConfig.__init__ (v4.57.6, +// v5.2.0) applies every key it does not know with setattr: the internal field +// directly, and _attn_implementation through its property setter, which +// writes the internal field (and a sub-config's, from a dict). to_dict drops +// the internal field and never writes the property, so save_pretrained +// writes neither. +var attnFields = map[string]bool{"_attn_implementation_internal": true, "_attn_implementation": true} + +// overrides reads the fields of one config.json, at any depth (a composite +// model builds each sub-config from its dict the same way), that make a +// loader run code without trust_remote_code. +func overrides(rel string, obj map[string]any) []override { + if path.Base(rel) != "config.json" { + return nil + } + var out []override + var walk func(prefix string, v any, depth int) + walk = func(prefix string, v any, depth int) { + if depth > maxDepth || len(out) > maxRefsPerFile { + return + } + switch x := v.(type) { + case map[string]any: + for _, k := range sortedKeys(x) { + switch { + case attnFields[k]: + for _, leaf := range stringLeaves("", x[k], depth) { + where := rel + ": " + prefix + k + if leaf.key != "" { + where += "." + leaf.key + } + if o, ok := kernelField(where, leaf.s); ok { + out = append(out, o) + } + } + case k == "trust_remote_code" && truthy(x[k]): + out = append(out, trustField(rel+": "+prefix+k, x)) + } + walk(prefix+k+".", x[k], depth+1) + } + case []any: + for i, e := range x { + walk(fmt.Sprintf("%s[%d].", strings.TrimSuffix(prefix, "."), i), e, depth+1) + } + } + } + walk("", obj, 0) + return out +} + +// kernelField judges an attention implementation that names a Hub kernel. +// transformers 4.56.0 to 5.2.x load any owner's kernel (CVE-2026-4372); +// 5.3.0 and later load only kernels-community's unless told otherwise. A +// value after "paged|" is the kernel the paged wrapper loads. +func kernelField(where, value string) (override, bool) { + name := strings.TrimSpace(value) + if i := strings.LastIndex(name, "|"); i >= 0 { + name = name[i+1:] + } + if !kernelRef.MatchString(name) { + return override{}, false // eager, sdpa, flash_attention_2: the library's own + } + owner := name[:strings.Index(name, "/")] + if owner == curatedKernels { + return override{pattern: "config-hub-kernel", where: where, what: value, + detail: "names a kernels-community Hub kernel, which transformers 4.56.0 and later download and run when the model loads, " + + "without trust_remote_code; save_pretrained never writes this field"}, true + } + return override{pattern: "config-kernel-injection", where: where, what: value, fail: true, + detail: "names a Hub kernel from " + owner + ", which transformers 4.56.0 to 5.2.x download and run when the model loads, " + + "with trust_remote_code=False, once the kernels package is installed (CVE-2026-4372, fixed in 5.3.0); " + + "save_pretrained never writes this field"}, true +} + +// trustField is a config that grants its own loader trust_remote_code. +// transformers' LightGlue config read it before 5.5.0 and passed it to the +// AutoConfig.from_pretrained that loads its keypoint detector from the +// repository keypoint_detector_config._name_or_path names, which imports that +// repository's code when the caller had set trust_remote_code=False +// (CVE-2026-5241). Other loaders ignore the key, so it is a LEAD. +func trustField(where string, obj map[string]any) override { + what := "trust_remote_code: " + jsonText(obj["trust_remote_code"]) + if kd, ok := obj["keypoint_detector_config"].(map[string]any); ok { + if s, ok := kd["_name_or_path"].(string); ok && s != "" { + what += ", keypoint_detector_config._name_or_path: " + s + } + } + return override{pattern: "config-trust-remote-code", where: where, what: what, + detail: "a config that grants its own loader trust_remote_code: transformers before 5.5.0 passed LightGlue's to the " + + "AutoConfig.from_pretrained that loads its keypoint detector, importing another repository's code with " + + "trust_remote_code=False (CVE-2026-5241)"} +} + +// truthy is Python's truth value of a decoded JSON value. +func truthy(v any) bool { + switch x := v.(type) { + case nil: + return false + case bool: + return x + case float64: + return x != 0 + case string: + return x != "" + case []any: + return len(x) > 0 + case map[string]any: + return len(x) > 0 + } + return true +} + +func jsonText(v any) string { + b, err := json.Marshal(v) + if err != nil { + return fmt.Sprint(v) + } + return string(b) +} diff --git a/internal/checks/remotecode/fields_test.go b/internal/checks/remotecode/fields_test.go new file mode 100644 index 0000000..9b9d398 --- /dev/null +++ b/internal/checks/remotecode/fields_test.go @@ -0,0 +1,95 @@ +package remotecode + +import ( + "strings" + "testing" + + "github.com/defilantech/socair/internal/checks" +) + +// CVE-2026-4372: a Hub kernel named in an attention field of config.json is +// downloaded and run by a plain from_pretrained in transformers 4.56.0 to +// 5.2.x. Every shape the loader reads is a FAIL that names the field. +func TestKernelInConfigFails(t *testing.T) { + for name, cfg := range map[string]string{ + "internal field": `{"model_type":"llama","_attn_implementation_internal":"research-lab/optimized-attn-kernel"}`, + "property field": `{"model_type":"llama","_attn_implementation":"research-lab/optimized-attn-kernel"}`, + "sub-config": `{"model_type":"llava","text_config":{"model_type":"llama","_attn_implementation_internal":"research-lab/optimized-attn-kernel"}}`, + "per sub-config map": `{"model_type":"llava","_attn_implementation":{"":"sdpa","text_config":"research-lab/optimized-attn-kernel"}}`, + "paged": `{"model_type":"llama","_attn_implementation_internal":"paged|research-lab/optimized-attn-kernel"}`, + "revision and name": `{"model_type":"llama","_attn_implementation_internal":"research-lab/optimized-attn-kernel@v2:attention"}`, + } { + t.Run(name, func(t *testing.T) { + r := inspect(t, map[string]string{"config.json": cfg}) + if r.Status != checks.Fail || len(r.Findings) != 1 || r.Findings[0].Pattern != "config-kernel-injection" { + t.Fatalf("status %s, findings %+v", r.Status, r.Findings) + } + f := r.Findings[0] + if !strings.Contains(f.Span, "_attn_implementation") || !strings.Contains(f.Span, "research-lab/optimized-attn-kernel") || + !strings.Contains(f.Detail, "CVE-2026-4372") || !strings.Contains(r.Notes, "Do not load") { + t.Fatalf("evidence does not name the field, the kernel, or the CVE: %+v (%s)", f, r.Notes) + } + }) + } +} + +// A component's config.json is a transformers config too (a diffusers text +// encoder), and a FAIL there outranks an auto_map LEAD beside it. +func TestKernelInComponentConfigOutranksLead(t *testing.T) { + r := inspect(t, map[string]string{ + "config.json": `{"model_type":"bench","auto_map":{"AutoModel":"modeling_bench.BenchModel"}}`, + "modeling_bench.py": "class BenchModel:\n pass\n", + "text_encoder/config.json": `{"model_type":"clip_text_model","_attn_implementation_internal":"attacker/kernel"}`, + }) + pats := map[string]bool{} + for _, f := range r.Findings { + pats[f.Pattern] = true + } + if r.Status != checks.Fail || !pats["config-kernel-injection"] || !pats["auto_map"] { + t.Fatalf("status %s, findings %+v", r.Status, r.Findings) + } +} + +// transformers 5.3.0 and later still load a kernels-community kernel from +// the field, without trust_remote_code: code a loader runs, a LEAD. +func TestCuratedKernelInConfigIsLead(t *testing.T) { + r := inspect(t, map[string]string{"config.json": `{"model_type":"llama","_attn_implementation":"kernels-community/flash-attn2"}`}) + if r.Status != checks.Lead || len(r.Findings) != 1 || r.Findings[0].Pattern != "config-hub-kernel" { + t.Fatalf("status %s, findings %+v", r.Status, r.Findings) + } +} + +// The library's own implementations are not code from a repository, a +// public attn_implementation at the top level is replaced by the caller's +// own (from_dict), and only config.json is a PreTrainedConfig: none of these +// is a finding. Pinned so the stated limits stay true. +func TestAttentionFieldsWithoutKernelPass(t *testing.T) { + for name, files := range map[string]map[string]string{ + "library implementations": {"config.json": `{"model_type":"llama","_attn_implementation":"flash_attention_2","text_config":{"_attn_implementation_internal":"sdpa"},"vision_config":{"_attn_implementation":"paged|eager"}}`}, + "public field": {"config.json": `{"model_type":"llama","attn_implementation":"research-lab/optimized-attn-kernel"}`}, + "other config file": {"config.json": `{"model_type":"llama"}`, "generation_config.json": `{"_attn_implementation_internal":"research-lab/optimized-attn-kernel"}`}, + "null and false": {"config.json": `{"model_type":"lightglue","_attn_implementation_internal":null,"trust_remote_code":false}`}, + } { + t.Run(name, func(t *testing.T) { + if r := inspect(t, files); r.Status != checks.Pass || len(r.Findings) != 0 { + t.Fatalf("status %s, findings %+v (%s)", r.Status, r.Findings, r.Notes) + } + }) + } +} + +// CVE-2026-5241: a config that grants its own loader trust_remote_code is a +// LEAD that names the repository LightGlue's loader would import from. +func TestConfigGrantingTrustIsLead(t *testing.T) { + r := inspect(t, map[string]string{"config.json": `{"model_type":"lightglue","trust_remote_code":true,"keypoint_detector_config":{"model_type":"bench_detector","_name_or_path":"attacker/detector"}}`}) + if r.Status != checks.Lead || len(r.Findings) != 1 || r.Findings[0].Pattern != "config-trust-remote-code" { + t.Fatalf("status %s, findings %+v", r.Status, r.Findings) + } + if f := r.Findings[0]; !strings.Contains(f.Span, "attacker/detector") || !strings.Contains(f.Detail, "CVE-2026-5241") { + t.Fatalf("evidence %+v", f) + } + nested := inspect(t, map[string]string{"config.json": `{"model_type":"bench","sub":{"trust_remote_code":1}}`}) + if nested.Status != checks.Lead { + t.Fatalf("a truthy trust_remote_code in a sub-config: %s", nested.Status) + } +} diff --git a/internal/engine/scan.go b/internal/engine/scan.go index 30a3116..228f12c 100644 --- a/internal/engine/scan.go +++ b/internal/engine/scan.go @@ -35,7 +35,7 @@ import ( ) // CheckSetVersion names the set of checks this engine runs. -const CheckSetVersion = "tier1/0.11" +const CheckSetVersion = "tier1/0.12" // Version is the socair version: "dev" in a source build, the release tag // in a release build (scripts/build-release.sh sets it with -ldflags -X). diff --git a/internal/report/frameworks.go b/internal/report/frameworks.go index 356a5d5..dd422a1 100644 --- a/internal/report/frameworks.go +++ b/internal/report/frameworks.go @@ -26,6 +26,9 @@ var patternSeverity = map[string]string{ "embedded-binary": SeverityCritical, "repo-binary": SeverityCritical, "denylist-match": SeverityCritical, + // A config field that makes a loader run a Hub kernel without + // trust_remote_code (CVE-2026-4372). + "config-kernel-injection": SeverityCritical, // A file write outside the save directory when the tokenizer is saved. "template-name-path": SeverityHigh, // Positive evidence of tampering, or a strong sign of a hidden payload. @@ -105,6 +108,8 @@ var patternSeverity = map[string]string{ "custom-generate": SeverityMedium, "inference-handler": SeverityMedium, "imported-code": SeverityMedium, + "config-hub-kernel": SeverityMedium, + "config-trust-remote-code": SeverityMedium, // An ONNX external data location that systems resolve differently. "external-data-path": SeverityMedium, // No longer emitted (tier1/0.9, #254): any .py file was a LEAD. Kept so diff --git a/internal/report/model.go b/internal/report/model.go index 8fc2888..a3c9f2c 100644 --- a/internal/report/model.go +++ b/internal/report/model.go @@ -143,6 +143,7 @@ var passMeaning = map[string]string{ "Remote code": "No config a loader reads names Python in this directory or in another repository (through an auto_map entry, " + "custom_pipelines, a sentence-transformers module, or a diffusers custom pipeline or component), and no file sits where a loader imports one by its path " + "(custom_generate/generate.py, or an inference toolkit's handler.py, pipeline.py, or code/inference.py). " + + "No config.json names a Hub kernel as its attention implementation or sets trust_remote_code. " + "Other Python and script files are listed in the notes and not judged: a loader Socair does not model, or a person, can still run them.", "Hash, provenance, lineage": "A trusted publisher signature verified over this artifact, or an operator-supplied manifest binds this hash " + "to a repository at an immutable commit. The notes say which; a manifest is the operator's claim, not a signature. " + From fe02c9f8f8a63958486b383a81b5e4b73434b7bd Mon Sep 17 00:00:00 2001 From: Christopher Maher Date: Mon, 19 Oct 2026 08:04:52 -0700 Subject: [PATCH 2/2] Name the pull request in the benchmark and dev notes Signed-off-by: Christopher Maher --- docs/detection-benchmark.md | 4 ++-- docs/dev.md | 2 +- internal/benchmark/corpus.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/detection-benchmark.md b/docs/detection-benchmark.md index c41c3fc..cd0c707 100644 --- a/docs/detection-benchmark.md +++ b/docs/detection-benchmark.md @@ -211,7 +211,7 @@ skips) from failing the Tokenizer row as an id outside the vocabulary; every case and control here writes its special-token ids inside the vocabulary. Check set tier1/0.12 adds two Remote code cases for config fields that make -transformers run code with `trust_remote_code=False` (#NNN). +transformers run code with `trust_remote_code=False` (#291). `dir-config-kernel-injection` names an attacker's Hub kernel in `_attn_implementation_internal`, the CVE-2026-4372 shape, which transformers 4.56.0 to 5.2.x download and run in a plain `from_pretrained`. It is the @@ -371,7 +371,7 @@ 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 #NNN, so its counts cover 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: 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. diff --git a/docs/dev.md b/docs/dev.md index f82f75b..2d3667f 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -61,7 +61,7 @@ artifact: those import, transitively. A reference to another repository or outside the directory is a LEAD too. Any other `.py` file is listed in the notes, not judged (`internal/checks/remotecode`, #254). Two `config.json` fields run - code without `trust_remote_code` at all (#NNN): a Hub kernel outside + code without `trust_remote_code` at all (#291): a Hub kernel outside `kernels-community` named as the attention implementation FAILs (CVE-2026-4372), and a `kernels-community` kernel there, or a truthy `trust_remote_code` (CVE-2026-5241), is a LEAD. diff --git a/internal/benchmark/corpus.go b/internal/benchmark/corpus.go index adf8d62..2902257 100644 --- a/internal/benchmark/corpus.go +++ b/internal/benchmark/corpus.go @@ -544,7 +544,7 @@ func Cases() []Case { "train_script.py": []byte("from sentence_transformers import SentenceTransformer\nmodel = SentenceTransformer('nreimers/MiniLM-L6-H384-uncased')\n"), })}, // microsoft/Phi-4-multimodal-instruct ships _attn_implementation set - // to one of the library's own implementations: not a kernel (#NNN). + // to one of the library's own implementations: not a kernel (#291). {"control-dir-attention-field", "model directory", "benign config.json whose _attn_implementation names flash_attention_2, as Phi-4-multimodal ships", "control", "", Clean, repo(map[string][]byte{"config.json": []byte(`{"_name_or_path":"org/bench","model_type":"phi4mm","_attn_implementation":"flash_attention_2"}`)})}, // An uploader's SHA256SUMS that matches its files is noted, never @@ -763,7 +763,7 @@ func Cases() []Case { repo(map[string][]byte{"config.json": []byte(`{"_name_or_path":"org/bench","model_type":"bench","auto_map":{"AutoModel":"attacker/code--modeling_bench.BenchModel"}}`)})}, {"dir-remote-code-outside-path", "model directory", "auto_map naming a module by an absolute path outside the directory", srcRemoteCode, "Remote code", Detect, repo(map[string][]byte{"config.json": []byte(`{"_name_or_path":"org/bench","model_type":"bench","auto_map":{"AutoModel":"/tmp/payload.BenchModel"}}`)})}, - // A config field that runs code with no trust_remote_code (#NNN). + // A config field that runs code with no trust_remote_code (#291). {"dir-config-kernel-injection", "model directory", "config.json naming an attacker's Hub kernel in _attn_implementation_internal", srcConfigKern, "Remote code", Detect, repo(map[string][]byte{"config.json": []byte(`{"_name_or_path":"org/bench","model_type":"llama","_attn_implementation_internal":"attacker/bench-attn-kernel"}`)})}, {"dir-config-trust-remote-code", "model directory", "a LightGlue config.json that grants trust_remote_code to the keypoint detector it loads from another repository", srcConfigTrust, "Remote code", Detect,