Skip to content

本页剖析 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-runESM entry:launcherPathprobegrantArgs、契约常量;随包发布 C 源码以便审计
native/landlock-run/packages/linux-x64x86_64 Linux 的预编译静态二进制
native/landlock-run/packages/linux-arm64aarch64 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_EXIT125)——契约常量。

CLI 契约

固定于 native/landlock-run/docs/cli-contract.md。参数拼写为 --ro/--rw(镜像执行器包装的 bwrap argv 形态):

text
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 enforcedlandlock: 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.tsindex.tslandlockProfileArgs 为一种策略构造授权:

ts
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.tspackages/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_privsprctl(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.mdnative/landlock-run/docs/release.md;三道关卡把已安装的二进制与打包来源的 workspace 构建逐字节对账。

支持矩阵与回退

支持矩阵固定于 native/landlock-run/docs/support-matrix.md

平台包正式构建源备注
…-landlock-run-linux-x64ubuntu-24.04静态 musl——glibc 与 musl 发行版皆可
…-landlock-run-linux-arm64ubuntu-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.sandbox runner 链中的位置及其周边的升级词汇。
  • 文件系统观察与沙箱策略——Landlock 授权(以及 fs 围栏)所表达的文件影响策略。
  • native/landlock-run/docs/cli-contract.md——完整的 launcher 调用语法、退出码与报告行。
  • native/landlock-run/docs/support-matrix.md——权威的平台/内核支持矩阵。
  • native/landlock-run/packages/entry/src/index.tspackages/entry/src/main.c——JS API 与 C11 launcher 源码。
  • packages/sandbox/sandbox-local/src/profiles.ts——landlockProfileArgs,dsh 如何把一种模式映射为 --ro/--rw 授权。