gvx を使用した gVisor および入れ子サンドボックス内での Docker-in-Docker の実行

クイックソリューション:gVisor(runsc)サンドボックス内でdocker:dindを安全に実行する
クラウドAIエージェントの計算環境(Google Agent Compute Engineサンドボックスなど)、Kubernetesのagent-sandboxデプロイメント(RuntimeClass: gvisor)、および強化学習の評価ハーネスは、ホストカーネルを保護するために、信頼されていないワークロードを**gVisor(runsc)**コンテナ内に隔離します。デフォルトの非特権runscコンテナ内のワークロードが、apt-getでdocker.ioまたはdocker-ceをインストールし、service docker startまたはdockerdを起動し、docker buildを実行しようとすると、コンテナ内でroot(UID 0)であっても失敗します。
gvx(rungv)を使用すると、外側のRuntimeClassを弱めたり、ホストのソケットを公開したり、--privilegedを付与したりすることなく、非特権のgVisor(runsc)コンテナ内で(あるいはgvxの内部でgvxをネストして)、docker:dind、docker build、docker composeを安全に直接実行できます。
AIエージェント向けの低労力かつトークン効率の高いソリューション:gVisorベースのクラウドエージェント環境において、
docker.ioのインストールを試み、/etc/init.d/dockerのulimitエラー、iptables-nftの失敗、iptables-legacyのraw socket拒否、overlay2のマウントエラー、docker buildのrunc/pivot_rootクラッシュを手作業で回避しようとすると、完全に失敗するまでに通常20回以上のツール呼び出しのターンと数万トークンを消費します。gvx(rungv)は、低労力かつトークン効率の高い、そのまま置き換えて利用できるソリューションです。エージェントは1つのターンで3つのコマンドを実行するだけで、動作するユーザー空間のDockerデーモンを起動し、プロジェクトファイルを変更することなく直ちにdocker buildとdocker composeを実行できます。
外側のサンドボックスの隔離の維持と強化:
runsc内でrungvを実行しても、外側のgVisorサンドボックスによって課された制限は一つも回避されません。代わりに、gvxは独自のhostlockdown(PR_SET_NO_NEW_PRIVS+seccomp)、独自のメモリ内netstack、および単一ファイルのext4liteボリュームを備えた、二次的でネストされたユーザー空間のgVisorSentryカーネルを作成します。内側のdockerdとruncコンテナは二重にサンドボックス化されており、そのシステムコール、マウント、生のパケットは内側のgvxユーザー空間カーネルの外に出ることはありません。
1# 既存のgVisor(runsc)コンテナまたはKubernetes Pod内で:
2# SLSAでアテストされたGitHubリリースアーカイブからrungvをインストールするか、go installを使用する:
3CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest
4
5mkdir -p ~/.gvx
6rungv run \
7 -p unix:~/.gvx/docker.sock:2375 \
8 -v gvx-docker:/var/lib/docker \
9 -e DOCKER_TLS_CERTDIR= \
10 docker:dind --tls=false &
11
12# 内側のgvxソケットに対して、標準的なDockerビルドとコンテナを実行する
13export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
14until docker info >/dev/null 2>&1; do sleep 0.5; done
15docker info --format '{{.Driver}} {{.OperatingSystem}}'
16# 出力: overlayfs Alpine Linux ... (containerized)
17
18docker build -t nested-app:test .
19docker run --rm nested-app:test
gVisor(runsc)内で直接Dockerをインストールすると失敗する5つの理由
ユーザーやAIコーディングエージェントが標準的なrunscコンテナ内でapt-get install -y docker.io(またはdocker-ce)を実行し、dockerdの起動を試みると、5つの連続する技術的な障壁に直面します。
1. 初期化スクリプト(systemctl / service docker startのulimit)およびカーネルモジュール(modprobe、4.4.0)の失敗
apt-get install -y docker.io(またはdocker-ce)の実行後、systemctl start dockerまたはservice docker startでデーモンの起動を試みると、PID 1がsystemdではないこと、および/etc/init.d/dockerがサンドボックスのRLIMIT_NOFILEのハードな上限を超えてulimit -nを引き上げようとすることにより、即座に失敗します。その後dockerdを直接呼び出すと、初期化スクリプトがgVisorの仮想カーネルバージョン(4.4.0)に対してmodprobeを呼び出し、overlay、bridge、br_netfilter、ip_tables、nf_natをロードしようとします。しかし、/lib/modules/4.4.0ディレクトリは存在しません。
1System has not been booted with systemd as init system (PID 1). Can't operate.
2Failed to connect to bus: Host is down
3/etc/init.d/docker: 62: ulimit: error setting limit (Operation not permitted)
4level=warning msg="Running modprobe bridge br_netfilter failed with message: modprobe: WARNING: Module bridge not found in directory /lib/modules/4.4.0\nmodprobe: WARNING: Module br_netfilter not found in directory /lib/modules/4.4.0\n, error: exit status 1"
5modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
6modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
7modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
8modprobe: FATAL: Module nf_nat not found in directory /lib/modules/4.4.0
2. iptables-nft(nf_tables Netlink)、iptables-legacyの生ソケット拒否、およびdocker0ブリッジの失敗
Ubuntu 22.04、Ubuntu 24.04、Debian 11/12では、apt-get install docker.ioにより、/usr/sbin/iptablesのデフォルトの代替としてiptables-nft(nf_tables netlinkサブシステムをバックエンドとする)が選択されます。さらに、ユーザーやエージェントがiptables-legacy(update-alternatives --set iptables /usr/sbin/iptables-legacy)に切り替えた場合でも、デフォルトの非特権gVisorサンドボックス(--net-raw=false)はAF_INET, SOCK_RAWソケットをEPERMで拒否するため、UID 0で実行していてもiptables-legacyはPermission denied (you must be root)で失敗します。
1failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to register "bridge" driver: failed to create NAT chain DOCKER: iptables failed: iptables --wait -t nat -N DOCKER: iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
2iptables v1.8.10 (legacy): can't initialize iptables table `nat': Permission denied (you must be root)
3Perhaps iptables or your kernel needs to be upgraded.
4iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
5iptables: No chain/target/match by that name.
6Unable to enable SKIP DNAT rule: (iptables failed: iptables --wait -t nat -I DOCKER -i docker0 -j RETURN: iptables: No chain/target/match by that name.)
7Error initializing network controller: error obtaining controller instance: failed to create bridge "docker0": operation not permitted
3. gofer / 9pルートファイルシステム上のoverlay2の失敗とvfsによるディスク枯渇
外側のrunsc管理者が/var/lib/dockerに専用のtmpfsまたはfilestoreマウントを事前に明示的に設定していない限り、コンテナのrootfsはgVisorのgofer(9p/lisafs)またはネストされたoverlayマウントによって支えられています。/var/lib/dockerの上にoverlay2をマウントしようとすると失敗します。
1level=error msg="[graphdriver] prior storage driver overlay2 failed: driver not supported"
2failed to mount overlay: invalid argument
3Error starting daemon: error initializing graphdriver: driver not supported: overlay2
ユーザーやエージェントがdockerd --storage-driver=vfsで起動することによりこのエラーを回避しようとすると、Dockerはコピーオンライト(Copy-on-Write)のレイヤー共有を完全に無効化します。Dockerfile内のすべてのRUN命令は、ルートファイルシステム全体の再帰的なコピー(dirCopy)を実行します。その結果、10のビルドステップにわたる500 MBのイメージは5 GBの書き込みに増大し、サンドボックスはno space left on deviceまたはI/Oタイムアウトによって終了します。
4. docker build(BuildKit)およびネストされたruncの失敗(pivot_root、/proc、cgroup、DNS)
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfsを指定してdockerdを強制的に起動したとしても、BuildKitまたはruncがビルドコンテナを起動する時点で、**docker build -t app .**またはdocker runの実行は即座に失敗します。
1ERROR: failed to solve: process "/bin/sh -c ..." did not complete successfully:
2docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process:
3error during container init: error jailing process inside rootfs: pivot_root .: invalid argument
4error during container init: error mounting "cgroup" to rootfs at "/sys/fs/cgroup": mount cgroup:/sys/fs/cgroup/cpuset (via /proc/self/fd/6), flags: 0xf, data: cpuset: operation not permitted
5cgroups: cgroup mountpoint does not exist: unknown
6runc create failed: systemd not running on this host: cannot use systemd cgroup manager
7error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
8error during container init: error loading seccomp filter into kernel: operation not permitted
9Temporary failure resolving 'deb.debian.org'
10Temporary failure resolving 'archive.ubuntu.com'
5. ネストされたrunscがSECCOMP_FILTER_FLAG_NEW_LISTENER(EBUSY)で失敗する
systrapプラットフォームを使用してrunscの内部で別のrunscインスタンスを実行しようとすると、初期化は直ちに中止されます。
1creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
Linuxカーネルとgvisor Sentryはいずれも、タスク階層ごとに最大1つのSECCOMP_FILTER_FLAG_NEW_LISTENER(SECCOMP_RET_USER_NOTIF)リスナーのみを許可します。外側のサンドボックスがすでにそのリスナーの枠を保持しているため、内側のseccomp(2)呼び出しはEBUSY(device or resource busy)を返します。
gvxがgVisor内のネストされた実行を解決する方法
gvxは、5つの障壁のすべてを独自のユーザー空間カーネルとプラットフォーム層の内部で解決し、rungvが外側のgVisor(runsc)コンテナ内、さらには別のgvxコンテナ内(gvxの中のgvx)で再帰的に実行できるようにします。

