DirectfsgVisor 容器文件系统直通访问机制全解析【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor导读Directfs 是 gVisor容器应用内核中一项让沙箱内核Sentry安全直连容器文件系统、绕过文件系统 gofer 代理往返调用的核心特性如今它已是 runsc 的默认文件系统访问模式。本文将以官方博客为骨架结合本仓库源码runsc、pkg/sentry/fsimpl/gofer等逐层拆解 Directfs 的起源、隔离原理、seccomp 适配、配置方式与性能收益帮助你理解 gVisor 如何在提升文件系统性能的同时维持纵深防御的安全模型。从 Gofer 说起沙箱文件访问的安全代理gVisor 在 Google 内部被用于承载多种服务与负载。构建 gVisor 时面临的一大挑战是如何让沙箱安全地访问尤其是远程文件系统。gVisor 严格的安全模型与纵深防御思路详见 安全架构文档假定沙箱可能与不可信应用共享执行上下文、存在被攻破的可能因此沙箱绝不能直接持有访问 Google 内部远程文件系统所需的敏感密钥与凭据。为解决这一问题gVisor 引入了一个可信的文件系统代理称为gofergofer 运行在沙箱外部它为不可信的容器提供访问远程文件系统的安全接口出于架构简单性的考虑gofer 同时被用于服务本地文件系统与远程文件系统。上图中的 Filesystem gofer proxy图 1清晰展示了这一代理模型所有文件系统操作都经由沙箱外的 gofer 进程转发。runsc 中的容器文件系统隔离问题当 gVisor 以 runsc 的形式开源后同样的 gofer 模型被原样移植以维持同样的安全保证runsc 为每个容器启动一个 gofer 进程gofer 通过预定义的协议即现在的 LISAFS向沙箱提供容器文件系统然而 gofer 引入了一层间接层带来了显著的开销。从源码注释可以印证这一点在 runsc/config/config.go 中DirectFS 标志的说明明确写道Gofer process still exists, but is mostly idlegofer 进程仍然存在但大部分时间处于空闲状态。关键在于gofer 模型是为远程文件系统设计的而在 runsc 的使用场景中gofer 服务的所有文件系统如 rootfs 和 bind mount都本地挂载在宿主机上gofer 直接通过文件系统系统调用访问它们——为远程场景设计的代理模型在这里几乎毫无优势。Linux 为此提供了一些有效隔离本地文件系统的安全原语mount namespaces挂载命名空间pivot_root切换根目录detached bind mounts分离式绑定挂载1Directfs正是利用这些原语、以安全方式将容器文件系统暴露给沙箱的新文件系统访问模式。沙箱对文件系统树的视图被严格限制为仅容器文件系统本身无法访问宿主机更广泛文件系统上挂载的任何内容。即便沙箱被攻破这些机制也能提供额外的屏障防止更大范围的系统沦陷。Directfs直通访问的工作机制在 Directfs 模式下gofer 仍然作为沙箱外部的协作进程存在但职责发生了根本转变gofer 侧gofer 进入一个新的挂载命名空间设置合适的 bind mount 以在一个新目录中构建容器文件系统然后通过pivot_root(2)切换根目录到该目录沙箱侧沙箱进程进入新的 user namespace 与 mount namespace然后pivot_root(2)切换根目录到一个空目录确保它无法通过路径遍历访问任何其他内容关键变化沙箱不再通过 RPC 向 gofer 请求文件系统操作而是通过 SCM_RIGHTS 消息 请求 gofer 提供所有挂载点的文件描述符FD沙箱随后直接执行FD 相对系统调用如fstatat(2)、openat(2)、mkdirat(2)等来完成文件系统操作。上图中的 Directfs configuration图 2对比了传统 gofer 代理与 Directfs 直通两种配置的数据流差异。源码中的 directfsInode宿主 inode 实现在 Sentry 内部Directfs 由pkg/sentry/fsimpl/gofer包实现。核心数据结构是 directfsInode// directfsInode is a host inode implementation. It represents a inode // backed by a host file descriptor. All operations are directly performed on // the host. A gofer is only involved for some operations on the mount point // dentry (when dentry.parent nil). We are forced to fall back to the gofer // due to the lack of procfs in the sandbox process. type directfsInode struct { inode // controlFD is the host FD to this file. controlFD int // controlFDLisa is a lisafs control FD on this dentry. // 用于在以下场景回退到 lisafs RPC // * 需要父 dentry 但 dentry.parent nil根 dentry // * socket 上基于路径的系统调用如 connect(2)、bind(2)。 controlFDLisa lisafs.ClientFD }值得注意的工程细节所有常规文件操作都在宿主机上直接执行gofer 仅在挂载点 dentrydentry.parent nil的某些操作中参与——因为沙箱进程内没有 procfs在directfs_inode.go中宿主机打开文件时强制使用hostOpenFlags unix.O_NOFOLLOW | unix.O_CLOEXECL40-L42与博客所述 O_NOFOLLOW 为强制要求 完全对应打开顺序采用tryOpen策略L44-L71先尝试O_RDONLY | O_NONBLOCK对 FIFO 使用非阻塞防止卡在 open(2) 中失败后再尝试O_PATH适用于符号链接、socket。seccomp 过滤器的相应调整在 gofer 承担全部文件系统操作的旧模式下可以在沙箱进程中用 seccomp拒绝所有这些系统调用。但启用 Directfs 后沙箱进程的 seccomp 过滤器必须允许这些系统调用的使用尤其是允许openat(2)它允许路径遍历但带有严格的限制条件O_NOFOLLOW为强制要求禁止访问 procfs禁止使用来自宿主的目录 FD同时必须授予沙箱与 gofer 相同的权限例如CAP_DAC_OVERRIDE与CAP_DAC_READ_SEARCH使其能够执行同样的文件系统操作。仓库中的具体实现位于 runsc/boot/filter/config/config_main.go 的hostFilesystemFilters()函数。注释明确写道Directfs allows FD-based filesystem syscalls. We deny these syscalls with negative FD values (like AT_FDCWD or invalid FD numbers). We try to be as restrictive as possible because any restriction here improves security.该函数为以下系统调用设置了NonNegativeFD非负 FD等逐参数约束系统调用约束要点openat(2)FD 非负且 flags 必须带O_NOFOLLOWMaskedEqual强制fstatat(2)FD 非负其余参数任意fchownat(2)FD 非负flags 必须为AT_EMPTY_PATH \| AT_SYMLINK_NOFOLLOWfchmodat(2)/unlinkat(2)/getdents64(2)/mkdirat(2)/mknodat(2)/readlinkat(2)/utimensat(2)FD 非负其余参数任意linkat(2)两个 FD 均非负flags 必须为 0symlinkat(2)目标 FD 非负renameat2(2)两个 FD 均非负fgetxattr/flistxattr/fremovexattr/fsetxattrFD 非负也就是说虽然沙箱现在可以直接执行这类系统调用但任何以AT_FDCWD或负 FD 发起路径解析的企图都会被 seccomp 拒绝从而把路径解析牢牢限制在 gofer 捐赠的合法 FD 之上。安全边界为什么逃不出去一个值得强调的安全性质只有受信任的 gofer 会向沙箱提供容器文件系统的FD。沙箱无法通过 .. 向后回退也无法跟随恶意符号链接逃出容器文件系统。实质上gVisor 团队降低了对系统调用过滤器捕获恶意行为的依赖同时相应增加了对 Linux 文件系统隔离保护的依赖——这正是博客与源码共同体现的防御策略转移。配置 Directfsrunsc 的--directfs标志Directfs 现在是 runsc 的默认模式。其全局开关在 runsc/config/config.go 中定义// DirectFS sets up the sandbox to directly access/mutate the filesystem from // the sentry. Sentry runs with escalated privileges. Gofer process still // exists, but is mostly idle. Not supported in rootless mode. DirectFS bool flag:directfs要点Sentry 以提升的特权运行获得CAP_DAC_OVERRIDE等能力gofer 进程仍存在但基本空闲rootless 模式不支持 Directfs。在运行runsc时可通过--directfstrue/false显式控制也可直接使用默认值。该配置还会影响指标元数据——在Config.MetricMetadata()runsc/config/config.go中fsmode标签会在directfs与goferfs之间切换便于按文件系统模式观测性能指标。挂载选项如何传递Directfs 的启用最终体现在 gofer 挂载选项中。核心逻辑在 runsc/boot/vfs.go 的goferMountDatafunc goferMountData(fd int, fa config.FileAccessType, conf *config.Config, suppressDirectFS bool) []string { opts : []string{ transfd, rfdno strconv.Itoa(fd), wfdno strconv.Itoa(fd), } if fa config.FileAccessShared { opts opts [cacheremote_revalidating] } if conf.DirectFS !suppressDirectFS { opts append(opts, directfs) } ... }Sentry 侧在 pkg/sentry/fsimpl/gofer/gofer.go 定义moptDirectfs directfs并在解析挂载选项时L604-L607将其转换为fsopts.directfs.enabled true从而启用直通 inode 路径。按挂载粒度关闭 Directfs某些挂载可能需要回退到传统 gofer 路径例如需要 LISAFS 系统调用的场景。gVisor 提供了挂载级注解suppress_directfs其实现位于 runsc/boot/mount_hints.go// SuppressDirectFS suppresses the directfs gofer mount option for this // mount even if --directfs is enabled globally. It does not enable directfs // when --directfs is disabled because directfs requires some sandbox-wide // settings (like seccomp filters) that can not be selectively enabled on // containers. SuppressDirectFS bool json:suppressDirectFS对应的 OCI 注解值为directfsoff或directfsdefaultsetDirectFSannotations: dev.gvisor.mount.suppress_directfs.mount-name: off注意注释中强调的一个细节Directfs 无法被逐容器选择性启用因为它依赖沙箱级设置如 seccomp 过滤器所以该注解只能关闭不能打开。同时runsc/fsgofer的预编译 seccomp 程序runsc/fsgofer/filter/config/config_precompiled.go也会对 DirectFS 开/关两种组合分别预编译确保在部分挂载抑制 Directfs的混合模式下gofer 侧仍能正确保留 LISAFS 系统调用所需的能力。与文件访问模式、overlay 的交互file-access 模式FileAccessTyperunsc/config/config.go区分exclusive默认用于 rootfs独占访问、激进缓存、性能更好与shared默认用于非 root 卷需要重新验证以检测外部变更。在 shared 模式下会追加cacheremote_revalidating挂载选项。overlay 交互由于 rootfs 通常位于 overlayfs 之上createMountNamespace会在 gofer 挂载数据中追加overlayfs_stale_readrunsc/boot/vfs.go以处理陈旧读取问题。这也解释了博客中基准为何选择 bind mount 而非 rootfs——bind mount 没有 overlay 层所有操作都直接走 goferfs/directfs 挂载更能反映 Directfs 本身的收益。性能绕过 gofer 往返的收益每次文件系统操作都向 gofer 发起 RPC 会给 runsc 带来大量开销因此避免 gofer 往返能显著提升性能。博客中的基准采用当时新发布的 systrap 平台并在 bind mount 上运行而非 rootfs以模拟更贴近现实的容器文件系统配置场景。stat 微基准stat 微基准 反复对文件执行stat(2)。测试源码BM_Stat会构建一个嵌套目录层级深度 1~100在其中创建文件并反复调用statvoid BM_Stat(benchmark::State state) { int depth state.range(0); // 创建给定深度的嵌套目录 // 创建将被 stat 的文件 struct stat st; for (auto _ : state) { ASSERT_THAT(stat(file.path().c_str(), st), SyscallSucceeds()); } } BENCHMARK(BM_Stat)-Range(1, 100)-UseRealTime();基准结果显示stat(2)系统调用提速超过 2 倍。不过博客也提醒微基准并不代表真实应用不应直接外推这些结果。真实世界基准真实世界基准 覆盖了更贴近实际的工作负载。以仓库中的 Ruby 开发负载为例rubydev_test.go 定义了两种基准BenchmarkRubyNoOpTest运行一个无操作 Ruby 测试Stripe 曾用它来评估 gVisor 的构建性能场景BenchmarkRubySpecTest运行来自 Fastlane 项目的复杂测试套件期望输出 3613 examples, 0 failures并通过回调提取Ruby 加载时间ExtractRubyLoadTime作为自定义指标上报。测试基础设施由 fsbench/fsbench.go 提供每种工作负载都会在 bindfs、tmpfs、rootfs、fuse 等不同文件系统类型以及清/不清缓存的组合变体下重复运行TypicalVariants并验证输出正确性WantOutput与清理残留CleanCmd。基准结果显示这些工作负载的绝对运行时间减少 12%Ruby 加载时间减少 17%。结论安全与性能的再平衡runsc 中的 gofer 模型在访问宿主机文件方面过于保守。gVisor 团队利用 Linux 已有的文件系统隔离机制mount namespace、pivot_root、detached bind mount在不牺牲安全性的前提下绕过了 gofer安全边界从禁止一切路径类系统调用转为限制在 gofer 捐赠的 FD 上执行路径类系统调用配合O_NOFOLLOW强制、无 procfs、无宿主目录 FD 等 seccomp 约束对 Linux 文件系统隔离原语的依赖加深而对 syscall 过滤器捕获恶意行为的依赖相应减弱Directfs 显著提升了特定工作负载的性能是 gVisor 持续性能优化工作的一部分。如果你希望深入代码建议从以下几个入口继续探索挂载选项组装与 Directfs 启用runsc/boot/vfs.goSentry 侧挂载选项解析pkg/sentry/fsimpl/gofer/gofer.go直通 inode 实现与 LISAFS 回退pkg/sentry/fsimpl/gofer/directfs_inode.goseccomp 过滤器约束runsc/boot/filter/config/config_main.go挂载级抑制注解runsc/boot/mount_hints.go性能基准test/perf/linux/stat_benchmark.cc 与 test/benchmarks/fs分离式绑定挂载可通过先用 mount(MS_BIND) 创建绑定挂载再用 umount(MNT_DETACH) 将其从文件系统树中分离来创建。↩【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考 SEO 优化官网定制响应式建站教育培训建站