本页剖析 Linux 隔离后端——也就是 sandbox-local 在 bubblewrap 不可用时使用的后端。二进制名为 landlock-run——一个自限制再执行的 Landlock launcher,位于 native/landlock-run/ 下,以包家族形式构建,并被 dsh 沙箱接口消费。它是机制而非策略:dsh 决定一次运行可读或可写哪些路径;本包提供强制这些授权的 launcher 以及解析并用它通信的 JavaScript API。
Landlock 是什么
Landlock 是 Linux 内核的 LSM(Linux Security Module),用于非特权沙箱。landlock-run 在自身装上规则集,然后 exec 被包装的命令;规则集跨 execve 继承,因此该命令(及其派生的每个进程)都受隔离,而发起进程保持不受限制。规则集是允许列表:未授予的一切都被拒绝。
Landlock 不需要 bubblewrap 所依赖的任何特权(不需要非特权用户命名空间、不需要 mount),这正是它在 bwrap 探测失败后成为 Linux 兜底方案的原因。对非目录的授权只保留其文件兼容的访问位——这就是 --rw /dev/null 授权得以成立的方式。
包家族
| 包 | 角色 |
|---|---|
native/landlock-run(workspace 根) | 构建/发布项目,拥有独立的版本号 |
native/landlock-run/packages/entry(@deepseek-ai/node-addon-landlock-run) | ESM entry:launcherPath、probe、grantArgs、契约常量;随包发布 C 源码以便审计 |
native/landlock-run/packages/linux-x64 | x86_64 Linux 的预编译静态二进制 |
native/landlock-run/packages/linux-arm64 | aarch64 Linux 的预编译静态二进制 |
该家族分两层,沿用 node-addon-require-builtin 模式:一个 entry 包(持有 CLI 契约)+ 按平台的二进制包(一个静态 bin/landlock-run、一份 prebuilds.json、无 JavaScript)。因为 CLI 解析器与二进制在同一包家族内统一版本,entry 包永远不可能落后于二进制。entry 包把每个平台包列为 optionalDependency,npm 的 os/cpu 字段在安装时选择匹配的那个。
公开的 JavaScript API
entry 包导出一个刻意保持精简的表面(native/landlock-run/packages/entry/src/index.ts):
launcherPath()——本机 launcher 的绝对路径,从@deepseek-ai/node-addon-landlock-run-<platform>-<arch>解析。在不支持的平台上,回退路径永远不存在(是否存在于已check过是刻意为之——探测才是可用性信号)。probe(launcher?, { timeoutMs? })——'full' | 'partial' | 'unusable'。一种功能探测:运行landlock-run --probe,在短生命周期子进程中构建并强制一个真实的最大规则集,因为仅做版本检查会漏掉"有 syscall 却拒绝强制"的内核。grantArgs({ readOnly?, readWrite? })——--ro/--rw参数列表;未授予的一切都被拒绝。LAUNCHER_BIN('landlock-run')与LAUNCHER_FAILURE_EXIT(125)——契约常量。
CLI 契约
固定于 native/landlock-run/docs/cli-contract.md。参数拼写为 --ro/--rw(镜像执行器包装的 bwrap argv 形态):
landlock-run [--ro <path>]... [--rw <path>]... -- <argv>...
landlock-run --probe--ro <path>——在<path>下授予 read + execute。--rw <path>——在<path>下授予完整文件系统访问(协商后的内核 ABI 能管控的每一个访问)。- 未授予的一切都被拒绝——Landlock 规则集是允许列表。
--——强制分隔符;其后皆为命令 argv,通过execvp执行,launcher 的环境保持不变。--probe——与授权和命令互斥。- 没有其他标志,也没有任何环境变量输入(哪个二进制隔离某个进程,绝不能由环境决定)。
退出码:任何 launcher 级失败(用法错误、内核无法强制、无法打开的授权根、exec 失败)都返回 125——被包装的命令没有运行。成功 exec 后,子进程状态原样透传,包括 125,因此消费者必须同时看到状态 125 和 landlock-run: 致命行,才能归因于 launcher 失败。
报告行:--probe 打印 landlock: fully enforced 或 landlock: partially enforced (older ABI);在 partial-ABI 内核上的受隔离运行会在 stderr 打印 landlock-run: partial enforcement (older Landlock ABI) 并继续(对内核支持的每件事仍受隔离);每个致命错误在退出 125 前打印一行 landlock-run: 。
dsh 如何调用它
seam 侧的 glue 位于 packages/sandbox/sandbox-local/src/profiles.ts 与 index.ts。landlockProfileArgs 为一种策略构造授权:
export function landlockProfileArgs(policy: SandboxPolicy): string[] {
const readWrite = ['/dev/null']
if (policy.mode === 'workspace-write') {
readWrite.push('/tmp', policy.workspaceRoot)
}
return landlockGrantArgs({ readOnly: ['/'], readWrite })
}因此每种模式都授予 readOnly: ['/'](处处可读 + 可执行),并把写入限制在 read-only 下的 /dev/null,在 workspace-write 下再加 /tmp 与会话工作区根。之后 confine 以 [launcher, ...grants, '--', ...argv] 形式 spawn。
LocalSandboxProvider 只有在 bwrap 探测失败后才选择 Landlock 环节(Linux 链 ['bwrap', 'landlock']),方式是通过 entry 包的功能探测 probe(launcher, { timeoutMs })。端到端行为在 packages/sandbox/sandbox-local/tests/landlock.e2e.ts 与 packages/shell/bash-sandbox/tests/landlock.e2e.ts 中得到验证。
它教给消费者的拒绝方言是 ['permission denied'],而其 runner 失败规则以带版本的退出码 125 加上 landlock-run: 致命行为门槛——examples/acp-agent/tests/fixtures/partial-landlock-sandbox.ts 里的 partial-landlock 夹具镜像了同一元组。
launcher 源码本身
C 源码位于 native/landlock-run/packages/entry/src/main.c——约 300 行 C11,直接基于原始 Landlock UAPI,静态链接 musl,不含此之外的任何 libc。依赖只有本文件加上内核稳定的 syscall 契约。值得注意的机制:
- 先设置
no_new_privs(prctl(PR_SET_NO_NEW_PRIVS, 1, …))——非特权规则集的强制前提; - 通过
syscall(__NR_landlock_create_ruleset, …)与LANDLOCK_CREATE_RULESET_VERSION协商所选 Landlock ABI; - 以
syscall(__NR_landlock_restrict_self, ruleset_fd, 0)应用规则集; - 用
execvp(cli.command[0], cli.command)启动被包装的命令。
UAPI 结构体在本地定义,而非从 <linux/landlock.h> 引入——内核 ABI 在契约上稳定,自行定义可使构建独立于工具链的 header 年代,也让这些定义充当"此 launcher 究竟触碰了哪些内核 API"的审计记录。
构建、打包与发布
- 构建仅限原生:
scripts/build.ts用发行版的musl-gcc编译当前架构的二进制(静态——glibc 与 musl 发行版都适用同一个二进制)。CI 的按架构 runner 是正式构建源;仓库中没有交叉工具链——某个平台包只有在具备能构建并验证它的原生 runner 时才会加入。 - 包矩阵是已检入的元数据(
prebuilds.json+os/cpu字段);scripts/github-matrix.mjs由此派生 CI 与发布矩阵。 - 发布流程与说明:
native/landlock-run/docs/packaging.md、native/landlock-run/docs/release.md;三道关卡把已安装的二进制与打包来源的 workspace 构建逐字节对账。
支持矩阵与回退
支持矩阵固定于 native/landlock-run/docs/support-matrix.md:
| 平台包 | 正式构建源 | 备注 |
|---|---|---|
…-landlock-run-linux-x64 | ubuntu-24.04 | 静态 musl——glibc 与 musl 发行版皆可 |
…-landlock-run-linux-arm64 | ubuntu-24.04-arm | 静态 musl——glibc 与 musl 发行版皆可 |
强制还需要一个启用了 Landlock 的内核(5.13+)。协商后的 ABI 级别决定探测结论——此构建已知能管控的每个访问 → full;管控其子集的较旧 ABI → partial(对支持的一切仍受隔离);Landlock 缺失或被禁用 → unusable。权威是探测,而非内核版本:一个编译时未启用 Landlock、或禁用该 LSM 的内核,无论其版本如何,都会探测为 unusable。
刻意不支持的平台:darwin(macOS 用随系统附带的 sandbox-exec/Seatbelt 隔离)、win32(不同机制),以及其他 Linux 架构(riscv64、s390x 等,目前没有正式构建源)。
dsh 中的回退:在没有 landlock-run 的主机上(不支持的平台、缺失的 optional 依赖、或不支持的内核),launcherPath() 解析到一条永不存在路径,probe() 报告 unusable,而这与二进制缺失刻意不可区分。随后 sandbox-local 把 Landlock 环节视为不可用,除非 bwrap 可用,否则抛出 SandboxUnavailableError:命令绝不在未隔离的情况下运行。CI 的 darwin 分支验证了这一文档化的降级。
延伸阅读
- 沙箱架构:总览——Landlock 环节在
ctx.sandboxrunner 链中的位置及其周边的升级词汇。 - 文件系统观察与沙箱策略——Landlock 授权(以及 fs 围栏)所表达的文件影响策略。
native/landlock-run/docs/cli-contract.md——完整的 launcher 调用语法、退出码与报告行。native/landlock-run/docs/support-matrix.md——权威的平台/内核支持矩阵。native/landlock-run/packages/entry/src/index.ts与packages/entry/src/main.c——JS API 与 C11 launcher 源码。packages/sandbox/sandbox-local/src/profiles.ts——landlockProfileArgs,dsh 如何把一种模式映射为--ro/--rw授权。