gvx: 1つの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 仮想マシンカーネルを起動し、その VM 内で永続的なバックグラウンドデーモンを実行する。
gvx(rungv CLI および z.gfire.dev/gvx/pkg/gvx Go SDK)は、安全でゼロ特権の代替手段を提供する。Google の gVisor のユーザー空間 Linux カーネル(Sentry)およびユーザー空間ネットワークスタック(netstack)の、アップストリームに追従可能なフォークとして構築された gvx は、Windows、macOS、Linux 上の単一の非特権かつ自己ロックされたホストプロセス内で、変更されていない linux/amd64 および linux/arm64 の ELF64 バイナリおよび OCI コンテナを直接実行する。
セキュリティ保証(三重リング多層防御):
gvxはホスト OS のセキュリティ境界を弱めることは決してなく、カーネルドライバをインストールすることもなく、ホスト上でroot、sudo、--privileged、またはCLONE_NEWUSERを必要とすることも決してない。ゲスト命令を1つも実行する前に、gvxは自身のホストプロセスを、Windows Restricted Tokens と Job Objects、macOS Seatbelt(deny default)、または Linux のPR_SET_NO_NEW_PRIVSと BPFseccompを用いてロックダウンする。すべてのゲストシステムコール、仮想UID 0権限、マウント(VFS2)、およびネットワークテーブル(netstack)は、メモリ安全な Go 言語製のSentryカーネル内に厳密に閉じ込められる。
| ランタイム | ホスト OS | コールドスタート | メモリフットプリント | 管理者権限または sudo の必要性 | バックグラウンド VM デーモン | OCI イメージの直接実行 |
|---|---|---|---|---|---|---|
gvx (whpx / hvf) | Windows (amd64, arm64) & macOS (arm64) | ~20 ms | ~30 MiB | いいえ | いいえ | はい |
gvx (systrap) | Windows (amd64, arm64) & Linux (amd64, arm64) | ~25 to 65 ms | ~31 MiB | いいえ | いいえ | はい |
| WSL2 / Docker Desktop | Windows & macOS | 800 to 2500 ms | > 512 MiB | はい(初回セットアップ時) | はい | デーモンが必要 |
| QEMU (Full System) | Windows & macOS | 1500 to 4000 ms | > 256 MiB | いいえ | いいえ | ディスクイメージが必要 |
Windows および macOS 上で Linux VM カーネルは起動しない。ホスト上でバックグラウンドデーモンは実行されない。ホスト OS において、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 サンドボックスは1つの非特権ホストプロセス内で実行され、3段階のライフサイクルを適用する。すなわち、必要なすべてのホストファイルディスクリプタおよびソケットの事前オープン、pkg/hostlockdown によるホスト OS プロセスのロックダウン、そしてプロセス内の Sentry 実行ループへの移行である。

最初のゲスト命令へ制御を移す前に、gvx はキャッシュ済みの .erofs イメージレイヤーのディスクリプタ、.ext4 ボリュームのファイルディスクリプタ、/workspace の *os.Root ハンドル、およびホストのネットワークソケットを事前にオープンし、その後 hostlockdown.Apply() を呼び出す:
- Windows NT(
lockdown_windows.go):AdjustTokenPrivilegesをSE_PRIVILEGE_REMOVEDとともに呼び出して権限をその場で剥奪し、Low 整合性レベル(S-1-16-4096)のDISABLE_MAX_PRIVILEGE制限付き偽装トークンを割り当て、プロセスをJOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1を設定した無名の Job Object に割り当てて(子プロセスの生成をすべてブロックする)、SetProcessMitigationPolicyを介してDisallowWin32kSystemCalls = 1を有効化する。 - macOS XNU(
lockdown_darwin.go):RLIMIT_NPROC = 0を設定し、(version 1)(deny default)から始まりprocess-fork、process-exec*、およびすべての新規ホストファイルオープンをブロックする Seatbelt プロファイル(sandbox_init_with_parameters)を適用する。 - 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)のホストページを使用する。さらに、各 OS は、Linux VM を起動することなく、Linux の SYSCALL(amd64)および SVC #0(arm64)命令を傍受するために、それぞれ異なるメカニズムを必要とする。
gvx は、pkg/sentry/platform.Context を実装する4つの実行バックエンドを通じて、これら両方の問題を解決する:

1. Windows ハードウェアエンジン: platform/whpx(WinHvPlatform.dll)
windows/amd64 および windows/arm64 上では、platform/whpx がゲスト OS カーネルを起動することなく、WHvCreatePartition を介して軽量なハイパーバイザパーティションを作成する:
- 64 KB から 4 KB へのページングデカップラ:
whpxは、pgalloc.MemoryFileの各 64 MiB アリーナを、WHvMapGpaRangeを介して一度だけパーティションに登録する。その後のMapFileおよびUnmapの呼び出しは、パーティション内のゲストページテーブルのみを変更し、Windows の 64 KB 割り当て粒度にもかかわらず、正確な 4 KB 単位のR/W/X権限を強制する。 CommPageを介したゼロ API のレジスタ交換:amd64では、ゲストの Ring-3SYSCALLがパーティション内の Ring-0MSR_LSTARスタブ(stubgen_x86.go)に入る。このスタブは高速パスのシステムコール用 GPR(RAX, RDI, RSI, RDX, R10, R8, R9, RIP, RSP)を共有の 4 KBCommPageに退避させ、HLTを実行してWHvRunVirtualProcessorから脱出する。arm64では、ゲストの EL0SVC #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)内でコンテナのスレッドを実行する:
- 2段階の 4 KB over 16 KB MMU 変換: Stage-2(
hv_vm_map)は、1 GiB のメモリファイルチャンクを、HV_MEMORY_READ | HV_MEMORY_WRITE | HV_MEMORY_EXECを伴い、16 KB のホストページ粒度で中間物理アドレス(IPA)空間へ一度だけマップする。vCPU 内では、EL1 のbluepillスタブ(bluepill_arm64.go)がTTBR0_EL1においてTCR_EL1.TG0 = 0b00(4 KB granule)を設定し、正確なR-XまたはRW-(UXN=1)権限を持つ 4 KB の Stage-1 ページテーブルエントリを配置する。ARM64 MMU は Stage-1 と Stage-2 の権限の共通部分を強制するため、ハードウェアは正確な 4 KB 境界で権限フォルトをトラップする。 - システムコールのトラップ: ゲストの EL0
SVC #0は EL1 のVBAR_EL1へトラップし、レジスタを共有 4 KBCommPageに退避させ、HVC #0を実行してhv_vcpu_runから Go の Sentry スレッドへ戻る。
3. Windows ユーザーモードフォールバック: platform/systrap(winsystrap)
ハードウェア仮想化が利用できない場合(ネストされたクラウド VM 内など)、winsystrap は windows/amd64 および windows/arm64 上でユーザーモードにおいてゲストコードを実行する:
- ハードウェアの
CR3/TTBR0_EL1分離: ゲストスレッドは、生成された 1 KB の実行可能ファイル(stubimage_xplat.go)から起動された隔離されたスタブサブプロセス内で実行され、Low 整合性トークンおよび Job Object によって閉じ込められる。 - その場でのバイナリパッチングと
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 Debug Object(WaitForDebugEventEx)上で停止する。
4. Linux ユーザーモードエンジン: platform/systrap
linux/amd64 および linux/arm64 上では、systrap は、SECCOMP_RET_TRAP(SIGSYS)を介してゲストのシステムコールをトラップし、syscall 箇所を SYS_FUTEX によって同期される共有メモリのトランポリン(sysmsg)へバイナリパッチするゲストスレッド(sysmsg)を実行する。一方、スタブである syscallThread(syscall_thread.go)は、SECCOMP_RET_USER_NOTIF(_RUN_SECCOMP_LOOP)を介してアドレス空間の操作(mmap、munmap、clone)を実行する。gvx が、単一の SECCOMP_FILTER_FLAG_NEW_LISTENER スロットが EBUSY を返す外側の gVisor(runsc)サンドボックス内で実行される場合、gvx は自動的に syscallThread を共有メモリのフュテックスループ(_RUN_SYSCALL_LOOP)へ切り替える。詳細は本シリーズの第3部で述べる。
gvx がゲストの Linux システムコールを仮想サブシステムおよびホストハンドルへ対応付ける仕組み
プラットフォームエンジンがゲストの SYSCALL(amd64)または SVC #0(arm64)命令をトラップし、ゲストのレジスタを 4 KB の共有ページ(CommPage、sysmsg、または winSysmsg)に書き込むと、制御は Go の Sentry タスクゴルーチン(pkg/sentry/kernel/task_syscall.go)へ戻る。Linux のシステムコールを基盤となるホスト OS カーネルへ転送する代わりに、Task.doSyscall() は元のレジスタ状態(SyscallSaveOrig())を保存し、ゲスト内の BPF seccomp フィルタおよび ptrace 停止を評価した上で、linux.AMD64 または linux.ARM64(pkg/sentry/syscalls/linux/linux64.go、約370個の Linux システムコールを網羅)を通じてシステムコール番号を発送し、4つの内部ドメインへ振り分ける:

