Skip to content

Fix export-time trust_remote_code bypass in FP8/INT8/GGUF-LoRA export#6869

Merged
danielhanchen merged 5 commits into
mainfrom
fix-torchao-remote-code-consent
Jul 5, 2026
Merged

Fix export-time trust_remote_code bypass in FP8/INT8/GGUF-LoRA export#6869
danielhanchen merged 5 commits into
mainfrom
fix-torchao-remote-code-consent

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Summary

The FP8/INT8 (portable torchao), compressed-tensors, and LoRA GGUF export paths re-read the just-merged checkpoint to apply quantization. They derived trust_remote_code for that reload from the checkpoint config's static auto_map (the torchao path additionally scanned the staged tokenizer_config.json / processor_config.json / preprocessor_config.json).

That is the wrong source. A model can load with built-in Transformers classes while still carrying an auto_map entry. Loading it does not require trust_remote_code, so it never triggers the load-time remote-code consent scan (which only runs when the load itself requested trust_remote_code). At export the same model then had trust_remote_code flipped on from that auto_map metadata, so its custom code ran during the reload without ever being scanned or approved. In Studio this bypasses the CRITICAL/HIGH remote-code consent gate.

Fix

Derive the reload trust_remote_code from the previously approved load decision rather than from untrusted config metadata. A new helper _loaded_via_remote_code(obj) reports whether the in-memory model / tokenizer was itself loaded from custom code, by checking whether its class lives in Transformers' transformers_modules package (where auto_map code is imported). It walks PEFT / wrapper layers so a LoRA over a custom-code base is still detected.

  • A model that loaded with built-in classes no longer gains trust_remote_code from config auto_map, so no unvetted remote code runs at export.
  • A genuine custom-code model (which had to be loaded with trust_remote_code, and in Studio was consent-scanned at load) still reports as remote-code-loaded, so it reloads and exports correctly. No regression for Nemotron and similar.

Applied to all three reload sites: _unsloth_save_torchao, _unsloth_save_compressed_tensors, and the LoRA GGUF converter invocation. The staged-config auto_map scan in the torchao path is removed.

Testing

  • tests/saving/test_torchao_remote_code_consent.py (new, CPU-only): AST-extracts and exercises _loaded_via_remote_code (built-in vs transformers_modules, PEFT unwrap, wrapper walk, and the key case that an auto_map in config alone does not grant trust), plus asserts every export path dropped the config-auto_map trust derivation.
  • tests/saving/test_export_api_surface.py and tests/saving/test_export_dispatch.py pass.
  • python -m py_compile unsloth/save.py is clean.

The torchao, compressed-tensors, and LoRA GGUF export paths re-read the merged
checkpoint and used to set trust_remote_code from the checkpoint config's static
auto_map (the torchao path also scanned the staged tokenizer/processor configs).
A model that loads with built-in Transformers classes can carry an auto_map entry,
which skips the load-time remote-code consent scan (that only runs when the load
already requested trust_remote_code) yet flips trust_remote_code on at export,
running unvetted custom code.

Derive the reload trust_remote_code from the approved load decision instead: a new
_loaded_via_remote_code() checks whether the in-memory model / tokenizer was itself
loaded from custom code (its class lives in the transformers_modules package),
walking PEFT / wrapper layers. Built-in-loaded models no longer gain trust from
config metadata; genuine custom-code models (loaded with consent) still reload
correctly. Add CPU-only regression tests.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses a security bypass where export paths derived trust_remote_code from a checkpoint's static auto_map config rather than the actual load decision. It introduces a helper _loaded_via_remote_code to verify if the model or tokenizer was loaded via custom code by inspecting its module path, and updates export paths to use this helper. Feedback suggests making the module name check in _loaded_via_remote_code more robust by using getattr and checking if the module is a string to prevent potential AttributeErrors when module is None or missing, along with adding a corresponding test case.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread unsloth/save.py Outdated
Comment on lines +243 to +244
if type(node).__module__.startswith("transformers_modules"):
return True

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Directly accessing type(node).__module__ and calling .startswith() can raise an AttributeError if __module__ is None or missing (which can happen for certain dynamically created classes, built-in types, or mock objects). To make this check more robust and prevent potential crashes during export, defensively retrieve the module name using getattr and verify it is a string before calling .startswith().

