fix: harden syft-restrict against verifier and obfuscator bypasses#9436
fix: harden syft-restrict against verifier and obfuscator bypasses#9436pjwerneck wants to merge 14 commits into
Conversation
…or f-string conversions
… scope checks Fix ast.comprehension check relying on unavailable lineno, therefore unreachable code.
…handling and range errors
0213862 to
fc54f9d
Compare
There was a problem hiding this comment.
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/clstrust, 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.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
|
||
| --- | ||
|
|
||
| ## The JAX / serialization denylist |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
again, I am not completely following this sentence
| --- | ||
|
|
||
| ## Container / aliasing tricks | ||
|
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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`). |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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).
Summary of the latest changesThis push reworks the policy denylist and simplifies/tightens the verifier. Full suite green (147 passed). Policy: hardcoded denylist → user-configurable disallow list
Verifier tightening & simplification
Tests & docs
|
Summary
Closes a series of bypasses in
syft-restrict's static verifier and obfuscator, found throughiterative adversarial review. Update documentation.
Changes
type,__build_class__,print, and thedunder-proxy builtins
repr/str/ascii/format/bytes.from X import name, homoglyph identifiers, andcommon identifier-aliasing patterns that evaded the call-target checks.
!r/!s/!a), the{x=}debug form, and plaininterpolation (
f"{x}") all invoke__format__/__repr__/__str__with noCallnode — allare now rejected instead of only the conversion-flag forms.
self/clsparameter vetting: reassigningself/cls, reusing them as an unrelatedparameter name, and non-first-parameter/nested-function/lambda cases no longer grant the
self.<name>trust exemption.self.<attr>trust bypasses: tuple-unpack and for-loop assignment targets are now trackedby the safety table, and local-variable aliasing (including multi-hop copies) is tracked so
tmp = self.fn; tmp(x)is checked the same wayself.fn(x)is.def/classstatements whose name shadows a trusted import alias or visible wrapper name(previously only rebinding via assignment was checked).
tokenizer (previously left un-obfuscated on 3.12+).
docs/disallowed-ast-examples.mdas per-construct subheadings instead of a wide table;add
docs/code-layout.md; simplifyREADME.md.astutil.pyand refactorverifier.py/obfuscator.pyforreadability (central violation-code registry,
_SelfAttrTrusthelper, split obfuscator passes),with no behavior change.
Testing
tests/verify/test_bypasses.pywith a regression test per bypass classtests/verify/test_ranges.pyfor the range-validation fix.Asana task
https://app.asana.com/1/1185126988600652/project/1216249688888494/task/1216266561077080?focus=true