-
Notifications
You must be signed in to change notification settings - Fork 49
feat(build): add --build-isolation for sandboxed builds #1073
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: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #!/bin/bash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Run command with build isolation for untrusted build backends. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Uses an ephemeral Unix user for file-level isolation: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # - Cannot read credential files like .netrc (owned by root, mode 600) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # - Gets its own /tmp entries (sticky bit prevents cross-user access) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Combined with Linux namespaces for: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # - Network isolation (no routing in new net namespace) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # - PID isolation (build cannot see other processes) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # - IPC isolation (isolated shared memory, semaphores, message queues) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # - UTS isolation (separate hostname) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # The ephemeral user is created before entering the namespace, then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # unshare runs as that user with --map-root-user so it has enough | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # privilege to bring up loopback and set hostname inside the namespace. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # This works in unprivileged containers (Podman/Docker) without --privileged | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # or --cap-add SYS_ADMIN. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Ubuntu 24.04: needs `sysctl kernel.apparmor_restrict_unprivileged_userns=0` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set -e | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set -o pipefail | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [ "$#" -eq 0 ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Usage: $0 command [args...]" >&2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exit 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --- Ephemeral user creation (before namespace entry) --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BUILD_USER="fmr_$(head -c4 /dev/urandom | od -An -tu4 | tr -d ' ')" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useradd -r -M -d /nonexistent -s /sbin/nologin "$BUILD_USER" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trap 'userdel "$BUILD_USER" 2>/dev/null || true' EXIT | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Make build dir writable by ephemeral user if set | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [ -n "${FROMAGER_BUILD_DIR:-}" ] && [ -d "$FROMAGER_BUILD_DIR" ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| chmod -R o+rwX "$FROMAGER_BUILD_DIR" 2>/dev/null || true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+40
to
+42
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.
🛠 Suggested fix-if [ -n "${FROMAGER_BUILD_DIR:-}" ] && [ -d "$FROMAGER_BUILD_DIR" ]; then
- chmod -R o+rwX "$FROMAGER_BUILD_DIR" 2>/dev/null || true
-fi
+if [ -n "${FROMAGER_BUILD_DIR:-}" ] && [ -d "$FROMAGER_BUILD_DIR" ]; then
+ chown -R "$BUILD_USER":"$BUILD_USER" "$FROMAGER_BUILD_DIR" 2>/dev/null || true
+fi📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --- Enter namespaces as ephemeral user --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # setpriv drops to the ephemeral user, then unshare creates namespaces. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --map-root-user maps the ephemeral user to UID 0 inside the namespace | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # so it can run ip/hostname. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BUILD_UID=$(id -u "$BUILD_USER") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BUILD_GID=$(id -g "$BUILD_USER") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exec setpriv --reuid="$BUILD_UID" --regid="$BUILD_GID" --clear-groups -- \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unshare --uts --net --pid --ipc --fork --map-root-user -- \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+52
to
+53
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. 🧩 Analysis chain🌐 Web query:
💡 Result: To run unshare --mount --map-root-user as an unprivileged (non-root) user, the following requirements must be met: Kernel configuration: - CONFIG_USER_NS=y (user namespaces enabled at compile time). - CONFIG_NAMESPACES=y (general namespace support). Runtime kernel settings (check with cat /proc/sys/kernel/unprivileged_userns_clone; some distros like older Debian disable it): - kernel.unprivileged_userns_clone=1 (allows unprivileged creation of user namespaces). Set with sudo sysctl kernel.unprivileged_userns_clone=1. - Sufficient user.max_user_namespaces limit (default usually allows it; check /proc/sys/user/max_user_namespaces). User setup: - Entries in /etc/subuid and /etc/subgid for the user (e.g., username:100000:65536), as --map-root-user uses subordinate IDs to map the caller's UID/GID to root (0:0) inside the namespace. The newuidmap/newgidmap setuid helpers (provided by util-linux or shadow) use these. util-linux version: - Recent enough for --map-root-user (available since ~2.26; works in modern versions). Process state: - Not in a chroot (EPERM if caller's root dir differs from mount namespace root). - Caller must have write access to /proc/self/{uid_map,gid_map,setgroups} post-unshare (handled by unshare). How it works: 1. unshare creates a user namespace (CLONE_NEWUSER, unprivileged since Linux 3.8). 2. --map-root-user writes to uid_map/gid_map (e.g., 0 1) and setgroups=deny, gaining CAP_SYS_ADMIN in the new user namespace. 3. This CAP_SYS_ADMIN (in user ns owning the mount ns) allows unsharing/using mount namespace (CLONE_NEWNS) for mounts like bind mounts, proc, tmpfs (but not block devices). Examples from man pages work unprivileged: unshare --user --map-root-user sh -c whoami # outputs root unshare --map-root-user --mount mount --bind foo bar If it fails with "Operation not permitted": - Check/enable kernel.unprivileged_userns_clone. - Verify subuid/subgid (use id -u and check files). - Ensure kernel >=3.8, CONFIG_USER_NS=y (grep CONFIG_USER_NS /boot/config-$(uname -r)). - No seccomp/container restrictions (e.g., Docker blocks by default). As of 2026, most distros (Ubuntu 22+, Fedora, etc.) enable by default; older/secure setups may need tweaks. Citations:
🏁 Script executed: cat -n src/fromager/run_build_isolation.sh | head -60Repository: python-wheel-build/fromager Length of output: 2580 🏁 Script executed: # Search for Issue `#1019` references in the codebase
rg "1019" --type-list | head -20
rg "mount" src/fromager/run_build_isolation.shRepository: python-wheel-build/fromager Length of output: 460 🏁 Script executed: # Check the full context around lines 52-53 and look for any mount setup
sed -n '40,70p' src/fromager/run_build_isolation.shRepository: python-wheel-build/fromager Length of output: 895 🏁 Script executed: # Search for /tmp isolation or mount setup elsewhere
rg "/tmp" src/fromager/ -A 2 -B 2Repository: python-wheel-build/fromager Length of output: 880 🏁 Script executed: # Search for references to Issue 1019 or mount-related isolation setup
rg "1019\|mount" src/fromager/ -iRepository: python-wheel-build/fromager Length of output: 53 🏁 Script executed: # Check if there's any mount setup or planned isolation features in the codebase
rg "FROMAGER.*MOUNT\|ISOLATION.*MOUNT" -iRepository: python-wheel-build/fromager Length of output: 53 🏁 Script executed: # Look for any TODOs or FIXMEs related to mount isolation
rg "TODO\|FIXME" src/fromager/run_build_isolation.shRepository: python-wheel-build/fromager Length of output: 53 🏁 Script executed: # Check the __main__.py file more thoroughly for sandbox-isolation context
rg -A 10 -B 5 "mount.*PID.*IPC" src/fromager/__main__.pyRepository: python-wheel-build/fromager Length of output: 541 Add The help text (
Add The 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /bin/bash -c ' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # bring loopback up | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if command -v ip 2>&1 >/dev/null; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ip link set lo up | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # set hostname | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if command -v hostname 2>&1 >/dev/null; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hostname localhost | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+62
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. Redirection order is reversed.
🛠 Suggested fix- if command -v ip 2>&1 >/dev/null; then
+ if command -v ip >/dev/null 2>&1; then
ip link set lo up
fi
# set hostname
- if command -v hostname 2>&1 >/dev/null; then
+ if command -v hostname >/dev/null 2>&1; then
hostname localhost
fi📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exec "$@" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ' -- "$@" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+64
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. Ephemeral user leaks:
Run the child without 🛠 Suggested fix-exec setpriv --reuid="$BUILD_UID" --regid="$BUILD_GID" --clear-groups -- \
- unshare --uts --net --pid --ipc --fork --map-root-user -- \
- /bin/bash -c '
+setpriv --reuid="$BUILD_UID" --regid="$BUILD_GID" --clear-groups -- \
+ unshare --uts --net --pid --ipc --fork --map-root-user -- \
+ /bin/bash -c '
# bring loopback up
if command -v ip 2>&1 >/dev/null; then
ip link set lo up
fi
# set hostname
if command -v hostname 2>&1 >/dev/null; then
hostname localhost
fi
exec "$@"
' -- "$@"
+status=$?
+exit "$status"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
Probe runs on every CLI invocation — creates and deletes a real system user for
fromager --help.detect_build_isolation()executes at module import, which means every invocation of the CLI (including--help,--version, shell-completion calls, and subcommands that never use isolation) forksuseradd/userdelunder root. Combined with theEXIT-trap-vs-execleak flagged inrun_build_isolation.sh, this will steadily pollute/etc/passwdon any host that runs Fromager frequently.Consider detecting lazily only when
--build-isolationis requested, or make the probe a no-op (e.g., just check that the script exists andunshare --user --map-root-user -- trueworks) instead of actually creating a user:🛠 Lazy-detect sketch
…then call
_ensure_build_isolation_supported()inside theif build_isolation:branch at line 252.🤖 Prompt for AI Agents