Skip to main content

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);
// @doctest id="411a"

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);
// @doctest id="5473"
You can also pass a plain string. The accepted values are host, docker, podman, firejail, and bubblewrap:
$sandbox = Sandbox::fromPolicy($policy)->using('firejail');
// @doctest id="5b87"
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();']);
// @doctest id="6da1"
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")']);
// @doctest id="94fd"
When to use: Production workloads, untrusted code execution, any scenario requiring strong isolation.

Container Hardening

Every Docker execution applies the following security measures automatically:
SettingValuePurpose
--read-onlyEnabledRoot filesystem is read-only
--cap-drop=ALLAll capabilities droppedNo elevated privileges
--security-opt no-new-privilegesEnabledPrevents privilege escalation
-u 65534:65534nobody userNon-root execution
--pids-limit=2020 processesPrevents fork bombs
--memoryFrom policy (default 128M)Memory cap
--cpus0.5CPU throttle
--network=noneWhen network disabledNetwork isolation
--tmpfs /tmprw,noexec,nodev,nosuid,size=64mWritable 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']);
// @doctest id="3fa6"

Custom Image

The default image is alpine:3. Pass any Docker image as the second argument:
$sandbox = Sandbox::docker($policy, image: 'node:20-alpine');
// @doctest id="3a36"

Binary Override

If Docker is not on the default PATH, specify the binary location:
$sandbox = Sandbox::docker($policy, dockerBin: '/usr/local/bin/docker');
// @doctest id="feac"
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');
// @doctest id="6591"
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');
// @doctest id="4843"
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']);
// @doctest id="a6e8"
When to use: Linux systems where you want sandbox isolation without the overhead of pulling container images.

Sandbox Configuration

Firejail applies the following restrictions:
SettingValuePurpose
--net=noneWhen network disabledNetwork isolation
--rlimit-nproc=2020 processesFork bomb prevention
--rlimit-nofile=100100 file descriptorsFile descriptor limit
--rlimit-fsize=1048576010 MBMaximum file size
--rlimit-cpuPolicy timeout + 1 secondCPU 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');
// @doctest id="ff2d"
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']);
// @doctest id="205c"
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');
// @doctest id="3e76"
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

FeatureHostDockerPodmanFirejailBubblewrap
File-system isolationNoFullFullPartialPartial
Network isolationNoYesYesYesYes
Memory enforcementNoYesYes*NoNo
CPU throttlingNoYesYes*Via rlimitNo
Process limitNoYesYesYesNo
Requires runtimeNoDockerPodmanFirejailbwrap
PlatformAllLinux/macOS/WinLinuxLinuxLinux
*Podman skips memory and CPU limits on WSL2 for compatibility.