---
id: 83fb2c108243a7df80db3a6e78290daf
author: Lemon Mint
title: Eseguire Docker-in-Docker all'interno di gVisor e sandbox annidate con gvx
description: Perché l'installazione di docker.io o l'annidamento di runsc all'interno di un container gVisor fallisce con modprobe, iptables-nft, overlay2, cgroups e seccomp EBUSY, e come gvx esegue in modo sicuro docker:dind annidato all'interno di un kernel isolato in user-space senza --privileged.
language: it
date: 2026-11-06T12:02:55.931762327Z
path: /blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03
go_package: z.gfire.dev/gvx
go_repourl: https://github.com/gfire-sigs/gvx.git
category: lowlevel
---

## Soluzione rapida: esecuzione sicura di `docker:dind` all'interno di una sandbox `gVisor` (`runsc`) senza privilegi

Le piattaforme cloud per agenti di IA, le distribuzioni Kubernetes `agent-sandbox` (`RuntimeClass: gvisor`) e gli ambienti di valutazione per l'apprendimento per rinforzo isolano i carichi di lavoro non attendibili all'interno di container **gVisor (`runsc`)**, con l'obiettivo specifico di proteggere il kernel dell'host. Quando un carico di lavoro all'interno di un container `runsc` predefinito e privo di privilegi tenta di installare `docker.io` o `docker-ce` tramite `apt-get` e di avviare `dockerd`, l'operazione fallisce anche se il processo dispone dell'utente `root` (`UID 0`) all'interno del container.

Con **[gvx](https://github.com/gfire-sigs/gvx)** (`rungv`) è possibile eseguire **in modo sicuro** `docker:dind`, `docker build` e `docker compose` direttamente all'interno di un container gVisor (`runsc`) privo di privilegi (oppure annidare `gvx` all'interno di `gvx`), **senza indebolire il `RuntimeClass` esterno, esporre socket dell'host o concedere `--privileged`**.

> **Preservazione e rafforzamento dell'isolamento della sandbox esterna**: l'esecuzione di `rungv` all'interno di `runsc` non aggira alcuna restrizione imposta dalla sandbox gVisor esterna. Al contrario, `gvx` crea un secondo kernel `Sentry` di gVisor annidato, in user space, dotato di un proprio `hostlockdown` (`PR_SET_NO_NEW_PRIVS` + `seccomp`), di un proprio `netstack` in memoria e di un volume `ext4lite` costituito da un unico file. I processi `dockerd` e i container `runc` interni sono sottoposti a doppio sandboxing: le loro chiamate di sistema, i mount e i pacchetti grezzi non escono mai dal kernel user-space di `gvx` interno.

```sh
# All'interno di un container gVisor (runsc) esistente o di un Pod Kubernetes:
# Installare rungv da un archivio di release GitHub con attestazione SLSA oppure tramite go install:
CGO_ENABLED=0 go install z.gfire.dev/gvx/cmd/rungv@latest

mkdir -p ~/.gvx
rungv run \
  -p unix:~/.gvx/docker.sock:2375 \
  -v gvx-docker:/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &

# Eseguire build e container Docker standard tramite il socket gvx interno
export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done
docker info --format '{{.Driver}} {{.OperatingSystem}}'
# Output: overlayfs Alpine Linux ... (containerized)

docker build -t nested-app:test .
docker run --rm nested-app:test
```

---

## Cinque motivi per cui l'installazione diretta di Docker non riesce all'interno di `gVisor` (`runsc`)

Quando un utente o un agente di codifica basato sull'IA esegue `apt-get install -y docker.io` (oppure `docker-ce`) all'interno di un container `runsc` standard e tenta di avviare `dockerd`, incontra cinque ostacoli tecnici in sequenza.

### 1. Caricamento dei moduli del kernel (`modprobe`) rispetto al kernel virtuale `4.4.0`

Durante l'installazione dei pacchetti e l'avvio di `dockerd`, gli script di inizializzazione eseguono `modprobe` per caricare `overlay`, `br_netfilter`, `ip_tables`, `nf_nat` e `xt_conntrack`. Il Sentry di gVisor esterno blocca `init_module(2)` e `finit_module(2)` e riporta una versione virtuale del kernel (`4.4.0`) alla quale non corrisponde alcuna directory `/lib/modules/4.4.0`:

```text
modprobe: FATAL: Module overlay not found in directory /lib/modules/4.4.0
modprobe: FATAL: Module br_netfilter not found in directory /lib/modules/4.4.0
modprobe: FATAL: Module ip_tables not found in directory /lib/modules/4.4.0
modprobe: FATAL: Module nf_nat not found in directory /lib/modules/4.4.0
```

### 2. Errori di `iptables-nft` (netlink `nf_tables`) e del bridge `docker0`

Su Ubuntu 22.04, Ubuntu 24.04 e Debian 11/12, `apt-get install docker.io` seleziona `iptables-nft` (basato sul sottosistema netlink `nf_tables`) come alternativa predefinita di `/usr/sbin/iptables`. In un container gVisor standard privo di privilegi di amministrazione di rete o di supporto netlink per `nf_tables`, `dockerd` si interrompe durante l'inizializzazione della catena NAT `DOCKER` e del bridge `docker0`:

```text
failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to create NAT chain DOCKER: iptables failed: iptables -t nat -N DOCKER
iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Invalid argument
iptables v1.8.10 (legacy): can't initialize iptables table 'nat': Table does not exist (do you need to insmod?)
iptables: No chain/target/match by that name.
Unable 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.)
Error initializing network controller: error obtaining controller instance: failed to create bridge "docker0": operation not permitted
```

### 3. Errore di `overlay2` sul rootfs `gofer` / `9p` e saturazione del disco con `vfs`

A meno che l'amministratore del `runsc` esterno non abbia configurato esplicitamente un mount `tmpfs` o filestore dedicato in `/var/lib/docker`, il rootfs del container è supportato dal `gofer` di gVisor (`9p`/`lisafs`) oppure da un mount overlay annidato. Il mount di `overlay2` su `/var/lib/docker` fallisce:

```text
level=error msg="[graphdriver] prior storage driver overlay2 failed: driver not supported"
failed to mount overlay: invalid argument
Error starting daemon: error initializing graphdriver: driver not supported: overlay2
```

Quando utenti o agenti tentano di aggirare questo errore avviando `dockerd --storage-driver=vfs`, Docker disabilita completamente la condivisione dei layer tramite Copy-on-Write. Ogni istruzione `RUN` in un `Dockerfile` esegue una copia ricorsiva completa (`dirCopy`) dell'intero filesystem radice, trasformando un'immagine da 500 MB distribuita su 10 passaggi di build in 5 GB di scritture e causando la terminazione della sandbox con `no space left on device` oppure con un timeout di I/O.

### 4. Rifiuti di mount di cgroup e `/proc` nel `runc` annidato

Anche se `dockerd` viene forzato ad avviarsi con `dockerd --iptables=false --bridge=none --storage-driver=vfs`, l'esecuzione di `docker run` o `docker build` invoca `runc` per creare il container interno. In un container gVisor predefinito privo di privilegi, `runc` fallisce durante il mount dei cgroup, il rimontaggio di `/proc` o il caricamento dei filtri seccomp interni:

```text
docker: 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:
error 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
cgroups: cgroup mountpoint does not exist: unknown
runc create failed: systemd not running on this host: cannot use systemd cgroup manager
error during container init: error mounting "proc" to rootfs at "/proc": mount proc:/proc (via /proc/self/fd/6), flags: 0xe: operation not permitted
error during container init: error loading seccomp filter into kernel: operation not permitted
```

### 5. `runsc` annidato fallisce con `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`EBUSY`)

Se un utente tenta di eseguire un'altra istanza di `runsc` all'interno di `runsc` utilizzando la piattaforma `systrap`, l'inizializzazione si interrompe immediatamente:

```text
creating subprocess: sysmsg: failed to initialize thread: failed to install seccomp notify rules: device or resource busy
```

Sia il kernel Linux sia il Sentry di gVisor consentono **al massimo un** listener `SECCOMP_FILTER_FLAG_NEW_LISTENER` (`SECCOMP_RET_USER_NOTIF`) per gerarchia di task. Poiché la sandbox esterna occupa già lo slot del listener, la chiamata interna `seccomp(2)` restituisce `EBUSY` (`device or resource busy`).

---

## Come `gvx` risolve l'esecuzione annidata all'interno di `gVisor`

`gvx` affronta tutti e cinque gli ostacoli all'interno del proprio kernel user-space e del proprio livello di piattaforma, consentendo a `rungv` di funzionare all'interno di un container `gVisor` esterno (`runsc`) o persino ricorsivamente all'interno di un altro container `gvx` (`gvx` in `gvx`).

![Esecuzione annidata di gvx all'interno di gVisor esterno](/assets/images/gvx/gvx-nested-seccomp-futex.png)

### 1. Fallback automatico di `systrap` da `EBUSY` al ciclo futex `_RUN_SYSCALL_LOOP`

In `systrap` di Linux, ogni sottoprocesso stub esegue due tipi di thread:

- **Thread di esecuzione del guest (`sysmsg`)**: intercettano le chiamate di sistema del guest tramite `SECCOMP_RET_TRAP` (`SIGSYS`) e trampolini `jmp rel32` modificati binariamente (`syshandler`), scambiando lo stato dei registri con il Sentry tramite memoria condivisa e `SYS_FUTEX`.
- **Un `syscallThread` stub (`syscall_thread.go`)**: esegue, per conto del Sentry, le chiamate di sistema dello spazio di indirizzi (`mmap`, `munmap`, `clone`) all'interno del sottoprocesso stub.

Nel gVisor upstream, `syscallThread` supporta due modalità di risveglio in `stub_amd64.s` e `stub_arm64.s`:

1. **Ciclo seccomp User-Notify (`_RUN_SECCOMP_LOOP = 6`, quando `t.seccompNotify != nil`)**: durante `syscallThread.init()`, `t.installSeccompNotify()` installa un listener `SECCOMP_RET_USER_NOTIF` (`seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, stubSyscallRules)`), in modo che il Sentry possa inviare richieste tramite `ioctl(SECCOMP_IOCTL_NOTIF_SEND)` e `ioctl(SECCOMP_IOCTL_NOTIF_RECV)`.
2. **Ciclo di chiamate di sistema futex (`_RUN_SYSCALL_LOOP = 5`, quando `t.seccompNotify == nil`)**: il `syscallThread` esegue `syscall_loop` (`stub_amd64.s` / `stub_arm64.s`), mettendosi in attesa su `SYS_FUTEX` (`FUTEX_WAIT`) sulla regione condivisa di 2 pagine, composta da `syscallSentryMessage` (`PROT_READ` nello stub) e `syscallStubMessage` (`PROT_READ | PROT_WRITE` nello stub).

Il gVisor upstream invoca incondizionatamente `t.installSeccompNotify()` ogni volta che `seccompNotifyIsSupported` è vero, e interrompe la creazione dello stub se `seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, ...)` restituisce `EBUSY`.

In `gvx` (`pkg/sentry/platform/systrap/seccomp_notify_xplat.go` e `syscall_thread.go`), `syscallThread.init()` invoca `t.installSeccompNotifyIfFree()`:

```go
// pkg/sentry/platform/systrap/seccomp_notify_xplat.go
var seccompNotifyBusy atomic.Bool

func (t *syscallThread) installSeccompNotifyIfFree() (*os.File, error) {
	if seccompNotifyBusy.Load() {
		return nil, nil
	}
	f, err := t.installSeccompNotify()
	if errors.Is(err, unix.EBUSY) {
		if seccompNotifyBusy.CompareAndSwap(false, true) {
			log.Warningf("Another seccomp listener exists in the filter chain of the host process. " +
				"The stub threads use the slower futex loop instead of seccomp-unotify.")
		}
		return nil, nil
	}
	return f, err
}
```

Quando `installSeccompNotify()` restituisce `unix.EBUSY` perché una sandbox `gVisor` (`runsc`) o `gvx` esterna occupa già l'unico slot listener della catena di filtri del processo:

- `installSeccompNotifyIfFree()` imposta `seccompNotifyBusy` (`atomic.Bool`), registra un unico avviso per il processo e lascia `t.seccompNotify == nil` per quello stub e per tutti gli stub successivi.
- In `syscallThread.detach()` (`syscall_thread_amd64.go` e `syscall_thread_arm64.go`), con `t.seccompNotify == nil` viene impostato `regs.Rbx = _RUN_SYSCALL_LOOP` (`5`) su `amd64` (oppure `regs.Regs[9] = _RUN_SYSCALL_LOOP` su `arm64`), al posto di `_RUN_SECCOMP_LOOP` (`6`).
- In `syscallThread.syscall()` (`syscall_thread.go`), il Sentry interno invia le chiamate di sistema dello stub esclusivamente tramite memoria condivisa e `SYS_FUTEX`:

```go
// pkg/sentry/platform/systrap/syscall_thread.go
// Notifica al thread di sistema la presenza di una nuova richiesta di chiamata di sistema.
atomic.AddUint32(&sentryMsg.state, 1)
futexWakeUint32(&sentryMsg.state)

// Attende la risposta.
futexWaitWake(&sentryMsg.state, atomic.LoadUint32(&sentryMsg.state))
```

Poiché i filtri `SECCOMP_RET_TRAP` sui thread del guest possono stratificarsi a qualsiasi profondità e `syscallThread` ricade su `_RUN_SYSCALL_LOOP` senza richiedere un secondo listener di user-notification, `gvx` viene eseguito correttamente all'interno di `runsc` e supporta l'annidamento ricorsivo (`gvx` in `gvx`).

### 2. `overlayfs` nativo su volumi `ext4lite` costituiti da un unico file

Per evitare di montare `overlayfs` sopra il filesystem radice `gofer` (`9p`/`lisafs`) del container gVisor esterno, `-v gvx-docker:/var/lib/docker` sostiene `/var/lib/docker` con il filesystem interno di `gvx` denominato **`ext4lite`**, memorizzato in un unico file regolare (`gvx-docker.ext4`).

Per `runsc` esterno, `gvx-docker.ext4` è semplicemente un file regolare a cui si accede tramite `pread64(2)` e `pwrite64(2)`. All'interno di `gvx`, `ext4lite` fornisce un filesystem `ext4` con journaling JBD2, attributi estesi `trusted.overlay.*` e `security.capability`, nodi whiteout di tipo dispositivo a caratteri (`0:0`), voci di directory `d_type` e `RENAME_EXCHANGE`, consentendo al `dockerd` interno di utilizzare il proprio driver di storage nativo `overlayfs`.

### 3. `netstack` autocontenuto con `docker0` e `iptables-legacy`

All'interno di `gvx`, `dockerd` non interagisce con le interfacce di rete né con le tabelle netfilter del container `runsc` esterno. Al contrario:

- `dockerd` crea il proprio bridge `docker0` e le proprie coppie `veth` all'interno del `netstack` user-space di `gvx`.
- Tutte le regole `iptables-legacy` (`-t nat -N DOCKER`, `MASQUERADE`, `conntrack`, `addrtype`) vengono eseguite sull'implementazione netfilter interna di `gvx`.
- I container `runc` annidati montano le proprie gerarchie `/proc`, `/sys` e `/sys/fs/cgroup` all'interno della tabella dei mount `VFS2` in memoria di `gvx`, senza mai invocare la syscall `mount(2)` del `runsc` esterno.

---

## Verifica di `docker:dind` annidato all'interno di `gVisor` (oppure di `gvx` in `gvx`)

È possibile verificare entrambi gli scenari (`rungv` all'interno di `runsc`, oppure `rungv` all'interno di `rungv`) con i seguenti comandi:

```sh
# All'interno di un container gVisor (runsc) esterno o di un container rungv esterno:
mkdir -p ~/.gvx
rungv run \
  -p unix:~/.gvx/docker.sock:2375 \
  -v gvx-docker:/var/lib/docker \
  -e DOCKER_TLS_CERTDIR= \
  docker:dind --tls=false &
DIND_PID=$!

export DOCKER_HOST="unix://$HOME/.gvx/docker.sock"
until docker info >/dev/null 2>&1; do sleep 0.5; done

# Verificare che il daemon Docker interno utilizzi overlayfs nativo (e non vfs)
docker info --format 'Storage Driver: {{.Driver}}'
# Output: Storage Driver: overlayfs

# Eseguire una build multi-layer e avviare un container annidato
mkdir -p /tmp/nested-test && cd /tmp/nested-test
cat <<'EOF' > Dockerfile
FROM alpine:3.20
RUN adduser -D -u 1001 appuser && mkdir -p /app && chown appuser:appuser /app
USER appuser
WORKDIR /app
RUN echo "nested-build-ok" > status.txt
CMD ["cat", "/app/status.txt"]
EOF

docker build -t nested-test:latest .
docker run --rm nested-test:latest
# Output: nested-build-ok

kill -INT $DIND_PID
```

---

## Ulteriori letture in questa serie

1. **[gvx: esecuzione di container Linux, Docker e Kubernetes in un unico processo senza privilegi](/blog/posts/gvx-cross-platform-linux-containers-z48916d01)**: panoramica di `gvx` su Windows (`whpx`, `systrap`), macOS (`hvf`) e Linux (`systrap`).
2. **[Esecuzione di Docker, docker build e docker compose senza root né user namespace](/blog/posts/run-docker-without-root-or-userns-gvx-z48916d02)**: come `gvx` esegue `docker:dind` quando `root`, `sudo`, `unshare(CLONE_NEWUSER)`, `mount(2)` e `/dev/fuse` sono bloccati.
3. **[Esecuzione di Docker-in-Docker all'interno di gVisor e di sandbox annidate con gvx](/blog/posts/docker-in-docker-inside-gvisor-with-gvx-z48916d03)** (questo articolo)
4. **[Ambienti Docker isolati e veloci per i rollout di RL di agenti IA con gvx](/blog/posts/fast-docker-environments-for-agent-rl-rollouts-z48916d04)**: come utilizzare `gvx` e gli snapshot copy-on-write `ext4lite` costituiti da un unico file per eseguire centinaia di ambienti paralleli per `SWE-bench` e per i rollout di RL, con ripristini in 100 millisecondi.
