gvx:在单个 Unprivileged Process 中运行 Linux Containers、Docker 和 Kubernetes

引言:无需 root 或宿主守护进程的安全加固容器
在开发机、CI 运行器或多租户云节点内运行 Linux 容器、docker build 或 Kubernetes,通常迫使用户在安全性上做出令人不快的权衡:
- 在 Linux 上,标准 OCI 运行时(
runc、crun、runsc)需要root权限、暴露宿主机的/var/run/docker.sock、使用--privileged(CAP_SYS_ADMIN),或使用会扩大宿主内核攻击面的用户命名空间(unshare(CLONE_NEWUSER))。 - 在 Windows 和 macOS 上,容器引擎(
Docker Desktop、Podman Machine、Lima、OrbStack)会在 WSL2、Hyper-V 或Virtualization.framework中启动一个完整的 Linux 虚拟机内核,并在该虚拟机内运行持久的后台守护进程。
gvx(rungv CLI 与 z.gfire.dev/gvx/pkg/gvx Go SDK)提供了一种安全、零特权的替代方案。gvx 是 Google gVisor 用户态 Linux 内核(Sentry)与用户态网络协议栈(netstack)的可跟随上游更新的分支,可在 Windows、macOS 和 Linux 上,直接在单个无特权、自我锁定的宿主进程中执行未经修改的 linux/amd64 与 linux/arm64 ELF64 二进制文件及 OCI 容器。
安全保证(三环纵深防御):
gvx从不削弱宿主操作系统的安全边界,从不安装内核驱动程序,也从不在宿主上要求root、sudo、--privileged或CLONE_NEWUSER。在执行任何客户机指令之前,gvx会使用 Windows 受限令牌(Restricted Tokens)与作业对象(Job Objects)、macOS Seatbelt(deny default),或 Linux 的PR_SET_NO_NEW_PRIVS加 BPFseccomp锁定其自身的宿主进程。所有客户机系统调用、虚拟UID 0权限、挂载(VFS2)以及网络表(netstack)均被严格限制在内存安全的 GoSentry内核之内。
| 运行时 | 宿主操作系统 | 冷启动时间 | 内存占用 | 是否需要管理员或 sudo | 后台虚拟机守护进程 | 直接运行 OCI 镜像 |
|---|---|---|---|---|---|---|
gvx (whpx / hvf) | Windows (amd64, arm64) 与 macOS (arm64) | ~20 ms | ~30 MiB | 否 | 否 | 是 |
gvx (systrap) | Windows (amd64, arm64) 与 Linux (amd64, arm64) | ~25 至 65 ms | ~31 MiB | 否 | 否 | 是 |
| WSL2 / Docker Desktop | Windows 与 macOS | 800 至 2500 ms | > 512 MiB | 是(初始设置) | 是 | 需要守护进程 |
| QEMU(全系统) | Windows 与 macOS | 1500 至 4000 ms | > 256 MiB | 否 | 否 | 需要磁盘镜像 |
在 Windows 或 macOS 上不会启动任何 Linux 虚拟机内核。宿主上也不运行任何后台守护进程。宿主操作系统上不需要 root、sudo、用户命名空间(CLONE_NEWUSER)、mount(2)、TUN/TAP 驱动程序或 CAP_NET_ADMIN 权限。在该无特权进程内部,gvx 可运行 Docker-in-Docker(docker:dind)、docker build、docker compose,以及多节点高可用 Kubernetes(带嵌入式 etcd 与 Calico CNI 的 k3s)。
容器生命周期与三环宿主锁定
每个 gvx 沙箱都运行在一个无特权的宿主进程之内,并强制执行三阶段生命周期:预先打开所有所需的宿主文件描述符与套接字,通过 pkg/hostlockdown 锁定宿主操作系统进程,然后进入进程内的 Sentry 执行循环。

