GoSuda

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

By Lemon Mint views ...

引言:无需 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 加 BPF seccomp 锁定其自身的宿主进程。所有客户机系统调用、虚拟 UID 0 权限、挂载(VFS2)以及网络表(netstack)均被严格限制在内存安全的 Go Sentry 内核之内。

运行时宿主操作系统冷启动时间内存占用是否需要管理员或 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 DesktopWindows 与 macOS800 至 2500 ms> 512 MiB是(初始设置)是需要守护进程
QEMU(全系统)Windows 与 macOS1500 至 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 容器生命周期与宿主自锁定

在将控制权交给第一条客户机指令之前,gvx 会预先打开其缓存的 .erofs 镜像层描述符、.ext4 卷文件描述符、/workspace 的 *os.Root 句柄以及宿主网络套接字,然后调用 hostlockdown.Apply():

  1. 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。
  2. macOS XNU(lockdown_darwin.go):设置 RLIMIT_NPROC = 0,并应用一个 Seatbelt 配置文件(sandbox_init_with_parameters),该配置文件从 (version 1)(deny default) 开始,阻止 process-fork、process-exec* 以及所有新的宿主文件打开操作。
  3. Linux(lockdown_linux.go + pkg/gvx/syscallfilter_linux.go):设置 PR_SET_NO_NEW_PRIVS,并通过 SECCOMP_FILTER_FLAG_TSYNC 在所有线程上安装 Sentry 的 BPF seccomp 允许列表,同时对 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 的执行后端解决了这两个问题:

gvx 平台系统调用拦截与 4KB 分页机制

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 KB R/W/X 权限。
  • 通过 CommPage 进行零 API 寄存器交换:
    • 在 amd64 上,客户机 Ring-3 的 SYSCALL 进入分区内的 Ring-0 MSR_LSTAR 桩(stubgen_x86.go),该桩将快速路径系统调用所用的通用寄存器(RAX, RDI, RSI, RDX, R10, R8, R9, RIP, RSP)写入共享的 4 KB CommPage,并执行 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 KB CommPage,并执行 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 完整性令牌与作业对象的限制。
  • 原地二进制修补与 winSysmsg IPC:在 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 系统调用)分派系统调用号,将其送入四个内部领域:

gvx 如何将客户机 Linux 系统调用映射到子系统与宿主操作系统

系统调用领域代表性客户机 Linux 系统调用内部 gvx 子系统(pkg/sentry)宿主操作系统边界交互(hostlockdown 之后)
A. 进程、命名空间、挂载与同步clone、execve、unshare、setns、mount、umount2、pivot_root、setuid、capset、futex、epoll_wait、pipe2kernel.Task、虚拟凭据、futex 表,以及内存中的 VFS2(procfs、sysfs、cgroupfs v2、tmpfs、overlayfs)零宿主系统调用(100% 为内存中的 Go 状态;对宿主操作系统完全不可见)
B. 虚拟内存(MM)mmap、munmap、mremap、mprotect、brk、madvise、memfd_createmm.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_watchvfs.VirtualFilesystem 挂载表路由器(fsimpl/erofs、fsimpl/ext4lite、fsimpl/hostfs v2)对 1 个固定的 .erofs FD 或 1 个 .ext4 FD 进行双向 pread/pwrite,或在 *os.Root 上进行受限的相对路径遍历
D. 网络与 Netfiltersocket、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 VFS2 存储引擎与双轨 Netstack

存储引擎在 gvx 中的挂载角色后端宿主资源运行时宿主 openat / NtCreateFile 调用次数Windows Defender(WdFilter.sys)每文件扫描次数
fsimpl/erofs只读 OCI 镜像下层每层单个 <sha256>.erofs 文件00
fsimpl/ext4lite命名卷(-v name:/path)每个卷单个 <name>.ext4 文件00
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}

本系列的延伸阅读

  1. gvx:在一个无特权进程中运行 Linux 容器、Docker 与 Kubernetes(本文)
  2. 无需 root 或用户命名空间运行 Docker、docker build 与 docker compose:如何在 sudo、unshare(CLONE_NEWUSER)、mount(2) 与 /dev/fuse 均被阻止的受限沙箱中引导 docker:dind。
  3. 在 gVisor 与嵌套沙箱中借助 gvx 运行 Docker-in-Docker:为什么在 gVisor 内执行 apt-get install docker.io 与嵌套 runsc 会因五道内核障碍而失败,以及 gvx 如何借助共享内存 futex 循环运行嵌套的 docker:dind。
  4. 借助 gvx 为 AI 智能体强化学习推演提供快速隔离的 Docker 环境:利用数百个并行的无特权 docker:dind 实例以及 100 毫秒级写时复制 .ext4 卷重置,扩展 SWE-bench 与智能体强化学习推演。