Introduction
This page covers the most common issues you may encounter when using the Sandbox package. Each entry describes the symptom, explains the root cause, and provides concrete solutions.Driver Binary Not Found
Symptom: ARuntimeException with the message “Failed to start docker” (or podman, firejail, bwrap) is thrown when calling execute().
Cause: The driver cannot locate the required binary on the system. The ProcRunner wraps the proc_open failure and reports it with the driver name.
Solutions:
-
Verify the binary is installed and executable:
-
Check what PHP sees as
PATH. In web server or systemd contexts, thePATHis often more restrictive than your shell’s:
-
Set the binary path explicitly through an environment variable before your PHP process starts:
- Pass the binary path directly to the static factory:
PATH: /usr/bin, /usr/local/bin, /opt/homebrew/bin, /opt/local/bin, and /snap/bin. On Windows, .exe extensions are tried automatically.
Invalid Driver Name
Symptom: AnInvalidArgumentException is thrown listing the valid driver names.
Cause: A string passed to Sandbox::fromPolicy($policy)->using() does not match any known driver value.
Solution: Use the SandboxDriver enum to avoid typos:
host, docker, podman, firejail, bubblewrap. These match the SandboxDriver enum’s backing values exactly.
Command Times Out
Symptom: TheExecResult has timedOut() returning true and exitCode() returning 124. The output may be incomplete.
Cause: The command exceeded either the wall-clock timeout or the idle timeout specified in the execution policy. The TimeoutTracker monitors both independently and terminates the process as soon as either limit is reached.
Solutions:
- Increase the wall-clock timeout:
- If the process produces output in bursts with long pauses, increase or disable the idle timeout:
- Use the streaming callback to monitor progress and identify where the command stalls:
- For container drivers, keep in mind that the timeout includes container startup time. If image pulling is needed on the first run, it may consume a significant portion of the budget. Pre-pull images to avoid this.
SIGTERM first and waits briefly, then escalates to SIGKILL if the process does not exit. For container drivers, this terminates the entire process group (via setsid) to ensure no orphan processes remain.
Truncated Output
Symptom: Output appears incomplete, andtruncatedStdout() or truncatedStderr() returns true.
Cause: The command produced more output than the policy’s output caps allow. The StreamAggregator retains only the most recent bytes up to the cap, discarding earlier content. This tail-preserving strategy ensures error messages and final status information are always captured.
Solution: Increase the output caps in the policy:
Working Directory Errors
Symptom: ARuntimeException with the message “Base directory is invalid or not writable” is thrown.
Cause: The baseDir specified in the policy does not exist, is not a directory, or is not writable by the PHP process. The Workdir::create() method validates this before attempting to create a temporary subdirectory.
Solutions:
-
Verify the directory exists and has correct permissions:
-
Create the directory if it does not exist:
-
Use a known-writable location:
0700 and cleaned up in a finally block, ensuring removal even when the command fails or throws an exception.
File Access Denied in Sandbox
Symptom: The sandboxed command cannot read or write files at expected paths. Cause: Container and sandbox drivers restrict file-system access by default. Only the working directory and explicitly mounted paths are accessible. Solutions:-
For files the command needs to read, add them as readable paths:
-
For files the command needs to write, add them as writable paths:
-
Remember that
withReadablePaths()andwithWritablePaths()replace the current list. Pass all paths in a single call: -
Paths containing symlinks,
..components, or colons are silently skipped for security. Userealpath()to resolve paths before passing them to the policy: -
For Docker and Podman, paths are mounted at
/mnt/ro0,/mnt/ro1, … (readable) and/mnt/rw0,/mnt/rw1, … (writable). Your command must reference these container paths, not the host paths. For Bubblewrap, paths are mounted at their original host locations.
Docker / Podman Permission Errors
Symptom: The command fails with “permission denied” errors inside the container. Cause: Container drivers run commands as thenobody user (UID 65534, GID 65534) with a read-only root filesystem and all capabilities dropped. This prevents writing to most locations inside the container.
Solutions:
-
The working directory (
/work) is mounted as writable. Write output files there. -
A writable tmpfs is mounted at
/tmpinside the container (64 MB, withnoexec,nodev,nosuidflags). Use it for temporary files — but note that executables cannot be run from/tmpdue tonoexec. -
For additional writable locations, add them through
withWritablePaths()in the policy. - If the container image requires root to set up (e.g., installing packages), build a custom image with a Dockerfile that performs setup as root and then switches to user 65534.
Podman on WSL2
Symptom: Podman commands fail with cgroup-related errors under WSL2. Cause: WSL2’s default cgroup configuration is not fully compatible with Podman’s expectations for resource limits. What the driver does automatically: ThePodmanSandbox detects WSL2 environments by checking /proc/version for “WSL2” or “microsoft” strings, and by checking /proc/self/cgroup for the root cgroup indicator. When WSL2 is detected:
- The
--cgroup-manager=cgroupfsflag is added as a global Podman flag. - Memory (
--memory) and CPU (--cpus) resource limits are skipped entirely.
- Your WSL2 distribution has cgroup v2 mounted.
- Podman is configured for rootless operation.
- The Podman binary is accessible (check with
PODMAN_BINorwhich podman).
Network Connectivity Issues
Symptom: The sandboxed command cannot reach external services (DNS resolution fails, connections time out). Cause: Network access is disabled by default in the execution policy. Solution: Enable network access explicitly:| Driver | Mechanism | Notes |
|---|---|---|
| Host | None | withNetwork() is a policy declaration only |
| Docker | --network=none | Full network stack isolation |
| Podman | --network=none | Full network stack isolation |
| Firejail | --net=none | Linux network namespace |
| Bubblewrap | --unshare-net | Linux network namespace |
Environment Variables Not Available
Symptom: The sandboxed command does not see expected environment variables. Cause: By default, the host environment is not inherited. Additionally, security-sensitive variables are always stripped byEnvUtils, even when inheritance is enabled.
Solutions:
-
Pass specific variables explicitly:
-
Enable environment inheritance with your overrides on top:
-
Be aware that the following variable patterns are always blocked and cannot be overridden:
Pattern matching uses
Category Patterns Dynamic linker LD_PRELOAD,LD_LIBRARY_PATH,LD_AUDITmacOS linker DYLD_INSERT_LIBRARIES,DYLD_LIBRARY_PATH,DYLD_FRAMEWORK_PATHPHP config PHP_INI_SCAN_DIR,PHPRCAWS credentials AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKENGoogle Cloud GOOGLE_APPLICATION_CREDENTIALS,GCP_*Azure AZURE_CLIENT_ID,AZURE_CLIENT_SECRETRuby GEM_HOME,GEM_PATH,RUBY*Node.js NODE_OPTIONS,NPM_*Python PYTHON*,PIP_*fnmatch(), soAWS_*matches any variable starting withAWS_.
FakeSandbox Throws “No Response” Error
Symptom: ARuntimeException with the message “FakeSandbox has no response for command: …” is thrown.
Cause: The command key (argv joined with spaces) does not match any registered response, and no default response was provided.
Solutions:
-
Verify the command key matches exactly. The key is formed by joining the argv array with spaces:
-
Provide a default response for unmatched commands:
-
If a command is called multiple times, ensure enough responses are enqueued. Each call consumes one response from the queue. Use
enqueue()to add more: -
Check
$sandbox->commands()after a test failure to see exactly which commands were called and in what order.
Memory Limit Format Errors
Symptom: AnInvalidArgumentException with the message “Invalid memory limit format” or “Unbounded memory limit (-1) is not allowed” is thrown when creating or modifying a policy.
Cause: The withMemory() method validates the format strictly. The value must be a positive integer optionally followed by K, M, or G. The value -1 (commonly used in PHP to mean “unlimited”) is explicitly rejected.
Solutions:
-
Use a valid format:
-
Avoid passing raw byte counts without a suffix. The value
134217728(128 MB in bytes) would be interpreted as 134,217,728 bytes without a unit, which is valid but may not produce the result you expect. Prefer usingMorGsuffixes for clarity. -
The maximum memory limit is clamped to 1 GB. Any value above this is silently reduced to
1024M.
Process Group Termination
Symptom: After a timeout, child processes spawned by the sandboxed command continue running. Cause: The command spawned child processes that were not in the same process group. How the package handles this: All container drivers usesetsid (when available on the system) to run the command in a new session group. On timeout, SIGTERM is sent to the entire process group (kill -15 -$PID), followed by a brief wait and then SIGKILL (kill -9 -$PID) if the process is still running. The host driver relies on Symfony Process’s built-in termination logic.
If orphan processes persist, ensure that:
setsidis available on your system (check/usr/bin/setsidor/bin/setsid).- The container driver is being used instead of the host driver for better isolation.
- Your command does not detach processes into separate sessions.