class

Cordon::Bwrap

Inherits Cordon::Runner < Reference < Object

Linux sandbox runner using Bubblewrap (bwrap).

bwrap uses unprivileged Linux user namespaces — no root required. Rather than evaluating path rules at access time (like SBPL on macOS), it constructs a fresh mount namespace: an allowlist view assembled from explicit bind mounts. Anything not bound simply does not exist inside.

Requires bwrap >= 0.3.0. Available on all major distributions: apt install bubblewrap dnf install bubblewrap pacman -S bubblewrap

Note: some hardened kernels disable unprivileged user namespaces (kernel.unprivileged_userns_clone=0). Call available? before use.

Constants

BINARY = "bwrap"
DEFAULT_ENV_PASSTHROUGH = ["PATH", "TERM", "LANG", "LC_ALL", "LANGUAGE", "TZ"] of ::String

Passed through from the parent environment after --clearenv. Everything else is stripped; add to policy.env for additional vars.

SYSTEM_RO_PATHS = ["/usr/lib", "/usr/lib64", "/lib", "/lib64", "/usr/share/locale", "/usr/share/zoneinfo"] of ::String

Bound read-only with --ro-bind-try (silently skipped if absent). Covers dynamic linker and shared-library paths across major Linux distributions — needed for any dynamically-linked binary to start at all, since the kernel loads the ELF interpreter (ld-linux.so) as part of the same execve(2) that launches the target command.

Binary directories (/usr/bin, /bin, /usr/sbin, /sbin) are DELIBERATELY NOT included here. bwrap's mount namespace has no concept of "exec permission" separate from "visible" — unlike SBPL on macOS, which can grant file-read* without process-exec, a bind mount makes a path both readable AND exec-able simultaneously. A policy with no explicit grants would still be able to exec /usr/bin/ruby, /usr/bin/python3, or anything else living in these directories, if they were unconditionally mounted — the exact same class of bypass fixed on the macOS side (see SandboxExec::BASELINE's comment). If your command needs to shell out (e.g. via system()/popen(), or a script with a #!/bin/sh line), merge in Preset::System, which mounts these directories explicitly.

Instance methods

available?

Returns true if the underlying sandbox binary is present and usable.

Source
build_argv(command : Array(String), policy : Policy, shell : Bool = false) : Array(String)

Returns the full argv that would be passed to the OS. Useful for inspection, dry-run output, or logging.

See Runner#run for shell's contract.

Source
exec(command : Array(String), policy : Policy) : NoReturn

Replaces the current process with command, inside the sandbox described by policy. Used for self-relaunch (Cordon.relaunch) — the caller does not resume; either the sandboxed command takes over the process image, or this raises.

Source
name

Short platform-facing name for this runner (e.g. "bwrap", "sandbox-exec"), used in ConfirmReport output.

Source
run(command : Array(String), policy : Policy, shell : Bool = false) : Result

Runs command inside the sandbox described by policy.

If shell is true, command must be a single-element array holding a full shell script string, executed via /bin/sh -c. No positional-arg forwarding is supported — build the complete script string yourself before calling. /bin/sh must be exec-granted by policy (e.g. via Preset::System), same as any other target; shell: true is not an implicit exec exception.

If shell is false (the default), command is the literal argv — command[0] is exec'd directly with command[1..] as its arguments, no shell involved.

Source