Suggested change
if type(node).__module__.startswith("transformers_modules"):
return True
module = getattr(type(node), "__module__", None)
if isinstance(module, str) and module.startswith("transformers_modules"):
return True

Comment on lines +53 to +54

def test_none_is_not_remote_code():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a test case to verify that _loaded_via_remote_code handles objects with a None module gracefully without raising an AttributeError.

Suggested change
def test_none_is_not_remote_code():
def test_none_is_not_remote_code():
assert _loaded_via_remote_code(None) is False
def test_none_module_is_not_remote_code():
assert _loaded_via_remote_code(_obj(None)) is False

Read type(node).__module__ via getattr and require a string before startswith,
so a dynamically created or C-extension class with a None module does not raise
during export. Add a regression test.
@danielhanchen

Copy link
Copy Markdown
Member Author

Applied both in 04c2d17. _loaded_via_remote_code now reads type(node).__module__ via getattr(..., None) and requires a string before calling .startswith, so a dynamically created or C-extension class with a None/missing module returns False instead of raising. Added test_none_module_is_not_remote_code to cover it.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04c2d17f61

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread unsloth/save.py Outdated
# trust_remote_code must reflect the approved load decision (whether the model / tokenizer
# was actually loaded from custom code), not the config's static auto_map, so a
# built-in-loadable model carrying auto_map cannot run unvetted code in the subprocess.
trust_remote_code = _loaded_via_remote_code(model) or _loaded_via_remote_code(tokenizer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep model and tokenizer remote-code trust separate

When the compressed export sees an approved custom tokenizer/processor but a built-in-loadable model that still carries a model auto_map, this or sets the single subprocess flag and unsloth/_compressed_quantize.py passes that same args.trust_remote_code to both auto_model.from_pretrained and auto_proc.from_pretrained. In that scenario the tokenizer's consent enables the staged model's unapproved custom code during compressed quantization, preserving the export-time consent bypass that the torchao path avoids by tracking model_trust and tok_trust separately.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 99ce1b8: the subprocess now takes a separate --trust-remote-code-tokenizer flag for the processor load while --trust-remote-code stays model-only, and save.py passes model_trust / tok_trust individually like the torchao path. Call-site test added.

Comment thread unsloth/save.py Outdated
except Exception:
nxt = None
if nxt is None or nxt is node:
nxt = getattr(node, "base_model", None) or getattr(node, "model", None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve trust for processor-held custom tokenizers

When the tokenizer argument is a built-in ProcessorMixin or wrapper that contains an approved remote-code component under .tokenizer, .image_processor, or .feature_extractor, this walker never descends into those processor attributes and returns False. The previous staged-config scan covered tokenizer/processor/preprocessor auto_map entries, but now torchao and compressed exports can pass trust_remote_code=False to the processor reload for an already-approved custom tokenizer/processor, causing those exports to fail or reload the wrong component.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 99ce1b8: the walker is now a bounded BFS that also descends into tokenizer, image_processor, feature_extractor, and video_processor attributes, so an approved custom component inside a built-in processor keeps its trust on reload. Tests added for custom and built-in components plus cycle termination.

…rocessor components

The compressed-tensors export collapsed model and tokenizer trust into
one --trust-remote-code flag, so an approved custom tokenizer would have
let an unapproved model's custom code run inside the quantization
subprocess. The subprocess now takes --trust-remote-code-tokenizer for
the processor load and keeps --trust-remote-code for the model loads,
matching the torchao path's separate model_trust / tok_trust.

_loaded_via_remote_code now also walks processor components (tokenizer,
image_processor, feature_extractor, video_processor), so an approved
custom tokenizer held inside a built-in ProcessorMixin keeps its trust
on the export reload instead of failing with trust_remote_code=False.
The walk is a bounded BFS with a seen set so wrapper cycles terminate.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: aa2d1fbd0a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danielhanchen
danielhanchen merged commit 64f6526 into main Jul 5, 2026
51 checks passed
@danielhanchen
danielhanchen deleted the fix-torchao-remote-code-consent branch July 5, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant