This page dissects the Linux confinement backend that sandbox-local uses when bubblewrap is unusable. The binary is landlock-run — a self-restrict-then-exec Landlock launcher living under native/landlock-run/, built as a package family and consumed by the dsh sandbox seam. It is mechanism, not policy: dsh decides which paths a run may read or write; this package provides the launcher that enforces those grants and the JavaScript API that resolves and speaks to it.
What Landlock is
Landlock is a Linux kernel LSM (Linux Security Module) for unprivileged sandboxing. landlock-run installs a ruleset on itself and execs the wrapped command; the ruleset is inherited across execve, so the command — and every process it spawns — runs confined while the invoking process stays unrestricted. Rulesets are allow-lists: everything not granted is denied.
Landlock needs none of the privileges bubblewrap relies on (no unprivileged user namespaces, no mount), which is why it is the Linux fallback when bwrap fails a functional probe. A grant on a non-directory keeps only its file-compatible access bits — this is how a --rw /dev/null grant works.
The package family
| Package | Role |
|---|---|
native/landlock-run (workspace root) | Build/release project with its own versioning |
native/landlock-run/packages/entry (@deepseek-ai/node-addon-landlock-run) | ESM entry: launcherPath, probe, grantArgs, contract constants; ships the C source for auditability |
native/landlock-run/packages/linux-x64 | Prebuilt static binary for x86_64 Linux |
native/landlock-run/packages/linux-arm64 | Prebuilt static binary for aarch64 Linux |
The family is two-layer, on the node-addon-require-builtin model: one entry package (owning the CLI contract) + per-platform binary packages (a static bin/landlock-run, a prebuilds.json, no JavaScript). Because the CLI parser and binary version together in one family, the entry package can never drift behind the binary. The entry package lists every platform package as an optionalDependency, and npm's os/cpu fields select the matching one at install time.
The public JavaScript API
The entry package exports a deliberately small surface (native/landlock-run/packages/entry/src/index.ts):
launcherPath()— absolute path of this host's launcher, resolved from@deepseek-ai/node-addon-landlock-run-<platform>-<arch>. On an unsupported platform the fallback path never exists (existence is deliberately unchecked — the probe is the availability signal).probe(launcher?, { timeoutMs? })—'full' | 'partial' | 'unusable'. A functional probe: it runslandlock-run --probe, which builds and enforces a real maximal ruleset in a short-lived child, because a version check would miss a kernel that has the syscalls but refuses enforcement.grantArgs({ readOnly?, readWrite? })— the--ro/--rwargument list; everything not granted is denied.LAUNCHER_BIN('landlock-run') andLAUNCHER_FAILURE_EXIT(125) — contract constants.
The CLI contract
Pinned in native/landlock-run/docs/cli-contract.md. Arguments are spelled --ro/--rw (mirroring the bwrap argv shape the executor wraps):
landlock-run [--ro <path>]... [--rw <path>]... -- <argv>...
landlock-run --probe--ro <path>— grant read + execute beneath<path>.--rw <path>— grant full filesystem access beneath<path>(every access the negotiated kernel ABI can govern).- Everything not granted is denied — Landlock rulesets are allow-lists.
--— mandatory separator; everything after it is the command argv,exec'd viaexecvpwith the launcher's environment unchanged.--probe— mutually exclusive with grants and a command.- No other flags, no environment-variable inputs (which binary confines a process must never be decidable by the ambient environment).
Exit codes: 125 on every launcher-level failure (usage error, kernel that cannot enforce, unopenable grant root, failed exec) — the wrapped command was not run. After a successful exec, child statuses pass through unchanged, including 125, so consumers require both status 125 and a landlock-run: fatal line to attribute launcher failure.
Report lines: --probe prints landlock: fully enforced or landlock: partially enforced (older ABI); a confined run on a partial-ABI kernel prints landlock-run: partial enforcement (older Landlock ABI) on stderr and proceeds (still confined for everything the kernel supports); every fatal error prints one landlock-run: line before exit 125.
How dsh invokes it
The seam-side glue lives in packages/sandbox/sandbox-local/src/profiles.ts and index.ts. landlockProfileArgs builds the grants for one policy:
export function landlockProfileArgs(policy: SandboxPolicy): string[] {
const readWrite = ['/dev/null']
if (policy.mode === 'workspace-write') {
readWrite.push('/tmp', policy.workspaceRoot)
}
return landlockGrantArgs({ readOnly: ['/'], readWrite })
}So every mode grants readOnly: ['/'] (read + execute everywhere) and restricts writes to /dev/null under read-only, adding /tmp and the workspace root under workspace-write. confine then spawns [launcher, ...grants, '--', ...argv].
LocalSandboxProvider selects the Landlock rung only after the bwrap probe fails (the Linux chain ['bwrap', 'landlock']), via the functional probe probe(launcher, { timeoutMs }) from the entry package. The e2e behavior is exercised in packages/sandbox/sandbox-local/tests/landlock.e2e.ts and packages/shell/bash-sandbox/tests/landlock.e2e.ts.
The denial dialect it teaches consumers is ['permission denied'], and its runner-failure rule is gated on the versioned exit 125 plus the landlock-run: fatal line — the same tuple the partial-landlock fixture at examples/acp-agent/tests/fixtures/partial-landlock-sandbox.ts mirrors.
The launcher source itself
The C sits at native/landlock-run/packages/entry/src/main.c — ~300 lines of C11 over the raw Landlock UAPI, statically linked against musl, no libc beyond it. The only dependencies are this file plus the kernel's stable syscall contract. Notable mechanics:
no_new_privsis set first (prctl(PR_SET_NO_NEW_PRIVS, 1, …)) — mandatory for an unprivileged ruleset;- the chosen Landlock ABI is negotiated via
syscall(__NR_landlock_create_ruleset, …)andLANDLOCK_CREATE_RULESET_VERSION; - the ruleset is applied with
syscall(__NR_landlock_restrict_self, ruleset_fd, 0); - the wrapped command is launched with
execvp(cli.command[0], cli.command).
The UAPI structs are defined locally rather than pulled from <linux/landlock.h> — the kernel ABI is stable by contract, self-defining keeps the build independent of the toolchain header vintage, and the definitions double as an audit record of exactly which kernel API the launcher touches.
Build, packaging, and release
- Builds are native-only:
scripts/build.tscompiles the running architecture's binaries with the distromusl-gcc(static — one binary works for both glibc and musl distros). CI's per-architecture runners are the builders of record; there is no cross toolchain in the repo — a platform package is added only together with a native runner that builds and proves it. - The package matrix is checked-in metadata (
prebuilds.json+os/cpufields);scripts/github-matrix.mjsderives the CI and Release matrices from it. - Release flow and notes:
native/landlock-run/docs/packaging.md,native/landlock-run/docs/release.md; three gates byte-pin installed binaries against the workspace builds they were packed from.
Support matrix and fallback
The support matrix is pinned in native/landlock-run/docs/support-matrix.md:
| Platform package | Builder of record | Notes |
|---|---|---|
…-landlock-run-linux-x64 | ubuntu-24.04 | static musl — glibc and musl distros alike |
…-landlock-run-linux-arm64 | ubuntu-24.04-arm | static musl — glibc and musl distros alike |
Enforcement additionally requires a kernel with Landlock enabled (5.13+). The negotiated ABI level decides the probe verdict — every access this build knows governed → full; an older ABI governing a subset → partial (still confined for everything it supports); Landlock absent or disabled → unusable. The probe, not the kernel version, is the authority: a kernel built without Landlock, or with the LSM disabled, probes unusable regardless of version.
Deliberately unsupported: darwin (macOS confines via shipped sandbox-exec/Seatbelt), win32 (a different mechanism), and other Linux architectures (riscv64, s390x, …) with no native builder of record yet.
Fallback in dsh: on a host with no landlock-run — unsupported platform, missing optional dependency, or an unenforcing kernel — launcherPath() resolves a path that never exists, probe() reports unusable, and the probe is indistinguishable from a missing binary on purpose. sandbox-local then treats the Landlock rung as unavailable and, unless bwrap works, throws SandboxUnavailableError: the command never runs unconfined. The documented degradation is exercised by CI's darwin leg.
Further reading
- Sandbox architecture: overview — where the Landlock rung sits in the
ctx.sandboxrunner chain and the escalation vocabulary around it. - Filesystem observation & sandbox policy — the file-effect policy the Landlock grants (and the fs fence) express.
native/landlock-run/docs/cli-contract.md— the full launcher invocation grammar, exit codes, and report lines.native/landlock-run/docs/support-matrix.md— the authoritative platform/kernel support matrix.native/landlock-run/packages/entry/src/index.tsandpackages/entry/src/main.c— the JS API and the C11 launcher source.packages/sandbox/sandbox-local/src/profiles.ts—landlockProfileArgs, how dsh maps a mode to--ro/--rwgrants.