Skip to content

Drivers

Introduction

The Sandbox package ships with five drivers, each offering a different trade-off between convenience and isolation. All drivers implement the CanExecuteCommand interface, so your application code works identically regardless of which backend is in use.

Choosing a driver depends on your security requirements, platform, and operational constraints. You might use the host driver during development for simplicity and switch to Docker or Podman in production for full container isolation.

Driver Selection

Static Factory Methods

The Sandbox class provides dedicated static methods for each driver:

use Cognesy\Sandbox\Sandbox;
use Cognesy\Sandbox\Config\ExecutionPolicy;

$policy = ExecutionPolicy::in('/tmp');

$host       = Sandbox::host($policy);
$docker     = Sandbox::docker($policy, image: 'php:8.3-cli-alpine');
$podman     = Sandbox::podman($policy, image: 'alpine:3');
$firejail   = Sandbox::firejail($policy);
$bubblewrap = Sandbox::bubblewrap($policy);

Enum-Based Selection

When the driver is determined at runtime, use the SandboxDriver enum with the fluent builder:

use Cognesy\Sandbox\Enums\SandboxDriver;
use Cognesy\Sandbox\Sandbox;

$sandbox = Sandbox::fromPolicy($policy)->using(SandboxDriver::Docker);

You can also pass a plain string. The accepted values are host, docker, podman, firejail, and bubblewrap:

$sandbox = Sandbox::fromPolicy($policy)->using('firejail');

An InvalidArgumentException is thrown if the string does not match any known driver.

Host Driver

The host driver executes commands directly on the host machine using the Symfony Process component. It provides no file-system or network isolation -- the command runs with the same privileges as the PHP process, constrained only by the execution policy's timeout and output caps.

$sandbox = Sandbox::host($policy);
$result = $sandbox->execute(['php', '-r', 'echo phpversion();']);

When to use: Development, trusted scripts, CI pipelines where container overhead is unnecessary.

Key characteristics: - Commands run in the policy's baseDir directly (no temporary subdirectory is created). - Timeout enforcement uses Symfony Process's built-in timeout and idle timeout. - Environment variables are filtered through EnvUtils to strip security-sensitive patterns. - Memory limits and network settings are policy declarations only -- they are not enforced at the OS level.

Docker Driver

The Docker driver runs each command inside an ephemeral Docker container with aggressive security hardening applied by default.

$sandbox = Sandbox::docker($policy, image: 'python:3.12-alpine');
$result = $sandbox->execute(['python3', '-c', 'print("hello")']);

When to use: Production workloads, untrusted code execution, any scenario requiring strong isolation.

Container Hardening

Every Docker execution applies the following security measures automatically:

Setting Value Purpose
--read-only Enabled Root filesystem is read-only
--cap-drop=ALL All capabilities dropped No elevated privileges
--security-opt no-new-privileges Enabled Prevents privilege escalation
-u 65534:65534 nobody user Non-root execution
--pids-limit=20 20 processes Prevents fork bombs
--memory From policy (default 128M) Memory cap
--cpus 0.5 CPU throttle
--network=none When network disabled Network isolation
--tmpfs /tmp rw,noexec,nodev,nosuid,size=64m Writable temp with noexec

Working Directory

A unique temporary directory is created on the host inside the policy's baseDir for each execution. This directory is mounted into the container at /work as the writable working directory. It is automatically cleaned up after execution, even if the command fails.

File Mounts

Readable paths from the policy are mounted at /mnt/ro0, /mnt/ro1, etc. Writable paths are mounted at /mnt/rw0, /mnt/rw1, etc. Your command should reference these container paths:

$policy = ExecutionPolicy::in('/tmp')
    ->withReadablePaths('/data/input')
    ->withWritablePaths('/data/output');

$sandbox = Sandbox::docker($policy, image: 'alpine:3');

// Inside the container: /mnt/ro0 is /data/input, /mnt/rw0 is /data/output
$result = $sandbox->execute(['cp', '/mnt/ro0/file.txt', '/mnt/rw0/copy.txt']);

Custom Image

The default image is alpine:3. Pass any Docker image as the second argument:

$sandbox = Sandbox::docker($policy, image: 'node:20-alpine');

Binary Override

If Docker is not on the default PATH, specify the binary location:

$sandbox = Sandbox::docker($policy, dockerBin: '/usr/local/bin/docker');

Or set the DOCKER_BIN environment variable before your PHP process starts.

Podman Driver

The Podman driver works identically to the Docker driver but uses Podman as the container runtime. It is designed for rootless container execution on Linux.

