syft-restrict 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- syft_restrict-0.1.0/.gitignore +200 -0
- syft_restrict-0.1.0/PKG-INFO +109 -0
- syft_restrict-0.1.0/README.md +99 -0
- syft_restrict-0.1.0/docs/blacklist.md +257 -0
- syft_restrict-0.1.0/docs/code-layout.md +23 -0
- syft_restrict-0.1.0/docs/verify.md +363 -0
- syft_restrict-0.1.0/examples/gemma_inference.certificate.json +86 -0
- syft_restrict-0.1.0/examples/gemma_inference.obfuscated.py +414 -0
- syft_restrict-0.1.0/examples/gemma_inference.py +414 -0
- syft_restrict-0.1.0/examples/gemma_inference_marked.certificate.json +86 -0
- syft_restrict-0.1.0/examples/gemma_inference_marked.obfuscated.py +474 -0
- syft_restrict-0.1.0/examples/gemma_inference_marked.py +510 -0
- syft_restrict-0.1.0/examples/generate.py +103 -0
- syft_restrict-0.1.0/examples/generate_marked.py +58 -0
- syft_restrict-0.1.0/pyproject.toml +18 -0
- syft_restrict-0.1.0/src/syft_restrict/__init__.py +32 -0
- syft_restrict-0.1.0/src/syft_restrict/astutil.py +153 -0
- syft_restrict-0.1.0/src/syft_restrict/errors.py +28 -0
- syft_restrict-0.1.0/src/syft_restrict/markers.py +174 -0
- syft_restrict-0.1.0/src/syft_restrict/obfuscator.py +260 -0
- syft_restrict-0.1.0/src/syft_restrict/policy.py +182 -0
- syft_restrict-0.1.0/src/syft_restrict/runner.py +106 -0
- syft_restrict-0.1.0/src/syft_restrict/verifier.py +1023 -0
- syft_restrict-0.1.0/tests/fixtures/compliant_model.py +42 -0
- syft_restrict-0.1.0/tests/fixtures/marked_model.py +42 -0
- syft_restrict-0.1.0/tests/obfuscate/__init__.py +0 -0
- syft_restrict-0.1.0/tests/obfuscate/test_obfuscate.py +110 -0
- syft_restrict-0.1.0/tests/test_markers.py +239 -0
- syft_restrict-0.1.0/tests/test_run.py +124 -0
- syft_restrict-0.1.0/tests/verify/__init__.py +0 -0
- syft_restrict-0.1.0/tests/verify/conftest.py +35 -0
- syft_restrict-0.1.0/tests/verify/helpers.py +29 -0
- syft_restrict-0.1.0/tests/verify/test_bypasses.py +617 -0
- syft_restrict-0.1.0/tests/verify/test_disallowed.py +328 -0
- syft_restrict-0.1.0/tests/verify/test_ranges.py +20 -0
- syft_restrict-0.1.0/tests/verify/test_whitelist.py +321 -0
- syft_restrict-0.1.0/tests/verify/test_whitelisted_lib.py +146 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
credentials/
|
|
7
|
+
|
|
8
|
+
# C extensions
|
|
9
|
+
*.so
|
|
10
|
+
|
|
11
|
+
# Distribution / packaging
|
|
12
|
+
.Python
|
|
13
|
+
build/
|
|
14
|
+
develop-eggs/
|
|
15
|
+
dist/
|
|
16
|
+
downloads/
|
|
17
|
+
eggs/
|
|
18
|
+
.eggs/
|
|
19
|
+
lib/
|
|
20
|
+
lib64/
|
|
21
|
+
parts/
|
|
22
|
+
sdist/
|
|
23
|
+
var/
|
|
24
|
+
wheels/
|
|
25
|
+
share/python-wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
MANIFEST
|
|
30
|
+
|
|
31
|
+
# PyInstaller
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
.python-version
|
|
87
|
+
|
|
88
|
+
# python stuff
|
|
89
|
+
*__pycache__*
|
|
90
|
+
*.pyc
|
|
91
|
+
*.egg-info/
|
|
92
|
+
*.swp
|
|
93
|
+
*.swo
|
|
94
|
+
*.ipynb_checkpoints*
|
|
95
|
+
**.pytest_cache/
|
|
96
|
+
**/pip-wheel-metadata
|
|
97
|
+
|
|
98
|
+
# pipenv
|
|
99
|
+
Pipfile.lock
|
|
100
|
+
|
|
101
|
+
# poetry
|
|
102
|
+
poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
.pdm.toml
|
|
106
|
+
|
|
107
|
+
# PEP 582
|
|
108
|
+
__pypackages__/
|
|
109
|
+
|
|
110
|
+
# Celery stuff
|
|
111
|
+
celerybeat-schedule
|
|
112
|
+
celerybeat.pid
|
|
113
|
+
|
|
114
|
+
# SageMath parsed files
|
|
115
|
+
*.sage.py
|
|
116
|
+
|
|
117
|
+
# Environments
|
|
118
|
+
.env
|
|
119
|
+
.venv
|
|
120
|
+
env/
|
|
121
|
+
venv/
|
|
122
|
+
ENV/
|
|
123
|
+
env.bak/
|
|
124
|
+
venv.bak/
|
|
125
|
+
|
|
126
|
+
# Spyder project settings
|
|
127
|
+
.spyderproject
|
|
128
|
+
.spyproject
|
|
129
|
+
|
|
130
|
+
# Rope project settings
|
|
131
|
+
.ropeproject
|
|
132
|
+
|
|
133
|
+
# mkdocs documentation
|
|
134
|
+
/site
|
|
135
|
+
|
|
136
|
+
# mypy
|
|
137
|
+
.mypy_cache/
|
|
138
|
+
.dmypy.json
|
|
139
|
+
dmypy.json
|
|
140
|
+
|
|
141
|
+
# Pyre type checker
|
|
142
|
+
.pyre/
|
|
143
|
+
|
|
144
|
+
# pytype static type analyzer
|
|
145
|
+
.pytype/
|
|
146
|
+
|
|
147
|
+
# Cython debug symbols
|
|
148
|
+
cython_debug/
|
|
149
|
+
|
|
150
|
+
# PyCharm
|
|
151
|
+
.idea/
|
|
152
|
+
|
|
153
|
+
# VS Code
|
|
154
|
+
.vscode/
|
|
155
|
+
|
|
156
|
+
# macOS
|
|
157
|
+
.DS_Store
|
|
158
|
+
|
|
159
|
+
# Google Drive credentials
|
|
160
|
+
credentials.json
|
|
161
|
+
client_secret*.json
|
|
162
|
+
token.pickle
|
|
163
|
+
token.json
|
|
164
|
+
*creds.json
|
|
165
|
+
|
|
166
|
+
# SyftBox related
|
|
167
|
+
~/SyftBox/
|
|
168
|
+
SyftBox/
|
|
169
|
+
|
|
170
|
+
# Temporary files
|
|
171
|
+
*.tmp
|
|
172
|
+
*.bak
|
|
173
|
+
*.swp
|
|
174
|
+
*~
|
|
175
|
+
|
|
176
|
+
# Claude AI settings
|
|
177
|
+
.claude/*
|
|
178
|
+
!.claude/settings.json
|
|
179
|
+
CLAUDE.md
|
|
180
|
+
|
|
181
|
+
# CI test outputs
|
|
182
|
+
test_outputs/
|
|
183
|
+
test_suite_output.log
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# Notebooks
|
|
187
|
+
notebooks/e2e/sales_mock.csv
|
|
188
|
+
notebooks/e2e/sales_private.csv
|
|
189
|
+
notebooks/e2e/readme.mdpackages/syft-bg/docs/sync-service.md
|
|
190
|
+
packages/syft-bg/docs/sync-review-notes.md
|
|
191
|
+
notebooks/ai_audit/internal/data/pdfs
|
|
192
|
+
notebooks/ai_audit/internal/data/manifest.csv
|
|
193
|
+
notebooks/ai_audit/internal/data/readme.md
|
|
194
|
+
|
|
195
|
+
.agents/
|
|
196
|
+
AGENTS.md
|
|
197
|
+
|
|
198
|
+
# Local JupyterLab state (jupyter MCP server runs from repo root)
|
|
199
|
+
.jupyter/
|
|
200
|
+
.jupyter_ystore.db
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: syft-restrict
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Verify + obfuscate: prove JAX/Flax inference code only does math (no data theft) while hiding the model architecture
|
|
5
|
+
Author-email: OpenMined <info@openmined.org>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: pydantic>=2
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# syft-restrict
|
|
12
|
+
|
|
13
|
+
A **static analyzer** for Python source files that **default-denies** dynamic
|
|
14
|
+
constructs in a **private** region, typically the model logic that must be kept private, while allowing the **public** region to be reviewed by the data owner in an **obfuscated** copy.
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
`syft-restrict` splits a Python file into two regions:
|
|
19
|
+
|
|
20
|
+
- **Private** — the lines the user marks as private, typically the model logic that
|
|
21
|
+
must not be readable. Mark them with `# syft-restrict: obfuscate-start` /
|
|
22
|
+
`# syft-restrict: obfuscate-end` or `# syft-restrict: hide-start` / `# syft-restrict: hide-end` comments in the source.
|
|
23
|
+
- **Public** — every other line outside the marked ranges: imports, data loading, wrappers. It is
|
|
24
|
+
never checked or obfuscated. The recipient of the obfuscated file can read it directly.
|
|
25
|
+
|
|
26
|
+
Given that split, `syft-restrict` executes the following two steps:
|
|
27
|
+
|
|
28
|
+
- **The verifier** statically walks the private region and analyzes it against a default-deny
|
|
29
|
+
policy. Every construct must be explicitly allowed, or verification fails
|
|
30
|
+
and reports each offending line.
|
|
31
|
+
- **The obfuscator** runs after a clean verify. It creates an obfuscated copy by rewriting the private
|
|
32
|
+
region — renaming identifiers, blanking constants, replacing lines with `■■■■■■■■` — so the logic can't be read
|
|
33
|
+
back out, while the public region is copied through untouched.
|
|
34
|
+
|
|
35
|
+
The **verifier** is inspired by
|
|
36
|
+
[**RestrictedPython**](https://github.com/zopefoundation/RestrictedPython), but
|
|
37
|
+
differs in a few ways:
|
|
38
|
+
|
|
39
|
+
- **It analyzes — it doesn't run.** It never executes the source. Only the static checks decide pass/fail.
|
|
40
|
+
- The public code may **import allowlisted libraries** and call into them.
|
|
41
|
+
Imports are not allowed in private code, and any library call must be
|
|
42
|
+
**explicitly allow-listed**.
|
|
43
|
+
- **Dynamic Python lives in the public part.** Anything dynamic the author needs
|
|
44
|
+
(file/tokenizer loading, the generation loop, wrappers around library methods)
|
|
45
|
+
must be written in the **public** region — where it can be reviewed directly — and
|
|
46
|
+
may be **called by** the private part.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
Mark the private region in the source with comments:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# syft-restrict: obfuscate-start
|
|
54
|
+
def attention(x):
|
|
55
|
+
...
|
|
56
|
+
# syft-restrict: obfuscate-end
|
|
57
|
+
|
|
58
|
+
MODEL_ID = "gemma-2b" # syft-restrict: hide
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
_See [docs/verify.md](docs/verify.md#public-vs-private) for the full marker syntax (single-line markers, nesting rules, and error cases)._
|
|
62
|
+
|
|
63
|
+
Then run:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import syft_restrict as restrict
|
|
67
|
+
|
|
68
|
+
result = restrict.run(
|
|
69
|
+
"gemma_inference.py",
|
|
70
|
+
allow_functions=["jax.*", "flax.linen.*"], # functions callable BY NAME (path-resolved)
|
|
71
|
+
allow_operators=["arithmetic", "indexing", "comparison"], # operators allowed ON A VALUE
|
|
72
|
+
)
|
|
73
|
+
# On success: writes gemma_inference.obfuscated.py and returns result.certificate.
|
|
74
|
+
# On a policy violation: raises PolicyViolation naming each offending line.
|
|
75
|
+
# If the file has no markers and obfuscate=/hide= are both omitted: raises MarkerError.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`obfuscate=`/`hide=` still work as an explicit escape hatch — pass either one (even an
|
|
79
|
+
empty list) and marker scanning is skipped entirely in favor of your own 1-based line
|
|
80
|
+
ranges:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
result = restrict.run(
|
|
84
|
+
"gemma_inference.py",
|
|
85
|
+
obfuscate=[[22, 93], [99, 280]], # 1-based ranges: identifiers renamed, constants blanked
|
|
86
|
+
hide=[], # 1-based ranges: whole line replaced with ■■■■■■■■
|
|
87
|
+
allow_functions=["jax.*", "flax.linen.*"],
|
|
88
|
+
allow_operators=["arithmetic", "indexing", "comparison"],
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This is how **[examples/gemma_inference.py](examples/gemma_inference.py)** generates
|
|
93
|
+
**[examples/gemma_inference.obfuscated.py](examples/gemma_inference.obfuscated.py)** —
|
|
94
|
+
see [examples/generate.py](examples/generate.py).
|
|
95
|
+
|
|
96
|
+
The same model marked up with comment blocks instead of numeric ranges lives in
|
|
97
|
+
**[examples/gemma_inference_marked.py](examples/gemma_inference_marked.py)** /
|
|
98
|
+
[examples/generate_marked.py](examples/generate_marked.py).
|
|
99
|
+
|
|
100
|
+
If syft-restrict was successfully executed on the true original file, the obfuscated file can be safely shared without exposing the private section.
|
|
101
|
+
|
|
102
|
+
Pass `strict=False` to `run` to get a `RunResult` with `.ok` / `.violations`
|
|
103
|
+
instead of an exception.
|
|
104
|
+
|
|
105
|
+
## Documentation
|
|
106
|
+
|
|
107
|
+
- [docs/verify.md](docs/verify.md) — how verification works and what private code may do (allow side).
|
|
108
|
+
- [docs/blacklist.md](docs/blacklist.md) — default-deny catalog and violation codes (deny side).
|
|
109
|
+
- [docs/code-layout.md](docs/code-layout.md) — source modules and test layout.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# syft-restrict
|
|
2
|
+
|
|
3
|
+
A **static analyzer** for Python source files that **default-denies** dynamic
|
|
4
|
+
constructs in a **private** region, typically the model logic that must be kept private, while allowing the **public** region to be reviewed by the data owner in an **obfuscated** copy.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`syft-restrict` splits a Python file into two regions:
|
|
9
|
+
|
|
10
|
+
- **Private** — the lines the user marks as private, typically the model logic that
|
|
11
|
+
must not be readable. Mark them with `# syft-restrict: obfuscate-start` /
|
|
12
|
+
`# syft-restrict: obfuscate-end` or `# syft-restrict: hide-start` / `# syft-restrict: hide-end` comments in the source.
|
|
13
|
+
- **Public** — every other line outside the marked ranges: imports, data loading, wrappers. It is
|
|
14
|
+
never checked or obfuscated. The recipient of the obfuscated file can read it directly.
|
|
15
|
+
|
|
16
|
+
Given that split, `syft-restrict` executes the following two steps:
|
|
17
|
+
|
|
18
|
+
- **The verifier** statically walks the private region and analyzes it against a default-deny
|
|
19
|
+
policy. Every construct must be explicitly allowed, or verification fails
|
|
20
|
+
and reports each offending line.
|
|
21
|
+
- **The obfuscator** runs after a clean verify. It creates an obfuscated copy by rewriting the private
|
|
22
|
+
region — renaming identifiers, blanking constants, replacing lines with `■■■■■■■■` — so the logic can't be read
|
|
23
|
+
back out, while the public region is copied through untouched.
|
|
24
|
+
|
|
25
|
+
The **verifier** is inspired by
|
|
26
|
+
[**RestrictedPython**](https://github.com/zopefoundation/RestrictedPython), but
|
|
27
|
+
differs in a few ways:
|
|
28
|
+
|
|
29
|
+
- **It analyzes — it doesn't run.** It never executes the source. Only the static checks decide pass/fail.
|
|
30
|
+
- The public code may **import allowlisted libraries** and call into them.
|
|
31
|
+
Imports are not allowed in private code, and any library call must be
|
|
32
|
+
**explicitly allow-listed**.
|
|
33
|
+
- **Dynamic Python lives in the public part.** Anything dynamic the author needs
|
|
34
|
+
(file/tokenizer loading, the generation loop, wrappers around library methods)
|
|
35
|
+
must be written in the **public** region — where it can be reviewed directly — and
|
|
36
|
+
may be **called by** the private part.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Mark the private region in the source with comments:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# syft-restrict: obfuscate-start
|
|
44
|
+
def attention(x):
|
|
45
|
+
...
|
|
46
|
+
# syft-restrict: obfuscate-end
|
|
47
|
+
|
|
48
|
+
MODEL_ID = "gemma-2b" # syft-restrict: hide
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
_See [docs/verify.md](docs/verify.md#public-vs-private) for the full marker syntax (single-line markers, nesting rules, and error cases)._
|
|
52
|
+
|
|
53
|
+
Then run:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import syft_restrict as restrict
|
|
57
|
+
|
|
58
|
+
result = restrict.run(
|
|
59
|
+
"gemma_inference.py",
|
|
60
|
+
allow_functions=["jax.*", "flax.linen.*"], # functions callable BY NAME (path-resolved)
|
|
61
|
+
allow_operators=["arithmetic", "indexing", "comparison"], # operators allowed ON A VALUE
|
|
62
|
+
)
|
|
63
|
+
# On success: writes gemma_inference.obfuscated.py and returns result.certificate.
|
|
64
|
+
# On a policy violation: raises PolicyViolation naming each offending line.
|
|
65
|
+
# If the file has no markers and obfuscate=/hide= are both omitted: raises MarkerError.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`obfuscate=`/`hide=` still work as an explicit escape hatch — pass either one (even an
|
|
69
|
+
empty list) and marker scanning is skipped entirely in favor of your own 1-based line
|
|
70
|
+
ranges:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
result = restrict.run(
|
|
74
|
+
"gemma_inference.py",
|
|
75
|
+
obfuscate=[[22, 93], [99, 280]], # 1-based ranges: identifiers renamed, constants blanked
|
|
76
|
+
hide=[], # 1-based ranges: whole line replaced with ■■■■■■■■
|
|
77
|
+
allow_functions=["jax.*", "flax.linen.*"],
|
|
78
|
+
allow_operators=["arithmetic", "indexing", "comparison"],
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This is how **[examples/gemma_inference.py](examples/gemma_inference.py)** generates
|
|
83
|
+
**[examples/gemma_inference.obfuscated.py](examples/gemma_inference.obfuscated.py)** —
|
|
84
|
+
see [examples/generate.py](examples/generate.py).
|
|
85
|
+
|
|
86
|
+
The same model marked up with comment blocks instead of numeric ranges lives in
|
|
87
|
+
**[examples/gemma_inference_marked.py](examples/gemma_inference_marked.py)** /
|
|
88
|
+
[examples/generate_marked.py](examples/generate_marked.py).
|
|
89
|
+
|
|
90
|
+
If syft-restrict was successfully executed on the true original file, the obfuscated file can be safely shared without exposing the private section.
|
|
91
|
+
|
|
92
|
+
Pass `strict=False` to `run` to get a `RunResult` with `.ok` / `.violations`
|
|
93
|
+
instead of an exception.
|
|
94
|
+
|
|
95
|
+
## Documentation
|
|
96
|
+
|
|
97
|
+
- [docs/verify.md](docs/verify.md) — how verification works and what private code may do (allow side).
|
|
98
|
+
- [docs/blacklist.md](docs/blacklist.md) — default-deny catalog and violation codes (deny side).
|
|
99
|
+
- [docs/code-layout.md](docs/code-layout.md) — source modules and test layout.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# What is not allowed
|
|
2
|
+
|
|
3
|
+
This document lists everything the checker rejects in the **private** region.
|
|
4
|
+
|
|
5
|
+
- The model is **default-deny**. Tables below name common rejections with clear
|
|
6
|
+
codes.
|
|
7
|
+
- Anything not listed as allowed in [verify.md](verify.md) is also
|
|
8
|
+
rejected.
|
|
9
|
+
- Each entry includes the **violation code** returned by `verify()`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Index of violation codes
|
|
14
|
+
|
|
15
|
+
| Code | Meaning |
|
|
16
|
+
| ------------------- | ------------------------------------------------------------------------ |
|
|
17
|
+
| `node-type` | Syntax not on the allow list (walrus, `match`, …) |
|
|
18
|
+
| `banned-name` | Reference or call to a banned builtin (`open`, `eval`, …) |
|
|
19
|
+
| `banned-construct` | Forbidden statement/expression form (import, `with`, f-string, …) |
|
|
20
|
+
| `call-not-allowed` | Library path not allow-listed (or hit by `disallow_functions`) |
|
|
21
|
+
| `call-unresolved` | Call target not provably safe |
|
|
22
|
+
| `method-on-value` | Named method on an unknown value (`x.reshape`) |
|
|
23
|
+
| `attr-on-value` | Attribute read/write on an unknown value, or bad `self` chain |
|
|
24
|
+
| `attr-not-allowed` | Library attribute path not allow-listed |
|
|
25
|
+
| `dunder-attr` | Dunder attribute (`.__class__`, …) |
|
|
26
|
+
| `dunder-name` | Bare dunder name (`__class__`) |
|
|
27
|
+
| `dunder-def` | Defining a forbidden magic method |
|
|
28
|
+
| `class-keyword` | e.g. `metaclass=` |
|
|
29
|
+
| `class-base` | Base class not allow-listed |
|
|
30
|
+
| `reserved-name` | Rebinding a trusted name (`jnp`, public wrapper, private def, `self`, …) |
|
|
31
|
+
| `operator-disabled` | Operator used without enabling its group in `allow_operators` |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Forbidden constructs
|
|
36
|
+
|
|
37
|
+
These constructs are banned outright in private code.
|
|
38
|
+
|
|
39
|
+
Code: **`banned-construct`**.
|
|
40
|
+
|
|
41
|
+
| Construct | Example | Why |
|
|
42
|
+
| --------------------- | ----------------------------------- | ------------------------------------------------------ |
|
|
43
|
+
| Import | `import os`, `from os import path` | Imports belong in public code |
|
|
44
|
+
| `with` | `with open(f) as g: ...` | Runs enter/exit hooks; often I/O |
|
|
45
|
+
| `try` / `raise` | `try: ... finally: ...` | Exception tricks / host surfaces |
|
|
46
|
+
| `global` / `nonlocal` | `global x` | Escape local naming rules |
|
|
47
|
+
| `del` | `del x` | Can remove names the policy relies on |
|
|
48
|
+
| `assert` | `assert cond` | Disappears under `python -O` |
|
|
49
|
+
| Async | `async def`, `await`, … | Out of scope for pure inference |
|
|
50
|
+
| Generators | `yield`, `yield from` | Suspended execution |
|
|
51
|
+
| F-strings | `f"hi"`, `f"{x}"` | Interpolation runs formatting with no normal call site |
|
|
52
|
+
| Decorators | `@property`, `@nn.compact`, `@evil` | Run code when the def/class is reached |
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# rejected
|
|
56
|
+
import os
|
|
57
|
+
y = f"value={x}"
|
|
58
|
+
with ctx() as g:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@nn.compact
|
|
62
|
+
def __call__(self, x): ...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Unknown / future syntax
|
|
66
|
+
|
|
67
|
+
Any syntax not on the allow list is rejected.
|
|
68
|
+
|
|
69
|
+
Code: **`node-type`**.
|
|
70
|
+
|
|
71
|
+
| Example | Notes |
|
|
72
|
+
| ---------------------- | ---------------- |
|
|
73
|
+
| `y = (z := 1)` | Walrus |
|
|
74
|
+
| `match x: case 1: ...` | Pattern matching |
|
|
75
|
+
|
|
76
|
+
New Python syntax stays blocked until reviewed and explicitly allowed.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Banned builtins
|
|
81
|
+
|
|
82
|
+
These names may never be **used** (i.e, loaded) in private code, even if they are
|
|
83
|
+
**not called**.
|
|
84
|
+
|
|
85
|
+
Code: **`banned-name`**.
|
|
86
|
+
|
|
87
|
+
### Dynamic code / reflection / I/O
|
|
88
|
+
|
|
89
|
+
- `eval`
|
|
90
|
+
- `exec`
|
|
91
|
+
- `compile`
|
|
92
|
+
- `__import__`
|
|
93
|
+
- `getattr`
|
|
94
|
+
- `setattr`
|
|
95
|
+
- `delattr`
|
|
96
|
+
- `hasattr`
|
|
97
|
+
- `vars`
|
|
98
|
+
- `globals`
|
|
99
|
+
- `locals`
|
|
100
|
+
- `dir`
|
|
101
|
+
- `open`
|
|
102
|
+
- `input`
|
|
103
|
+
- `breakpoint`
|
|
104
|
+
- `memoryview`
|
|
105
|
+
- `type`
|
|
106
|
+
- `__build_class__`
|
|
107
|
+
- `print`
|
|
108
|
+
|
|
109
|
+
### Formatting / buffer builtins
|
|
110
|
+
|
|
111
|
+
Same idea as calling dunders on a value (`x.__repr__()`), spelled as a bare call:
|
|
112
|
+
|
|
113
|
+
- `repr`
|
|
114
|
+
- `str`
|
|
115
|
+
- `ascii`
|
|
116
|
+
- `format`
|
|
117
|
+
- `bytes`
|
|
118
|
+
|
|
119
|
+
> [!WARNING] > `bytes(x)` can dump raw buffer contents. `print` is a stdout channel. F-strings are banned
|
|
120
|
+
> separately as constructs (above), including with no `{...}` at all.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# all rejected (banned-name)
|
|
124
|
+
f = open
|
|
125
|
+
d = {"o": open}
|
|
126
|
+
def run(op=open): ...
|
|
127
|
+
open("/etc/passwd") # call reports banned-name once
|
|
128
|
+
y = [v for v in open(path)] # passive positions still checked
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Calls and attributes
|
|
134
|
+
|
|
135
|
+
| What | Example | Code |
|
|
136
|
+
| -------------------------------------------- | ------------------------------------------ | ------------------ |
|
|
137
|
+
| Library path not allowed | `np.dot(a, b)` | `call-not-allowed` |
|
|
138
|
+
| Disallow list hits an otherwise-allowed path | `jnp.save(...)` under `disallow_functions` | `call-not-allowed` |
|
|
139
|
+
| Call target not proven safe | `fn(x)` where `fn` is a parameter | `call-unresolved` |
|
|
140
|
+
| Named method on a value | `x.reshape(8, -1)`, `items.append(1)` | `method-on-value` |
|
|
141
|
+
| Attribute on a value | `x.shape`, `x.T`, `obj.send = data` | `attr-on-value` |
|
|
142
|
+
| Deep `self` chain | `self.a.b`, `self.sub.evil(...)` | `attr-on-value` |
|
|
143
|
+
| Unsafe `self.<name>(...)` | stashed `open` on `self` in `setup` | `attr-on-value` |
|
|
144
|
+
| Dunder attribute | `obj.__class__`, `self.__dict__` | `dunder-attr` |
|
|
145
|
+
| Bare dunder name | `c = __class__` | `dunder-name` |
|
|
146
|
+
| Library attr not allowed | `np.pi` when numpy is not allowed | `attr-not-allowed` |
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# rejected
|
|
150
|
+
a = x.reshape(8, -1) # method-on-value
|
|
151
|
+
b = x.shape # attr-on-value
|
|
152
|
+
c = obj.__class__ # dunder-attr
|
|
153
|
+
|
|
154
|
+
def apply(fn, x):
|
|
155
|
+
return fn(x) # call-unresolved
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
> [!IMPORTANT] > **`call-unresolved` is default-deny for callees.** It catches dangerous callables that never
|
|
160
|
+
> mention a banned builtin by name (parameters, opaque locals, `d["k"](x)`).
|
|
161
|
+
|
|
162
|
+
Only single-level `self.<name>` / `cls.<name>` is special-cased. Rules for when `self.x(...)` is
|
|
163
|
+
safe are in [verify.md](verify.md#self-and-flax-style-modules).
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Classes and definitions
|
|
168
|
+
|
|
169
|
+
| What | Example | Code |
|
|
170
|
+
| ---------------------- | ------------------------------------- | --------------- |
|
|
171
|
+
| Class keywords | `class M(nn.Module, metaclass=Meta)` | `class-keyword` |
|
|
172
|
+
| Bad base class | `class M(SomeLib)`, `class M(object)` | `class-base` |
|
|
173
|
+
| Forbidden magic method | `def __getattr__`, `def __reduce__` | `dunder-def` |
|
|
174
|
+
|
|
175
|
+
Allowed hooks only: `setup`, `__call__`.
|
|
176
|
+
|
|
177
|
+
Decorators are banned outright as a [forbidden construct](#forbidden-constructs).
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
# rejected
|
|
181
|
+
class M(object): # class-base
|
|
182
|
+
def __getattr__(self, name): # dunder-def
|
|
183
|
+
return None
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Reserved names
|
|
189
|
+
|
|
190
|
+
Rebinding a name the checker trusts reports **`reserved-name`**.
|
|
191
|
+
|
|
192
|
+
| Reserved | Example rebind | Why |
|
|
193
|
+
| -------------------- | --------------------------------------------- | -------------------------------------- |
|
|
194
|
+
| Import aliases | `jnp = make_evil()` | Would lie about resolved library paths |
|
|
195
|
+
| Public wrappers | nested `def transpose` shadowing a public one | Defeats the reviewed wrapper |
|
|
196
|
+
| Private defs/classes | `helper = evil` after `def helper` | Bare calls trust the def by name |
|
|
197
|
+
| Safe builtins | `list = None` | Bare calls trust `list(...)` by name |
|
|
198
|
+
| `self` / `cls` | `self = x`, nested `def f(self):` | `self.*` is trusted by spelling |
|
|
199
|
+
|
|
200
|
+
Applies to assignment, `for` / comprehension targets, parameters, and nested defs.
|
|
201
|
+
Private def names are also protected against rebind in **public** glue between private ranges.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
# rejected
|
|
205
|
+
import jax.numpy as jnp
|
|
206
|
+
jnp = 1 # reserved-name
|
|
207
|
+
|
|
208
|
+
def helper(x):
|
|
209
|
+
return x
|
|
210
|
+
def f(evil):
|
|
211
|
+
helper = evil # reserved-name
|
|
212
|
+
return helper(1)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Operator bundles
|
|
218
|
+
|
|
219
|
+
If a group of operators is not in `allow_operators`, using any of its operators
|
|
220
|
+
reports **`operator-disabled`**.
|
|
221
|
+
|
|
222
|
+
| Bundle | If missing, these fail |
|
|
223
|
+
| ------------ | ---------------------- |
|
|
224
|
+
| `arithmetic` | `a + b`, `-a`, … |
|
|
225
|
+
| `comparison` | `a < b`, `a and b`, … |
|
|
226
|
+
| `indexing` | `x[0]`, `x[1:3]`, … |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Optional `disallow_functions`
|
|
231
|
+
|
|
232
|
+
There is no built-in library denylist. Everything not explicitly allow-listed is
|
|
233
|
+
rejected. Safety comes from **`allow_functions`**.
|
|
234
|
+
|
|
235
|
+
When you use a broad allow (`jax.*`), pass `disallow_functions=[...]` for a hard floor. Hits report
|
|
236
|
+
**`call-not-allowed`**.
|
|
237
|
+
|
|
238
|
+
Useful patterns under broad JAX/Flax allows:
|
|
239
|
+
|
|
240
|
+
- Host / debug / experimental: `jax.experimental.*`, `jax.debug.*`, `jax.pure_callback`, `*.io_callback`, `*.host_callback*`
|
|
241
|
+
- FFI / interop: `jax.dlpack.*`, `jax.ffi*`
|
|
242
|
+
- Array ↔ disk: `jax.numpy.save`, `load`, `tofile`, `fromfile`, `memmap`, …
|
|
243
|
+
- Checkpointing: `flax.serialization.*`, `flax.training.checkpoints.*`, `orbax.*`
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
# public import, private use — still resolved and checked
|
|
247
|
+
from jax.numpy import save as persist
|
|
248
|
+
persist(x, "out.npz") # call-not-allowed if disallow includes jax.numpy.save
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## See also
|
|
254
|
+
|
|
255
|
+
- [verify.md](verify.md) — how checking works and what is allowed
|
|
256
|
+
- `tests/verify/test_disallowed.py` — disallowed tests
|
|
257
|
+
- `tests/verify/test_bypasses.py` — multi-step attack regressions
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Code layout
|
|
2
|
+
|
|
3
|
+
Map of `src/syft_restrict/`:
|
|
4
|
+
|
|
5
|
+
| Module | Role |
|
|
6
|
+
| --------------- | --------------------------------------------------------------------- |
|
|
7
|
+
| `astutil.py` | Line ranges, import scan, small syntax helpers. No policy. |
|
|
8
|
+
| `policy.py` | Allow lists, safe/banned builtins, hook list, `Policy`. |
|
|
9
|
+
| `verifier.py` | Static checker: walks private lines default-deny, reports violations. |
|
|
10
|
+
| `obfuscator.py` | After a clean verify: rename private identifiers, blank constants. |
|
|
11
|
+
| `runner.py` | `run()` — verify → obfuscate → certificate. |
|
|
12
|
+
| `errors.py` | `RestrictError`, `PolicyViolation`. |
|
|
13
|
+
| `__init__.py` | Public exports. |
|
|
14
|
+
|
|
15
|
+
Tests under `tests/verify/`:
|
|
16
|
+
|
|
17
|
+
| File | Role |
|
|
18
|
+
| ------------------------- | --------------------------------------------------------------- |
|
|
19
|
+
| `test_whitelist.py` | Green path (aligns with [verify.md](verify.md)) |
|
|
20
|
+
| `test_whitelisted_lib.py` | Library paths + operator bundles |
|
|
21
|
+
| `test_disallowed.py` | Default-deny catalog (aligns with [blacklist.md](blacklist.md)) |
|
|
22
|
+
| `test_bypasses.py` | Multi-step escape regressions |
|
|
23
|
+
| `test_ranges.py` | Invalid private ranges |
|