在将控制权交给第一条客户机指令之前,gvx 会预先打开其缓存的 .erofs 镜像层描述符、.ext4 卷文件描述符、/workspace 的 *os.Root 句柄以及宿主网络套接字,然后调用 hostlockdown.Apply():
- Windows NT(
lockdown_windows.go):调用带有SE_PRIVILEGE_REMOVED的AdjustTokenPrivileges,就地剥离权限;绑定一个以 Low 完整性级别(S-1-16-4096)运行、带DISABLE_MAX_PRIVILEGE的受限模拟令牌;将进程分配到一个匿名作业对象,其JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1会阻止创建任何子进程;并通过SetProcessMitigationPolicy启用DisallowWin32kSystemCalls = 1。 - macOS XNU(
lockdown_darwin.go):设置RLIMIT_NPROC = 0,并应用一个 Seatbelt 配置文件(sandbox_init_with_parameters),该配置文件从(version 1)(deny default)开始,阻止process-fork、process-exec*以及所有新的宿主文件打开操作。 - Linux(
lockdown_linux.go+pkg/gvx/syscallfilter_linux.go):设置PR_SET_NO_NEW_PRIVS,并通过SECCOMP_FILTER_FLAG_TSYNC在所有线程上安装 Sentry 的 BPFseccomp允许列表,同时对execve、execveat、clone、openat和socket显式设置EPERM规则。
跨平台系统调用拦截与 4 KB 分页(pkg/sentry/platform)
Linux ELF64 二进制文件假定 mmap(MAP_FIXED) 段映射使用 4 KB(0x1000)的内存页粒度。然而,Windows 的 VirtualAlloc 与 MapViewOfFile3 强制 64 KB(0x10000)对齐,而 Apple Silicon macOS(darwin/arm64)使用 16 KB(0x4000)的宿主页。此外,每个操作系统都需要不同的机制来拦截 Linux 的 SYSCALL(amd64)与 SVC #0(arm64)指令,且不能启动 Linux 虚拟机。
gvx 通过四个实现 pkg/sentry/platform.Context 的执行后端解决了这两个问题:

1. Windows 硬件引擎:platform/whpx(WinHvPlatform.dll)
在 windows/amd64 与 windows/arm64 上,platform/whpx 通过 WHvCreatePartition 创建一个轻量级超级管理程序分区,且不启动客户机操作系统内核:
- 64 KB 到 4 KB 分页解耦器:
whpx通过WHvMapGpaRange一次性地将pgalloc.MemoryFile的每个 64 MiB 区域注册到分区中。随后的MapFile与Unmap调用仅修改分区内的客户机页表,从而在 Windows 64 KB 分配粒度的前提下,强制执行精确的 4 KBR/W/X权限。 - 通过
CommPage进行零 API 寄存器交换:- 在
amd64上,客户机 Ring-3 的SYSCALL进入分区内的 Ring-0MSR_LSTAR桩(stubgen_x86.go),该桩将快速路径系统调用所用的通用寄存器(RAX, RDI, RSI, RDX, R10, R8, R9, RIP, RSP)写入共享的 4 KBCommPage,并执行HLT以退出WHvRunVirtualProcessor。 - 在
arm64上,客户机 EL0 的SVC #0进入 EL1 向量(stubgen_a64.go),将X0..X8, SP_EL0, ELR_EL1, PSTATE写入CommPage,并从未映射的 GPA 零地址加载数据,从而触发一次内存退出,返回 Sentry。
- 在
2. macOS 硬件引擎:platform/hvf(Hypervisor.framework)
在 Apple Silicon(darwin/arm64)上,platform/hvf 在 Hypervisor.framework 的 vCPU(hv_vm_create、hv_vcpu_run)中运行容器线程:
- 基于 16 KB 宿主页的两阶段 4 KB MMU 转换:第二阶段(
hv_vm_map)以 16 KB 宿主页粒度,将 1 GiB 的内存文件块一次性映射到中间物理地址(IPA)空间中,权限为HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXEC。在 vCPU 内部,EL1 的bluepill桩(bluepill_arm64.go)在TTBR0_EL1中配置TCR_EL1.TG0 = 0b00(4 KB 粒度),并填充 4 KB 的第一阶段页表项,其权限精确为R-X或RW-(UXN=1)。由于 ARM64 MMU 强制执行第一阶段与第二阶段权限的交集,硬件会在精确的 4 KB 边界上捕获权限错误。 - 系统调用陷阱:客户机 EL0 的
SVC #0陷入 EL1 的VBAR_EL1,将寄存器写入共享的 4 KBCommPage,并执行HVC #0以从hv_vcpu_run返回 Go Sentry 线程。
3. Windows 用户态回退:platform/systrap(winsystrap)
当硬件虚拟化不可用时(例如在嵌套的云虚拟机内部),winsystrap 会在 windows/amd64 与 windows/arm64 上以用户态运行客户机代码:
- 硬件
CR3/TTBR0_EL1隔离:客户机线程运行在隔离的桩子进程中,这些进程由一个生成的 1 KB 可执行映像(stubimage_xplat.go)启动,并受 Low 完整性令牌与作业对象的限制。 - 原地二进制修补与
winSysmsgIPC:在amd64上,修补器(patcher_amd64_xplat.go)将 2 字节的syscall(0f 05)指令替换为 5 字节的jmp rel32跳转(当 5 字节窗口不可用时则使用CC 90断点陷阱),跳入一个快速路径岛。在arm64上,svc #0被修补为指向岛槽位的B分支。该岛将寄存器写入共享的winSysmsg页面,并通过WaitOnAddress与WakeByAddressSingle与 Sentry 同步;而未修补的陷阱与页错误则会停在 Windows 调试对象上(WaitForDebugEventEx)。
4. Linux 用户态引擎:platform/systrap
在 linux/amd64 与 linux/arm64 上,systrap 运行客户机线程(sysmsg),它通过 SECCOMP_RET_TRAP(SIGSYS)捕获客户机系统调用,并将 syscall 指令位置二进制修补为共享内存蹦床(sysmsg),该蹦床通过 SYS_FUTEX 同步;同时,一个桩 syscallThread(syscall_thread.go)通过 SECCOMP_RET_USER_NOTIF(_RUN_SECCOMP_LOOP)执行地址空间操作(mmap、munmap、clone)。当 gvx 运行在外层 gVisor(runsc)沙箱中,且唯一的 SECCOMP_FILTER_FLAG_NEW_LISTENER 槽位返回 EBUSY 时,gvx 会自动将 syscallThread 切换为共享内存 futex 循环(_RUN_SYSCALL_LOOP),详见本系列第三部分。
gvx 如何将客户机 Linux 系统调用映射到虚拟子系统与宿主句柄
一旦平台引擎捕获客户机的 SYSCALL(amd64)或 SVC #0(arm64)指令,并将客户机寄存器写入 4 KB 共享页面(CommPage、sysmsg 或 winSysmsg),控制权便返回到 Go Sentry 任务协程(pkg/sentry/kernel/task_syscall.go)。Task.doSyscall() 并不将 Linux 系统调用转发给底层宿主操作系统内核,而是保存原始寄存器状态(SyscallSaveOrig()),评估客户机内的 BPF seccomp 过滤器与 ptrace 停止点,然后通过 linux.AMD64 或 linux.ARM64(pkg/sentry/syscalls/linux/linux64.go,覆盖约 370 个 Linux 系统调用)分派系统调用号,将其送入四个内部领域:

