Go 1.28 给子进程加 Landlock:在 fork 与 execve 之间收紧权限
Landlock 是 Linux 5.13 引入的安全特性,属于 Linux Security Modules(LSM)框架的一部分,只在 Linux 上可用。普通进程不需要 root 就能用它限制自己访问文件、目录等资源;较新的内核还支持部分网络限制。限制通过 ruleset 描述:控制哪些操作、在哪些位置允许。Landlock 是 allowlist 模型——ruleset 处理了某个操作但没有显式允许,该操作就会被拒绝。它只能收紧已有权限,不能授予新权限。规则一旦应用,会被之后创建的子进程继承。
在 Go 1.28 之前,go-landlock 这类库和 Landrun 这类工具已经支持 Landlock,难点在于只限制子进程、让父进程保持不受限。Landlock 正常限制的是调用线程及其后代,而 Go 运行时管理多个 OS 线程,直接在某个 goroutine 里调用并不稳妥。常见做法是写一个单独的 launcher,在 exec 目标程序之前应用策略。Go 1.28 把这件事收进 syscall.SysProcAttr,新增四个字段:
NoNewPrivs:阻止通过执行获得新权限;UseLandlock:为子进程启用 Landlock 强制执行;LandlockFD:指定要应用的 ruleset;LandlockFlags:附加的执行标志。
Go 会在执行目标程序之前把 ruleset 应用到子进程,父进程不受影响。os/exec 和 os.StartProcess 都能用。注意 Go 只负责应用 ruleset,不负责创建:ruleset 仍要用 golang.org/x/sys/unix 调 SYS_LANDLOCK_CREATE_RULESET,或者通过 go-landlock 之类的库构建。
示例:禁止子进程创建目录
下面的程序启动一个 mkdir 子进程,并让这个子进程无法创建目录,同时父进程自己仍能正常创建目录。它使用开发中的 Go 1.28(预计 2027 年 2 月发布,API 可能变化)。
//go:build linux && go1.28
package main
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
)
func createRuleset() (int, error) {
attr := struct {
HandledAccessFS uint64
}{
HandledAccessFS: unix.LANDLOCK_ACCESS_FS_MAKE_DIR,
}
fd, _, errno := unix.Syscall(
unix.SYS_LANDLOCK_CREATE_RULESET,
uintptr(unsafe.Pointer(&attr)),
unsafe.Sizeof(attr),
0,
)
if errno != 0 {
return -1, errno
}
return int(fd), nil
}
func main() {
dir, err := os.MkdirTemp("", "landlock-demo-")
if err != nil {
log.Fatal(err)
}
defer os.RemoveAll(dir)
rulesetFD, err := createRuleset()
if err != nil {
log.Fatal(err)
}
defer unix.Close(rulesetFD)
cmd := exec.Command("mkdir", filepath.Join(dir, "from-child"))
cmd.SysProcAttr = &syscall.SysProcAttr{
NoNewPrivs: true,
UseLandlock: true,
LandlockFD: rulesetFD,
LandlockFlags: 0,
}
output, err := cmd.CombinedOutput()
if err == nil {
log.Fatal("Child unexpectedly created a directory")
}
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
log.Fatalf("Could not start child: %v", err)
}
fmt.Printf("Child: %s", output)
err = os.Mkdir(filepath.Join(dir, "from-parent"), 0o700)
if err != nil {
log.Fatal(err)
}
fmt.Println("Parent: directory created successfully")
}
createRuleset 创建的 ruleset 处理 LANDLOCK_ACCESS_FS_MAKE_DIR。因为没有规则允许这个操作,子进程创建目录会被拒绝;其他文件系统操作不受这个 ruleset 限制。NoNewPrivs 阻止通过执行提权,也满足非特权 Landlock 执行的常见要求。UseLandlock 告诉 Go 应用 LandlockFD 指向的 ruleset,应用发生在 exec mkdir 之前。
运行与预期输出
需要启用 Landlock 的 Linux、Go 1.28 开发工具链,以及系统里的 mkdir。
go mod init example.com/landlock-demo
go get golang.org/x/sys/unix
go run .
在支持的系统上输出类似:
Child: mkdir: cannot create directory '/tmp/landlock-demo-87340035/from-child': Permission denied
Parent: directory created successfully
如果 Landlock 不被支持或未启用,创建 ruleset 会直接失败,而不会静默跳过限制继续执行。
收益与边界
这组字段把 Landlock 自然接入了 Go 的进程执行 API,不需要单独的 launcher,错误处理也直接:Go 无法执行所要求的策略时,启动子进程就会失败,目标程序不会在无限制状态下运行。一个实现正确的 launcher 能提供同样的保证;Go 1.28 并不会让 Landlock 本身更强,只是减少了进程管理代码。
内核支持仍然重要:Landlock 必须启用,可用的访问权限取决于内核的 Landlock ABI 版本。策略也由使用方负责——示例只禁止创建目录,并没有阻止读取敏感文件。Landlock 不是完整的沙箱:部分操作不在其范围内,继承来的文件描述符仍然可以访问资源,按需求可能还需要 seccomp 等机制。如果确实需要沙箱,失败不能被忽略。
如果需求只是给某个子进程削减一小块能力,比如构建脚本不该写目录、插件进程只应访问某个只读路径,又不想引入 launcher 或容器,这组字段足够直接。需要多租户隔离、完整挂载视图、网络与资源限制时,由 namespace、cgroup、seccomp 组成的容器方案更合适;Landlock 本身不提供这些。
提案背景
golang/go#68595 提议在 Linux 上扩展 syscall.SysProcAttr,加入 NoNewPrivs、UseLandlock、LandlockFD、LandlockFlags。子进程执行期间,在 fork(2) 与 execve(2) 之间,如果设置了 UseLandlock,就调用 landlock_restrict_self(ruleset_fd, flags);如果设置了 NoNewPrivs,就调用 prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)。策略构建(landlock_create_ruleset + landlock_add_rule)由调用方 Go 进程完成,可以手工做,也可以通过 go-landlock 之类的库。传给 landlock_restrict_self 的参数与它本身一致,因此 Landlock LSM 新增标志时不需要改动这层接口。
没有这个提案时,要在 Landlock 沙箱中运行子进程,只能把整个调用进程放进同一个沙箱,让子进程继承限制——这并不总是想要的结果。