Skip to content

fix: harden syft-restrict against verifier and obfuscator bypasses#9436

Open
pjwerneck wants to merge 32 commits into
devfrom
pjwerneck/fix-restrict-bypasses
Open

fix: harden syft-restrict against verifier and obfuscator bypasses#9436
pjwerneck wants to merge 32 commits into
devfrom
pjwerneck/fix-restrict-bypasses

Conversation

@pjwerneck

Copy link
Copy Markdown
Collaborator

Summary

Closes a series of bypasses in syft-restrict's static verifier and obfuscator, found through
iterative adversarial review. Update documentation.

Changes

  • Ban additional dynamic-escape/reflection builtins: type, __build_class__, print, and the
    dunder-proxy builtins repr/str/ascii/format/bytes.
  • Fix multiple aliasing bypasses: bare names imported via from X import name, homoglyph identifiers, and
    common identifier-aliasing patterns that evaded the call-target checks.
  • Fix f-string handling: conversion flags (!r/!s/!a), the {x=} debug form, and plain
    interpolation (f"{x}") all invoke __format__/__repr__/__str__ with no Call node — all
    are now rejected instead of only the conversion-flag forms.
  • Harden self/cls parameter vetting: reassigning self/cls, reusing them as an unrelated
    parameter name, and non-first-parameter/nested-function/lambda cases no longer grant the
    self.<name> trust exemption.
  • Close self.<attr> trust bypasses: tuple-unpack and for-loop assignment targets are now tracked
    by the safety table, and local-variable aliasing (including multi-hop copies) is tracked so
    tmp = self.fn; tmp(x) is checked the same way self.fn(x) is.
  • Ban def/class statements whose name shadows a trusted import alias or visible wrapper name
    (previously only rebinding via assignment was checked).
  • Fix the obfuscator to correctly blank f-string literal text under Python 3.12+'s PEP 701
    tokenizer (previously left un-obfuscated on 3.12+).
  • Rewrite docs/disallowed-ast-examples.md as per-construct subheadings instead of a wide table;
    add docs/code-layout.md; simplify README.md.
  • Extract shared AST helpers into astutil.py and refactor verifier.py/obfuscator.py for
    readability (central violation-code registry, _SelfAttrTrust helper, split obfuscator passes),
    with no behavior change.
  • Raise on a malformed/inverted private-line range instead of silently verifying nothing in it.

Testing

  • Added tests/verify/test_bypasses.py with a regression test per bypass class
  • Added tests/verify/test_ranges.py for the range-validation fix.

Asana task

https://app.asana.com/1/1185126988600652/project/1216249688888494/task/1216266561077080?focus=true

@github-actions github-actions Bot added the bugfix label Jul 6, 2026
@pjwerneck pjwerneck force-pushed the pjwerneck/fix-restrict-bypasses branch from 0213862 to fc54f9d Compare July 6, 2026 21:01

Copilot AI 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.

Pull request overview

This PR strengthens syft-restrict’s static verification and obfuscation layers to close multiple identified bypass techniques, adds regression tests for each bypass class, and refreshes the documentation to match the evolved threat model and implementation.

Changes:

  • Harden verifier rules around call-target resolution, aliasing, self/cls trust, and f-string interpolation to prevent reflection/dynamic-escape bypasses.
  • Update obfuscation to correctly blank f-string literal text under Python 3.12+ (PEP 701 tokenization), plus refactor shared AST/range utilities into astutil.py.
  • Add targeted regression tests (bypasses + range validation) and restructure docs/README for clarity.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/syft-restrict/tests/verify/test_whitelist.py Updates whitelist expectations for f-strings to reflect new interpolation rejection behavior.
