Problem
During build_sdist and build_wheel, Fromager executes arbitrary code from upstream packages (their setup.py, build backends, custom build scripts). This code currently runs with the same privileges as Fromager itself — full filesystem access, full environment variables, and
(when running in a builder container) access to credentials like .netrc.
A compromised or malicious build backend can:
- Read
.netrc credentials (GitLab tokens, PyPI upload credentials)
- Modify system directories (
/usr) or Fromager's own venv
- Access sensitive environment variables (
NETRC, TWINE_PASSWORD, NGC_API_KEY)
- Interfere with parallel builds via shared
/tmp, IPC, or process signaling
- Exfiltrate stolen data over the network (when network isolation is disabled)
Proposal
Extend the existing unshare-based network isolation (#477) to create mount, PID, and IPC namespaces for build steps. A new --build-isolation flag sandboxes PEP 517 hook calls with:
- Mount namespace: Makes
/usr read-only (bind + remount), hides credentials by overmounting $HOME with tmpfs, gives each build its own /tmp
- PID namespace: Build process cannot see or signal other processes
- IPC namespace: Isolated shared memory, semaphores, message queues
- Network namespace: Inherited from existing
--network-isolation
- Environment scrubbing: Strips
NETRC, TWINE_PASSWORD, NGC_API_KEY and similar sensitive variables before executing the build backend
The sandbox applies only to build_sdist and build_wheel (via the PEP 517 hook runner in dependencies.py). Source download, dependency installation, and upload steps are unaffected since they need network access and/or credentials.
Alternatives and Their Caveats
Why unshare and not bubblewrap: bwrap was evaluated in #473 and rejected because it requires --privileged or CAP_SYS_ADMIN inside containers. This is still the case (containers/bubblewrap#505 remains open). The unshare approach works in unprivileged Podman/Docker containers out of the box.
Why not ephemeral user accounts (useradd per package): Tested inside builder containers. Basic file permission blocking works (.netrc returns "Permission denied"), but /tmp, IPC, and process list are still shared across builds. Parallel useradd contends on /etc/passwd locks. Once you mitigate all the gaps, you end up reimplementing unshare.
Why not systemd DynamicUser=yes: Would be ideal — it provides exactly the isolation we need. But it requires systemd as PID 1 inside the container, which build containers don't have. Tested: systemd-run fails with "System has not been booted with systemd as init system." Design influenced by systemd's DynamicUser= (reference) — same kernel primitives, applied without the systemd dependency.
Problem
During
build_sdistandbuild_wheel, Fromager executes arbitrary code from upstream packages (theirsetup.py, build backends, custom build scripts). This code currently runs with the same privileges as Fromager itself — full filesystem access, full environment variables, and(when running in a builder container) access to credentials like
.netrc.A compromised or malicious build backend can:
.netrccredentials (GitLab tokens, PyPI upload credentials)/usr) or Fromager's own venvNETRC,TWINE_PASSWORD,NGC_API_KEY)/tmp, IPC, or process signalingProposal
Extend the existing
unshare-based network isolation (#477) to create mount, PID, and IPC namespaces for build steps. A new--build-isolationflag sandboxes PEP 517 hook calls with:/usrread-only (bind + remount), hides credentials by overmounting$HOMEwith tmpfs, gives each build its own/tmp--network-isolationNETRC,TWINE_PASSWORD,NGC_API_KEYand similar sensitive variables before executing the build backendThe sandbox applies only to
build_sdistandbuild_wheel(via the PEP 517 hook runner independencies.py). Source download, dependency installation, and upload steps are unaffected since they need network access and/or credentials.Alternatives and Their Caveats
Why
unshareand not bubblewrap: bwrap was evaluated in #473 and rejected because it requires--privilegedorCAP_SYS_ADMINinside containers. This is still the case (containers/bubblewrap#505 remains open). Theunshareapproach works in unprivileged Podman/Docker containers out of the box.Why not ephemeral user accounts (
useraddper package): Tested inside builder containers. Basic file permission blocking works (.netrcreturns "Permission denied"), but/tmp, IPC, and process list are still shared across builds. Paralleluseraddcontends on/etc/passwdlocks. Once you mitigate all the gaps, you end up reimplementingunshare.Why not systemd
DynamicUser=yes: Would be ideal — it provides exactly the isolation we need. But it requires systemd as PID 1 inside the container, which build containers don't have. Tested:systemd-runfails with "System has not been booted with systemd as init system." Design influenced by systemd'sDynamicUser=(reference) — same kernel primitives, applied without the systemd dependency.