@@ -4,16 +4,18 @@ package tools
44
55import (
66 "context"
7+ "fmt"
78 "os"
89 "os/exec"
910 "path/filepath"
11+ "runtime"
1012 "strings"
1113 "sync"
1214 "syscall"
1315 "time"
16+ "unsafe"
1417
15- "github.com/landlock-lsm/go-landlock/landlock"
16- llsys "github.com/landlock-lsm/go-landlock/landlock/syscall"
18+ "golang.org/x/sys/unix"
1719)
1820
1921// Linux native 隔离,三级择优:
@@ -60,13 +62,23 @@ func bwrapAvailable() bool {
6062// landlockAvailable 仅查询内核 Landlock ABI 版本(纯探测,不施加任何限制)。≥1 即支持。
6163func landlockAvailable () bool {
6264 llProbeOnce .Do (func () {
63- if v , err := llsys .LandlockGetABIVersion (); err == nil && v >= 1 {
64- llProbeOK = true
65- }
65+ llProbeOK = landlockABIVersion () >= 1
6666 })
6767 return llProbeOK
6868}
6969
70+ // landlockABIVersion 用裸 syscall 查询内核 Landlock ABI 版本,返回 0 表示不支持
71+ // (内核 <5.13 / 未编进 / 未在 cmdline 启用)。landlock_create_ruleset 在 attr=NULL、size=0、
72+ // flags=LANDLOCK_CREATE_RULESET_VERSION 时直接返回 ABI 版本号(正整数)而非 fd。
73+ // 走标准库 syscall(仅当前线程),不经 go-landlock/libpsx 的全线程同步,故 cgo/iscgo 下也安全。
74+ func landlockABIVersion () int {
75+ r , _ , e := syscall .Syscall (uintptr (unix .SYS_LANDLOCK_CREATE_RULESET ), 0 , 0 , unix .LANDLOCK_CREATE_RULESET_VERSION )
76+ if e != 0 {
77+ return 0
78+ }
79+ return int (r )
80+ }
81+
7082// nativeIsolationAvailable 报告本机能否做 native OS 隔离(bwrap 或 Landlock 任一)。
7183// 都没有 → false,SandboxCheck 退软黑名单。探测各缓存一次。
7284func nativeIsolationAvailable () bool {
@@ -140,15 +152,14 @@ func RunSandboxTrampolineIfRequested() {
140152 _ = os .Chdir (cwd )
141153 }
142154
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 ... )
155+ // 关键:把"prctl(NO_NEW_PRIVS) → landlock_restrict_self → execve"钉在同一 OS 线程上。
156+ // Landlock 域与 no_new_privs 都是线程属性、随 execve 继承;execve 会终止其余线程并以本线程映像
157+ // 替换整个进程,故只需限制本线程即可约束到真正的命令及其后代 —— 无需 go-landlock 的全线程同步,
158+ // 从而彻底避开 cgo/iscgo 下 runtime 拒绝 doAllThreadsSyscall 的 panic(issue #138)。不解锁(execve 永不返回)。
159+ runtime .LockOSThread ()
160+
161+ // BestEffort:Landlock 不可用 / 施加失败一律吞掉,绝不因此拒跑命令(最坏退化为不隔离)。
162+ _ = applyLandlockRestrict (roots )
152163
153164 // 清掉跳板自用的 env,避免泄漏给子命令(否则子命令里再起 deepx 会被误判为跳板)。
154165 env := make ([]string , 0 , len (os .Environ ()))
@@ -168,3 +179,102 @@ func RunSandboxTrampolineIfRequested() {
168179 _ = syscall .Exec (sh , []string {"sh" , "-c" , command }, env )
169180 os .Exit (127 ) // 只有 exec 失败才会走到这
170181}
182+
183+ // === Landlock 裸 syscall 施加 ===
184+ //
185+ // 不用 go-landlock 库:它在内核 ABI <V8 时必须把 PR_SET_NO_NEW_PRIVS 经 libpsx 同步到所有 OS 线程,
186+ // 而 Go runtime 在 cgo/iscgo=true(deepx 因 purego 动态链接正是如此)下拒绝该全线程 syscall 并 panic
187+ // (issue #138)。这里改用标准库 syscall 在当前(已 LockOSThread 的)线程上直接施加 —— 因跳板施加完
188+ // 立刻 execve、Landlock 域随 execve 继承,单线程足矣,且彻底不碰 libpsx。
189+ //
190+ // 参数结构体 / 常量取自 golang.org/x/sys/unix(内核 <linux/landlock.h> 的官方映射)。
191+
192+ // landlockFSReadExec 是只读根 / 授予的访问位:执行 + 读文件 + 列目录,不含任何写 / 改位。
193+ const landlockFSReadExec = unix .LANDLOCK_ACCESS_FS_EXECUTE |
194+ unix .LANDLOCK_ACCESS_FS_READ_FILE |
195+ unix .LANDLOCK_ACCESS_FS_READ_DIR
196+
197+ // landlockHandledFS 返回该 ABI 下要"接管"(handle)的全部 FS 访问位。
198+ // 只请求内核认识的位:V1 的 13 个基础位,Refer(≥V2)/Truncate(≥V3)/IoctlDev(≥V5)按版本叠加。
199+ // 请求子集永不 EINVAL;授给可写根时用这整套(含写),授给只读根时只给其中的读 / 执行子集。
200+ func landlockHandledFS (abi int ) uint64 {
201+ var m uint64 = unix .LANDLOCK_ACCESS_FS_EXECUTE |
202+ unix .LANDLOCK_ACCESS_FS_WRITE_FILE |
203+ unix .LANDLOCK_ACCESS_FS_READ_FILE |
204+ unix .LANDLOCK_ACCESS_FS_READ_DIR |
205+ unix .LANDLOCK_ACCESS_FS_REMOVE_DIR |
206+ unix .LANDLOCK_ACCESS_FS_REMOVE_FILE |
207+ unix .LANDLOCK_ACCESS_FS_MAKE_CHAR |
208+ unix .LANDLOCK_ACCESS_FS_MAKE_DIR |
209+ unix .LANDLOCK_ACCESS_FS_MAKE_REG |
210+ unix .LANDLOCK_ACCESS_FS_MAKE_SOCK |
211+ unix .LANDLOCK_ACCESS_FS_MAKE_FIFO |
212+ unix .LANDLOCK_ACCESS_FS_MAKE_BLOCK |
213+ unix .LANDLOCK_ACCESS_FS_MAKE_SYM
214+ if abi >= 2 {
215+ m |= unix .LANDLOCK_ACCESS_FS_REFER
216+ }
217+ if abi >= 3 {
218+ m |= unix .LANDLOCK_ACCESS_FS_TRUNCATE
219+ }
220+ if abi >= 5 {
221+ m |= unix .LANDLOCK_ACCESS_FS_IOCTL_DEV
222+ }
223+ return m
224+ }
225+
226+ // applyLandlockRestrict 在当前 OS 线程施加 Landlock 文件写禁闭:读 / 执行整个根可,只写 writable 根。
227+ // 必须在 runtime.LockOSThread() 之后、execve 之前调用。任何一步失败都返回 error(调用方按 BestEffort 吞掉)。
228+ func applyLandlockRestrict (writable []string ) error {
229+ abi := landlockABIVersion ()
230+ if abi < 1 {
231+ return fmt .Errorf ("landlock 不可用" )
232+ }
233+ handled := landlockHandledFS (abi )
234+
235+ attr := unix.LandlockRulesetAttr {Access_fs : handled }
236+ fd , _ , e := syscall .Syscall (uintptr (unix .SYS_LANDLOCK_CREATE_RULESET ),
237+ uintptr (unsafe .Pointer (& attr )), unsafe .Sizeof (attr ), 0 )
238+ if e != 0 {
239+ return fmt .Errorf ("landlock_create_ruleset: %v" , e )
240+ }
241+ defer syscall .Close (int (fd ))
242+
243+ // 读 / 执行整个根(不含写位),保证能跑二进制、读任意文件。
244+ if err := landlockAddPathRule (int (fd ), "/" , landlockFSReadExec ); err != nil {
245+ return err
246+ }
247+ // 可写根:授全部已接管的位(含写)。打不开的目录(不存在等)跳过 —— 等价 IgnoreIfMissing。
248+ for _ , dir := range writable {
249+ if dir == "" {
250+ continue
251+ }
252+ _ = landlockAddPathRule (int (fd ), dir , handled )
253+ }
254+
255+ // 非 root 下 landlock_restrict_self 要求先置 no_new_privs;单线程 prctl 即可(不经 libpsx)。
256+ if err := unix .Prctl (unix .PR_SET_NO_NEW_PRIVS , 1 , 0 , 0 , 0 ); err != nil {
257+ return fmt .Errorf ("prctl(NO_NEW_PRIVS): %v" , err )
258+ }
259+ if _ , _ , e := syscall .Syscall (uintptr (unix .SYS_LANDLOCK_RESTRICT_SELF ), fd , 0 , 0 ); e != 0 {
260+ return fmt .Errorf ("landlock_restrict_self: %v" , e )
261+ }
262+ return nil
263+ }
264+
265+ // landlockAddPathRule 给 ruleset 加一条 path_beneath 规则:dir 子树允许 access 指定的访问位。
266+ // 用 O_PATH 打开目录(轻量、不需读权限),fd 仅供本次 add_rule 引用。
267+ func landlockAddPathRule (rulesetFd int , dir string , access uint64 ) error {
268+ dirFd , err := unix .Open (dir , unix .O_PATH | unix .O_CLOEXEC , 0 )
269+ if err != nil {
270+ return fmt .Errorf ("open %q: %v" , dir , err )
271+ }
272+ defer unix .Close (dirFd )
273+ attr := unix.LandlockPathBeneathAttr {Allowed_access : access , Parent_fd : int32 (dirFd )}
274+ if _ , _ , e := syscall .Syscall6 (uintptr (unix .SYS_LANDLOCK_ADD_RULE ),
275+ uintptr (rulesetFd ), unix .LANDLOCK_RULE_PATH_BENEATH ,
276+ uintptr (unsafe .Pointer (& attr )), 0 , 0 , 0 ); e != 0 {
277+ return fmt .Errorf ("landlock_add_rule %q: %v" , dir , e )
278+ }
279+ return nil
280+ }
0 commit comments