$sandbox = Sandbox::podman($policy, image: 'alpine:3');

When to use: Linux environments where rootless containers are preferred over Docker.

WSL2 Compatibility

The Podman driver automatically detects WSL2 environments (by reading /proc/version and /proc/self/cgroup) and applies compatibility adjustments:

  • Switches to cgroupfs as the cgroup manager (via --cgroup-manager=cgroupfs).
  • Disables memory and CPU resource limits, which are unreliable under WSL2's cgroup configuration.

All other security hardening (read-only root, dropped capabilities, nobody user, etc.) remains active.

Binary Override

$sandbox = Sandbox::podman($policy, podmanBin: '/usr/bin/podman');

Or set the PODMAN_BIN environment variable.

Firejail Driver

The Firejail driver uses Linux namespaces and seccomp filtering to sandbox commands without requiring a container runtime. It offers lighter weight isolation than Docker or Podman.

$sandbox = Sandbox::firejail($policy);
$result = $sandbox->execute(['python3', 'script.py']);

When to use: Linux systems where you want sandbox isolation without the overhead of pulling container images.

Sandbox Configuration

Firejail applies the following restrictions:

Setting Value Purpose
--net=none When network disabled Network isolation
--rlimit-nproc=20 20 processes Fork bomb prevention
--rlimit-nofile=100 100 file descriptors File descriptor limit
--rlimit-fsize=10485760 10 MB Maximum file size
--rlimit-cpu Policy timeout + 1 second CPU time limit

The working directory is bind-mounted at /work with a whitelist applied. Readable and writable paths follow the same /mnt/ro* and /mnt/rw* convention, with readable paths additionally marked as --read-only.

Binary Override

$sandbox = Sandbox::firejail($policy, firejailBin: '/usr/bin/firejail');

Or set the FIREJAIL_BIN environment variable.

Bubblewrap Driver

The Bubblewrap (bwrap) driver provides minimal Linux namespace isolation. It is the lightest-weight option and is commonly used in Flatpak applications.

$sandbox = Sandbox::bubblewrap($policy);
$result = $sandbox->execute(['ls', '-la']);

When to use: Linux systems where you need basic namespace isolation with minimal dependencies.

Namespace Isolation

Bubblewrap applies the following namespace unsharing:

  • --unshare-pid -- Process ID namespace
  • --unshare-uts -- Hostname namespace
  • --unshare-ipc -- IPC namespace
  • --unshare-cgroup -- Cgroup namespace
  • --unshare-net -- Network namespace (when network is disabled)
  • --die-with-parent -- Sandbox terminates if the parent process exits

The host root filesystem is mounted read-only (--ro-bind / /) to make system binaries available. The working directory is bind-mounted to /tmp inside the sandbox. Writable and readable paths are mounted at their original host paths (not /mnt/rw* like container drivers).

Binary Override

$sandbox = Sandbox::bubblewrap($policy, bubblewrapBin: '/usr/bin/bwrap');

Or set the BWRAP_BIN environment variable.

Binary Discovery

All drivers that depend on an external binary follow the same discovery strategy:

  1. Check the corresponding environment variable (DOCKER_BIN, PODMAN_BIN, FIREJAIL_BIN, BWRAP_BIN).
  2. Search the system PATH.
  3. Search additional common directories: /usr/bin, /usr/local/bin, /opt/homebrew/bin, /opt/local/bin, /snap/bin.
  4. Fall back to the bare binary name (e.g., docker), which will fail at execution time if the binary truly is not available.

You can bypass discovery entirely by passing the binary path to the constructor.

Process Management

Container drivers (Docker, Podman, Firejail, Bubblewrap) use proc_open directly for process management, while the host driver uses the Symfony Process component. All drivers:

  • Use setsid (when available) to run commands in a new session group, ensuring clean termination of the entire process tree on timeout.
  • Send SIGTERM first, wait briefly, then escalate to SIGKILL if the process does not exit.
  • Create and automatically clean up temporary working directories (container drivers only).

Driver Comparison

Feature Host Docker Podman Firejail Bubblewrap
File-system isolation No Full Full Partial Partial
Network isolation No Yes Yes Yes Yes
Memory enforcement No Yes Yes* No No
CPU throttling No Yes Yes* Via rlimit No
Process limit No Yes Yes Yes No
Requires runtime No Docker Podman Firejail bwrap
Platform All Linux/macOS/Win Linux Linux Linux

*Podman skips memory and CPU limits on WSL2 for compatibility.