Skip to content

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

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

fix: harden syft-restrict against verifier and obfuscator bypasses#9436
pjwerneck wants to merge 14 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>

---

## The JAX / serialization denylist

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I dont get it, do we have a specific jax denylist?


## Reserved-name rebinding

Rebinding a name the resolver trusts would poison verification. Rejected wherever the name is

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what does it mean to trust


- A trusted module alias (`jnp`, `nn`, `lax`, …) — rebinding makes the import table a lie, so every
"allow-listed path" through that name becomes attacker-controlled.
- A visible wrapper name — rebinding `transpose = evil` defeats the wrapper's type guard.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what doe "defeats the wrappers type guard" mean? Can we. simplify this or just say that its not possible even though we dont know how it can be used exactly

- A trusted module alias (`jnp`, `nn`, `lax`, …) — rebinding makes the import table a lie, so every
"allow-listed path" through that name becomes attacker-controlled.
- A visible wrapper name — rebinding `transpose = evil` defeats the wrapper's type guard.
- `self` / `cls` — the exemption is trusted by identifier alone; rebinding it (or reusing it as an

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

again, I am not completely following this sentence

---

## Container / aliasing tricks

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nice, is this just builtin references or would there be other attack vectors here I am wondering

# syft-restrict

## 1. Overview
Static analysis for JAX/Flax inference code. You mark the parts of a file that contain the model

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

not specific for Jax/Flax


## 1. Overview
Static analysis for JAX/Flax inference code. You mark the parts of a file that contain the model
math as _private_; `syft-restrict` checks that those lines only do trusted computation — no sneaky

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tbh, I think this opening section/summary is worse than the original. I think we should revert or end up somewhere in the middle

hide=[], # 1-based ranges: whole line replaced with ■■■■■■■■
allow_functions=["jax.*", "flax.linen.*"], # things callable BY NAME (path-resolved)
allow_methods=["arithmetic", "indexing", "comparison"], # operators allowed ON A VALUE
obfuscate=[[22, 93], [99, 280]], # 1-based ranges: identifiers renamed, constants blanked

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can you make sure this aligns with the example, I think currently this is out of sync (it already was)


## 5. What this whitelist does NOT stop
1. Parse the whole file and build an import binding table (`import jax.numpy as jnp` → `jnp` maps to `jax.numpy`).
2. Walk the private lines with default-deny. Each AST node must match an explicit rule: allowed node

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this describes it in terms that are not introduced yet

`restrict.run()` verifies first, then obfuscates:

## 5. What this whitelist does NOT stop
1. Parse the whole file and build an import binding table (`import jax.numpy as jnp` → `jnp` maps to `jax.numpy`).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

concept of binding table not introduced yet, describe without it, more generic

2. **Timing & cache side channels.** Execution time, memory-access and CPU-cache patterns leak information regardless of which ops ran; the whitelist can't enforce constant-time execution. → enclave-level side-channel mitigations.
3. **A future JAX host-callback API under an already-allowed prefix.** Default-deny closes unknown _new_ paths automatically, but a newly added dangerous symbol inside an allowed prefix (e.g. a hypothetical `jax.numpy.<new_io>`) is a gap until the denylist is updated. → keep the policy versioned and reviewed against each pinned JAX release.
4. **Bugs or supply-chain compromise in the trusted libraries.** The model only constrains the _caller's_ code; a malicious jax/flax/orbax build defeats it. → attest the exact library versions/hashes; the DO must trust those releases.
- [docs/verify.md](docs/verify.md) — how the checker decides what's allowed, what order things run in, edge cases, and known limits.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

checker, not introduced yet


`syft-restrict` statically analyzes the **private** lines of a source file. The goal is to show
those lines only perform trusted math: no file or network access, no dynamic Python, no reaching
into interpreter internals. The code is never executed. You get a violation list (empty on success)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I dont thing using "You" is the tone of voice we want


## The whitelist

Private inference code needs to wire up JAX operations: Flax modules with `setup`/`__call__`, JAX

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

it assume inference code too much now, that needs more of a nuance

Private inference code needs to wire up JAX operations: Flax modules with `setup`/`__call__`, JAX
function calls, arithmetic, shape/dtype plumbing (lists, dicts, tuples, slices, f-strings for
einsum strings), and control flow that doesn't depend on secret data. It should not be able to touch
the host, filesystem, network, interpreter internals, or build code from strings.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would seperate this out as one of the use cases we want to support but not make it part of the package internals explanation

4. **Bugs or supply-chain compromise in trusted libraries.** The checker only constrains caller
code; a malicious jax/flax build defeats it. Attest exact library versions and hashes.

## Examples

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel like this section does not make clear these are not allowed, also, if they are not why are they not on the blacklist?

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.

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