| 系统调用领域 | 代表性客户机 Linux 系统调用 | 内部 gvx 子系统(pkg/sentry) | 宿主操作系统边界交互(hostlockdown 之后) |
|---|---|---|---|
| A. 进程、命名空间、挂载与同步 | clone、execve、unshare、setns、mount、umount2、pivot_root、setuid、capset、futex、epoll_wait、pipe2 | kernel.Task、虚拟凭据、futex 表,以及内存中的 VFS2(procfs、sysfs、cgroupfs v2、tmpfs、overlayfs) | 零宿主系统调用(100% 为内存中的 Go 状态;对宿主操作系统完全不可见) |
B. 虚拟内存(MM) | mmap、munmap、mremap、mprotect、brk、madvise、memfd_create | mm.MemoryManager(4 KB VMA 树)与 pgalloc.MemoryFile | 通过 WHvMapGpaRange、hv_vm_map + TTBR0_EL1 或桩 mmap/mprotect 进行双向 4 KB 页表更新 |
C. 文件与存储(VFS2) | openat、pread64、pwrite64、getdents64、statx、renameat2、setxattr、mknod、inotify_add_watch | vfs.VirtualFilesystem 挂载表路由器(fsimpl/erofs、fsimpl/ext4lite、fsimpl/hostfs v2) | 对 1 个固定的 .erofs FD 或 1 个 .ext4 FD 进行双向 pread/pwrite,或在 *os.Root 上进行受限的相对路径遍历 |
| D. 网络与 Netfilter | socket、bind、listen、connect、sendmsg、recvmsg、netlink、setsockopt(IPT_SO_SET_REPLACE) | 用户态 pkg/tcpip(lo、eth0、docker0、veth、vxlan、wireguard、ipset、iptables-legacy) | 沙箱内部流量不使用任何宿主系统调用;外部流量映射到 net.Dial/net.Listen(-net=nat)或 1 个 UDP 套接字(-net=wg) |
当处理函数返回 (rval, ctrl, err) 时,Task.doSyscallInvoke() 会将任何错误转换为负的 Linux errno(-ExtractErrno(err, sysno)),将返回值写回共享寄存器页面(amd64 上为 RAX,arm64 上为 X0),然后恢复客户机执行。
存储(VFS2)与双轨用户态网络(netstack)
所有客户机文件与网络操作都在 Go Sentry 内部通过三个 VFS2 文件系统以及两条 pkg/netbackend 轨道来处理:

