Skip to contents

The sandbox

With sandbox = TRUE, the child R process runs under restrictions set by the operating system. It can read what R needs to run and write to a temp directory. It can’t use the network or touch the rest of your files. Each platform does this differently, as described below.

What happens on each call

A sandboxed call goes through five steps:

01CREATESTART Ra child process02CONFIGURELOCK IT DOWNsandbox andresource limits03EXECUTERUN THE CODEand answer itstool calls04COLLECTRETURNthe result andthe output05CLEAN UPSTOP OR REUSEthe child processREUSE FOR THE NEXT CALL
Fig. 1 · The five steps of a sandboxed call

macOS (Seatbelt)

securer writes a Seatbelt profile and starts R with sandbox-exec. The profile denies everything by default, then allows a short list of things R needs. The child can read R itself, your R library paths, system libraries, and temp directories, but not your home directory, so files like ~/.ssh stay out of reach. It can write only to temp directories. TCP and UDP are blocked. Unix domain sockets are allowed, because that’s how the child talks to your session.

session <- SecureSession$new(sandbox = TRUE)

# Computation works normally:
session$execute("1 + 1")
#> [1] 2

# Network access is blocked:
session$execute('readLines(url("http://example.com"))')
#> Error: Operation not permitted

# Writing outside temp is blocked:
session$execute('writeLines("test", "~/file.txt")')
#> Error: Operation not permitted

session$close()

Linux (bubblewrap)

securer starts R with bwrap, which gives the child its own namespaces for processes, network, users, mounts, host name, and IPC. System libraries and R are mounted read-only, and /tmp is an empty, writable in-memory directory. Because the child has its own empty network namespace, it has no network.

You need bwrap installed (apt install bubblewrap on Debian and Ubuntu). Without it, securer warns you and runs the code without a sandbox.

Windows

Windows has no sandbox like the ones above. securer gives the child a clean environment: it keeps only a short list of safe variables, empties R_LIBS_USER, and points HOME, TMPDIR, TMP, and TEMP at a private temp directory. It can also set memory, CPU, and process limits with a Job Object. It can’t restrict files or the network, and it prints a message saying so the first time you start a session. If you need those restrictions on Windows, set SECURER_SANDBOX_MODE=docker-spawn to run each session in its own Docker container, or use a Linux VM.

Resource limits

Resource limits cap what the child process can use. They’re set with ulimit and work with or without the sandbox:

execute_r("1 + 1", limits = list(cpu = 10, memory = 256 * 1024 * 1024))

The limits you can set:

Name Unit ulimit flag Description
cpu seconds -t CPU time
memory bytes -v Virtual address space
fsize bytes -f Maximum file size
nproc count -u Maximum processes
nofile count -n Maximum open files
stack bytes -s Stack size

If you use the sandbox and don’t pass limits, securer applies these defaults. Pass limits = list() to turn them off:

Code pre-validation

validate_code() checks code for syntax errors and for calls that look risky, before the code goes to the child process:

# Valid code
validate_code("1 + 1")

# Syntax error
validate_code("if (TRUE {")

# Risky call: a warning, not a block
validate_code("system('ls')")

Checking the sandbox is available

Before you deploy, check that the sandbox tools exist on the machine. If they’re missing, securer runs code without a sandbox, or stops with an error if sandbox_strict = TRUE:

if (Sys.info()[["sysname"]] == "Linux") stopifnot(nzchar(Sys.which("bwrap")))
if (Sys.info()[["sysname"]] == "Darwin") stopifnot(file.exists("/usr/bin/sandbox-exec"))

Strict sandbox mode

By default, if the sandbox tools aren’t there, securer warns you and runs the code without a sandbox. In production you probably want it to stop instead. Set sandbox_strict = TRUE along with sandbox = TRUE and the session fails to start if it can’t set up the sandbox:

# Error if sandbox not available (recommended for production)
session <- SecureSession$new(sandbox = TRUE, sandbox_strict = TRUE)

With sandbox_strict = FALSE, the default, you get a warning and the session runs without the OS sandbox. Resource limits, the socket checks, and the clean environment still apply.

Strict mode doesn’t stop a session on Windows, because the clean environment there counts as the sandbox.

Architecture

OS SANDBOXHOST R SESSIONCHILD R PROCESSSESSION START · SecureSession$new()START R WITH CALLRR starts inside the sandboxCONNECT THE UNIX SOCKETINJECT THE RUNTIME.securer_call_tool() definedINJECT TOOL WRAPPERStool_name() definedEACH CALL · $execute(code)THE CODEeval(code){"tool": ...} AS JSONruns fn(arg)blocksRESULT AS JSONcarries onPROCESS COMPLETEreturns the final value
Fig. 2 · What happens between the two processes, from start-up to a finished call

securer starts the child through a wrapper script, passed to callr with callr::r_session_options(arch = ...). The socket lives under /tmp, because macOS limits socket paths to about 104 characters and R’s own temp directory can be longer than that. vignette("security-model") goes into the message format and what each part of the system trusts.

Why not just callr or Docker?

callr runs code in a separate R process, so a crash there doesn’t take down your session. But it doesn’t restrict anything. The child can read your SSH keys, make HTTP requests, and write anywhere on disk. securer uses callr to start the child and adds the restrictions.

Docker isolates the filesystem, network, and processes well, but it needs a running daemon and an image, and containers are slower to start. It also has no way for the code inside to call back into your R session. You’d have to build that yourself. securer’s docker-spawn backend does use Docker, with a new container per session, for when you need it, such as on Windows.

By default securer uses the sandbox your operating system already has (Seatbelt on macOS, bubblewrap on Linux). There’s no daemon, sessions start in under a second, and tool calls, limits, timeouts, and the audit log come with it.

Feature callr Docker securer
Process isolation Yes Yes Yes
Filesystem restrictions No Yes Yes
Network restrictions No Yes Yes
Tool-call IPC No No Yes
Resource limits No Yes Yes
Audit logging No No Yes
No daemon required Yes No Yes
Sub-second startup Yes No Yes