-
-
Notifications
You must be signed in to change notification settings - Fork 2k
fix: harden syft-restrict against verifier and obfuscator bypasses #9436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
2d2f548
1268fbe
4daaa7f
2cfe2cf
1502f4a
888f9b3
4eeca96
c69587a
5cc5267
fc54f9d
085f0a0
8c99d39
5808248
4ce5ab8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| # What is not allowed | ||
|
|
||
| Everything the checker rejects in the private region. For the reasoning behind the rules see [verify.md](verify.md). For example snippets see [disallowed-ast-examples.md](disallowed-ast-examples.md). | ||
|
|
||
| The model is default-deny: the tables below name what's rejected explicitly (for clear violation | ||
| messages), but anything missing from the whitelist in [verify.md](verify.md) is rejected too. | ||
|
|
||
| Each entry lists the error `code` reported by the checker. | ||
|
|
||
| --- | ||
|
|
||
| ## Banned statement / expression node types | ||
|
|
||
| Rejected immediately — they reach the host, filesystem, or interpreter, or reintroduce dynamic | ||
| control flow. (`_BANNED_NODES` in `verifier.py`.) Code: `banned-construct`. | ||
|
|
||
| | Node | Why | | ||
| | ---------------------------------------------------- | --------------------------------------------------------------------------------------- | | ||
| | `Import`, `ImportFrom` | Imports belong in the public region; inside private code they'd name arbitrary modules. | | ||
| | `With` | Context managers run `__enter__`/`__exit__` code. | | ||
| | `Try`, `Raise` | Exception control flow isn't needed for pure inference math. | | ||
| | `Global`, `Nonlocal` | Rebinding outer-scope names breaks the local-only name rules. | | ||
| | `Delete` | `del` can remove names the checker relies on. | | ||
| | `Assert` | Asserts vanish under `python -O`; they must never carry safety guarantees. | | ||
| | `AsyncFunctionDef`, `AsyncFor`, `AsyncWith`, `Await` | Async machinery is out of scope. | | ||
| | `Yield`, `YieldFrom` | Generators suspend and resume execution. | | ||
|
|
||
| ## Unknown / future syntax | ||
|
|
||
| Any node type not on the allow-list in [verify.md](verify.md#always-on-allow-list) is rejected — | ||
| walrus (`NamedExpr`), `match`/`case`, etc. Code: `node-type`. New Python syntax stays denied until | ||
| reviewed. | ||
|
|
||
| --- | ||
|
|
||
| ## Banned builtins | ||
|
|
||
| These names may never be called or referenced — aliasing them, putting them in a container, or | ||
| returning them is caught at the reference site. (`BANNED_NAMES` in `policy.py`.) | ||
|
|
||
| **Dynamic-code / reflection / IO hatches** — code `banned-call`: | ||
| `eval`, `exec`, `compile`, `__import__`, `getattr`, `setattr`, `delattr`, `hasattr`, `vars`, | ||
| `globals`, `locals`, `dir`, `open`, `input`, `breakpoint`, `memoryview`, `type`, `__build_class__`, | ||
| `print`. | ||
|
|
||
| **Dunder-proxy builtins** — same escape as calling a dunder on a value (`x.__repr__()`), spelled as | ||
| a bare call. Code `banned-call`: `repr`, `str`, `ascii`, `format`, `bytes`. (`bytes(x)` losslessly | ||
| serializes an array's raw memory; `print` is a stdout exfil channel.) | ||
|
|
||
| The same escape via **any f-string interpolation** (`f"{x}"`, `f"{x!r}"`, `f"{x!s}"`, `f"{x!a}"`, `f"{x=}"`) has no `Call` node and is rejected as `method-on-value`. | ||
|
|
||
| --- | ||
|
|
||
| ## The optional user disallow list | ||
|
|
||
| There is no built-in denylist. Safety comes from passing a _specific_ `allow_functions` list (exact | ||
| leaf paths rather than broad globs). For authors who do use a broad glob (`jax.*`) and still want a | ||
| hard floor, `run(..., disallow_functions=[...])` takes glob patterns that **beat the allowlist** — | ||
| rejected even under an otherwise-allowed module. (`disallowed_functions` on `Policy` in `policy.py`.) | ||
| Hitting one by dotted path, bare public import, or import alias reports `call-not-allowed`. | ||
|
|
||
| Typical patterns to disallow when allowing a broad JAX/Flax surface: host-callback / IO / FFI / | ||
| serialization paths that can run host code or touch disk — e.g. `jax.experimental.*`, `jax.debug.*`, | ||
| `jax.pure_callback`, `*.io_callback`, `*.host_callback*`, `jax.dlpack.*`, `jax.ffi*`, the | ||
| array↔disk `jax.numpy.{save,savez,load,tofile,fromfile,memmap,savetxt,loadtxt,genfromtxt}` family, | ||
| and `flax.serialization.*`, `flax.training.checkpoints.*`, `orbax.*`. | ||
|
|
||
| --- | ||
|
|
||
| ## Calls, attributes, and names | ||
|
|
||
| | What | Example | Code | | ||
| | ---------------------------------------- | --------------------------------------- | ------------------ | | ||
| | Non-allow-listed library path | `np.dot(a, b)` (numpy not allow-listed) | `call-not-allowed` | | ||
| | Named method on an opaque value | `x.reshape(8, -1)`, `items.append(1)` | `method-on-value` | | ||
| | Attribute read on an opaque value | `x.shape`, `x.T`, `x.ndim` | `attr-on-value` | | ||
| | Dunder attribute read on any object | `obj.__class__`, `obj.__dict__` | `dunder-attr` | | ||
| | Bare dunder name reference | `c = __class__` | `dunder-name` | | ||
| | Non-`self` attribute write | `obj.send = data` | `attr-on-value` | | ||
| | Self chain deeper than one level | `self.sub.evil(...)`, `self.a.b` | `attr-on-value` | | ||
| | Non-allow-listed attribute off a library | `np.pi` (numpy not allow-listed) | `attr-not-allowed` | | ||
|
|
||
| Only single-level `self.<name>` / `cls.<name>` reads and writes are allowed (see the | ||
| [self-attribute safety table](verify.md#edge-cases) in verify.md). | ||
|
|
||
| --- | ||
|
|
||
| ## Classes, decorators, and definitions | ||
|
|
||
| | What | Example | Code | | ||
| | --------------------------------------------------------------- | ----------------------------------- | --------------- | | ||
| | Non-allow-listed decorator (incl. `@property`) | `@evil`, `@property` | `decorator` | | ||
| | Class keyword argument | `class M(object, metaclass=Meta)` | `class-keyword` | | ||
| | Non-allow-listed base class | `class M(SomeLib)` | `class-base` | | ||
| | Magic/hook method other than `setup`/`__call__`/`__post_init__` | `def __getattr__`, `def __reduce__` | `dunder-def` | | ||
|
|
||
| `@property` is rejected because it runs code on bare attribute access (`block.w`) — same hook | ||
| class as a dunder def. | ||
|
|
||
| --- | ||
|
|
||
| ## Reserved-name rebinding | ||
|
|
||
| Rebinding a name the resolver trusts would poison verification. Rejected wherever the name is | ||
| bound — assignment, `for`/comprehension target, or parameter. Code: `reserved-name`. | ||
|
|
||
| - 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. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| - `self` / `cls` — the exemption is trusted by identifier alone; rebinding it (or reusing it as an | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. again, I am not completely following this sentence |
||
| unrelated parameter) would grant an attacker's object the same trust. See | ||
| [verify.md#edge-cases](verify.md#edge-cases). | ||
|
|
||
| --- | ||
|
|
||
| ## Container / aliasing tricks | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| Storing a banned-builtin reference where it could be dispatched later is rejected at construction | ||
| time (we don't track which slot holds what). Code: `banned-construct`. | ||
|
|
||
| - Banned reference inside a `list`/`dict`/`set`/`tuple` literal — `con = [eval]`, `d = {"o": open}`. | ||
| - Storing a banned reference into a subscript slot — `d["k"] = open`. | ||
|
|
||
| The reference is also caught in every other position: `f = eval` (alias), `a = b = open` (chained), | ||
| `a, b = (1, open)` (unpack), `return open` then `leak()(...)`, `op=open` (default arg), | ||
| `open if c else eval` (IfExp branch), and via a homoglyph copy of a previously-stashed name. All | ||
| report `banned-call` at the reference. | ||
|
|
||
| --- | ||
|
|
||
| ## Operator bundles not enabled | ||
|
|
||
| If the policy's `allow_methods` doesn't enable a bundle, using its operators reports | ||
| `bundle-disabled`: `arithmetic` (`BinOp`/`UnaryOp`), `comparison` (`Compare`/`BoolOp`), `indexing` | ||
| (`Subscript`/`Slice`). | ||
There was a problem hiding this comment.
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