Skip to content

Commit cf3fcdb

Browse files
tiranpavank63claude
committed
docs: add sandboxing API proposal
Proposal for a pluggable sandboxing API that lets users configure custom sandboxing solutions for build process confinement. Co-Authored-By: Pavan Kalyan Reddy Cherupally <pcherupa@redhat.com> Co-Authored-By: Claude <claude@anthropic.com> Signed-off-by: Christian Heimes <cheimes@redhat.com>
1 parent b10d2e4 commit cf3fcdb

3 files changed

Lines changed: 277 additions & 4 deletions

File tree

docs/proposals/sandboxing-api.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Pluggable sandboxing API for external commands
2+
3+
- Author: Christian Heimes, Pavan Kalyan
4+
- Created: 2026-05-26
5+
- Status: Open
6+
- Issue: [#1019](https://github.com/python-wheel-build/fromager/issues/1019)
7+
8+
## What
9+
10+
A plugin API for running external processes that lets users plug in different
11+
sandboxing solutions.
12+
13+
## Why
14+
15+
Sandboxing prevents build processes from modifying the host system, reading
16+
sensitive data, or interfering with other packages. On Linux, Fromager has
17+
simple network isolation with `unshare`. Users need the ability to plug in
18+
their own sandboxing configuration to confine build processes with tools like
19+
[bubblewrap](https://github.com/containers/bubblewrap),
20+
[firejail](https://github.com/netblue30/firejail),
21+
[Landlock](https://landlock.io/integrations/) /
22+
[Landlock API](https://docs.kernel.org/userspace-api/landlock.html), or
23+
container runtimes.
24+
25+
## Goals
26+
27+
- Platform-agnostic API that supports a wide range of sandboxing tools and
28+
that does not assume a specific OS, container runtime, or privilege level.
29+
- Life cycle hooks to set up and tear down persistent sandboxing environments
30+
31+
## Non-goals
32+
33+
- Implementing sandboxing beyond the current network isolation. Sandboxing is
34+
hard to get right and there are many existing tools to choose from. Fromager
35+
should delegate to those tools rather than re-implement confinement.
36+
37+
## How
38+
39+
### Hooks
40+
41+
Four hooks in global settings control sandboxing:
42+
43+
```yaml
44+
external_commands:
45+
setup_sandbox: fromager.external_commands:default_setup_sandbox
46+
teardown_sandbox: fromager.external_commands:default_teardown_sandbox
47+
run_sandboxed: fromager.external_commands:default_run_sandboxed
48+
run_unconfined: fromager.external_commands:default_run_unconfined
49+
```
50+
51+
All four hooks require `ctx`, `req`, and `sdist_root_dir` keyword arguments.
52+
53+
The `run_sandboxed` and `run_unconfined` hooks accept a subset of
54+
[`subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run)
55+
keyword arguments (`stdin`, `stdout`, `stderr`, `cwd`, `timeout`, `text`,
56+
`env`) and return a `subprocess.CompletedProcess` object.
57+
58+
The `run_unconfined` hook is included so users can monitor and police
59+
unconfined calls.
60+
61+
### Life cycle
62+
63+
`setup_sandbox` runs after the `prepare_source` hook and before sdists and
64+
wheels are built. `teardown_sandbox` runs after `build_wheel`. These hooks
65+
exist for sandboxing solutions that require persistent state across multiple
66+
commands, such as creating namespaces, mounting filesystems, or adjusting
67+
file permissions.
68+
69+
The sandbox is set up and torn down for each package+version
70+
combination. A failed `setup_sandbox` aborts the build.
71+
`teardown_sandbox` always runs (via `finally`) regardless of build
72+
success or failure.
73+
74+
### Writable directories and isolation
75+
76+
Only `sdist_root_dir.parent` (`work-dir/{name}-{version}`) should be
77+
writable. Sandboxing tools may create `tmp` and `home` subdirectories
78+
there for persistent temporary or home directories (e.g. XDG cache or
79+
config directories).
80+
81+
The `build_sdist` and `build_wheel` hooks should write output to
82+
`sdist_root_dir.parent / "dist"`. Fromager will create this directory
83+
before the build and move the resulting files into the correct location
84+
afterwards. When moving output files, fromager must verify they are
85+
regular files and not symlinks or other special files (e.g. device
86+
nodes, FIFOs) to prevent symlink escape attacks where a build creates
87+
a symlink pointing outside the sandbox boundary.
88+
89+
The `pyproject_hooks` library communicates with build backends via
90+
temporary files. These IPC files must be inside the writable
91+
`sdist_root_dir.parent` directory, not in `/tmp`, so that sandbox
92+
hooks can give each build a private `/tmp` without breaking the IPC
93+
channel.
94+
95+
Other shared state like writing to the Fromager installation, modifying
96+
the host OS, or using shared caches like ccache, sccache, or the Rust
97+
cache defeats the purpose of sandboxing and isolation between builds.
98+
99+
### API changes
100+
101+
- `BuildEnvironment` gains `req` and `sdist_root_dir` arguments
102+
- New methods: `BuildEnvironment.run_sandboxed`,
103+
`WorkContext.run_sandboxed`, `WorkContext.run_unconfined`
104+
- `external_commands.run()` becomes internal (should no longer be used by
105+
external code)
106+
- `BuildEnvironment.run` is deprecated and will eventually be removed
107+
108+
`BuildEnvironment.run_unconfined` is intentionally omitted until there is a
109+
valid use case.
110+
111+
### CLI migration
112+
113+
`--network-isolation` becomes an alias for `--enable-sandbox` /
114+
`--disable-sandbox`. The existing network isolation feature stays as the
115+
default sandboxing implementation.
116+
117+
## Sandboxing considerations
118+
119+
When implementing a sandbox hook, consider the following threat
120+
vectors (see [#1019] for the full analysis):
121+
122+
- **Credential access:** Build backends can read files like
123+
`$HOME/.netrc` or environment variables containing tokens
124+
(`GITHUB_TOKEN`, `GITLAB_PRIVATE_TOKEN`, `NGC_API_KEY`, etc.).
125+
Hide the user's home directory (e.g. mount a tmpfs over `$HOME`)
126+
and scrub sensitive variables from the build environment.
127+
- **Network access:** A build can exfiltrate stolen data or
128+
download payloads. Block network access entirely and configure
129+
only a loopback device.
130+
- **Process and IPC visibility:** Builds can enumerate processes
131+
via `/proc` or interfere through shared memory and semaphores.
132+
Isolate PID, IPC, and UTS namespaces.
133+
- **Persistence:** A malicious build can leave backdoors (`.pth`
134+
files, shell profile entries, background daemons) that affect
135+
every subsequent build. Make `/usr`, `/var`, the fromager
136+
installation, and settings directories read-only. Only
137+
`sdist_root_dir.parent` should be writable.
138+
- **Shared temporary directories:** Parallel builds sharing `/tmp`
139+
can interfere with each other. Give each sandbox a private `TMP`,
140+
`HOME`, and `XDG` directories.
141+
- **Untrusted source tree:** Sdist contents are untrusted. Tar
142+
unpacking should block device files, FIFOs, and the setuid bit.
143+
Mount the source tree (`sdist_root_dir`) with `nodev` and
144+
`nosuid` as an additional safeguard.
145+
- **Syscall filtering:** Even inside namespaces, a build process can
146+
attempt dangerous syscalls (`ptrace`, `mount`, `personality`,
147+
`keyctl`). Consider restricting syscalls with seccomp-bpf where
148+
the sandbox tool supports it (nsjail, firejail).
149+
- **Resource exhaustion:** A malicious build can fork-bomb, exhaust
150+
memory, or fill writable directories. Apply cgroups or rlimits
151+
to constrain CPU, memory, and process count. Tools like nsjail
152+
and systemd-run support this; bubblewrap and Landlock do not.
153+
154+
Available tools each cover a different subset of these vectors:
155+
156+
| Tool | Filesystem | Network | PID/IPC | Needs root | Notes |
157+
| -- | -- | -- | -- | -- | -- |
158+
| `unshare` | No | Yes | Yes | No | Current default; works in unprivileged Podman |
159+
| bubblewrap | Yes | Yes | Yes | In containers | Requires `CAP_SYS_ADMIN` inside containers |
160+
| firejail | Yes | Yes | Yes | No | Feature-rich; available on most distributions |
161+
| Landlock | Yes | Yes (6.7+) | Partial (6.11+) | No | Filesystem since 5.13, TCP since 6.7, signal/socket scoping since 6.11 |
162+
| systemd-run | Yes | Yes | Yes | No | Requires systemd as PID 1 |
163+
| nsjail | Yes | Yes | Yes | No | Namespaces + cgroups + seccomp |
164+
165+
No single tool covers all deployment models (root in a container,
166+
unprivileged user on a workstation, CI without root). The pluggable
167+
hook API lets each deployment choose the appropriate solution.
168+
169+
[#1019]: https://github.com/python-wheel-build/fromager/issues/1019
170+
171+
## Future work
172+
173+
Fromager may ship hook implementations for popular sandboxing tools such as
174+
bubblewrap, firejail, or Podman in the future.

docs/proposals/sandboxing_api.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import os
2+
import pathlib
3+
import subprocess
4+
from collections.abc import Mapping, Sequence
5+
from typing import IO, Any
6+
7+
from packaging.requirements import Requirement
8+
9+
from fromager import context
10+
11+
12+
def setup_sandbox(
13+
*,
14+
ctx: context.WorkContext,
15+
req: Requirement,
16+
sdist_root_dir: pathlib.Path,
17+
) -> None:
18+
"""Set up sandboxing
19+
20+
Executed after `prepare_source` hook. Can be used to set up sandbox or chown/chmod the sdist_root_dir.
21+
"""
22+
23+
24+
def teardown_sandbox(
25+
*,
26+
ctx: context.WorkContext,
27+
req: Requirement,
28+
sdist_root_dir: pathlib.Path,
29+
) -> None:
30+
"""Tear down sandboxing
31+
32+
Executed when cleaning up the build environment.
33+
"""
34+
35+
36+
def run_sandboxed(
37+
args: Sequence[str | bytes | os.PathLike[str] | os.PathLike[bytes]],
38+
*,
39+
# Fromager args
40+
ctx: context.WorkContext,
41+
req: Requirement,
42+
sdist_root_dir: pathlib.Path,
43+
# subprocess args
44+
stdin: int | IO[Any] | None = None,
45+
stdout: int | IO[Any] | None = None,
46+
stderr: int | IO[Any] | None = None,
47+
cwd: os.PathLike[str] | os.PathLike[bytes] | None = None,
48+
timeout: float | None = None,
49+
text: bool | None = None,
50+
env: Mapping[str, str] | None = None,
51+
) -> subprocess.CompletedProcess[bytes] | subprocess.CompletedProcess[str]:
52+
"""Run command in a sandbox"""
53+
raise NotImplementedError
54+
55+
56+
def run_unconfined(
57+
args: Sequence[str | bytes | os.PathLike[str] | os.PathLike[bytes]],
58+
*,
59+
# Fromager args
60+
ctx: context.WorkContext,
61+
req: Requirement,
62+
sdist_root_dir: pathlib.Path,
63+
# subprocess args
64+
stdin: int | IO[Any] | None = None,
65+
stdout: int | IO[Any] | None = None,
66+
stderr: int | IO[Any] | None = None,
67+
cwd: os.PathLike[str] | os.PathLike[bytes] | None = None,
68+
timeout: float | None = None,
69+
text: bool | None = None,
70+
env: Mapping[str, str] | None = None,
71+
) -> subprocess.CompletedProcess[bytes] | subprocess.CompletedProcess[str]:
72+
"""Run command unconfined without a sandbox"""
73+
raise NotImplementedError

docs/spelling_wordlist.txt

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@ args
22
backend
33
backends
44
backoff
5+
backdoors
6+
bpf
7+
bubblewrap
58
canonicalize
69
canonicalized
10+
ccache
11+
cgroups
712
changelog
813
changelogs
914
cheeseshop
1015
cn
1116
codebase
1217
config
1318
containerfile
14-
cpu
1519
cooldown
20+
cpu
1621
csv
1722
customizations
1823
cython
@@ -21,7 +26,10 @@ dir
2126
downloader
2227
env
2328
environ
29+
exfiltrate
2430
filesystem
31+
filesystems
32+
firejail
2533
formatters
2634
fromager
2735
frontend
@@ -34,8 +42,10 @@ iterable
3442
iteratively
3543
json
3644
keyring
45+
Landlock
3746
lexicographically
3847
libcurl
48+
loopback
3949
linter
4050
linters
4151
localhost
@@ -44,24 +54,33 @@ mdformat
4454
mergify
4555
mypy
4656
namespace
57+
namespaces
58+
nsjail
4759
numpy
4860
openssl
4961
platlib
62+
pluggable
5063
podman
5164
pre
5265
prebuilt
5366
purelib
5467
py
5568
pydantic
56-
PyPI
5769
pypi
70+
PyPI
5871
pyproject
5972
recurses
60-
reproducibility
6173
repo
74+
reproducibility
75+
rlimits
6276
rollout
77+
runtimes
78+
sandboxing
79+
sccache
6380
scm
6481
sdist
82+
seccomp
83+
setuid
6584
sdists
6685
setuptools
6786
statelessly
@@ -70,14 +89,20 @@ stdin
7089
stdout
7190
subcommand
7291
subcommands
92+
subdirectories
93+
subdirectory
7394
subgraph
7495
subgraphs
75-
subdirectory
7696
submodule
7797
submodules
7898
subprocesses
99+
symlink
100+
symlinks
101+
syscalls
102+
systemd
79103
tagname
80104
templating
105+
tmpfs
81106
toml
82107
toplevel
83108
tos
@@ -86,6 +111,7 @@ tracebacks
86111
txt
87112
unsatisfiable
88113
unshare
114+
untrusted
89115
url
90116
urls
91117
vendored

0 commit comments

Comments
 (0)