Skip to content

B2:Linux 原生 Sandbox(立在 B1 上) - #524

Closed
jiangkoumo wants to merge 3 commits into
Ephemeral-AI-Lab:mainfrom
jiangkoumo:b2-linux-native-v2
Closed

jiangkoumo wants to merge 3 commits into
Ephemeral-AI-Lab:mainfrom
jiangkoumo:b2-linux-native-v2

Conversation

@jiangkoumo

Copy link
Copy Markdown
Collaborator

对应 layerfs-connect 的 #46(Linux 原生 Sandbox 后端)。内容源自已经关闭的 #402,本次按「B1 / B2 / B3 / #515 四个 PR」的拆法重新提交。原 #498 会在四个 PR 都建好之后关闭。

本 PR 基于 B1(分支 b1-interface,提交 a32da5732,其父为 upstream/main 35d36575c),不是基于 main。


1. 这个 PR 在做什么(一句话)

让 LayerFS 在 Linux 上用「本机普通进程」跑 Sandbox:把 Sandbox 的 daemon 拉起成一个本机进程,记下它的 pid 与启动时刻(用来识别进程是否还活着、以及 pid 被复用的情况),命令以指定 uid/gid 身份在指定目录执行,停止时按「先停子进程组、再惰性卸载残留挂载点」的次序收拾干净。

这些能力在 Mac 上是同一份代码,区别只在「问内核」的那几行:

要做的事 Linux 的做法 macOS 的做法
进程是否活着 读 /proc/<pid>/stat + 系统 boot id 问 proc_pidinfo / sysctl
关机重启后 pid 是否还算同一个进程 读 /proc/sys/kernel/random/boot_id sysctl 的 kern.boottime(在 B3 里)
卸载残留挂载点 读 /proc/self/mountinfo + umount2(MNT_DETACH) mount 表 + umount -f

本 PR 只填「Linux 那几行的实现」:engine/crates/layerfs-sandbox/src/backend/linux/ 这一个目录,加上 4 个测试文件。


2. 为什么这个 PR 这么小(统一口径)

原来 #402 / #498 是一个大 PR,把「接口 + 三个后端 + VM + JS」全放在一起,评审根本读不完,几个 PR 又互相踩同一批文件,合并时冲突几十个 hunk。所以拆成四个:

PR 内容 基线
B1 接口层一次定全:Runtime/Sandbox/provider 选项与能力、Docker 后端、Linux 与 macOS 共用的本地进程实现(engine/crates/layerfs-sandbox/src/backend/native/* 约 1230 行),以及 layerfs-node 侧本地部署接入(src/local.rs、runtime.rs、discovery.rs 等) main
B2(本 PR) 只放 Linux 平台相关的实现 B1
B3 只放 macOS 平台相关的实现 B1
#515 只放 Namespaces / VM 相关实现(另一个 PR 提交,不在这里) B1

换句话说:平台无关的部分已经在 B1 里了。 本 PR 剩下的只有「问 Linux 内核」的那几行代码,所以它小不是省事,而是该在别处的代码本来就不在这里。

这种切法带来三个已经实测过的性质:

  1. B2、B3、docs(512): propose a VM Sandbox backend (Sandboxes as namespaces in one VM per Store) #515 两两合并 0 冲突,可以按任意顺序合入(预演:三个分支改动文件集合完全不相交,只有 5 个测试文件被两个分支同时携带且逐字相同)。
  2. 三者全合之后,代码树与拆之前 feat(sandbox): 用具体的 Runtime enum 统一 Docker / Linux 原生 / macOS 原生,并新增 Linux namespaces 后端(B1/B2/B3,按 #515,叠在 #427 上) #498 的头 9aa10e65c 逐字节一致——拆的是提交,不是代码。
  3. 每个 PR 自己都能编译通过、过 clippy(Linux -D warnings),不必等别的 PR 先合。

3. 改了什么

相对 B1 一共 6 个文件(+1099 / −15):

文件 性质 说明
engine/crates/layerfs-sandbox/src/backend/linux/mounts.rs 整实现 读 /proc/self/mountinfo(含八进制转义解码)、按深度倒序列出目录下的挂载点、umount2(MNT_DETACH) 惰性卸载后复查;filesystem() 查某个路径所在的文件系统类型(见 §3.1)
engine/crates/layerfs-sandbox/src/backend/linux/process.rs 整实现 读 /proc/<pid>/stat 的进程启动时刻、读 boot id,据此判断「还是那个进程」而不只是「pid 存在」
engine/crates/layerfs-sandbox/tests/native_provider.rs 测试(+124) 走真实 Runtime::LinuxNative:launch → reopen → 停止 → 删除;启动失败时保留部分资源并精确归还;非法名字与错后端选项在 Validate 阶段拒绝;Debug 输出不得泄露 setup 密文
engine/crates/layerfs-sandbox/tests/local_runtime.rs 测试(+672) 11 个测试:目录权限门禁、私有 setup 文件、监听行、命令身份/环境/目录、容器搜索路径、daemon 退出后的观察、失败启动的归还、僵尸进程不算活着、后代进程随沙箱结束、跨 boot 的 pid、主线程先死的 daemon 回收
engine/crates/layerfs-sandbox/tests/local_search_path.rs 测试(+113) #![cfg(target_os = "macos")];为了让 B2 与 B3 携带的测试文件逐字一致而随本 PR 走,在 Linux 上不产生测试
engine/crates/layerfs-sandbox/tests/local_stop_mount.rs 测试(+48) 同上(macOS 专用,且 #[ignore]:需人工提供一次性挂载点)

src/backend/linux/mod.rs 在 B1 里已经与最终形态逐字节一致(同签名的 Unsupported 桩),本 PR 不需要改它。

生产代码目录(本 PR 相对 B1;计数器 tools/production_loc.py,提交 45556f3db,只列 engine/crates/layerfs-sandbox/src/backend/linux/,测试在 tests/ 下不计入):

  engine/crates/layerfs-sandbox/src/backend/linux/                       [133]
    mod.rs                                                                   3
    mounts.rs                                                               90
    process.rs                                                              40

(B1 里这三个桩文件合计 30 行生产代码,所以本 PR 的生产代码增量是 +103。)

3.1 评审会注意到的一行:#[allow(dead_code)]

linux/mounts.rs 里的 filesystem() 唯一的调用方是 #515 的 namespaces 后端(它要确认 overlay 的可写层落在一个能当 overlay upper 的文件系统上)。B2 单独提出来时,这个函数在本分支上没人调用,而 CI 的 Linux clippy 车道用 -D warnings,会直接判死代码、让 B2 单独立不住。

按最小改动原则,在这个函数的声明处加一行 #[allow(dead_code)](附带两行注释说明原因),并写清楚:这个文件只由 B2 拥有,#515 不碰它,所以不会因为这一行产生冲突。#515 合入后这个属性变成冗余但无害(合并后的树已实测 clippy 仍 0 warning)。


4. 怎么验证

4.1 已在 macOS 上跑过(. ~/.cargo/env,cargo +1.85.1 --locked)

检查 命令 结果
全 workspace 编译 cargo +1.85.1 check --locked --workspace --all-targets exit 0(30.3 s)
全 workspace clippy cargo +1.85.1 clippy --locked --workspace --all-targets -- -D warnings exit 0,0 warning
Linux 目标的 clippy(本 PR 的关键面) cargo +1.85.1 clippy --locked --target x86_64-unknown-linux-gnu -p layerfs-sandbox --all-targets -- -D warnings 修前 exit 101(function filesystem is never used,-D dead-code);修后 exit 0
产品边界守卫 python3 -B tools/check_product_boundary.py PASS: scanned 991 ...,exit 0
fuser 完整性 python3 -B tools/check_fuser_integrity.py exit 0
工具自测 python3 -B -m unittest discover -s tools -p 'test_*.py' OK
格式 cargo +1.85.1 fmt --all --check 无差异
与 #515 试合 git merge --no-ff --no-commit <515分支> 0 冲突,合并树 clippy(Linux 目标 + 全 workspace)均 exit 0
与 B3 试合 同上 0 冲突,合并树 clippy 均 exit 0

macOS 上做不到的一条:在 macOS 上把整个 workspace 交叉编译到 x86_64-unknown-linux-gnu 会被 C 交叉工具链拦住(libsqlite3-sys 等 build script 报 failed to find tool "x86_64-linux-gnu-gcc")。CI 的 clippy 车道是在 Linux runner 上跑的整 workspace 形态,所以这一项在 Linux 机器上补跑(见 4.2)。

4.2 需要在 Linux 机器上跑的(真机验证)

cargo +1.85.1 test --locked -p layerfs-sandbox --all-targets --no-run   # 先编译,避免把编译时间算进测试
timeout 120 cargo +1.85.1 test --locked -p layerfs-sandbox --test local_runtime     # 11 个测试;root 与非 root 各跑一遍
timeout 120 cargo +1.85.1 test --locked -p layerfs-sandbox --test native_provider   # 需 root(测试会以 uid 501/gid 20 身份起命令)
timeout 120 cargo +1.85.1 test --locked -p layerfs-sandbox --test native_platform   # 跨平台拒绝
cargo +1.85.1 clippy --locked --workspace --exclude layerfs-node --all-targets -- -D warnings
  • local_runtime 源码注明不需要 root(root 身份下命令身份退化为 nobody),请两种身份都跑。
  • native_provider 的 Linux 分支必须 root:它要真的 setuid 到 uid 501/gid 20 才能验证「命令以指定身份执行」。非 root 下该测试只走「NativeProvider::new 必须失败」的早退分支——这本身就说明「非 root 不能启用 Linux 原生 Sandbox」的门禁。
  • 注意:上面这些命令不会真的卸载挂载点。linux/mounts.rs 的 detach()(/proc/self/mountinfo 解析 + umount2(MNT_DETACH) + 复查)在 local_runtime 的 stop/delete 里被真实执行,但沙箱目录下没有挂载,所以只走「读取 + 过滤 + 空结果」;真正的 umount2 成功路径与「卸载后复查失败」路径未被测试覆盖。macOS 侧有一个 #[ignore] 的 local_stop_mount 人工证明,Linux 暂无同形测试。如果评审要求,可另开一个需要一次性真实挂载点的人工证明(属新增测试,本 PR 未加)。

5. 风险

  1. Linux 真机测试尚未由本 PR 作者执行(作者只有 macOS 机器,且本项目 CI 只报告、不作为合并门禁)。4.2 的命令是合并前需要在 Linux 上确认的项;如果哪条失败,本 PR 不应合入。
  2. layerfs-node 侧的端到端链路缺一条 Linux 测试:engine/crates/layerfs-node/tests/native_local.rs(#![cfg(target_os = "linux")],来自 feat: macOS 原生适配——daemon 内置 loopback NFSv3 导出,普通用户 mount_nfs 挂载 Workspace(无 Docker / 无 macFUSE / 无 root) #427)按拆分归属随 B3 走,不在本 PR。也就是说,在 B2 合入、B3 未合入的窗口里,「经 N-API 绑定拉起 Linux 原生 Sandbox」这条链路没有自动化测试;B3 合入后自动补齐。这是拆分的已知代价,不是本 PR 的回归。
  3. #[allow(dead_code)] 是过渡属性:#515 合入后它变成冗余(无害)。若后续不想保留,应作为独立提交清理,不要塞进 docs(512): propose a VM Sandbox backend (Sandboxes as namespaces in one VM per Store) #515,否则会破坏「一个文件只有一个 owner、各 PR 0 冲突」的约定。
  4. 合并顺序:本 PR 基于 B1,必须先合 B1;合完 B1 之后 B2 / B3 / docs(512): propose a VM Sandbox backend (Sandboxes as namespaces in one VM per Store) #515 可以任意顺序合入(已预演两两 0 冲突、三者全合树与拆前 9aa10e65c 逐字节一致;正式分支定稿后会再核一次)。如果先合了别的 PR 再合 B2,git 会自动处理,不需要谁去 rebase。
  5. root 门禁的行为:Linux 非 root 用户不能启用 native 后端(后端会拒绝)。这是实现里的既有语义,不是本 PR 新增的限制;native_provider 在非 root 下的断言就是这个语义。
  6. Linux 没有「真的卸载了挂载」的自动化证明:linux/mounts.rs 的 detach() 在 local_runtime 的 stop/delete 里会真实执行,但沙箱目录下没有挂载,所以只覆盖「读取 + 过滤 + 空结果」;umount2(MNT_DETACH) 成功路径未覆盖(macOS 侧有 #[ignore] 的 local_stop_mount 人工证明,Linux 暂无同形测试)。这是「共用行为在 B1、平台细节在各平台」拆分下的已知空白,不影响本 PR 的编译与门禁结论,但属于合并后仍待补的验证。

…der (Area B1)

Extract the complete Sandbox runtime interface and shared native/node plumbing from 9aa10e6 onto upstream/main (35d3657), implementing the Docker backend and leaving same-signature Unsupported slots for backend/{linux,macos,namespaces}/ so B2, B3 and Ephemeral-AI-Lab#515 only replace their own backend directories.

In layerfs-sandbox:
- Define concrete Runtime (Docker, MacosNative, LinuxNative, Namespaces, Windows) and Sandbox (Docker, Native, Namespaces) enums in backend/runtime.rs.
- Add provider capabilities (including LoopbackNfs, MicroVm, RemoteCloud), LaunchPhase/LaunchFailure, ProviderLaunchOptions (Local, Namespaces, Docker) and SandboxRecord (Local, Namespaces, Docker).
- Add types::Image and cross-backend ExecRequest; move shared json and strings helpers to backend/.
- Implement DockerProvider and DockerInstance over existing Docker Engine lifecycle and streaming execution.
- Include shared backend/native/* lifecycle, identity, listener and streams modules under cfg(any(linux, macos)), with same-signature Unsupported stubs in backend/{linux,macos,namespaces}/.

In layerfs-api:
- Route SandboxApi through Arc<Runtime> via with_runtime and create_provider_file, retaining local resource ownership and command identity validation.
- Share TCP/Noise/Hello and Store install pipeline in sandbox/connection.rs.

In layerfs-node and @layerfs/agent:
- Take verbatim 9aa10e6 shared binding modules (local, discovery, runtime, sandbox, workspace, exec, registry, lib) with a no-op unmount slot for B3.
- Expose deploy.sandbox, deploy.localDir, deploy.sandboxBackend and deploy.imageArchive settings in @layerfs/agent.

Production LOC: 102831 -> 106022 (delta +3191)
First-parent comparison: upstream/main 35d3657 (tree ddea2b5) -> committed tree ae48e29.
Scopes:
  - core:   38395 -> 38395 (delta 0)
  - engine: 61777 -> 64938 (delta +3161)
    * layerfs-sandbox: 3417 -> 5526 (delta +2109)
    * layerfs-api:     1940 -> 2081 (delta +141)
    * layerfs-node:    2744 -> 3655 (delta +911)
  - api:     2659 ->  2689 (delta +30)
    * @layerfs/agent:   849 ->  879 (delta +30)
  - dsh:        0 ->     0 (delta 0)
  - reference: absent (retired R9c; last counted 65,417 at tree 498dd19)
Method: tools/production_loc.py (blob 3341e4d), --root <exact exported tree> --json for both snapshots; first-party production Rust, runtime SQL and JavaScript; tests, docs, tooling, generated and third-party files excluded.
Production LOC: 106022 -> 106124 (delta +102)
First-parent comparison: b1-interface a32da57 (tree ae48e29) -> rehearsal-b2 tree.
Scopes:
  - core:   38395 -> 38395 (delta 0)
  - engine: 64938 -> 65040 (delta +102; layerfs-sandbox 5526 -> 5628)
  - api:     2689 ->  2689 (delta 0)
  - dsh:        0 ->     0 (delta 0)
  - reference: absent (retired R9c; last counted 65,417 at tree 498dd19)
Method: tools/production_loc.py (blob 3341e4d), --root <exact tree> --json.
B2 replaces backend/linux/ with the real Linux native implementation, but
linux/mounts.rs::filesystem() has its only caller in the namespaces backend,
which lands as its own change. Alone on this branch the function is dead
code, and the Linux clippy lane (`cargo clippy --workspace --all-targets --
-D warnings`, which runs for x86_64-unknown-linux-gnu) fails on it, so B2
could not be merged on its own.

Allow that one function where it is declared, in a file only B2 owns, so the
branch is green on Linux before the namespaces change lands. Nothing about
the parsing, the mount table source or the lazy detach changes, and the
namespaces change still deletes or keeps the attribute freely because no
other patch touches this file.

Production LOC: 106124 -> 106125 (delta +1)
First-parent comparison: rehearsal-b2 336e296 (tree a85a639) -> committed tree 65a79ec.
Scopes:
  - core:   38395 -> 38395 (delta 0)
  - engine: 65040 -> 65041 (delta +1)
    * layerfs-sandbox: 5628 -> 5629 (delta +1)
  - api:     2689 ->  2689 (delta 0)
  - dsh:        0 ->     0 (delta 0)
  - reference: absent (retired R9c; last counted 65,417 at tree 498dd19)
Method: tools/production_loc.py (blob 3341e4d), --root <exact exported tree> --json for both snapshots; first-party production Rust, runtime SQL and JavaScript; tests, docs, tooling, generated and third-party files excluded.
Copilot AI balanced review requested due to automatic review settings October 31, 2026 14:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

yifanxuaaa added a commit that referenced this pull request Oct 31, 2026
…fs_v2_0_0

B2 (layerfs-connect#46): native Linux Sandbox, filling B1's Linux
process and mount-table backend (engine/crates/layerfs-sandbox/src/
backend/linux/) and adding its tests. Merged at PR head 45556f3 onto
the B1 merge d39bb6c; no conflicts. One merge adjustment: B2's new
native_provider test gives its Docker record `Some(...)` for the
volume, which is optional on this branch (#506).

Verified on macOS (Rust 1.85.1): workspace warning-denying Clippy, all
targets, and format check (only the existing minio.rs drift).
layerfs-sandbox tests: 49 passed, 2 failed, 1 ignored. The two failures
(local_search_path, native_provider) stop at B1's macOS placeholder
"macOS native process and mount table" (Unsupported), which B3 (#525)
replaces; B2's own head has the same placeholder, so they cannot pass on
macOS before B3. layerfs-node's tests are Linux-only. Not run: Linux
build and tests, where B2's code runs (no Linux C toolchain on the
host, Docker not running). Disposable only; Durable NOT_RUN, disabled
by owner.

Production LOC: 115107 -> 115210 (delta +103)
First-parent comparison: layerfs_v2_0_0 d39bb6c (tree cae5bb4) to
prepared merge tree c17f04f.
Scopes: core 38400 -> 38400 (+0), engine 73412 -> 73515 (+103),
api 3295 -> 3295 (+0). By crate: layerfs-sandbox +103.
Method: tools/production_loc.py (blob 686d269)
--root <exported tree> --json for both snapshots; first-party production
Rust, runtime SQL and JavaScript; tests, docs, tooling, generated and
third-party files excluded.
Reference absent (retired R9c; last counted 65,417 at tree 498dd19).
Deleted tests: none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yifanxuaaa

Copy link
Copy Markdown
Contributor

@jiangkoumo Merged into layerfs_v2_0_0 at this PR's head 45556f3db9 (merge commit 7d55f02), on top of B1 with no conflicts; the only adjustment is Some(...) for the now-optional Store volume in tests/native_provider.rs. Linux check: local_runtime and the other layerfs-sandbox tests pass. Closing here because this PR targets main. Thank you!

已合入 layerfs_v2_0_0,合入的是本 PR 的头 45556f3db9(合并提交 7d55f02),叠在 B1 之上,没有冲突;唯一的调整是 tests/native_provider.rs 里的 Store volume 改为 Some(...)(该字段在 v2 上是可选的)。Linux 验证:local_runtime 及 layerfs-sandbox 的其余测试通过。本 PR 的目标分支是 main,因此在此关闭。感谢!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants