-
Notifications
You must be signed in to change notification settings - Fork 101
Description
Problem
We grant broad read+write access to /tmp (and $TMPDIR) across all profiles via the system_write_linux and system_write_macos groups. This creates several attack vectors:
- Symlink attacks: After reboot, an attacker creates
/tmp/predictable-nameas a symlink pointing to a sensitive directory. The sandbox follows the symlink and writes to the attacker-controlled target. - Cross-user interference: On shared systems, other users can read/write files in
/tmpthat the sandboxed process creates. - Cross-session data leakage: Different nono sandbox sessions (potentially running different profiles with different trust levels) share the same
/tmpnamespace and can access each other's temp files.
Current state
system_write_linux group (policy.json line 294):
"write": ["/tmp", "/dev/null", "/dev/zero", "/dev/full", "/dev/tty", "/dev/pts", "$TMPDIR"]system_write_macos group (policy.json line 280):
"write": ["/private/tmp", "/tmp", "/private/var/folders", "/var/folders", "/dev", "$TMPDIR"]system_read_linux group also grants read to /tmp.
system_read_macos group also grants read to /tmp.
All built-in profiles include these groups, so every sandboxed process gets full /tmp access.
Proposed Solution
Create per-session ephemeral temp directories with ownership validation.
1. Per-session temp directory creation
Before sandbox application, create a unique temp directory:
/tmp/nono-{euid}-{profile_name}-{random}/
euid: effective user ID (prevents cross-user collision)profile_name: sanitized profile name (prevents cross-profile leakage)random: random suffix (prevents prediction)
2. TMPDIR override
Set TMPDIR environment variable in the child process to point to the per-session directory. This is straightforward — we already build the envp array before fork in all three execution strategies (execute_direct, execute_monitor, execute_supervised).
3. Restrict sandbox permissions
Replace the broad /tmp write grant with write access only to the per-session directory:
- Policy groups keep
/tmpread (many programs check/tmpexistence) - Write access restricted to
$TMPDIRonly (which now points to the per-session dir) - The
$TMPDIRexpansion inpolicy.rs:expand_path()already handles this — the variable just needs to be set correctly before expansion
4. Symlink ownership validation
Before trusting any TMPDIR path (inherited or created):
- If the path is a symlink,
lstat()to get symlink metadata - Get UID of both the symlink and its target
- Reject if symlink UID != target UID (prevents attacker-crafted symlinks)
- Reject if symlink UID != current euid
5. Cleanup
Options (not mutually exclusive):
- Clean up on session exit (parent removes the directory after child exits)
- Rely on system tmpwatch/systemd-tmpfiles for stale directories
- Parent cleanup is preferred since we already wait for child exit in monitor/supervised modes
Implementation Details
Where temp directory creation goes
In exec_strategy.rs, before the env building section. All three strategies (direct, monitor, supervised) build envp before fork — the temp dir must be created and the TMPDIR entry injected into the env array at that point.
Policy changes needed
Option A (minimal): Keep /tmp read in system_read groups, remove /tmp write from system_write groups, rely solely on $TMPDIR write grant (which points to per-session dir).
Option B (stricter): Also restrict /tmp read to the per-session dir only. This breaks programs that stat("/tmp") but is more secure.
Option A is recommended — it's compatible with existing programs while restricting writes.
macOS considerations
macOS uses /private/var/folders/xx/hash/T/ as the default TMPDIR. The per-session directory should be created under the resolved TMPDIR (not necessarily /tmp). The symlink validation is especially important on macOS where /tmp → /private/tmp.
Direct mode limitation
In direct mode, nono calls exec() and ceases to exist, so parent-driven cleanup isn't possible. Options:
- Accept that direct mode leaves temp dirs for system cleanup
- Register an
atexithandler in the child (but this is unreliable) - Document that monitor/supervised modes are recommended for full cleanup
Files to modify
| File | Change |
|---|---|
crates/nono-cli/src/exec_strategy.rs |
Create per-session tmpdir, inject into envp, cleanup on child exit |
crates/nono-cli/data/policy.json |
Remove /tmp from write groups, keep read + $TMPDIR write |
crates/nono-cli/src/config/mod.rs |
Update validated_tmpdir() to support per-session override |
Security properties
- Fail-closed: If temp dir creation fails, sandbox creation aborts (no fallback to shared
/tmp) - No prediction: Random suffix prevents pre-creation attacks
- No cross-user: UID in path + ownership validation prevents cross-user interference
- No cross-session: Each nono invocation gets its own directory
- Symlink-safe: Ownership validation rejects attacker-crafted symlinks