
字节笔记本
2026年10月24日 · 约 9 分钟读完
目录囚禁:Go 1.24 的 os.Root

本文是《从 0 写 Agent》系列第 31 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「os.Root:Go 1.24 的目录囚禁」。
上一章我们讲到
os.Root做 skill 文件读取的安全限制。这一章深入它,为什么传统 path 清洗不够,os.Root 怎么在内核层面防住路径逃逸。这是 Go 1.24 引入的安全编程新工具,理解它能让你避免一类严重的安全漏洞。
引子:路径遍历攻击
让 Agent 读文件,最怕的攻击是路径遍历(Path Traversal),用户或模型构造一个路径,逃出你限定的目录:
你限定在 skills/ 目录读文件
模型调用 read_skill_file("../../etc/passwd")
如果没防护 → 读到了系统密码文件!这是 OWASP Top 10 的经典漏洞。看起来简单,但防护很难做对。

为什么传统 path 清洗不够
最朴素的防护,检查路径里有没有 ..:
// 朴素防护(不安全!)
func safeRead(base, path string) []byte {
if strings.Contains(path, "..") {
return nil // 拒绝
}
return os.ReadFile(filepath.Join(base, path))
}这能防住直接 ../../etc/passwd,但防不住这些:
-
Symlink 逃逸,
skills/evil/SKILL.md是个符号链接,指向/etc/passwd。路径里没..,但读到的还是系统文件。 -
URL 编码,
..可能被编码成%2e%2e,某些解析路径会还原。 -
Windows 分隔符,
..\..\etc\passwd在 Windows 上有效,但strings.Contains(path, "..")可能因为只查/..而漏掉。 -
Null 字节,老漏洞
skills/good\0../../etc/passwd,某些 C 库会在 null 处截断。 -
混合大小写(Windows),
Skills/vsskills/。
手动清洗永远可能漏。 每补一个洞,又发现新的绕过方式。安全圈的经验,不要手动清洗路径,用系统提供的机制。
os.Root:内核级囚禁
Go 1.24 引入 os.Root(os.OpenRoot),从根本上解决路径遍历:
root, err := os.OpenRoot("skills/") // 打开一个"根"
// 之后所有操作通过 root,永远逃不出 skills/
data, err := root.ReadFile("write_pr/SKILL.md") // ✅ 正常
data, err := root.ReadFile("../../etc/passwd") // ❌ 直接报错
data, err := root.ReadFile("evil/SKILL.md") // ❌ 如果是 symlink 逃逸,报错关键,os.Root 在每次文件操作时,解析路径并检查"最终路径是否仍在根目录内"。包括 symlink 的解析,如果 symlink 指向根外,操作被拒绝。
实现原理
os.Root 内部用 openat2(Linux 5.6+)或等价系统调用,这是内核提供的路径解析限制。在解析路径的每一步,内核检查"这一步有没有走出根目录"。
因为是在内核层面,所有绕过手法(symlink、编码、null 字节)都被防住,内核解析路径时这些花招无效。
跨平台
os.Root 在不支持 openat2 的系统上,Go 运行时用用户态的"安全 path 解析"模拟(带 symlink 检查)。不如内核级彻底,但比手写清洗强得多。Linux 5.6+ 上是内核级,最安全。
agents-go 的用法
skill 的 ReadFileTool(skills.go:179)用 os.Root:
func ReadFileTool(root *os.Root) agents.Tool {
return agents.NewFunctionTool("read_skill_file", "...", func(ctx, tc, args) (any, error) {
data, err := root.ReadFile(args.Path) // os.Root 保证安全
if err != nil {
return nil, err // 路径逃逸 → 这里报错
}
// ...
})
}构造 root 的地方:
root, _ := os.OpenRoot(skillDir)
tool := skills.ReadFileTool(root)模型传什么路径都安全,root.ReadFile 内部检查,逃逸路径直接 error。模型不会读到 skills 目录外的任何东西。
设计权衡:os.Root vs chroot
有人会想到 Unix 的 chroot,把进程"关"在一个目录里。os.Root 和 chroot 的区别:
| os.Root | chroot | |
|---|---|---|
| 级别 | 文件操作级 | 进程级 |
| 需要 root 权限 | 否 | 是(chroot 需要 root) |
| 影响范围 | 只有通过 root 对象的操作 | 整个进程的所有文件操作 |
| 副作用 | 无 | 进程的根目录变了,影响所有相对路径 |
os.Root 更轻量、更安全(不需要 root 权限)、更精确(只影响指定的操作)。 它是"文件操作级的沙箱",不是"进程级的监狱"。
对 Agent 这种"只需要限制某个工具的文件访问"的场景,os.Root 是完美选择,不影响进程其他部分,精准限制工具的读取范围。
os.Root 的其他好处
除了安全,os.Root 还有:
- 清晰的 API,
root.ReadFile、root.ReadDir等,和 os 包 API 一致,迁移成本低 - 可组合,可以 OpenRoot 多个不同目录,给不同工具不同范围
- 测试友好,测试时 OpenRoot 一个临时目录,测试完删除,不污染
小结
读完这一章,你应该理解:
- 路径遍历是经典漏洞,
../../etc/passwd、symlink 逃逸 - 手动清洗不够,symlink、编码、null 字节、平台差异,总有漏
- os.Root 内核级囚禁,每次操作检查路径不逃出根,包括 symlink 解析
- 比 chroot 轻量,不需要 root 权限,只影响指定操作
- Go 1.24+ 才有,用旧版 Go 只能手写清洗(不推荐)
安全编程的铁律,永远不要手动清洗路径,用系统提供的机制(os.Root / openat2)。 agents-go 用 os.Root 保证 skill 文件读取的安全,这是 Go 安全编程的最佳实践。
下一篇
第 32 章,Sandbox 抽象:让 Agent 跑不可信代码。Agent 要能执行模型生成的代码(比如数据分析、自动化脚本),但代码不可信,必须隔离。agents-go 的 Sandbox 接口 + 三后端(local/docker/ssh)怎么设计?
本文是《从 0 写 Agent》系列第 31 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。

