Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions packages/syft-restrict/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ constructs in a **private** region, typically the model logic that must be kept
- **Public** — every other line outside the marked ranges: imports, data loading, wrappers. It is
never checked or obfuscated. The recipient of the obfuscated file can read it directly.

Given that split, `syft-restrict` executes the following two steps:
Given that split, `syft-restrict` executes the following three steps:

- **The auditor** checks the allow-list of library calls and operators against a
catalog of known safe/dual-use/unsafe paths, and warns on any uncatalogued
paths.
Comment on lines +18 to +20
- **The verifier** statically walks the private region and analyzes it against a default-deny
policy. Every construct must be explicitly allowed, or verification fails
and reports each offending line.
- **The obfuscator** runs after a clean verify. It creates an obfuscated copy by rewriting the private
region — renaming identifiers, blanking constants, replacing lines with `■■■■■■■■` — so the logic can't be read
back out, while the public region is copied through untouched.
- **The obfuscator** runs after a clean verify. It creates an obfuscated copy by
rewriting the private region — renaming identifiers, blanking constants,
replacing lines with `■■■■■■■■` — so the logic can't be read back out, while
the public region is copied through untouched.

The **verifier** is inspired by
[**RestrictedPython**](https://github.com/zopefoundation/RestrictedPython), but
Expand Down Expand Up @@ -74,12 +78,26 @@ result = restrict.run(
# If the file has no `# syft-restrict: ...` markers: raises MarkerError.
```

The **auditor** warns on any `allow_functions` that are categorized as dual-use
or unsafe, whether they are used or not, but the verifier fails only on actual
violations in the private region. The auditor is **advisory only**, and does not
block verification. See [docs/audit.md](docs/audit.md) for instructions on how
to build the catalog for the specific libraries in use.

> [!IMPORTANT]
> List the **specific** paths your model calls, as above — this is the default-deny posture the
> tool is built for: everything not named is denied. Avoid broad globs like `jax.*`: they pull in
> JAX's own host-callback and disk-IO functions (`jax.numpy.save`, `jax.debug.callback`,
> `jax.experimental.io_callback`, …) that the private region could call to exfiltrate data. If you
> must use a broad glob, pair it with a `disallow_functions` floor — see
>
> List the **specific** paths your model calls, as above. This is the
> default-deny posture the tool is built for: everything not named is denied, so
> no `disallow_functions` is needed.
>
> **Avoid broad globs like `jax.*`/`flax.*`**: a glob silently pulls in the
> library's host-callback, disk-IO, and network surface (`jax.numpy.save`,
> `jax.debug.callback`, `jax.profiler.start_server`,
> `jax.distributed.initialize`, …) that can be abused.
>
> A glob can be paired with a `disallow_functions` floor, but that is a **leaky
> backstop** — it only blocks what you remembered to list — not a substitute for
> a tight allow. See
> [docs/blacklist.md](docs/blacklist.md#optional-disallow_functions).

The private region is designated **only** by `# syft-restrict: ...` comment markers in the
Expand Down Expand Up @@ -108,4 +126,5 @@ instead of an exception.

- [docs/verify.md](docs/verify.md) — how verification works and what private code may do (allow side).
- [docs/blacklist.md](docs/blacklist.md) — default-deny catalog and violation codes (deny side).
- [docs/audit.md](docs/audit.md) — advisory allow-list audit and the risk catalog.
- [docs/code-layout.md](docs/code-layout.md) — source modules and test layout.
103 changes: 103 additions & 0 deletions packages/syft-restrict/docs/audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Allow-list audit

`syft_restrict.auditor.audit_allow_functions` classifies each path in an `allow_functions` list
against a curated catalog. It is advisory — it shows what an allow-list grants before a run; the
verifier's default-deny is what controls calls. `run()` performs this audit and attaches the report
to `result.audit`; it never affects `result.ok`.

## Verdicts

- **`unsafe`** — reads or writes files, opens network connections, or runs host callbacks.
- **`safe`** — pure computation (math, reductions, indexing, activations, constants, RNG,
initializers, and autodiff/compile transforms that wrap pure computation).
- **`dual_use`** — not pure computation and not a clear file/network/callback op, but with one other
capability the verifier cannot analyze; each entry states it. Rare — the examples are host/device
transfers (`jax.device_get`/`device_put`) and `flax.linen.Module`'s `allow_base_class_attributes`
flag.
- **`review`** — anything uncatalogued. A warning for a human, never a silent pass; unknown paths are
never assumed safe.

Matching is strictest-first: (`unsafe` → `dual_use` → `safe`); an unmatched path is `review`.

## Catalog layout

No catalog ships with the package. A catalog is a directory passed as `catalog_dir`, laid out as:

```
<catalog_dir>/
_common/default/catalog.json # cross-library patterns, merged into every path
<library>/<version>/catalog.json # rules for <library> at a matching version
```

- `<version>` is a prefix matched on dot boundaries: `0.11` covers `0.11.x`, never `0.19`. There is
no per-library fallback — an unmatched version contributes no rules, so its paths fall to `review`.
- `_common/default` holds cross-library patterns (`*.io_callback`, `*.tofile`, …) and rules for
libraries with no version-keyable import root (e.g. `orbax`).

A worked example lives in `examples/catalog`. Each file is
`{"_about": …, "unsafe": {…}, "dual_use": {…}, "safe": {…}}` with single-line string values. Keep it
sorted and normalized with the linter (`PATH` is the file or directory to lint):

```bash
uv run syft-restrict-lint PATH --fix
```

## Generate a catalog for a library

syft-restrict does **not** maintain catalogs; `examples/catalog` (`jax/0.11`, `flax/0.12`) is a worked
example. Uncatalogued paths fall to `review`, so a partial catalog is safe as long as it covers the
actual calls.

Keep the catalog in its own directory and point the auditor at it with `run(..., catalog_dir=…)`.

Fill in `{LIBRARY}` and `{VERSION}` below and paste it into an AI coding agent with Python and shell
access where that version is installed. Review every `unsafe` and `dual_use` entry before trusting
it.

```text
You make a risk catalog for a static allow-list audit tool. It classifies each dotted import path a
user allow-lists. Produce the catalog JSON for the commonly-used public API of {LIBRARY} {VERSION}.

CONTEXT
- Advisory tool: it shows a reviewer what an allow-list grants before a run.
- Any path you do NOT list becomes "review" (a warning). A partial catalog is fine. Cover the
functions and classes that real models and tutorials use, and leave out the rest.
- {LIBRARY} {VERSION} is installed. Verify every path resolves in that version (import the module and
check the attribute); drop any that do not.

CATEGORIES
- "unsafe": reads or writes files, opens network connections, or runs host callbacks (arbitrary host
code). Be complete. Use fnmatch globs for whole risky namespaces (e.g. "{LIBRARY}.io.*").
- "safe": pure computation — returns a value with no external effect. Arithmetic, linear algebra,
reductions, comparisons, indexing, activations, constants, random numbers, initializers, and
compile/autodiff transforms that wrap pure computation.
- "dual_use": not pure computation and not a clear file/network/callback op, but with one other
capability a static checker cannot analyze (e.g. moving data across the host/device boundary, or a
flag that widens what the checker accepts). State that capability. This category is often empty.

RULES
- If the only notable thing about a function is that it computes on data, it is "safe" — even when
very flexible. "dual_use" needs a capability that is not computation.
- Never put an uncertain path in "safe"; leave it out (it becomes "review"). Do not use a wide glob
like "{LIBRARY}.*" in "safe".
- Notes: one line, plain description. For "unsafe"/"dual_use", state the specific reason (for
"dual_use", something other than "computes on data"). Never describe how to misuse anything.

OUTPUT
Produce one JSON object in this shape (keys in any order; the linter sorts them):

{
"_about": "Risk rules for {LIBRARY} {VERSION}. 'unsafe' = disk/network/host-callback; 'safe' = pure computation; 'dual_use' = flagged for a specific capability beyond pure computation, reason stated per entry. This is advisory, not a proof.",
"unsafe": { "<dotted-path or fnmatch glob>": "why it is unsafe" },
"dual_use": { "<dotted-path>": "the specific non-computation capability it has" },
"safe": { "<dotted-path>": "what the op is" }
}

Omit an empty category. Every value is a single-line string. Use the primary public import path
(e.g. "{LIBRARY}.nn.relu"), not a deep private module path.

WRITE AND VALIDATE
- Write it to "catalog/{LIBRARY}/{VERSION}/catalog.json" (create the directories).
- Run "uv run syft-restrict-lint catalog --fix"; fix whatever it reports.
- Re-import every catalogued path in {LIBRARY} {VERSION}, drop any that fail, and lint again.
```
41 changes: 32 additions & 9 deletions packages/syft-restrict/docs/blacklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,22 +247,45 @@ reports **`operator-disabled`**.
## Optional `disallow_functions`

There is no built-in library denylist. Everything not explicitly allow-listed is
rejected. Safety comes from **`allow_functions`**.
rejected — safety comes from **`allow_functions`**, and the model is default-deny.

When you use a broad allow (`jax.*`), pass `disallow_functions=[...]` for a hard floor. Hits report
**`call-not-allowed`**.
**Prefer a tight `allow_functions`: list the exact leaves your code calls** (e.g.
`"jax.numpy.einsum"`, `"flax.linen.Module"`), not a glob. Under a tight allow-list `disallow_functions`
is unnecessary — a function that isn't named is already denied, so there is nothing to subtract.

Useful patterns under broad JAX/Flax allows:
**Avoid glob allows like `jax.*` / `flax.*`.** A glob silently allows in the library's entire surface,
including host-callback, disk-serialization, and network functions the private region could use to
exfiltrate data. If you nonetheless use a glob, add `disallow_functions=[...]` as a hard floor (hits
report **`call-not-allowed`**) — but treat this as a leaky backstop, not a substitute for a tight
allow: the floor only blocks what you remembered to list.

- Host / debug / experimental: `jax.experimental.*`, `jax.debug.*`, `jax.pure_callback`, `*.io_callback`, `*.host_callback*`
- FFI / interop: `jax.dlpack.*`, `jax.ffi*`
- Array ↔ disk: `jax.numpy.save`, `load`, `tofile`, `fromfile`, `memmap`, …
- Checkpointing: `flax.serialization.*`, `flax.training.checkpoints.*`, `orbax.*`
Recommended floor when a broad glob is unavoidable (dangerous JAX/Flax I/O surface):

```python
disallow_functions=[
# host / debug / experimental callbacks (arbitrary host code, stdout)
"jax.debug.*", "jax.experimental.*", "jax.pure_callback",
"*.io_callback", "*.host_callback*",
# profiler — writes traces to disk AND accepts gs://... paths / opens a network server
"jax.profiler.*",
# multi-host / distributed — outbound network connections
"jax.distributed.*", "jax.monitoring.*", "jax.experimental.multihost_utils.*",
# FFI / interop
"jax.dlpack.*", "jax.ffi.*",
# array <-> disk (save/savez/savez_compressed/savetxt, load/loadtxt, tofile/fromfile/memmap)
"jax.numpy.save*", "jax.numpy.load*", "*.tofile", "*.fromfile", "*.memmap",
# checkpointing / serialization (write to disk, incl. gs://)
"flax.serialization.*", "flax.training.checkpoints.*", "orbax.*",
]
```

Even this list is not exhaustive — that is the point of preferring a tight allow. A disallow glob is
resolved through the import table like any path, so a renamed re-export is still caught:

```python
# public import, private use — still resolved and checked
from jax.numpy import save as persist
persist(x, "out.npz") # call-not-allowed if disallow includes jax.numpy.save
persist(x, "out.npz") # call-not-allowed if disallow includes jax.numpy.save*
```

---
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"_about": "Library-agnostic risk rules, merged into every path's audit regardless of library or version. Patterns are dotted-path globs (fnmatch); 'unsafe' = disk/network/host-callback. Also holds blanket rules for libraries whose import root cannot be version-keyed (e.g. orbax: the 'orbax' import root is a namespace package with no __version__; the distribution is orbax-checkpoint). This is advisory, not a proof.",
"unsafe": {
"*.fromfile": "Reads raw bytes from a file on disk.",
"*.io_callback": "Runs a host Python callback with side effects (I/O).",
"*.memmap": "Memory-maps a file on disk.",
"*.tofile": "Writes raw array bytes to a file on disk.",
"orbax.*": "Orbax checkpointing — writes to disk and cloud storage (gs://, s3://). Blanket rule: orbax has no safe compute surface and its import root is not version-keyable."
}
}
Loading
Loading