Wrap the Studio backend's python and terminal tool subprocesses in an
OS-level sandbox: bubblewrap on Linux, Seatbelt (sandbox-exec) on macOS. This
confines LLM-driven tool execution to the per-session workdir with a read-only
view of the OS and interpreter, a fresh /tmp, and no network, instead of
running with the full privileges of the Studio user.
This ports the sandbox core from PR #5468 onto current main and redoes the
integration against today's tool-execution path. The existing in-process guard
(code-safety AST scan, command blocklist, credential-free env, rlimits, /proc
environ hardening) is unchanged and becomes a defense-in-depth layer: the OS
sandbox is the primary boundary when available, and execution falls back to the
existing guarded path (with a warning) when it is not.
core/inference/sandbox.py (new):
- sandbox_available(): cached, thread-safe, three-way probe (ok / fail /
transient-timeout) that confirms the primitive can actually apply in this
process context, not just that the binary exists. A transient timeout is not
cached, so a one-off slow probe does not disable the sandbox for the whole
process lifetime.
- build_sandbox_argv(inner_argv, workdir): the Linux bwrap argv (--unshare-all,
fresh /proc /dev /tmp, read-only OS + interpreter binds, rw workdir bind,
narrowed /etc) or the macOS Seatbelt profile (deny default, narrow read allow,
deny network, deny Keychain / LaunchServices browser-escape). NPROC is
reapplied inside the Linux user namespace by a small inner wrapper.
core/inference/tools.py:
- _python_exec / _bash_exec wrap the interpreter / shell argv with
build_sandbox_argv when sandbox_available() and not Bypass Permissions.
- _sandbox_preexec is split into _sandbox_preexec_impl(apply_no_new_privs,
apply_nproc); the Linux bwrap path uses _sandbox_preexec_for_bwrap, which
skips PR_SET_NO_NEW_PRIVS (breaks the setuid bwrap helper) and the
per-real-UID RLIMIT_NPROC (can EAGAIN bwrap's fork on busy hosts). macOS keeps
the full pre-exec.
- _get_workdir canonicalizes the workdir with realpath so the child's cwd and
the bwrap bind always match on symlinked-$HOME hosts.
- UNSLOTH_STUDIO_SANDBOX_STRICT=1 (opt-in) makes execution fail closed when the
sandbox cannot be applied; the default stays fail-open so locked-down installs
keep working.
run.py warms the availability probe at startup. install.sh installs bubblewrap
best-effort on Linux (never fatal; missing bwrap only downgrades to the
in-process guard). CI installs bubblewrap, relaxes the Ubuntu 24.04 userns
AppArmor restriction, and gates the enforcement tests on a probe so they run
where the sandbox applies and skip (green) where it cannot.
Co-authored-by: Nilay <118994073+NilayYadav@users.noreply.github.com>