ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

gVisor 深度解析:基于 Go 的应用内核、Sentry/Gofer 架构与 runsc OCI 运行时实战

gVisor 深度解析:基于 Go 的应用内核、Sentry/Gofer 架构与 runsc OCI 运行时实战 gVisor 深度解析基于 Go 的应用内核、Sentry/Gofer 架构与 runsc OCI 运行时实战【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor导读gVisor 是运行在用户态的应用内核Application Kernel它以内存安全的 Go 语言重实现了 Linux 系统调用接口为容器提供介于传统容器与虚拟机之间的强隔离层。本文以 gVisor 官方架构文档为主线系统讲解其设计定位、与 seccomp/VM 的路线差异、Sentry 与 Gofer 双进程架构、runscOCI 运行时的命令与使用方式并结合本仓库源码给出关键实现路径帮助你从原理到实战完整掌握 gVisor 的隔离机制。什么是 gVisor为容器而生的应用内核gVisor 在运行应用与宿主机操作系统之间提供了一层强隔离。它不是一个运行在虚拟机里的 Linux 内核而是一个实现 Linux 风格接口Linux-like interface的应用内核与 Linux 不同它使用内存安全的语言Go编写并且运行在用户态userspace——从宿主内核的视角看gVisor 本身只是一个普通的用户进程。gVisor 附带了一个符合 Open Container InitiativeOCI 规范的运行时名为runsc。runsc与 Docker、Kubernetes 等现有容器工具链无缝集成可以很方便地运行沙箱化容器。gVisor 支持三种使用路径官方为每一种都提供了详细指南Docker 快速开始上手最快的方式Kubernetes 快速开始在 K8s 集群中用 gVisor 隔离 PodOCI 快速开始专家模式直接使用runsc定制环境。gVisor 做什么把内核接口搬进独立的沙箱gVisor 提供的是一个虚拟化环境用于对容器进行沙箱化。其核心思想是将通常由宿主内核实现的系统接口移入一个独立于每个沙箱的应用内核中从而最小化容器逃逸container escape攻击的成功面。gVisor 的关键权衡在于不引入大的固定开销gVisor 不是为每个沙箱预分配固定的物理资源而是在运行时按需分配 CPU、内存等宿主机资源保留进程式的资源模型从资源利用的角度看gVisor 沙箱与普通进程类似资源可以灵活伸缩这与固定资源池的虚拟机模型有本质区别。第三条隔离路线与 seccomp 和 VM 的本质区别在 gVisor 出现之前业界对容器提供更强隔离通常走两条路线而 gVisor 明确不属于任何一条gVisor不是系统调用过滤器如seccomp-bpf也不是 Linux 隔离原语的包装如firejail、AppArmor 等gVisor也不是日常意义上的虚拟机如 VirtualBox、QEMU。gVisor 走的是第三条路线在获得虚拟机大量安全收益的同时保留普通用户态应用的低资源占用、快速启动和灵活性。下面逐一分析三条路线的权衡。路线一机器级虚拟化Machine-level virtualizationKVM、Xen 这类方案通过虚拟机监视器VMM向客户机内核暴露虚拟化硬件。这种虚拟化硬件通常是启蒙过的paravirtualized并可通过 balloon 驱动、半虚拟化自旋锁等机制改善客户机与宿主机之间的可见性。在独立虚拟机中运行容器可以获得优秀的隔离性、兼容性和性能尽管嵌套虚拟化可能带来挑战但通常需要额外的代理proxy和代理进程agent并可能需要更大的资源占用和更慢的启动时间。路线二基于规则的执行Rule-based executionseccomp、SELinux、AppArmor 等方案允许为应用或容器指定细粒度的安全策略依赖宿主内核内的钩子hooks执行规则。如果攻击面可以收缩得足够小这确实是在保持原生性能的同时沙箱化应用的好方法。但在实践中为任意的、之前未知的应用可靠地定义策略极其困难甚至不可能这使得该方案难以普适应用。因此基于规则的执行通常需要与其他层次叠加形成纵深防御defense-in-depth。路线三gVisor——拦截系统调用并充当客户机内核gVisor 提供了与上述两者都不同的第三种隔离机制拦截应用的系统调用并扮演客户机内核的角色无需通过虚拟化硬件做翻译。可以这样理解 gVisor 的定位把它看作合并的客户机内核 VMM或者把它看作加强版的 seccompseccomp on steroids。这一架构带来的收益与代价都非常明确收益资源占用灵活基于线程和内存映射而非固定的客户机物理资源同时降低了虚拟化的固定成本代价应用兼容性有所降低且每次系统调用有更高的开销per-system call overhead。在此基础上gVisor 还会叠加基于规则的执行手段来实现纵深防御下文安全模型部分详述。gVisor 的思路与User Mode LinuxUML类似但 UML 在内部虚拟化硬件因此资源占用是固定的而 gVisor 直接面向系统调用接口不虚拟化硬件从而保持了资源的灵活性。每种方案都有各自的适用场景例如机器级虚拟化在高密度部署上面临挑战而 gVisor 对系统调用密集的工作负载性能可能较差。为什么选择 Go内存安全内核gVisor 使用 Go 编写目的是避开长期困扰内核的安全陷阱强类型strong types内建的边界检查built-in bounds checks无未初始化变量无 use-after-free无栈溢出内建竞态检测器race detector。但使用 Go 也有挑战Go 运行时常常引入性能开销。这正是安全性与性能之间 trade-off 的直接体现也是 gVisor 文档与 性能指南 反复强调的内容。沙箱组件全景Sentry、Gofer 与 Application一个 gVisor 沙箱由多个进程组成这些进程共同构成一个可以运行一个或多个容器的环境。每个沙箱拥有自己独立的Sentry实例Sentry 是运行容器并拦截、响应应用系统调用的内核沙箱中的每个容器拥有自己独立的Gofer实例Gofer 为容器提供文件系统访问。Sentry最大的组件真正的应用内核Sentry 是 gVisor 中最大的组件可以被看作一个应用内核。它实现了应用所需的全部内核功能包括系统调用system calls信号投递signal delivery内存管理与缺页处理逻辑memory management and page faulting logic线程模型threading model以及其他内核功能。从本仓库源码结构看Sentry 的实现体量非常可观pkg/sentry/ 目录下包含 800 余个 Go 源文件覆盖了 系统调用、进程管理、文件系统、网络栈gVisor 自研的 netstack等模块。关键机制应用发起系统调用时Platform 会把调用重定向到 Sentry由 Sentry 完成必要的工作来服务该调用。需要特别强调的是Sentry不会把系统调用透传给宿主内核作为用户态应用Sentry 确实会发起一些宿主系统调用来支撑自身运行但绝不允许应用直接控制它发起的系统调用例如Sentry 不能直接打开文件超出沙箱范围的文件系统操作非沙箱内部的/proc文件、pipe 等都会交给 Gofer 处理。在 pkg/sentry/platform/platform.go 中可以找到Platform接口的完整定义它提供NewAddressSpace()创建内存上下文与NewContext()创建执行上下文等抽象是所有拦截机制的公共底座。Gofer受限的文件系统代理Gofer 是一个标准的宿主进程随每个容器启动并通过9P 协议pkg/p9/p9.go经由 socket 或共享内存通道与 Sentry 通信。Gofer 的实现主体位于 pkg/sentry/fsimpl/gofer/其中 filesystem.go 定义了文件系统核心逻辑此外较新的 gVisor 还引入了性能更高的 lisafs 通信协议README 中有说明。安全模型的关键点在于Sentry 进程运行在一个受严格 seccomp 限制的容器中没有文件系统资源访问权Gofer 作为略多信任一点的伴生进程中介mediate所有对文件系统资源的访问这为沙箱增加了一层额外的隔离即使 Sentry 被攻破也无法直接访问宿主文件系统。Application无需修改的普通 Linux 二进制Application 就是通过 OCI runtime bundle 提供给 gVisor 的普通 Linux 二进制。gVisor 的目标是提供与 Linux 等价的环境因此应用应当可以不加修改地运行。不过 gVisor 目前并未实现每一个系统调用、每一个/proc文件或/sys文件因此可能出现部分不兼容。详细情况见 兼容性指南仓库中还有对应的 兼容性测试 作为佐证。runscOCI 运行时入口运行沙箱化容器的入口是runsc可执行文件。runsc实现了 OCI 运行时规范Docker 和 Kubernetes 都使用该规范。这意味着runsc可以直接运行 OCI 兼容的filesystem bundle——bundle 由两部分组成config.json包含容器配置root filesystem容器的根文件系统。runsc实现了多个命令用于启动、停止、列出和查询容器状态等操作start、stop、list、status 等。从本仓库源码看runsc/main.go 是二进制入口它调用maincli.Main()真正的命令分发逻辑在 runsc/cli/cli.go 中基于google/subcommands框架注册所有子命令并统一完成日志、配置解析config.NewFromFlags、OCI spec 加载FetchSpec等初始化工作。所有子命令实现位于 runsc/cmd/ 目录下。用 runsc 直接运行 OCI 容器按照 安装指南 完成安装后可以按 OCI 快速开始 的步骤操作# 1. 创建 bundle 根目录 mkdir bundle cd bundle # 2. 用 Docker hello-world 镜像导出 rootfs mkdir --mode0755 rootfs docker export $(docker create hello-world) | sudo tar -xf - -C rootfs --same-owner --same-permissions # 3. 生成 config.json指定运行 /hello 程序 runsc spec -- /hello # 4. 运行容器 sudo runsc run hello快速验证runsc do 与 dmesgrunsc do是专为快速测试提供的便捷命令例如$ sudo runsc do echo Hello world Hello world注意命令前的sudo沙箱设置过程需要特权具体用于建立用户态网络栈一旦沙箱设置完成gVisor 会重新执行自身并丢弃所有特权之后才运行任何不可信代码。对于不需要网络的沙箱可以无 root 运行$ runsc --rootless --networknone do echo Hello world Hello world如何确认 gVisor 真的在工作试试会涉及宿主内核的操作例如读取内核日志的dmesg(1)# 无 gVisor未沙箱化 $ dmesg dmesg: read kernel buffer failed: Operation not permitted # 有 gVisor沙箱化 $ runsc --rootless --networknone do dmesg [ 0.000000] Starting gVisor... [ 0.498943] Waiting for children... [ 0.972223] Committing treasure map to memory... [ 1.192981] Segmenting fault lines... [ 1.591823] Verifying that no non-zero bytes made their way into /dev/zero... [ 1.787191] Consulting tar man page... [ 2.083245] Searching for needles in stacks... [ 2.534575] Forking spaghetti code... [ 2.742140] Digging up root... [ 2.921313] Gathering forks... [ 3.342436] Creating cloned children... [ 3.511124] Setting up VFS... [ 3.812459] Setting up FUSE... [ 4.233037] Ready!这组幽默的输出直接证明了dmesg(1)二进制在与gVisor 内核对话而非宿主 Linux 内核——这些日志是 gVisor 系统调用处理器按需生成的虚构内容因此每次运行结果都不同。需要提醒runsc do默认把宿主的整个文件系统以只读方式暴露给沙箱这只是测试便利功能。在实际作为 OCI 容器运行时宿主文件系统映射严格由 OCI 配置决定gVisor 只会暴露配置指定的路径并执行pivot_root(2)作为纵深防御禁止访问其他宿主目录。因此从安全角度测试时更推荐 安装为 Docker 运行时 后使用$ sudo docker run --rm --runtimerunsc -it -v /tmp/vol:/vol ubuntu /bin/bash这条命令会在 gVisor 沙箱内启动一个 Bash shell并把宿主目录/tmp/vol映射到沙箱内的/vol可以尝试探查沙箱边界。平台机制系统调用拦截的底层实现系统调用与缺页的拦截依赖所谓的gVisor Platform。在 平台指南 中Platform接口的核心可以简化为type Platform interface { NewAddressSpace() (AddressSpace, error) NewContext() Context } type Context interface { Switch(as AddressSpace, ac arch.Context) (..., error) } type AddressSpace interface { MapFile(addr hostarch.Addr, f File, fr FileRange, at hostarch.AccessType, ...) error Unmap(addr hostarch.Addr, length uint64) }当前支持三种平台实现各有不同的性能与硬件需求平台拦截机制适用场景备注systrap默认基于 seccomp 的SECCOMP_RET_TRAP让内核向触发线程发送SIGSYS将控制权交给 gVisor虚拟机内部、无虚拟化支持的机器2023 年年中取代 ptrace 成为默认平台KVM使用内核 KVM 功能Sentry 同时充当客户机 OS 与 VMM裸机bare-metal场景性能最佳在嵌套 VM 中可行但因嵌套虚拟化开销通常不如 systrapptrace使用PTRACE_SYSEMU执行用户代码不允许执行宿主系统调用任何 ptrace 可用的环境普适上下文切换开销高已不再支持预计最终移除选择建议裸机跑 KVM虚拟机内或无虚拟化支持时用 systrap。平台对系统管理员是透明可互换的但从安全角度看它们依赖的 Linux 内核功能不同拦截行为也有差异。安全模型纵深防御与攻击面控制gVisor 的安全设计围绕限制系统 API 攻击向量展开核心原则在 安全模型 中阐述不直接透传任何系统调用给宿主每个受支持的调用都在 Sentry 中有独立实现与宿主实现同时出现相同漏洞的可能性很低。这带来一个推论应用使用的所有内核特性都需要在 Sentry 内实现只实现通用、普适的功能不实现也不透传扩展属性、raw socket、ioctl 等专用 API最小化暴露给 Sentry 的宿主面Sentry 不允许在宿主上打开新文件、创建新 socket 等。在工程实践上仓库还执行若干硬性约束以降低 Sentry 被利用的风险所有 unsafe 代码隔离在文件名以unsafe.go结尾的文件中便于审计非 unsafe 文件不得导入 unsafe 包禁止使用 CGoSentry 必须是纯 Go 二进制核心包内一般不允许外部导入Sentry 内部可用的代码被严格管控。从 安全介绍 还可以看到更细的攻击面分析沙箱与宿主的交互被限制为极少数高层操作与 Gofer 进程通过已连接 socket 通信、调用极少量宿主系统调用不含创建新 socket 或打开文件除非启用了相应配置、读写虚拟以太网设备gVisor 不防御沙箱之前的栈高层攻击如 containerd 被攻破后绕过 gVisor 启动容器、Spectre 风格的 CPU 侧信道攻击、以及沙箱内部 workload 自身的漏洞例如 PHP 代码被利用。破解 gVisor 沙箱需要同时攻破 Sentry 内核与宿主 Linux 内核而这两者不共享任何代码这正是双内核架构的安全根基。安装与版本选择gVisor 支持 x86_64 与 ARM64要求 Linux 5.6。官方推荐通过apt仓库安装安装指南sudo apt-get update \ sudo apt-get install -y apt-transport-https ca-certificates curl gnupg curl -fsSL https://gvisor.dev/archive.key | sudo gpg --dearmor -o /usr/share/keyrings/gvisor-archive-keyring.gpg echo deb [arch$(dpkg --print-architecture) signed-by/usr/share/keyrings/gvisor-archive-keyring.gpg] https://storage.googleapis.com/gvisor/releases release main | sudo tee /etc/apt/sources.list.d/gvisor.list /dev/null sudo apt-get update sudo apt-get install -y runsc如果系统已安装 Dockerrunsc包会自动完成 Docker 运行时配置。版本渠道方面实验用 nightly生产用 latest release也可按具体日期或 point release 选择apt方式最面向未来是首选。从零到一把 gVisor 接入 Docker配置 Docker 运行时若使用 apt 或 automated 安装可跳过此步sudo runsc install sudo systemctl restart docker运行沙箱化容器docker run --runtimerunsc --rm hello-world docker run --runtimerunsc --rm -it ubuntu /bin/bashDocker 的大多数选项都与 gVisor 兼容例如docker run --runtimerunsc --rm --link backend:database -v ~/bin:/tools:ro -p 8080:80 --cpus0.5 -it busybox telnet towel.blinkenlights.nl安装带自定义参数的运行时runsc install可以接受在 Docker 调用运行时时会传入的 flags例如启用调试sudo runsc install --runtime runsc-debug -- \ --debug \ --debug-log/tmp/runsc-debug.log \ --strace \ --log-packets注意启用调试运行环境前请确保系统已禁用 SELinux。总结与延伸阅读一句话概括 gVisor 的定位它用内存安全的 Go 在用户态重新实现了 Linux 内核接口用拦截系统调用 独立实现内核逻辑 Gofer 中介文件访问的方式在保持进程式资源模型的同时提供了接近虚拟机的隔离强度。runsc则把这一切封装成标准的 OCI 运行时让 Docker、Kubernetes 生态可以零改造接入。想要继续深入可以阅读仓库中的以下文档与源码架构指南平台——三种拦截机制的详细权衡架构指南安全模型——威胁模型与纵深防御原则用户指南兼容性——已支持/未支持系统调用清单用户指南Docker 快速开始 与 OCI 快速开始——更多上手示例源码入口runsc 命令分发、Platform 抽象、Gofer 文件系统、9P 协议、lisafs。【免费下载链接】gvisorApplication Kernel for Containers项目地址: https://gitcode.com/GitHub_Trending/gv/gvisor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进