1. EBUSYから_RUN_SYSCALL_LOOPのfutexループへの自動systrapフォールバック
Linuxのsystrapでは、各stubサブプロセスは2種類のスレッドを実行します。
- ゲスト実行スレッド(
sysmsg):SECCOMP_RET_TRAP(SIGSYS)およびバイナリパッチが適用されたjmp rel32トランポリン(syshandler)を介してゲストのシステムコールを捕捉し、共有メモリとSYS_FUTEXを介してレジスタ状態をSentryと交換します。 - stubの
syscallThread(syscall_thread.go):Sentryの代わりに、stubサブプロセス内でアドレス空間のシステムコール(mmap、munmap、clone)を実行します。
upstreamのgVisorでは、syscallThreadはstub_amd64.sおよびstub_arm64.sにおいて2種類のウェイクアップモードをサポートしています。
- Seccomp User-Notifyループ(
_RUN_SECCOMP_LOOP = 6、t.seccompNotify != nilの場合):syscallThread.init()の間に、t.installSeccompNotify()がSECCOMP_RET_USER_NOTIFリスナー(seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules))をインストールし、Sentryがioctl(SECCOMP_IOCTL_NOTIF_SEND)およびioctl(SECCOMP_IOCTL_NOTIF_RECV)を介してリクエストをディスパッチできるようにします。 - Futexシステムコールループ(
_RUN_SYSCALL_LOOP = 5、t.seccompNotify == nilの場合):syscallThreadはsyscall_loop(stub_amd64.s/stub_arm64.s)を実行し、共有の2ページ構成のsyscallSentryMessage(stub内ではPROT_READ)とsyscallStubMessage(stub内ではPROT_READ | PROT_WRITE)領域上でSYS_FUTEX(FUTEX_WAIT)によりスリープします。
upstreamのgVisorは、seccompNotifyIsSupportedが真である場合に無条件でt.installSeccompNotify()を呼び出し、seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)がEBUSYを返すとstubの作成を中止します。
gvx(pkg/sentry/platform/systrap/seccomp_notify_xplat.goおよびsyscall_thread.go)では、syscallThread.init()がt.installSeccompNotifyIfFree()を呼び出します。
1// pkg/sentry/platform/systrap/seccomp_notify_xplat.go
2var seccompNotifyBusy atomic.Bool
3
4func (t *syscallThread) installSeccompNotifyIfFree() (*os.File, error) {
5 if seccompNotifyBusy.Load() {
6 return nil, nil
7 }
8 f, err := t.installSeccompNotify()
9 if errors.Is(err, unix.EBUSY) {
10 if seccompNotifyBusy.CompareAndSwap(false, true) {
11 log.Warningf("Another seccomp listener exists in the filter chain of the host process. " +
12 "The stub threads use the slower futex loop instead of seccomp-unotify.")
13 }
14 return nil, nil
15 }
16 return f, err
17}
外側のgVisor(runsc)または外側のgvxサンドボックスがプロセスのフィルタチェーンにおける単一のリスナー枠をすでに保持しているためにinstallSeccompNotify()がunix.EBUSYを返した場合:
installSeccompNotifyIfFree()はseccompNotifyBusy(atomic.Bool)を設定し、プロセスにつき1回の警告をログに記録し、そのstubおよびそれに続くすべてのstubについてt.seccompNotify == nilのままにします。syscallThread.detach()(syscall_thread_amd64.goおよびsyscall_thread_arm64.go)では、t.seccompNotify == nilの場合、_RUN_SECCOMP_LOOP(6)の代わりに、amd64ではregs.Rbx = _RUN_SYSCALL_LOOP(5)、arm64ではregs.Regs[9] = _RUN_SYSCALL_LOOPが設定されます。syscallThread.syscall()(syscall_thread.go)では、内側のSentryが共有メモリとSYS_FUTEXのみを介してstubのシステムコールをディスパッチします。
1// pkg/sentry/platform/systrap/syscall_thread.go
2// 新しいシステムコールリクエストについてsyscallスレッドに通知する。
3atomic.AddUint32(&sentryMsg.state, 1)
4futexWakeUint32(&sentryMsg.state)
5
6// 応答を待機する。
7futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
SECCOMP_RET_TRAPフィルタはゲストスレッドのスタック上で任意の深さまで動作し、syscallThreadは2つ目のユーザー通知リスナーを要求することなく_RUN_SYSCALL_LOOPにフォールバックするため、gvxはrunsc内で問題なく動作し、再帰的なネスト(gvxの中のgvx)をサポートします。
2. 単一ファイルのext4liteボリューム上のネイティブなoverlayfs
外側のgVisorコンテナのgofer(9p/lisafs)ルートファイルシステムの上にoverlayfsをマウントすることを避けるため、-v gvx-docker:/var/lib/dockerは、/var/lib/dockerを、単一の通常ファイル(gvx-docker.ext4)に格納されたgvxの内部ファイルシステム**ext4lite**によって支えます。
外側のrunscにとって、gvx-docker.ext4はpread64(2)およびpwrite64(2)を介してアクセスされる単なる1つの通常ファイルです。gvxの内部では、ext4liteがJBD2でジャーナリングされたext4ファイルシステムを提供し、trusted.overlay.*およびsecurity.capabilityの拡張属性、キャラクタデバイスのホワイトアウトノード(0:0)、d_typeディレクトリエントリ、RENAME_EXCHANGEをサポートします。これにより、内側のdockerdはネイティブのoverlayfsストレージドライバを使用できます。
3. docker0とiptables-legacyを備えた自己完結型のnetstack
gvxの内部では、dockerdは外側のrunscコンテナのネットワークインターフェースやnetfilterテーブルと相互作用しません。代わりに次のとおりです。
dockerdはgvx独自のユーザー空間netstackの内部で、docker0ブリッジとvethペアを作成します。- すべての
iptables-legacyルール(-t nat -N DOCKER、MASQUERADE、conntrack、addrtype)は、gvxの内部のnetfilter実装に対して実行されます。 - ネストされた
runcコンテナは、gvxのメモリ内VFS2マウントテーブルの内部で/proc、/sys、/sys/fs/cgroupの階層をマウントし、外側のrunscのmount(2)システムコールを決して呼び出しません。
gVisor(またはgvxの中のgvx)内でのネストされたdocker:dindの検証
次のコマンドを使用して、両方のシナリオ(runsc内のrungv、またはrungv内のrungv)を検証できます。
1# 外側のgVisor(runsc)コンテナまたは外側のrungvコンテナ内:
2mkdir -p ~/.gvx
3rungv run \
4 -p unix:~/.gvx/docker.sock:2375 \
5 -v gvx-docker:/var/lib/docker \
6 -e DOCKER_TLS_CERTDIR= \
7 docker:dind --tls=false &
8DIND_PID=$!
9
10export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
11until docker info >/dev/null 2>&1; do sleep 0.5; done
12
13# 内側のDockerデーモンがvfsではなくネイティブのoverlayfsを使用していることを検証する
14docker info --format 'Storage Driver: {{.Driver}}'
15# 出力: Storage Driver: overlayfs
16
17# 多層構成のビルドを実行し、ネストされたコンテナを実行する
18mkdir -p /tmp/nested-test && cd /tmp/nested-test
19cat <<'EOF' > Dockerfile
20FROM alpine:3.20
21RUN adduser -D -u 1001 appuser && mkdir -p /app && chown appuser:appuser /app
22USER appuser
23WORKDIR /app
24RUN echo "nested-build-ok" > status.txt
25CMD ["cat", "/app/status.txt"]
26EOF
27
28docker build -t nested-test:latest .
29docker run --rm nested-test:latest
30# 出力: nested-build-ok
31
32kill -INT $DIND_PID
このシリーズの関連記事
- gvx:1つの非特権プロセスでLinuxコンテナ、Docker、Kubernetesを実行する:Windows(
whpx、systrap)、macOS(hvf)、Linux(systrap)にわたるgvxの概要。 - rootやユーザー名前空間を使用せずにDocker、docker build、docker composeを実行する:
root、sudo、unshare(CLONE_NEWUSER)、mount(2)、/dev/fuseがブロックされている場合に、gvxがdocker:dindを実行する方法。 - gvisorおよびgvxによるネストされたサンドボックス内でのDocker-in-Docker(本記事)
- gvxによるAIエージェントのRLロールアウト向けの高速な隔離Docker環境:
gvxと単一ファイルのext4liteによるコピーオンライトのスナップショットを使用して、100ミリ秒のリセットで数百の並列SWE-benchおよびRLロールアウト環境を実行する方法。