ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

runc 中的 filepath-securejoin:从 SecureJoin 到 pathrs-lite 的容器路径安全演进

runc 中的 filepath-securejoin:从 SecureJoin 到 pathrs-lite 的容器路径安全演进 容器运行时云原生【免费下载链接】runcCLI tool for spawning and running containers according to the OCI specification项目地址https://gitcode.com/gh_mirrors/ru/runc点击查看免费下载本文基于当前 runc 仓库内 vendored 的 CHANGELOG.md 及其源码展开完整梳理github.com/cyphar/filepath-securejoin当前 vendored 版本v0.7.0见 go.mod从经典SecureJoin字符串级 API 到基于文件描述符的pathrs-lite新 API 的安全演进路线。读者将理解为什么把路径字符串拼在 root 后面在容器场景下是不安全的、runc 如何在构建期通过 build tag 在纯 Go 与 libpathrs 两种后端之间切换以及Openat2/EAGAIN重试、fsopen加固 procfs 等关键机制背后的设计与权衡。导读SecureJoin曾是容器运行时处理在 rootfs 内安全解析路径这一难题的标准答案但它本质上只是一个字符串级的防御手段返回的路径在后续被使用时仍可能被攻击者通过替换路径组件TOCTOU 竞态劫持。runc 仓库内 vendored 的filepath-securejoin在 0.3.0 之后引入了基于*os.File句柄的pathrs-lite子包把解析路径与使用路径合并为一次原子性的内核解析操作openat2(RESOLVE_IN_ROOT)从根本上消除了该竞态窗口。本文以 CHANGELOG 的版本脉络为主线结合 join.go、internal/pathrs 等源码与 runc 内的真实调用点讲解这套机制的来龙去脉与落地方式。一、从 0.1.0 到 0.2.xSecureJoin 的诞生与修补1.1 最初的实现filepath-securejoin的 0.1.02017-07-19就包含了一个完整的SecureJoin实现当时测试覆盖率达到 93.5%。其设计初衷是作为filepath.Join的更安全版本把路径查找限制在某个 root 目录之内——这个想法最早源自容器运行时内部的私有实现并曾被提议合入 Go 标准库。SecureJoin的核心语义见 join.go 的文档注释可以总结为返回的字符串必须是 root 的子路径且其中不残留任何符号链接组件全部已展开展开符号链接时所有链接目标都相对于 root 解析等价于用户在用户态模拟chroot(2)的路径语义不存在的路径组件不受影响与filepath.EvalSymlinks语义一致因此悬挂符号链接可以部分解析返回路径永远经过filepath.Clean不含..组件。README 中甚至给出了一个平凡实现直接 fork 一个chrootreadlink --canonicalize-missing子进程来做路径展开。当然这个平凡实现需要 root 权限、且要求 root 内的readlink可信远不如库内的纯用户态实现可靠。1.2 0.2.x 时代的修补在 0.2.x 系列中CHANGELOG 记录了一系列针对SecureJoin的健壮性修复0.2.1引入自己的IsNotExist实现join.go正确处理ENOTDIR..穿过非目录组件时报ENOENT的复杂情形0.2.2符号链接循环的基础错误改为syscall.ELOOP让调用方可以用errors.Is判断0.2.3切换到 Go 1.13 风格的%w错误包装移除对github.com/pkg/errors的依赖0.2.4安全修复修复了 Windows 上可能生成 rootfs 之外路径的问题GHSA-6xv5-86q9-7xr8并开始支持带卷名的 Windows 路径0.2.5词法组件..、.的路径生成细节调整无行为变化并修正了符号链接循环错误中引用的路径。0.2.0 还引入了SecureJoinVFSAPI它允许通过可注入的 VFS 接口 做单元测试 mock或实现自定义的查找行为——例如 rootless 容器中因为没有CAP_DAC_READ_SEARCH/CAP_DAC_OVERRIDE访问模式特殊如无读权限的目录需要特殊处理。SecureJoin只是SecureJoinVFS传nilVFS 的薄封装。关键认识尽管SecureJoin在一层层修补其根本缺陷无法在字符串 API 内修复。README 明确指出该 API 对在SecureJoin返回后、调用方使用该路径前修改路径组件的攻击者根本性地不安全——这是典型的 TOCTOU 竞态SecureJoinVFS的注释也直言这个 API 从根本上是错的你无法返回一个安全的路径字符串并保证它之后不被篡改。二、0.3.0新 API 登场0.3.02024-07-11是路线图上的分水岭新增了一组基于*os.File句柄的 API它们从 libpathrs 移植而来把解析与使用合并为内核中的一次原子操作。runc 仓库自 0.3.x 起逐步跟进迁移。2.1 OpenInRoot / OpenatInRoot / Reopenfunc OpenInRoot(root, unsafePath string) (*os.File, error) func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) func Reopen(handle *os.File, flags int) (*os.File, error)OpenInRoot是下面这段先 SecureJoin 再 OpenFile代码的安全替代path, err : securejoin.SecureJoin(root, unsafePath) file, err : os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)二者的差异在于OpenInRoot返回的是O_PATH文件描述符它无法用于读写但可以作为后续Reopen的原料升级为常规句柄或直接用于*at系列操作。这种先拿 O_PATH 句柄、需要时再 Reopen的分裂设计是为了避免用户意外打开可能造成 DoS 的坏 inode同时保留了 PTY 派生等需要句柄能力的特性。OpenatInRoot允许以*os.File形式传入 root保证多次调用基于同一个 rootfs 句柄。2.2 MkdirAll / MkdirAllHandlefunc MkdirAll(root, unsafePath string, mode int) error func MkdirAllHandle(root *os.File, unsafePath string, mode int) (*os.File, error)MkdirAll是os.MkdirAll的安全版本MkdirAllHandle额外返回最终创建目录的句柄——它保证该句柄指向的目录与刚创建的目录完全一致这是单纯先MkdirAll再OpenatInRoot无法保证的。新 API 有一个与SecureJoin显著不同的语义README 中反复强调一旦遇到悬挂符号链接或不存在的路径立即报错不再像SecureJoin那样把不存在的组件当作真实目录继续处理。2.3 底层机制openat2 与加固 procfs新 API 之所以安全是因为它机会主义地使用了一批较新的内核能力详见 READMEopenat2(2)Linux 5.6所有查找操作优先走openat2利用RESOLVE_IN_ROOT在 rootfs 内高效解析符号链接并能限制 magic-link 与 bind-mount 的穿越fsopen(2)/open_tree(2)Linux 5.2与特权为有权限的程序提供针对恶意/proc挂载的额外加固——通过fsopen创建进程内私有的 procfs 实例来躲避对宿主机/proc的挂载攻击如 runc 历史漏洞 CVE-2024-21626 所属的攻击面。这套加固逻辑正是 0.5.0 中大篇幅讨论的主题详见下文第四节。三、0.4.x收紧 SecureJoin 的输入校验0.4.0BreakingSecureJoin(VFS)开始拒绝包含..组件的 root 路径。动机来自一个真实问题把/symlink/..作为 root 传入时拼接后的路径会被放进/尽管/symlink/..可能指向的是另一个目录。CHANGELOG 明确指出这是消除 foot-gun式的破坏性变更预计真实用户几乎不受影响并感谢 Erik Sjölund 最初将其作为潜在安全问题报告。0.4.1上述限制被证实过严回退为仅当路径含..组件时才报错同时仍建议用户对 root 先做filepath.Clean最好再做filepath.EvalSymlinks但琐碎的不洁路径不再被惩罚。这一实现对应 join.go 中的errUnsafeRoot与hasDotDot检查。同一版本中MkdirAll/MkdirHandle的模式参数从unix.S_*风格改为os.FileMode风格S_ISVTX需改用os.ModeStickyS_ISUID/S_ISGID被视为非法位此前传入也会报错只是错误信息不同。四、0.5.xpathrs-lite 正式分家与 EAGAIN 重试机制4.1 0.5.0API 迁入 pathrs-lite 子包0.5.02025-09-26是一个破坏性大版本0.3.0 引入的新 API 整体迁移到子包github.com/cyphar/filepath-securejoin/pathrs-lite以更明确地标示它是 libpathrs 的一个精简纯 Go 移植版pathrs-lite/README.md顶层包保留过渡用的 deprecated wrapper并在下一个小版本0.6.0中移除顶层 API 仍然以 BSD-3-Clause 授权而 pathrs-lite 子包改为MPL-2.0双授权详见 COPYING.mdCHANGELOG 对此次再授权给出了明确说明。同一版本还导出了大部分安全的procfs APIpathrs-lite/procfs子包OpenProcRoot返回/proc的安全句柄尽可能使用subsetpid保护并用fsopen(2)躲避挂载竞态OpenUnsafeProcRoot则不做subsetpid泄露风险更高普通用户应优先用前者(*procfs.Handle).Open*系列可安全获取/proc子路径的O_PATH句柄。其中OpenThreadSelf返回的ProcThreadSelfCloser必须在用完句柄后调用——因为 Go 是多线程的/proc/thread-self在不runtime.LockOSThread时可能消失该 Closer 等价于runtime.UnlockOSThreadProcSelfFdReadlink获取 fd 在内核中的路径表示类似readlink(/proc/self/fd/...)但会校验没有能欺骗进程的 tricky 覆盖挂载。注意它只是某一时刻的快照攻击者仍可能移动目标文件因此只能用作安全属性的次级验证不能作为某个句柄一定对应某条路径的证明。4.2 0.5.1openat2 的 EAGAIN 与重试上限0.5.1 修复了一个在真实负载下暴露的问题当内核在解析含..组件的路径时检测到并发 rename 或 mountopenat2会返回-EAGAIN这是内核为避免 DoS 的必要设计但要求用户态重试。旧实现重试 32 次就放弃在 16 核机器上模拟攻击者每核密集 rename 的最坏情况下runc 中实测有约3% 的失败率。该版本做了两处改进重试上限提高到128 次典型场景下 O_PATH 解析器的系统调用数也在同一量级同样的基准测试失败率降至约0.12%将unix.EAGAIN错误向上透传让有更严格要求的调用方自行实现无限重试循环建议配合基于时间的 deadline避免无界 DoS。在 runc 侧internal/pathrs封装了retryEAGAIN辅助函数MkdirAllInRoot、OpenInRoot、CreateInRoot等入口都会包裹该重试逻辑见 mkdirall_pathrslite.go 与 root_pathrslite.go。4.3 0.5.2修复缓存导致的错误降级0.5.2 修复了一个隐蔽 bug此前判断用openat2还是回退O_PATH解析器的逻辑会缓存探测结果以减少无谓的探测调用。但当pathrs-lite被一个会给自己施加新 seccomp-bpf 过滤器的程序使用时若过滤器拒绝了openat2缓存会导致该错误被直接返回而非回退到O_PATH解析器。修复方式是只在出错时缓存、成功时不缓存。另外还移除了一处openat2wrapper 在RESOLVE_IN_ROOT需要dup时的文件描述符泄漏。五、0.6.x 与 0.7.0移除旧 wrapper接入 libpathrs 后端5.1 0.6.0移除 deprecated wrapper0.6.02025-11-03把 0.5.0 中过渡用的顶层 wrapperMkdirAll、MkdirAllHandle、OpenInRoot、OpenatInRoot、Reopen全部移除明确要求用户直接使用pathrs-lite。这是 0.5.0 预告过的计划性破坏变更。5.2 0.6.0libpathrs 构建期后端0.6.0 的另一项重要新增pathrs-lite现在支持以 libpathrs 作为后端通过构建期 build taglibpathrs启用0.7.0 时对应cyphar.com/go-pathrsv0.2.5。纯 Go 的pathrs-lite与 libpathrs 后端功能等价有集成测试保证因此下游库可以在不关心 CGo 的情况下先用纯 Go 实现发行商再决定是否为整个二进制开启 libpathrs——迁移对用户无感。在 runc 仓库中这个 build tag 的痕迹清晰可见features_libpathrs.go//go:build libpathrs通过pathrs.LibraryVersion()提供版本字符串features_pathrslite.go//go:build !libpathrs在纯 Go 模式下返回空字符串go.mod 同时直接依赖cyphar.com/go-pathrs v0.2.5与github.com/cyphar/filepath-securejoin v0.7.0。5.3 0.7.0跟随 pathrs 上游的编译期变更0.7.02025-06-17的唯一条目cyphar.com/go-pathrs0.2.5包含一次编译期 API 破坏本库做了适配规避。库本身的 API 未变但使用libpathrsbuild tag 且已升级到 libpathrs v0.2.5 的用户必须同步升级到 filepath-securejoin v0.7.0。六、runc 中 pathrs 的落地internal/pathrs 封装与调用链runc 没有直接在业务代码里散落pathrs-lite调用而是集中在internal/pathrs封装Apache-2.0 授权作者 Aleksa Sarai / SUSE6.1 关键封装函数OpenInRootroot_pathrslite.gopathrs.OpenatInRoot取得 O_PATH 句柄后经Reopen升级为带指定 flags 的常规句柄CreateInRoot同文件 L52-L69先MkdirAllParentInRoot确保父目录存在再以O_CREAT|O_NOFOLLOW创建新文件语义等价于open(O_CREAT|O_NOFOLLOW)O_EXCL由调用方传入避免跟随尾随符号链接UnlinkInRoot同文件 L74-L95解析出父目录后以unix.Unlinkat删除AT_REMOVEDIR控制删除目录还是普通 inodeSymlinkInRoot同文件 L100-L112创建父目录后以unix.Symlinkat建链接MkdirAllInRootmkdirall_pathrslite.go底层走pathrs.MkdirAllHandle且会丢弃0o7777之外的非模式位、并静默忽略 Linux 本就不生效的 suid/sgid 位记录 warning 日志。6.2 有趣的兼容细节hallucinateUnsafePath在 path.go 中有一个名字很形象的辅助函数hallucinateUnsafePath幻觉出 unsafe path由于旧版 runc 会容忍含悬挂符号链接作为路径组件的怪路径而pathrs-lite不支持这种行为runc 便先调用一次securejoin.SecureJoin纯粹为了生成一个半合理的候选路径再交给 pathrs-lite 安全操作。其 doc 注释明确警告该路径本身不安全必须经由 pathrs-lite 操作不能直接使用。同文件还提供了IsLexicallyInRoot、LexicallyCleanPath、LexicallyStripRoot等词法路径工具。6.3 runc 业务代码中的真实调用点通过搜索pathrs.前缀可以定位到 runc 各模块的落地场景调用点文件用途pathrs.OpenInRoot(rootFd, u, unix.O_PATH)libcontainer/criu_linux.gocheckpoint/restore 时打开挂载点pathrs.MkdirAllInRoot(c.root, b.Destination, 0o755)libcontainer/rootfs_linux.go创建 cgroup 子系统目录pathrs.OpenInRoot/CreateInRoot/MkdirAllInRootlibcontainer/rootfs_linux.gorootfs 内挂载目标处理pathrs.UnlinkInRoot(rootFd, /dev/ptmx, 0)libcontainer/rootfs_linux.go删除容器内/dev/ptmx旧节点pathrs.OpenInRoot(rootFd, unsafePath, flags)libcontainer/rootfs_linux.go打开容器内路径如/dev处理这些调用点共同印证在 rootfs 可能被同一容器内攻击者篡改的威胁模型下runc 已经把文件系统操作全面迁移到基于文件描述符的原子化路径解析范式上。七、选型与迁移建议基于仓库证据的总结从 CHANGELOG 与源码可以归纳出清晰的演进逻辑旧代码SecureJoin/SecureJoinVFS只适合处理路径字符串展示等非安全用途或作为生成候选路径的工具如hallucinateUnsafePath凡是解析后还要用该路径做文件操作的场景都应避免因为它无法抵御 TOCTOU 竞态。0.4.0 起还会对含..的 root 直接报错。新代码pathrs-lite以*os.File/O_PATH句柄为粒度通过openat2(RESOLVE_IN_ROOT)在内核中一次性完成限制在 rootfs 内 解析符号链接需要读写时用Reopen升级句柄创建目录树用MkdirAllHandle。这是 runc 当前v0.7.0的主要使用方式。libpathrsbuild tag为追求更强健的 procfs 加固、且可以接受 CGo 的发行版提供了零代码改动的后端切换路径见 pathrs-lite/README.md。内核版本依赖openat25.6、fsopen/open_tree5.2、statx(STATX_MNT_ID)5.8、STATX_MNT_ID_UNIQUE6.8逐级提供更强的保护CHANGELOG 也记录了 RHEL 8 回移植fsopen后性能异常、因此在 pre-5.2 内核上明确拒绝使用fsopen而回退open(/proc)的工程决策——这说明该库对兼容性有相当细致的打磨。如果要在自己的项目中采用这套方案可参考 runc 的做法把pathrs-lite的调用收敛到一个内部封装层如 internal/pathrs统一处理EAGAIN重试、模式位校验与路径词法预处理再在业务层使用OpenInRoot/MkdirAllInRoot/CreateInRoot等安全原语。参考与延伸阅读变更记录全文vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md顶层 API 与设计说明vendor/github.com/cyphar/filepath-securejoin/README.mdSecureJoin/SecureJoinVFS实现vendor/github.com/cyphar/filepath-securejoin/join.gopathrs-lite说明vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/README.mdrunc 侧封装internal/pathrs/path.go、internal/pathrs/root_pathrslite.go、internal/pathrs/mkdirall_pathrslite.go业务调用示例libcontainer/rootfs_linux.go、libcontainer/criu_linux.go依赖与版本go.mod赞分享容器运行时云原生【免费下载链接】runcCLI tool for spawning and running containers according to the OCI specification项目地址https://gitcode.com/gh_mirrors/ru/runc点击查看免费下载相关推荐Podman 中的 filepath-securejoin 安全路径解析从 SecureJoin 到 openat2 与 pathrs-lite 演进全解Podman 中的 filepath securejoin 安全路径解析从 SecureJoin 到 openat2 与 pathrs lite 演进全解 P容器运行时云原生CLIGo 文件路径安全库 filepath-securejoin 全解析从 SecureJoin 到 pathrs-lite 的 API 演进与安全机制Go 文件路径安全库 filepath securejoin 全解析从 SecureJoin 到 pathrs lite 的 API 演进与安全机制 导读 本后端微服务存储认证鉴权Go 安全路径解析库 filepath-securejoin 深度解析从 SecureJoin 到基于 openat2 的 pathrs-liteGo 安全路径解析库 filepath securejoin 深度解析从 SecureJoin 到基于 openat2 的 pathrs lite filep云原生网络服务网格可观测性网络安全eBPF上一篇终极指南如何用seamless_communication回译技术拯救低资源语言下一篇突破内存瓶颈jemalloc的dirty_decay_ms与muzzy_decay_ms参数调优指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表