@@ -4,34 +4,51 @@ package tools
44
55import (
66 "context"
7+ "os"
78 "os/exec"
9+ "path/filepath"
10+ "strings"
811 "sync"
12+ "syscall"
913 "time"
14+
15+ "github.com/landlock-lsm/go-landlock/landlock"
16+ llsys "github.com/landlock-lsm/go-landlock/landlock/syscall"
1017)
1118
12- // Linux native 隔离:用 bubblewrap(bwrap)。
13- // 根挂只读、workspace+临时+缓存挂可写、PID/UTS/IPC namespace 隔进程(看不到也杀不到 host 进程)。
14- // 网络保持开(不 --unshare-net)。bwrap 不在则退回裸 shell(由调用方决定是否套软黑名单)。
19+ // Linux native 隔离,三级择优:
20+ // 1. bubblewrap(bwrap):根只读 + 可写目录叠加 + PID/UTS/IPC namespace 隔进程。最强。
21+ // 2. Landlock(内核 ≥5.13):纯文件写禁闭(无进程隔离),按路径授权、不改文件标签。无需装任何东西。
22+ // 3. 都没有 → 退软黑名单(由 SandboxCheck 调 nativePolicyCheck)。
23+ // 网络始终开(否则 go mod / npm / git fetch 全断)。读不限,只禁"写到 workspace 外"。
1524//
16- // 没装 bwrap 的退路:目前直接退回软策略;landlock(内核 ≥5.13)做纯 FS 限制是后续可选项。
25+ // Landlock 的限制一旦施加于某进程便不可逆,所以不能加在长驻的 deepx 上,只能加在"将要执行命令的那个
26+ // 进程"里。做法是 re-exec 跳板:nativeShellCmd 让命令以「deepx 自身 + 一组 env 标记」启动,启动后的
27+ // deepx 在 main() 最早处(RunSandboxTrampolineIfRequested)识别标记 → 施加 Landlock → exec 真正的
28+ // sh -c <命令>。Landlock 限制随 execve 保留,从而约束到命令本身及其子进程。
29+
30+ const (
31+ sbxTrampolineEnv = "DEEPX_SBX_LANDLOCK" // =1 标记本进程是 Landlock 跳板
32+ sbxWritableEnv = "DEEPX_SBX_WRITABLE" // 可写根列表(PathListSeparator 分隔)
33+ sbxCmdEnv = "DEEPX_SBX_CMD" // 要执行的 shell 命令
34+ sbxCwdEnv = "DEEPX_SBX_CWD" // 工作目录
35+ )
1736
1837var (
1938 bwrapProbeOnce sync.Once
2039 bwrapProbeOK bool
40+ llProbeOnce sync.Once
41+ llProbeOK bool
2142)
2243
23- // nativeIsolationAvailable 报告本机能否做 native OS 隔离。
24- // 不只查 bwrap 是否存在,而是**实跑一个极简沙箱确认它真能用**——很多发行版禁用了非特权
25- // user namespace,bwrap 装了也会在运行时报错。探测一次、整会话缓存。
26- // 探测失败 → 一致退回软策略(裸 shell + 黑名单,状态面板显示"软策略")。
27- func nativeIsolationAvailable () bool {
44+ // bwrapAvailable 实跑一个极简 bwrap 沙箱确认真能用(很多发行版禁用非特权 userns,装了也运行时报错)。
45+ func bwrapAvailable () bool {
2846 bwrapProbeOnce .Do (func () {
2947 if _ , err := exec .LookPath ("bwrap" ); err != nil {
3048 return
3149 }
3250 ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Second )
3351 defer cancel ()
34- // 跑一个最小沙箱(含我们实际会用的 --unshare-pid):userns 被禁则此处失败
3552 err := exec .CommandContext (ctx , "bwrap" ,
3653 "--ro-bind" , "/" , "/" , "--proc" , "/proc" , "--unshare-pid" ,
3754 "sh" , "-c" , ":" ).Run ()
@@ -40,29 +57,114 @@ func nativeIsolationAvailable() bool {
4057 return bwrapProbeOK
4158}
4259
43- // nativeShellCmd 构造在 bwrap 沙箱里跑命令的 *exec.Cmd;bwrap 不可用则退回裸 shell。
60+ // landlockAvailable 仅查询内核 Landlock ABI 版本(纯探测,不施加任何限制)。≥1 即支持。
61+ func landlockAvailable () bool {
62+ llProbeOnce .Do (func () {
63+ if v , err := llsys .LandlockGetABIVersion (); err == nil && v >= 1 {
64+ llProbeOK = true
65+ }
66+ })
67+ return llProbeOK
68+ }
69+
70+ // nativeIsolationAvailable 报告本机能否做 native OS 隔离(bwrap 或 Landlock 任一)。
71+ // 都没有 → false,SandboxCheck 退软黑名单。探测各缓存一次。
72+ func nativeIsolationAvailable () bool {
73+ return bwrapAvailable () || landlockAvailable ()
74+ }
75+
76+ // nativeShellCmd 按优先级构造隔离命令:bwrap > Landlock > 裸 shell。
4477func nativeShellCmd (command , cwd string ) * exec.Cmd {
45- if ! nativeIsolationAvailable () {
46- return plainShellCmd (command , cwd )
78+ if bwrapAvailable () {
79+ return bwrapShellCmd (command , cwd )
80+ }
81+ if landlockAvailable () {
82+ if c := landlockShellCmd (command , cwd ); c != nil {
83+ return c
84+ }
4785 }
86+ return plainShellCmd (command , cwd )
87+ }
88+
89+ // bwrapShellCmd 构造在 bwrap 沙箱里跑命令的 *exec.Cmd。
90+ func bwrapShellCmd (command , cwd string ) * exec.Cmd {
4891 args := []string {
4992 "--ro-bind" , "/" , "/" , // 整个根只读
5093 "--dev" , "/dev" , // 干净的 /dev
5194 "--proc" , "/proc" , // 配合 PID namespace 的新 /proc
5295 "--unshare-pid" , "--unshare-uts" , "--unshare-ipc" , // 进程隔离
5396 "--die-with-parent" , // deepx 退出则沙箱进程一起死
5497 }
55- // 可写目录:在只读根之上叠加可写绑定(workspace + 临时 + 缓存)。
56- // 用 --bind-try 而非 --bind:候选里含 macOS 专属路径(/private/tmp、~/Library/Caches),
57- // 这些在 Linux 上不存在,普通 --bind 绑定不存在的 source 会致命报错
58- // "bwrap: Can't find source path ..." 导致每条命令都挂(见 issue #68)。--bind-try 跳过即可。
98+ // 可写目录:在只读根之上叠加可写绑定。用 --bind-try 而非 --bind:候选含 macOS 专属路径
99+ // (/private/tmp、~/Library/Caches),Linux 上不存在,普通 --bind 绑不存在的 source 会致命报错。
59100 for _ , p := range nativeWritableRoots (cwd ) {
60101 args = append (args , "--bind-try" , p , p )
61102 }
62103 if cwd != "" {
63104 args = append (args , "--chdir" , cwd )
64105 }
65106 args = append (args , "sh" , "-c" , command )
66- // cwd 通过 --chdir 传入沙箱;不设 c.Dir(宿主侧),避免与 bind 语义冲突
67107 return exec .Command ("bwrap" , args ... )
68108}
109+
110+ // landlockShellCmd 以 deepx 自身作 re-exec 跳板,带 env 标记启动;跳板进程负责施加 Landlock 再 exec 真命令。
111+ func landlockShellCmd (command , cwd string ) * exec.Cmd {
112+ exe , err := os .Executable ()
113+ if err != nil {
114+ return nil // 拿不到自身路径就放弃 Landlock,退裸 shell
115+ }
116+ c := exec .Command (exe )
117+ c .Env = append (os .Environ (),
118+ sbxTrampolineEnv + "=1" ,
119+ sbxWritableEnv + "=" + strings .Join (nativeWritableRoots (cwd ), string (os .PathListSeparator )),
120+ sbxCmdEnv + "=" + command ,
121+ sbxCwdEnv + "=" + cwd ,
122+ )
123+ return c
124+ }
125+
126+ // RunSandboxTrampolineIfRequested 必须在 main() 最早处调用。
127+ // 若本进程带 Landlock 跳板标记:施加"读全局 / 只写可写根"的 Landlock 写禁闭,然后 exec sh -c <命令>,
128+ // 永不返回。否则立即返回,deepx 正常启动。
129+ func RunSandboxTrampolineIfRequested () {
130+ if os .Getenv (sbxTrampolineEnv ) != "1" {
131+ return
132+ }
133+ cwd := os .Getenv (sbxCwdEnv )
134+ command := os .Getenv (sbxCmdEnv )
135+ var roots []string
136+ if w := os .Getenv (sbxWritableEnv ); w != "" {
137+ roots = filepath .SplitList (w )
138+ }
139+ if cwd != "" {
140+ _ = os .Chdir (cwd )
141+ }
142+
143+ // 读全局(RODirs 含执行权限,能跑二进制),只写可写根。缓存目录可能不存在 → IgnoreIfMissing。
144+ rules := []landlock.Rule {landlock .RODirs ("/" )}
145+ for _ , r := range roots {
146+ if r != "" {
147+ rules = append (rules , landlock .RWDirs (r ).IgnoreIfMissing ())
148+ }
149+ }
150+ // BestEffort:内核版本不够则尽力而为,绝不因 Landlock 失败而拒跑命令(最坏退化为不隔离)。
151+ _ = landlock .V5 .BestEffort ().RestrictPaths (rules ... )
152+
153+ // 清掉跳板自用的 env,避免泄漏给子命令(否则子命令里再起 deepx 会被误判为跳板)。
154+ env := make ([]string , 0 , len (os .Environ ()))
155+ for _ , kv := range os .Environ () {
156+ if strings .HasPrefix (kv , sbxTrampolineEnv + "=" ) || strings .HasPrefix (kv , sbxWritableEnv + "=" ) ||
157+ strings .HasPrefix (kv , sbxCmdEnv + "=" ) || strings .HasPrefix (kv , sbxCwdEnv + "=" ) {
158+ continue
159+ }
160+ env = append (env , kv )
161+ }
162+
163+ sh , err := exec .LookPath ("sh" )
164+ if err != nil {
165+ sh = "/bin/sh"
166+ }
167+ // exec 替换当前进程映像;Landlock 域随 execve 保留 → 真正约束 sh 及其后代。
168+ _ = syscall .Exec (sh , []string {"sh" , "-c" , command }, env )
169+ os .Exit (127 ) // 只有 exec 失败才会走到这
170+ }
0 commit comments