测试云原生质量保障【免费下载链接】originConformance test suite for OpenShift项目地址https://gitcode.com/gh_mirrors/or/origin点击查看免费下载导读pathrs-lite是 vendored 在 OpenShift 测试仓库中的github.com/cyphar/filepath-securejoin/pathrs-lite子包它以纯 Go 实现 libpathrsgithub.com/cyphar/libpathrs的核心安全路径解析与文件操作能力为现有 Go 项目提供无 CGo 依赖的过渡方案并可通过libpathrsbuild tag 一键切换回 CGo 后端。本文将以 vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/README.md 为骨架结合仓库内源码与真实消费方讲解其设计动机、公开 API、双后端架构、底层原理与安全模型帮助读者掌握在容器运行时、SELinux 工具等安全敏感场景中如何正确使用它。一、背景为什么需要 pathrs-lite1.1 libpathrs 解决的问题在容器运行时与文件系统工具中经常需要在某个根目录root内安全地解析并打开路径。朴素实现通常是两步走path, _ : securejoin.SecureJoin(root, unsafePath) handle, err : os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0)这种先解析字符串、再打开文件的模式存在经典的 TOCTOUTime-of-Check to Time-of-Use竞态窗口如果攻击者能在SecureJoin与os.OpenFile之间篡改文件系统树例如替换符号链接、移动目录、重命名挂载点最终返回的文件句柄就可能落在 root 之外造成容器逃逸或权限越界。libpathrs 的核心思路是全程基于文件描述符fd逐分量走查路径将解析与打开合并为一次原子操作从根上消除这个竞态窗口。而pathrs-lite则是这个库在 Go 生态中的轻量移植。1.2 pathrs-lite 的定位根据 README.md 的明确说明它提供 libpathrs 核心功能的最小化纯 Go 实现minimal pure Go implementation of the core bits of libpathrs它不是libpathrs 的完整替代品not intended to be a complete replacement主要定位是现有 Go 项目的过渡工具transition tool它最吸引人的特性是提供了非常容易切换回 libpathrs 的通道——即使pathrs-lite是被某个第三方包间接引入的、下游不想碰 CGo也可以在构建时通过libpathrsbuild tag 直接改用真正的 libpathrs 后端。两个后端在功能上等价functionally equivalent项目带有集成测试保证这一等价性因此迁移对用户几乎零感知。二、公开 API 全览四个核心函数包级文档doc.go将包名声明为pathrs构建约束为//go:build linux即这是一个Linux 专用包。其公开 API 由四个函数组成分布在mkdir.go与open.go两个入口文件中。2.1 MkdirAll竞态安全的递归建目录func MkdirAll(root, unsafePath string, mode os.FileMode) error签名与os.MkdirAll对齐但语义完全不同。mkdir.go 源码 中明确指出它等价于path, _ : securejoin.SecureJoin(root, unsafePath) err : os.MkdirAll(path, mode)但这套朴素实现是不安全的如果攻击者能在SecureJoin与os.MkdirAll之间改动文件系统树MkdirAll可能解析到不安全的符号链接分量在 root 之外创建目录。实现细节MkdirAll先用os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)打开根目录句柄再委托给MkdirAllHandle最后关闭句柄——它只是MkdirAllHandle的薄封装。2.2 MkdirAllHandle创建目录并返回句柄推荐func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (*os.File, error)这是MkdirAll的更安全变体其优势见 mkdir_purego.go 注释root 以*os.File句柄传入最好是 O_PATH 句柄调用方可以确信使用的是哪个根目录——/proc/self/fd/...只能近似模拟这一能力目录树创建完成后直接返回指向unsafePath的 O_PATH 句柄该句柄以实际无竞态的方式获得攻击者最多只能交换最后一个目录分量这是MkdirAll无法模拟的相比先MkdirAll再重新解析一次unsafePath用 SecureJoin 或 openat2返回句柄的获取高效得多。因此只要创建后打算打开该目录就应该用MkdirAllHandle。2.3 OpenInRoot / OpenatInRoot在根内安全打开路径func OpenInRoot(root, unsafePath string) (*os.File, error) func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error)OpenInRoot见 open.go等价于path, _ : securejoin.SecureJoin(root, unsafePath) handle, err : os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0)但同样更安全如果攻击者能在两次调用之间修改文件系统树朴素实现可能返回 root 之外的文件。OpenInRoot先打开 root 的 O_PATH 目录句柄再委托给OpenatInRoot——后者接受*os.File句柄作为 root确保使用的是调用方指定的那一个根目录。重要语义返回的句柄是O_PATH 句柄只有非常有限的一组操作能对它执行。这是刻意设计——避免意外打开不受信任的文件例如断开的 TTY 可能造成 DoS。要真正读写文件需要用Reopen把它升级成正常的句柄。2.4 Reopen通过 /proc/self/fd 安全重开句柄func Reopen(file *os.File, flags int) (*os.File, error)Reopen把 O_PATH 句柄通过/proc/self/fd重新打开为指定模式。朴素实现等价于fdPath : fmt.Sprintf(/proc/self/fd/%d, file.Fd()) os.OpenFile(fdPath, flags|unix.O_CLOEXEC, 0)但Reopen带有额外加固见 open_purego.go确保不会被恶意配置的 /proc 挂载欺骗。虽然这种攻击场景不常见但在容器运行时中上层运行时可能被诱导配置出不安全的 /proc从而攻击文件操作——这与 CVE-2019-19921runC 的 /proc 相关漏洞同源。README 在引用该漏洞时也给出了同样的链接说明这是设计上刻意防御的威胁模型。三、双后端架构pure Go 与 libpathrs 的无缝切换pathrs-lite的精髓在于同一套 API、两个可互换的实现后端全部通过 Go build tag 控制文件构建约束实现来源mkdir_purego.golinux !libpathrs内部internal/gopathrs纯 Go 实现mkdir_libpathrs.golibpathrscyphar.com/go-pathrs真正的 libpathrs Go 绑定open_purego.golinux !libpathrs纯 Go 实现 internal/procfsopen_libpathrs.golibpathrscyphar.com/go-pathrs3.1 默认纯 Go 后端不指定任何 tag 时编译走linux !libpathrs分支。MkdirAllHandle直接调用internal/gopathrs.MkdirAllHandleOpenatInRoot调用internal/gopathrs.OpenatInRootReopen则经由internal/procfs.ReopenFd实现procfs/procfs_purego.go。该后端不依赖 CGo也不需要目标环境预装 libpathrs 系统库非常适合纯 Go 项目与交叉编译场景。3.2 切换libpathrs 后端构建时加上libpathrstaggo build -tags libpathrs ./...mkdir_libpathrs.go与open_libpathrs.go会被编译内部改为调用cyphar.com/go-pathrs的pathrs.RootFromFile、rootRef.MkdirAll、rootRef.Resolve、pathrs.HandleFromFile等 API。README 特别强调两个后端功能等价并有集成测试验证所以从纯 Go 后端切换到 libpathrs 后端不会带来用户可见的影响。值得注意的一个细节libpathrs 后端的OpenThreadSelf/OpenSelf/OpenRoot/OpenPid见 procfs/procfs_libpathrs.go统一使用unix.O_PATH|unix.O_NOFOLLOW打开这保证了 procfs 句柄本身不会被符号链接替换。四、纯 Go 后端的底层实现从 openat2 到 O_PATH 逐分量走查4.1 优先使用 openat2(2)纯 Go 实现internal/gopathrs/lookup_linux.go的核心函数是lookupInRoot它以 fd 为单位逐分量走查路径语义对齐内核的openat2(RESOLVE_IN_ROOT)。首先尝试openat2系统调用。需要注意的是代码中的一条关键安全注释如果 openat2 能正常工作但本次查询失败不应回退到 O_PATH 解析器——攻击者可能在 O_PATH 解析器中找到 bug无条件回退会形成降级攻击downgrade attack。判断逻辑是openat2成功或内核本身不支持openat2linux.HasOpenat2()为 false时才走 O_PATH 路径。内核能力检测见 internal/linux/openat2_linux.govar HasOpenat2 func() bool { // 用 RESOLVE_NO_SYMLINKS | RESOLVE_IN_ROOT 探测 openat2 fd, err : unix.Openat2(unix.AT_FDCWD, ., unix.OpenHow{...}) ... }它用sawOpenat2Error做单向锁定一旦遇到错误就永久切换为 false。注释解释了原因——进程运行期间可能被 seccomp-bpf 过滤器禁用 openat2所以不能只探测一次就缓存结果。4.2 openat2 封装与 EAGAIN 重试internal/fd/openat2_linux.go 中Openat2是unix.Openat2的 Fd 封装带重试逻辑RESOLVE_IN_ROOT/RESOLVE_BENEATH在解析..期间若系统任意位置发生 mount 或 rename可能返回-EAGAIN——这可能是偶发也可能是攻击者在查询期间搞破坏带作用域的查找在complete_walk末尾还有安全检查若最终路径不在 root 内会返回-EXDEV因此对EAGAIN/EXDEV最多重试scopedLookupMaxRetries 128次防止攻击者让程序陷入无限重试循环同时强制设置O_CLOEXEC避免 fd 泄漏到子进程。4.3 O_PATH 逐分量走查与符号链接栈当内核不支持 openat2 时回退到手工实现其走查循环lookup_linux.go的关键机制逐分量打开每个分量用fd.Openat(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)打开O_NOFOLLOW保证符号链接不被内核自动跟随遇到符号链接用readlinkat读取链接目标把链接目标拆成分量压入 symlinkStack然后从新目标继续走查。symlinkStack 模拟 openat2(RESOLVE_IN_ROOT) 对悬空符号链接dangling symlink的处理若在解析符号链接过程中碰到不存在的分量就返回遇到该符号链接时的(dir, remainingPath)——把符号链接当作普通文件处理。因为存在递归符号链接链接目标里又含符号链接分量所以必须用栈而非单变量符号链接数量上限linksWalked consts.MaxSymlinkLimit时返回ELOOP。该常量定义在 internal/consts/consts.go值为 255注释说明当时 Linux 内核内部限制为 40这里刻意放宽到 255 以匹配上游filepath-securejoin的语义..分量防逃逸走查..时用/proc/self/fd校验两件事——root 本身没有被移动procfs.CheckProcSelfFdPath(logicalRootPath, root)以及走查结果与预期路径一致procfs.CheckProcSelfFdPath(fullPath, currentDir)。root 路径是通过procfs.ProcSelfFdReadlink(root)从/proc/self/fd读取的这样即使 root 是/proc/$pid/root之类的 magic-link 也能拿到真实路径尾随斜杠处理路径以/结尾时做一次相对的.打开从而在最终分量是非目录时正确返回错误对齐 openat2 语义——尾随斜杠与尾随/.完全等价。4.4 MkdirAllHandle 的创建流程internal/gopathrs/mkdir_linux.go 展示完整的建目录流水线模式校验toUnixMode拒绝文件类型位和未知模式位对 Linux 上mkdirat(2)会静默忽略的 suid/sgid 位mode ^ 0o1777 ! 0直接报错而非静默忽略——用户需要知道这些位不会被设置部分解析PartialLookupInRoot打开尽可能长的已存在路径前缀返回(currentDir, remainingPath)若出错且不是ENOENT则直接失败死 inode 检测fd.IsDeadInode(currentDir)主动探测攻击者删除目录的情况目录必须为空才能被删除所以一旦走入已删除目录后续必然失败这是防御性的质量检查重开为目录句柄用procfs.ReopenFd(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC)重开并验证确实拿到目录ENOTDIR时给出无法在 X 中创建子目录的明确错误拒绝..尚未创建的那部分路径若含..分量直接报错——注释解释了为什么不能简单filepath.Clean..会吞掉尾随的悬空符号链接产生与用户请求不符的路径逐分量 mkdirat对每个剩余分量执行unix.MkdiratEEXIST视为并发创建者已代劳继续即可若已存在的 inode 不是目录后续 open 会失败每创建一层就打开下一层句柄openat2 可用时使用RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_XDEV的O_DIRECTORY打开最终返回创建结果目录的句柄。代码注释还坦诚地说明了两个无法完全防御的边界创建后目录被交换无法有效防护MkdirAll 本来就复用已有目录攻击收益有限校验目录 owner/mode 会因 POSIX ACL、vfat 挂载选项uid1,gid2,umask0等产生误报因此不再校验。五、procfs 安全模块防御 /proc 攻击面pathrs-lite还导出一个独立的procfs子包procfs/procfs_purego.go为在 /proc 上安全操作提供 API。这是它区别于普通路径解析库的另一大价值点。5.1 安全的 /proc 句柄OpenProcRoot()优先尝试以subsetpid挂载选项Linux 5.8打开更安全的 /proc 句柄失败则回退到普通 /proc。除非需要大量OpenRoot操作否则应优先用它而不是OpenUnsafeProcRootOpenUnsafeProcRoot()没有任何 overmount 或 masked 路径保护的 /proc 句柄绝不能泄漏给容器用完应立即关闭以免遭遇已知 fd 号攻击OpenThreadSelf(subpath)返回/proc/thread-self/subpath句柄并附带回调底层为runtime.UnlockOSThread因为 Go 的 goroutine 可能被调度到不同线程跨线程使用线程相关 API 会产生虚假错误OpenSelf、OpenPid、OpenRoot分别面向当前进程、任意 pid/tid、全局 procfs 文件如 /proc/sys。OpenRoot的注释特别警告它从不使用subsetpid更容易成为 CVE-2024-21626runC 泄漏工作目录漏洞式攻击的目标句柄打开期间等效于泄漏了OpenUnsafeProcRoot。5.2 procfs 专用解析器internal/procfs/procfs_lookup_linux.go 是completeLookupInRoot的精简版专为 procfs 场景设计代码头注释明确说明它改编自 libpathrs 的 procfs 解析器opensuse/libpathrs v0.1.3 的src/resolvers/procfs.rs。为支持 procfs 走查做了如下取舍不支持..需要 os.Root 式重放或 procfs 验证而后者存在重入问题拒绝绝对符号链接procfs 中的绝对链接基本都是 magic-link本来就该跳过path.IsAbs(linkDest)时直接返回possible breakout错误若内核支持 statx会检测挂载点跨越checkSubpathOvermount——这是针对 /proc 的主要攻击手段攻击者可能在 /proc/thread-self 等路径上叠加符号链接挂载不支持部分查找partial lookup因此不需要 symlink 栈openat2 可用时用RESOLVE_BENEATH | RESOLVE_NO_XDEV | RESOLVE_NO_MAGICLINKS实现注释说明/proc/self虽是 magic-link但它不走nd_jump_link()因此RESOLVE_NO_MAGICLINKS允许它。每走查一个分量都调用verifyProcHandle确认仍在 procfs 上、checkSubpathOvermount确认未被 overmount最终句柄还要再做一次同样的双重校验。六、仓库中的真实消费场景go-selinux 的 one-shot openpathrs-lite在本仓库中最直接的消费方是 vendored 的 go-selinux 包vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go。该文件导入了pathrs-lite与pathrs-lite/procfs两个包实现了一组一次性打开one-shot opens辅助函数func openProcThreadSelf(subpath string, mode int) (*os.File, procfs.ProcThreadSelfCloser, error) { proc, err : procfs.OpenProcRoot() // 安全 /proc 句柄 handle, closer, err : proc.OpenThreadSelf(subpath) // O_PATH 句柄 file, err : pathrs.Reopen(handle, mode) // 升级为可读写句柄 return file, closer, nil }openProcSelf、openProcPid同理分别对应proc.OpenSelf与proc.OpenPid。它们的注释明确承诺若未出错返回的句柄保证恰好是/proc/thread-self/subpath或 /proc/self、/proc/$pid 对应路径不会有刁钻的挂载或符号链接让你操作到意外路径在 openat2 或 fsopen 之前的旧内核上存在少量 caveat。这些函数随后被readConThreadSelf、writeConThreadSelf、readConSelf、writeConSelf用于安全读写 SELinux 上下文文件如/proc/thread-self/attr/current与/proc/self/attr/current。这是一个非常典型的O_PATH 句柄 Reopen 升级组合模式先用OpenThreadSelf拿到指向确切路径的 O_PATH 句柄再由Reopen以os.O_RDONLY|unix.O_CLOEXEC或os.O_WRONLY|unix.O_CLOEXEC重开——这恰好验证了 README 中pathrs-lite 作为过渡工具为现有 Go 项目所用的定位也展示了编写安全 procfs 访问代码的推荐范式。七、使用指南如何在自己的项目中引入7.1 依赖声明在本仓库的 go.mod 中pathrs-lite随上游github.com/cyphar/filepath-securejoin v0.6.1一并引入标记为 indirectgo.sum 记录了两条对应校验和。在你的项目中引入方式相同go get github.com/cyphar/filepath-securejoin/pathrs-lite7.2 最小可运行示例以在 jail 根内安全创建目录树并写入文件为例组合使用前文四个 API//go:build linux package main import ( os github.com/cyphar/filepath-securejoin/pathrs-lite golang.org/x/sys/unix ) func main() { // 1. 打开根目录O_PATH 目录句柄 root, err : os.OpenFile(/var/lib/jail, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0) if err ! nil { panic(err) } defer root.Close() // 2. 在根内创建 data/sub并拿到 O_PATH 句柄 dir, err : pathrs.MkdirAllHandle(root, data/sub, 0o755) if err ! nil { panic(err) } defer dir.Close() // 3. 把 O_PATH 句柄升级为可写句柄 file, err : pathrs.Reopen(dir, os.O_WRONLY|unix.O_CLOEXEC) if err ! nil { panic(err) } defer file.Close() // 4. 写入内容此时已确认文件/目录一定在 root 内 if _, err : file.WriteString(jailed\n); err ! nil { panic(err) } }7.3 切换 libpathrs 后端的构建命令# 纯 Go 后端默认无 CGo 依赖 go build ./... # libpathrs 后端需目标环境提供 libpathrs 系统库 go build -tags libpathrs ./...README 强调两个后端功能等价且有集成测试背书因此从纯 Go 迁移到 libpathrs 时无需修改任何调用代码。八、安全模型总结与适用前提综合 README 与源码可将pathrs-lite的安全保证归纳为三层路径层O_PATH O_NOFOLLOW 逐分量走查符号链接由用户态显式处理root 内..走查用 /proc/self/fd 双重校验防逃逸openat2 可用时优先使用内核级RESOLVE_IN_ROOT/RESOLVE_BENEATH句柄层返回 O_PATH 句柄拒绝在不可信路径上直接获得可读写 fd需要读写时经Reopen带 /proc 恶意挂载加固显式升级procfs 层优先subsetpid安全挂载逐分量校验仍在 procfs 上且未被 overmount拒绝..与绝对 magic-link 符号链接面向 CVE-2019-19921、CVE-2024-21626 等真实 /proc 攻击面。适用前提与限制务必注意该包仅适用于 Linux//go:build linux非 Linux 平台无法编译它不是 libpathrs 的完整替代仅覆盖核心位功能完整的 C 实现仍在上游 libpathrs 中旧内核无 openat2、无 fsopen上走 O_PATH 解析器防御强度取决于 /proc 的可靠性存在少量 caveatMkdirAllHandle对创建后目录被交换、owner/mode 被 ACL 或特殊挂载选项篡改两类情况不做强校验源码注释已明确说明原因。对 OpenShift 测试仓库的读者而言理解pathrs-lite的价值在于当你在容器运行时、SELinux 工具链、安全沙箱等需要在不可信文件系统树上安全解析路径的组件中看到OpenInRoot、MkdirAllHandle、Reopen这些调用时能够准确判断其安全语义并复用同样的模式写出抗 TOCTOU 的文件操作代码。附相关文件索引关注点仓库路径关联文档本文主体pathrs-lite/README.md包级文档与构建约束pathrs-lite/doc.go公开 APIMkdirAll/MkdirAllHandlemkdir.go公开 APIOpenInRoot/OpenatInRoot/Reopenopen.go纯 Go 后端实现internal/gopathrs/lookup_linux.go、internal/gopathrs/mkdir_linux.golibpathrs 后端实现mkdir_libpathrs.go、open_libpathrs.goprocfs 安全 APIpathrs-lite/procfs/procfs_purego.go、procfs_libpathrs.goprocfs 解析器防 overmountinternal/procfs/procfs_lookup_linux.goopenat2 探测与封装internal/linux/openat2_linux.go、internal/fd/openat2_linux.go符号链接上限常量internal/consts/consts.go真实消费方go-selinuxselinux_linux.go依赖版本记录go.mod、go.sum赞分享测试云原生质量保障【免费下载链接】originConformance test suite for OpenShift项目地址https://gitcode.com/gh_mirrors/or/origin点击查看免费下载相关推荐pathrs-lite 深度解析runc 中纯 Go 安全路径操作的实现原理与 libpathrs 迁移指南pathrs lite 深度解析runc 中纯 Go 安全路径操作的实现原理与 libpathrs 迁移指南 导读 本文以 runc 仓库中 vendor 的云原生容器运行时CLIBuildah 容器路径安全操作解析pathrs-lite 纯 Go 实现与 libpathrs 双后端迁移指南Buildah 容器路径安全操作解析pathrs lite 纯 Go 实现与 libpathrs 双后端迁移指南 本篇文章聚焦 buildah 仓库中 ven云原生Kubernetes 中的 pathrs-lite纯 Go 根目录受限路径解析与 libpathrs 构建期迁移指南Kubernetes 中的 pathrs lite纯 Go 根目录受限路径解析与 libpathrs 构建期迁移指南 导读 本文围绕 Kubernetes 仓云原生容器编排集群管理微服务上一篇LinkSwift网盘直链下载助手终极指南一键获取九大网盘真实地址的完整教程下一篇抖音无水印视频下载器如何免费保存高清视频的完整解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
