Skip to contents

Common problems

“bwrap: No such file or directory” or “sandbox-exec not found”

The sandbox tool for your platform is missing. On Linux, securer needs bubblewrap (bwrap). On macOS it uses sandbox-exec, which comes with the OS, so you shouldn’t see this there.

# Linux: install bubblewrap
# Debian/Ubuntu: sudo apt install bubblewrap
# Fedora:        sudo dnf install bubblewrap

# With sandbox_strict = TRUE, a missing sandbox is an error, not a warning
session <- SecureSession$new(sandbox = TRUE, sandbox_strict = TRUE)

If you’d rather run without the sandbox when the tool is missing, leave sandbox_strict = FALSE, the default. securer warns you and carries on.

“Timeout waiting for client connection”

The child R process didn’t connect to the socket within a few seconds. Usually that means the child failed to start, often because the sandbox wrapper couldn’t run.

The socket path isn’t the problem. On macOS and Linux, securer always puts the socket in a new directory under /tmp, whatever TMPDIR is set to, because macOS limits socket paths to about 104 characters.

# /tmp has to be writable
file.access("/tmp", 2) == 0

# If this works, the sandbox is what's failing
session <- SecureSession$new(sandbox = FALSE)

“Session is not running”

The child R process has stopped. The OS may have killed it for using too much memory, it may have hit a resource limit, or something else sent it a signal.

session <- SecureSession$new(sandbox = TRUE)

# Check whether the child process is still alive
session$is_alive()

# After a timeout, securer restarts the session by itself.
# After any other crash, start a new one:
session$close()
session <- SecureSession$new(sandbox = TRUE)

Windows sandbox limitations

On Windows, securer only cleans the child’s environment (HOME, TMPDIR, and R_LIBS_USER, among others) and sets resource limits with Job Objects. It doesn’t restrict files or the network.

To get those restrictions, set SECURER_SANDBOX_MODE=docker-spawn so each session runs in its own Docker container. Or run R inside WSL2 with bubblewrap installed:

# Inside WSL2 with bwrap installed, securer uses the Linux sandbox backend
session <- SecureSession$new(sandbox = TRUE, sandbox_strict = TRUE)

“Maximum executions (N) reached for this session”

The session has run code as many times as max_executions allows. The limit is for throwaway agent sessions, where you want a hard cap on how often code can run.

# This session allows only 5 executions
session <- SecureSession$new(max_executions = 5)

# After 5 calls to $execute(), the next one is an error.
# Start a new session:
session$close()
session <- SecureSession$new(max_executions = 10)

# Or leave out the limit:
session <- SecureSession$new()

“Execution blocked by pre_execute_hook”

Your pre_execute_hook returned FALSE for this code. To see why, call the hook yourself on the code that was rejected.

# Example hook that blocks system() calls
my_hook <- function(code) {
  if (grepl("system\\(", code)) return(FALSE)
  TRUE
}

# Call it directly to see what it rejects
my_hook("x <- 1 + 1")          # TRUE: allowed
my_hook("system('whoami')")     # FALSE: blocked

session <- SecureSession$new(pre_execute_hook = my_hook)

Only FALSE blocks the code. Anything else the hook returns, NULL included, lets it run.

“Package ‘foo’ not found” in child process

securer empties R_LIBS_USER in the child on purpose, so a package in a user library can’t run code when it loads. Inside the sandbox you only get packages from R_LIBS_SITE and R.home("library").

# Option 1: Install the package system-wide
# install.packages("foo", lib = .Library)

# Option 2: Register a tool that uses the package in your session
tools <- list(
  securer_tool(
    name = "run_foo",
    description = "Run foo::bar() on the host",
    fn = function(x) foo::bar(x),
    args = list(x = "character")
  )
)
session <- SecureSession$new(tools = tools, sandbox = TRUE)
session$execute('run_foo("input")')

Slow session startup

execute_r() starts and stops a session every time you call it. If you run code many times, keep one session open or use a pool.

# Slow: a new session for every call
for (i in 1:100) {
  execute_r(paste("sqrt(", i, ")"))
}

# Fast: one session, reused
session <- SecureSession$new()
for (i in 1:100) {
  session$execute(paste("sqrt(", i, ")"))
}
session$close()

# Fast, and handles several callers at once: a pool
pool <- SecureSessionPool$new(size = 4)
for (i in 1:100) {
  pool$execute(paste("sqrt(", i, ")"))
}
pool$close()

“IPC authentication failed”

Whatever connected to the socket first didn’t send the random token securer gave the child. Either the child failed before it could send it, or another process on the machine connected first.

securer creates each socket directory with 0700 permissions, so only your user can get in. On a shared machine, check that those directories really are private.

# Socket directories are /tmp/securer_*. Check their permissions with:
# ls -ld /tmp/securer_*

“Audit log not written”

Pass audit_log when you create the session. The directory it goes in has to exist and be writable. securer creates the file with 0600 permissions and refuses a path that’s a symlink.

# Make sure the directory exists
dir.create("logs", showWarnings = FALSE)

session <- SecureSession$new(
  audit_log = "logs/securer-audit.jsonl"
)

session$execute("1 + 1")
session$close()

# Check the log was written
readLines("logs/securer-audit.jsonl")