packages/syft-restrict/tests/verify/test_ranges.py Adds regression test ensuring malformed private ranges raise instead of silently passing.
packages/syft-restrict/tests/verify/test_bypasses.py Adds comprehensive regression coverage for verifier bypass classes (aliasing, reflection, f-strings, self/cls trust, etc.).
packages/syft-restrict/tests/obfuscate/test_obfuscate.py Adds a test ensuring f-string literal text is blanked across tokenizer variants (incl. Python 3.12+).
packages/syft-restrict/src/syft_restrict/verifier.py Major verifier hardening + refactor: central violation codes, stronger call/name checks, self-attr trust tracking, and f-string interpolation rejection.
packages/syft-restrict/src/syft_restrict/runner.py Switches to shared astutil helpers for range normalization and scanning.
packages/syft-restrict/src/syft_restrict/policy.py Expands banned builtins and improves documentation/comments; minor cleanup.
packages/syft-restrict/src/syft_restrict/obfuscator.py Refactors passes; adds PEP 701 f-string token handling; uses shared astutil.
packages/syft-restrict/src/syft_restrict/astutil.py New shared helper module for AST and range utilities (scan, dotted paths, range normalization).
packages/syft-restrict/src/syft_restrict/init.py Updates package-level docs/pointers to new docs structure.
packages/syft-restrict/README.md Simplifies and reorients README toward usage + docs pointers.
packages/syft-restrict/docs/verify.md New/expanded explanation of verifier behavior, edge cases, and limitations.
packages/syft-restrict/docs/code-layout.md New short guide to module responsibilities.
packages/syft-restrict/docs/blacklist.md New consolidated “what is rejected” doc with violation codes.
packages/syft-restrict/disallowed-ast-examples.md Removes legacy table-style doc (replaced by new docs structure).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/syft-restrict/src/syft_restrict/verifier.py Outdated
Comment thread packages/syft-restrict/docs/verify.md Outdated
Comment thread packages/syft-restrict/docs/blacklist.md Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Comment thread packages/syft-restrict/docs/blacklist.md Outdated
Comment thread packages/syft-restrict/docs/blacklist.md Outdated
Comment thread packages/syft-restrict/docs/blacklist.md Outdated
Comment thread packages/syft-restrict/docs/blacklist.md Outdated
Comment thread packages/syft-restrict/docs/blacklist.md
Comment thread packages/syft-restrict/README.md Outdated
Comment thread packages/syft-restrict/README.md Outdated
Comment thread packages/syft-restrict/README.md Outdated
Comment thread packages/syft-restrict/README.md Outdated
Comment thread packages/syft-restrict/README.md Outdated
Comment thread packages/syft-restrict/README.md Outdated
Comment thread packages/syft-restrict/docs/verify.md Outdated
Comment thread packages/syft-restrict/docs/verify.md Outdated
Comment thread packages/syft-restrict/docs/verify.md Outdated
Comment thread packages/syft-restrict/docs/verify.md Outdated
Replace the hardcoded JAX_DENYLIST with a per-policy, user-supplied
disallow_functions glob list that beats the allow. Rename Policy fields
for clarity (allowed_functions/allowed_methods/reserved_names) and add
disallowed_functions; thread disallow_functions through run().

Tighten and simplify the verifier:
- base classes: only allow-listed imports may be a base (object and
  hidden-region classes are no longer accepted)
- _check_call: drop the BANNED_NAMES branch (_check_name is the single
  backstop) and the hidden/visible-def shadowing exclusions on the
  import-binding check
- rename dotted -> dotted_name; _check_self_cls_params ->
  _check_arguments_dont_abuse_self_or_cls; convert method comment blocks
  to docstrings

Update tests and docs accordingly. Full suite green (147 passed).
@koenvanderveen

Copy link
Copy Markdown
Collaborator

Summary of the latest changes

This push reworks the policy denylist and simplifies/tightens the verifier. Full suite green (147 passed).

Policy: hardcoded denylist → user-configurable disallow list

  • Removed the built-in JAX_DENYLIST (host-callback / IO / serialization globs).
  • Added a per-policy disallow_functions glob list that beats the allow, threaded through run(...) and Policy.parse(...). It's a hard floor an author can set over a broad allow like jax.*; empty by default.
  • Behavior change (intentional): with no disallow list, a bare jax.* allow now permits formerly-denied leaves such as jax.numpy.save. Safety now comes from a specific allow-list or an explicit disallow — documented and covered by test_no_disallow_permits_leaf_under_broad_allow.
  • Renamed Policy fields for clarity: functions → allowed_functions, methods → allowed_methods, reserved → reserved_names, plus the new disallowed_functions. policy_id now folds in the disallow list.

Verifier tightening & simplification

  • Base classes: only an allow-listed import may be a base class. object and hidden-region (locally-defined) classes are no longer accepted — a base's metaclass / __init_subclass__ runs at class-creation time, so it must resolve to something the policy vetted.
  • _check_call: dropped the BANNED_NAMES branch (banned builtins in call position are flagged by _check_name, the single backstop) and removed the hidden_defs/visible_defs shadowing exclusions on the import-binding check.
  • Readability: dotted → dotted_name; _check_self_cls_params → _check_arguments_dont_abuse_self_or_cls; per-method leading comment blocks converted to docstrings.

Tests & docs

  • Updated test_whitelist / test_bypasses for the base-class tightening (class M(object)class M:; test_class_bases_object_and_hidden_deftest_class_base_must_be_allow_listed_import).
  • Added test_user_disallow_beats_allow and test_no_disallow_permits_leaf_under_broad_allow; make_policy gained a disallow param.
  • Updated README, docs/verify.md, docs/blacklist.md, docs/code-layout.md, and regenerated gemma_inference.obfuscated.py + certificate.

pjwerneck added 18 commits July 9, 2026 11:30
… tests

f-strings with interpolation are banned, and f-strings with no interpolation placeholders serve no real purpose, so there's no point to keeping them.
_check_call and _check_name protect most of the attack surface, so some redundant checks can be removed.
…and operator bundles

- Updated test_disallowed.py to improve readability and add new tests for walrus operator and match statement.
- Enhanced test_ranges.py to ensure proper error handling for inverted ranges.
- Revised test_whitelist.py to streamline tests for allowed constructs and improve clarity.
- Modified test_whitelisted_lib.py to enhance structure and add new tests for user-supplied disallow lists.
- Updated the policy to use 'allowed_operators' instead of 'allowed_methods' to better reflect the purpose of operator bundles.
- Adjusted the run function and related documentation to align with the new naming convention.
- Modified tests to ensure compatibility with the updated policy structure.
- Changed violation codes from 'bundle-disabled' to 'operator-disabled' for consistency with the new terminology.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants