Skip to content

Commit cdf6fa4

Browse files
committed
🐛 fix landlock panic
1 parent 9224afe commit cdf6fa4

4 files changed

Lines changed: 218 additions & 21 deletions

File tree

go.mod

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ require (
1010
github.com/charmbracelet/x/ansi v0.11.7
1111
github.com/fastschema/qjs v0.0.6
1212
github.com/getcharzp/onnxruntime_purego v1.24.0
13-
github.com/landlock-lsm/go-landlock v0.8.1
1413
github.com/odvcencio/gotreesitter v0.19.1
1514
github.com/tiktoken-go/tokenizer v0.7.0
15+
golang.org/x/sys v0.45.0
1616
golang.org/x/tools v0.45.0
1717
gopkg.in/yaml.v3 v3.0.1
1818
)
@@ -46,7 +46,5 @@ require (
4646
golang.org/x/mod v0.36.0 // indirect
4747
golang.org/x/net v0.54.0 // indirect
4848
golang.org/x/sync v0.20.0 // indirect
49-
golang.org/x/sys v0.45.0 // indirect
5049
golang.org/x/text v0.37.0 // indirect
51-
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect
5250
)

go.sum

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
6161
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
6262
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
6363
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
64-
github.com/landlock-lsm/go-landlock v0.8.1 h1:Krs1co16IzN7bQcFYIdtNF+BKwZem3geRBkVsZtlCKU=
65-
github.com/landlock-lsm/go-landlock v0.8.1/go.mod h1:mn5GSi81Jf7yMs5WSi+SUi4sUeNLUGVdbT4Id6wXNQw=
6664
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
6765
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
6866
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
@@ -111,5 +109,3 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
111109
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
112110
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
113111
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
114-
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA=
115-
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=

tools/sandbox_native_linux.go

Lines changed: 124 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ package tools
44

55
import (
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 即支持。
6163
func 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 退软黑名单。探测各缓存一次。
7284
func 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+
}

tools/sandbox_native_linux_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
//go:build linux
2+
3+
package tools
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
10+
"golang.org/x/sys/unix"
11+
)
12+
13+
// TestMain 复刻 deepx main() 的最早一步:带跳板标记的子进程走 Landlock 跳板,施加后 exec 真命令、永不返回。
14+
// 没有这一步,下面端到端测试 re-exec 出来的子测试进程就不会进跳板(issue #138 的修复点正是这条路径)。
15+
func TestMain(m *testing.M) {
16+
RunSandboxTrampolineIfRequested()
17+
os.Exit(m.Run())
18+
}
19+
20+
// landlockHandledFS 的掩码必须与内核 ABI 逐级对齐(对照 go-landlock 的 abiInfos:
21+
// V1=(1<<13)-1、V2=(1<<14)-1、V3=(1<<15)-1、V5=(1<<16)-1)。写错一位 = 要么 EINVAL 要么沙箱漏写。
22+
func TestLandlockHandledFSMask(t *testing.T) {
23+
cases := []struct {
24+
abi int
25+
want uint64
26+
}{
27+
{1, 0x1fff}, // 13 个基础位
28+
{2, 0x3fff}, // + REFER
29+
{3, 0x7fff}, // + TRUNCATE
30+
{4, 0x7fff}, // V4 只加了 net,FS 位不变
31+
{5, 0xffff}, // + IOCTL_DEV
32+
{8, 0xffff}, // 更高版本 FS 位不再增加
33+
}
34+
for _, c := range cases {
35+
if got := landlockHandledFS(c.abi); got != c.want {
36+
t.Errorf("landlockHandledFS(%d) = %#x, want %#x", c.abi, got, c.want)
37+
}
38+
}
39+
// 只读根的访问位必须是"读/执行",且不含任何写 / 改 / refer / truncate 位。
40+
writeBits := landlockHandledFS(8) &^ (unix.LANDLOCK_ACCESS_FS_EXECUTE |
41+
unix.LANDLOCK_ACCESS_FS_READ_FILE | unix.LANDLOCK_ACCESS_FS_READ_DIR)
42+
if landlockFSReadExec&writeBits != 0 {
43+
t.Errorf("只读根访问位 %#x 不应含任何写位", landlockFSReadExec)
44+
}
45+
}
46+
47+
// TestLandlockTrampolineConfinesWrites 端到端验证 issue #138 的修复:
48+
// 通过 Landlock 跳板跑命令,workspace 内可写、workspace 外被内核拒绝,且全程不 panic。
49+
// 仅在内核支持 Landlock 时跑;否则跳过(CI / 老内核 / 容器未开)。
50+
func TestLandlockTrampolineConfinesWrites(t *testing.T) {
51+
if !landlockAvailable() {
52+
t.Skip("内核无 Landlock 支持,跳过端到端测试")
53+
}
54+
55+
ws := t.TempDir() // workspace = 可写根(也作 cwd)
56+
57+
// "workspace 外"目录:放在 $HOME 根下(我们的策略里 ~ 只读、只有 ~/.cache 等子目录可写),
58+
// 而非 /tmp(/tmp 是可写根,放那测不出禁闭)。
59+
home, err := os.UserHomeDir()
60+
if err != nil {
61+
t.Skipf("拿不到 HOME,跳过: %v", err)
62+
}
63+
outside, err := os.MkdirTemp(home, ".deepx-ll-test-")
64+
if err != nil {
65+
t.Skipf("无法在 HOME 下建测试目录,跳过: %v", err)
66+
}
67+
defer os.RemoveAll(outside)
68+
69+
run := func(command string) error {
70+
c := landlockShellCmd(command, ws) // 直接走 Landlock 跳板,绕过 bwrap 优先级
71+
if c == nil {
72+
t.Fatal("landlockShellCmd 返回 nil")
73+
}
74+
return c.Run()
75+
}
76+
77+
// 1) workspace 内写:应成功。
78+
if err := run("echo hi > inside.txt"); err != nil {
79+
t.Fatalf("workspace 内写应成功: %v", err)
80+
}
81+
if _, err := os.Stat(filepath.Join(ws, "inside.txt")); err != nil {
82+
t.Fatalf("inside.txt 应已创建: %v", err)
83+
}
84+
85+
// 2) workspace 外写:应被 Landlock 拒绝(命令非零退出 + 文件不存在)。
86+
escape := filepath.Join(outside, "escape.txt")
87+
if err := run("echo hi > " + escape); err == nil {
88+
t.Fatalf("workspace 外写应被拒,却成功了: %s", escape)
89+
}
90+
if _, err := os.Stat(escape); err == nil {
91+
t.Fatalf("escape.txt 不该被创建 —— 越界写未被拦住")
92+
}
93+
}

0 commit comments

Comments
 (0)