| 存储引擎 | 在 gvx 中的挂载角色 | 后端宿主资源 | 运行时宿主 openat / NtCreateFile 调用次数 | Windows Defender(WdFilter.sys)每文件扫描次数 |
|---|---|---|---|---|
fsimpl/erofs | 只读 OCI 镜像下层 | 每层单个 <sha256>.erofs 文件 | 0 | 0 |
fsimpl/ext4lite | 命名卷(-v name:/path) | 每个卷单个 <name>.ext4 文件 | 0 | 0 |
fsimpl/hostfs v2 | 宿主工作区(-workspace、-v /host:/guest) | 固定的 *os.Root 宿主目录 | 每次未缓存打开 O(depth) 次 | 每次关闭宿主文件 1 次 |
为什么 ext4lite 对 Docker、数据库和包管理器很重要
将数以万计的容器文件直接解压或写入 Windows NTFS 或 macOS APFS,会触发杀毒小过滤器驱动的扫描(WdFilter.sys)、大小写敏感性冲突以及缺失的 POSIX 扩展属性。
pkg/sentry/fsimpl/ext4lite 在一个预先打开的宿主文件中实现了纯 Go 的 ext4 文件系统(Windows 上为 %LOCALAPPDATA%\gvx\cache\volumes\<name>.ext4,macOS 上为 ~/Library/Caches/gvx/volumes/<name>.ext4,Linux 上为 ~/.cache/gvx/volumes/<name>.ext4),并具备以下特性:
- 4 KiB 块与 16 MiB 动态块组(
meta_bg):无需稀疏文件分配停顿即可动态增长(Linux 上使用fallocate,macOS 上使用fcntl(F_PREALLOCATE),Windows 上使用FileAllocationInfo),并在干净卸载时压缩未使用的尾部块组。 - JBD2 有序模式日志(inode 8):通过 CRC-32 校验和与撤销块实现原子事务提交,并提供
fsckLite恢复机制。 overlayfs上层支持:实现trusted.overlay.*与security.capability扩展属性、字符设备白出(0:0)、d_type目录项报告以及RENAME_EXCHANGE。
实践工作负载:Docker 引擎、Kubernetes(k3s + Calico)与 Go SDK
安装 rungv:经 SLSA 证明的预构建发布版或 go install
你可以从 GitHub Releases 下载适用于 linux/amd64、linux/arm64、darwin/arm64、windows/amd64 或 windows/arm64 的预构建 rungv 压缩包(每个压缩包均由 SLSA 发布流水线构建,并附带 Sigstore 溯源包与 SHA256SUMS),也可以使用 go install 直接从源码安装:
1# 方案 A:下载预构建发布版并验证 SLSA 构建溯源
2gh attestation verify rungv-v0.2.0-linux-amd64.tar.gz --repo gfire-sigs/gvx \
3 --signer-workflow gfire-sigs/gvx/.github/workflows/release.yml \
4 --source-ref refs/tags/v0.2.0 --deny-self-hosted-runners
5shasum -a 256 -c SHA256SUMS --ignore-missing
6tar -xzf rungv-v0.2.0-linux-amd64.tar.gz
7
8# 方案 B:通过 Go 直接安装(Linux / Windows)
9CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest
在 macOS(
darwin/arm64)上,预构建发布压缩包已使用com.apple.security.hypervisor权限进行签名(xattr -d com.apple.quarantine rungv可清除浏览器隔离属性)。如果你在 macOS 上通过go install或go build在本地构建,请使用codesign --sign - --entitlements entitlements.plist --force $(which rungv)对二进制文件进行签名。
1. 运行无特权 Docker 引擎(docker:dind)
按照 docs/docker-engine.md 的说明,你可以在 Windows、macOS 或 Linux 上的 rungv 中运行未经修改的 docker:dind 容器,并使用宿主机的 docker CLI 对其进行控制:
1# 1. 为转发的 Unix 套接字创建宿主目录
2mkdir -p ~/.gvx
3
4# 2. 在带有 ext4lite 卷的 gvx 沙箱中启动 docker:dind
5rungv run \
6 -p unix:~/.gvx/docker.sock:2375 \
7 -p 127.0.0.1:9000-9019:9000-9019 \
8 -v gvx-docker:/var/lib/docker \
9 -e DOCKER_TLS_CERTDIR= \
10 docker:dind --tls=false
在第二个终端中,将宿主机的 docker CLI 指向沙箱套接字:
1docker context create gvx --docker host=unix://$HOME/.gvx/docker.sock
2docker context use gvx
3
4docker info --format '{{.Driver}} {{.OperatingSystem}}'
5# 输出:overlayfs Alpine Linux ... (containerized)
6
7docker build -t myapp ./app
8docker compose up -d
2. 在无特权沙箱中运行 Kubernetes(k3s + Calico)
由于 gvx 实现了 /dev/kmsg、cgroup v2(memory.max 的 OOM 终止器与 cpu.max 带宽限流)、/proc/diskstats、wireguard 与 vxlan netlink 设备、ipset,以及 iptables-legacy(addrtype、conntrack、statistic、recent),一个未经修改的 rancher/k3s 镜像可以在单个 rungv 进程内达到 Ready 状态:
1mkdir -p ~/.gvx/k3s
2
3rungv run \
4 -e IPTABLES_MODE=legacy \
5 -p 127.0.0.1:6443:6443 \
6 -p 127.0.0.1:30000-30019:30000-30019 \
7 -v k3s:/var/lib/rancher \
8 --mount type=bind,src=$HOME/.gvx/k3s,dst=/output \
9 rancher/k3s:v1.31.4-k3s1 server \
10 --disable=traefik,servicelb \
11 --write-kubeconfig=/output/kubeconfig.yaml \
12 --write-kubeconfig-mode=644
你还可以通过 WireGuard 将多个 gvx 节点加入集群(flannel-backend=wireguard-native 或 wg0 网格),并配合嵌入式 etcd 实现高可用;或者运行 Calico(使用 FELIX_IPTABLESBACKEND=Legacy 的 calico-vxlan.yaml),从而完全在用户态中执行 Kubernetes NetworkPolicy。
3. 通过 z.gfire.dev/gvx/pkg/gvx 在 Go 中嵌入 Linux 容器
Go 应用程序可以直接嵌入 Linux 容器,无需外部容器守护进程:
1package main
2
3import (
4 "context"
5 "fmt"
6 "os"
7
8 "z.gfire.dev/gvx/pkg/gvx"
9)
10
11func main() {
12 ctx := context.Background()
13 sb, err := gvx.New(ctx, gvx.Config{
14 Platform: gvx.PlatformAuto,
15 NetworkMode: gvx.NetModeNAT,
16 OCIImage: "alpine:3.20",
17 WorkspacePath: "./repo",
18 Argv: []string{"/usr/bin/git", "status"},
19 Stdout: os.Stdout,
20 Stderr: os.Stderr,
21 })
22 if err != nil {
23 fmt.Fprintf(os.Stderr, "init sandbox: %v\n", err)
24 os.Exit(1)
25 }
26 defer sb.Close()
27
28 exitCode, err := sb.Run(ctx)
29 if err != nil {
30 fmt.Fprintf(os.Stderr, "run sandbox: %v\n", err)
31 os.Exit(1)
32 }
33 os.Exit(exitCode)
34}
本系列的延伸阅读
- gvx:在一个无特权进程中运行 Linux 容器、Docker 与 Kubernetes(本文)
- 无需 root 或用户命名空间运行 Docker、docker build 与 docker compose:如何在
sudo、unshare(CLONE_NEWUSER)、mount(2)与/dev/fuse均被阻止的受限沙箱中引导docker:dind。 - 在 gVisor 与嵌套沙箱中借助 gvx 运行 Docker-in-Docker:为什么在
gVisor内执行apt-get install docker.io与嵌套runsc会因五道内核障碍而失败,以及gvx如何借助共享内存 futex 循环运行嵌套的docker:dind。 - 借助 gvx 为 AI 智能体强化学习推演提供快速隔离的 Docker 环境:利用数百个并行的无特权
docker:dind实例以及 100 毫秒级写时复制.ext4卷重置,扩展SWE-bench与智能体强化学习推演。