| システムコールドメイン | 代表的なゲスト Linux システムコール | 内部 gvx サブシステム(pkg/sentry) | ホスト OS 境界との相互作用(hostlockdown 適用後) |
|---|---|---|---|
| A. プロセス、名前空間、マウント、同期 | clone, execve, unshare, setns, mount, umount2, pivot_root, setuid, capset, futex, epoll_wait, pipe2 | kernel.Task、仮想資格情報、フュテックステーブル、およびインメモリの VFS2(procfs、sysfs、cgroupfs v2、tmpfs、overlayfs) | ホストへのシステムコールはゼロ(100% インメモリの Go 状態であり、ホスト OS からは完全に不可視) |
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)
ゲストのすべてのファイルおよびネットワーク操作は、3つの VFS2 ファイルシステムと2つの pkg/netbackend トラックを通じて、Go の Sentry 内で処理される:

| ストレージエンジン | 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 ホストディレクトリ | キャッシュされていない open ごとに O(depth) | ホストファイルのクローズごとに 1 |
Docker、データベース、およびパッケージマネージャにおいて ext4lite が重要である理由
数万にのぼるコンテナファイルを Windows NTFS または macOS APFS へ直接展開または書き込むと、アンチウイルスのミニフィルタによるスキャン(WdFilter.sys)、大文字小文字の区別による衝突、および POSIX 拡張属性の欠落が発生する。
pkg/sentry/fsimpl/ext4lite は、事前にオープンされた1つのホストファイル(Windows では %LOCALAPPDATA%\gvx\cache\volumes\<name>.ext4、macOS では ~/Library/Caches/gvx/volumes/<name>.ext4、Linux では ~/.cache/gvx/volumes/<name>.ext4)内に、純粋な Go 言語による ext4 ファイルシステムを実装し、以下の機能を備える:
- 4 KiB ブロックおよび 16 MiB の動的ブロックグループ(
meta_bg): スパースファイルの割り当て停止を伴わずに動的に拡張し(Linux ではfallocate、macOS ではfcntl(F_PREALLOCATE)、Windows ではFileAllocationInfo)、クリーンなアンマウント時に未使用の末尾グループを圧縮する。 - JBD2 ordered-mode ジャーナル(inode 8): CRC-32 チェックサムおよび revoke ブロックを伴う原子的なトランザクションのコミットに加え、
fsckLiteによる回復を備える。 overlayfsのアッパーレイヤー対応:trusted.overlay.*およびsecurity.capabilityの拡張属性、キャラクタデバイスのホワイトアウト(0:0)、d_typeによるディレクトリエントリの報告、およびRENAME_EXCHANGEを実装する。
実用的なワークロード:Docker Engine、Kubernetes(k3s + Calico)、および Go SDK
rungv のインストール:SLSA 認証済みのビルド済みリリースまたは go install
linux/amd64、linux/arm64、darwin/arm64、windows/amd64、または windows/arm64 向けのビルド済み rungv アーカイブは GitHub Releases からダウンロードできる(各アーカイブは SLSA リリースパイプラインによってビルドされ、Sigstore の provenance バンドルおよび SHA256SUMS を伴う)。あるいは、go install によってソースから直接インストールすることもできる:
1# オプション A:ビルド済みリリースをダウンロードし、SLSA ビルドの provenance を検証する
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 Engine(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
2つ目のターミナルで、ホストの 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 killer および 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
埋め込み etcd による高可用性構成で、複数の gvx ノードを WireGuard(flannel-backend=wireguard-native または wg0 メッシュ)を介して結合することもできる。あるいは、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: 1つの非特権プロセスで Linux コンテナ、Docker、および Kubernetes を実行する(本記事)
- root またはユーザー名前空間を用いずに Docker、docker build、および docker compose を実行する:
sudo、unshare(CLONE_NEWUSER)、mount(2)、および/dev/fuseがブロックされたロックダウン済みのサンドボックスにおいて、docker:dindをブートストラップする方法。 - gvx を用いた gVisor およびネストされたサンドボックス内での Docker-in-Docker の実行:
apt-get install docker.ioおよびネストされたrunscが、gVisor内で5つのカーネル上の障壁によりなぜ失敗するのか、またgvxがどのようにして共有メモリのフュテックスループを用いてネストされたdocker:dindを実行するのかについて。 - gvx を用いた AI エージェントの RL ロールアウトのための高速な隔離 Docker 環境: 数百の並列な非特権
docker:dindインスタンスおよび100ミリ秒のコピーオンライト.ext4ボリュームリセットを用いて、SWE-benchおよびエージェントの強化学習ロールアウトを拡張する。