GoSuda

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

By Lemon Mint views ...

クイックソリューション: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ボリュームを備えた、二次的でネストされたユーザー空間のgVisor Sentryカーネルを作成します。内側の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)で再帰的に実行できるようにします。

gVisor内のネストされた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種類のウェイクアップモードをサポートしています。

  1. 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)を介してリクエストをディスパッチできるようにします。
  2. 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

このシリーズの関連記事

  1. gvx:1つの非特権プロセスでLinuxコンテナ、Docker、Kubernetesを実行する:Windows(whpx、systrap)、macOS(hvf)、Linux(systrap)にわたるgvxの概要。
  2. rootやユーザー名前空間を使用せずにDocker、docker build、docker composeを実行する:root、sudo、unshare(CLONE_NEWUSER)、mount(2)、/dev/fuseがブロックされている場合に、gvxがdocker:dindを実行する方法。
  3. gvisorおよびgvxによるネストされたサンドボックス内でのDocker-in-Docker(本記事)
  4. gvxによるAIエージェントのRLロールアウト向けの高速な隔離Docker環境:gvxと単一ファイルのext4liteによるコピーオンライトのスナップショットを使用して、100ミリ秒のリセットで数百の並列SWE-benchおよびRLロールアウト環境を実行する方法。