Skip to content

Secure per-session TMPDIR isolation #258

@lukehinds

Description

@lukehinds

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:

  1. Symlink attacks: After reboot, an attacker creates /tmp/predictable-name as a symlink pointing to a sensitive directory. The sandbox follows the symlink and writes to the attacker-controlled target.
  2. Cross-user interference: On shared systems, other users can read/write files in /tmp that the sandboxed process creates.
  3. Cross-session data leakage: Different nono sandbox sessions (potentially running different profiles with different trust levels) share the same /tmp namespace 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 /tmp read (many programs check /tmp existence)
  • Write access restricted to $TMPDIR only (which now points to the per-session dir)
  • The $TMPDIR expansion in policy.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):

  1. If the path is a symlink, lstat() to get symlink metadata
  2. Get UID of both the symlink and its target
  3. Reject if symlink UID != target UID (prevents attacker-crafted symlinks)
  4. 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 atexit handler 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions