Se sei arrivato qui da una ricerca, probabilmente hai davanti questo:
=> ERROR [3/5] RUN docker-php-ext-install mysqli pdo_mysql gd zip
xz: (stdin): Failed to enable the sandbox
tar: Child returned status 1
tar: Error is not recoverable: exiting now
La build fallisce sempre nello stesso punto, in modo riproducibile. E la
stessa identica immagine sul tuo portatile si costruisce senza problemi.
La soluzione, subito
Se ti serve solo sbloccare la build, sono tre passaggi.
1. Scarica il profilo seccomp predefinito della tua versione di Docker e
salvalo come nuovo file:
sudo cp default-seccomp.json \
/etc/docker/seccomp-nolandlock.json
2. Dentro, nell'elenco delle regole, aggiungi questo blocco:
{
"names": [
"landlock_create_ruleset",
"landlock_add_rule",
"landlock_restrict_self"
],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 38
}
Poi rendilo predefinito in /etc/docker/daemon.json:
{
"seccomp-profile": "/etc/docker/seccomp-nolandlock.json"
}
sudo systemctl restart docker
3. Ed ecco il passaggio che quasi tutti saltano — senza, la build
fallisce ancora e sembra che la soluzione non funzioni:
DOCKER_BUILDKIT=0 docker compose build
Il motivo di quest'ultimo comando è nella sezione finale, ed è la parte che
conviene leggere: spiega perché i primi due passaggi da soli non bastano.
Perché succede
Il messaggio va letto alla lettera. Non dice che l'archivio è corrotto. Dice
che xz non è riuscito ad attivare la sandbox.
Le versioni recenti di xz si auto-confinano prima di elaborare dati non
fidati: dichiarano al kernel "d'ora in poi non voglio più poter aprire file
né usare la rete", così una vulnerabilità nel decompressore non diventa una
compromissione della macchina. Su Linux questo si appoggia a Landlock.
Qui si incastrano tre pezzi che, presi singolarmente, sono tutti corretti.
Il kernel della distribuzione server. Se stai usando RHEL, CentOS Stream,
Rocky o AlmaLinux, il kernel ha una numerazione apparentemente vecchia ma è
pesantemente modificato dal vendor. Espone i syscall di Landlock, ma con una
ABI più arcaica di quella che xz recente si aspetta.
L'immagine. Le immagini ufficiali PHP sono basate su Debian recente, e
contengono un xz moderno che quella ABI antica non sa gestire.
Il profilo seccomp di Docker. Filtra i syscall che un container può
invocare. Quando ne incontra uno che non conosce, non risponde "non
supportato": risponde errore di permesso.
Ed è il terzo punto la chiave.
Se xz ricevesse "questa funzionalità non esiste" (ENOSYS), si
comporterebbe bene: rinuncerebbe alla sandbox e proseguirebbe. È il
comportamento previsto sui kernel senza Landlock. Ma riceve EACCES, che
significa una cosa diversa — la funzionalità c'è ma non puoi usarla — e di
fronte a quello fa la scelta prudente: si rifiuta di lavorare senza
protezione ed esce con errore.
Il programma sta funzionando esattamente come progettato. Sta rifiutando di
abbassare le proprie difese. Solo che nessuno lo sta attaccando: sta
decomprimendo il sorgente di un'estensione PHP.
Nessuno dei tre componenti è rotto. È rotta la loro combinazione. Ed è
per questo che cercando il messaggio d'errore si trova poco: chi ha quel
kernel di solito non usa quell'immagine, e chi usa quell'immagine di solito
ha un kernel recente.
Perché serve anche DOCKER_BUILDKIT=0
Docker ha due motori di costruzione. Quello moderno, attivo di default nelle
versioni recenti, non esegue i passaggi di build sotto il profilo seccomp
del demone: gestisce il proprio isolamento in autonomia.
Significa che il profilo che hai appena configurato vale per i container in
esecuzione, ma la build gli passa accanto. È il momento in cui si conclude
che la diagnosi era sbagliata — e invece manca solo di chiedere il motore
classico.
Conviene renderlo permanente per l'utente che costruisce le immagini, perché
prima o poi qualcuno lancerà il comando senza la variabile e si troverà
davanti un errore che nessuno ricorda più:
echo 'export DOCKER_BUILDKIT=0' >> ~/.bashrc
Una nota di onestà
Stiamo disattivando un irrobustimento di sicurezza.
Il rischio reale è modesto: Landlock su quel kernel non era comunque
utilizzabile, quindi non stiamo rinunciando a una protezione che avevamo —
stiamo smettendo di fingere di averla. Ma è una rinuncia, non un fix, e va
scritta nella documentazione dell'infrastruttura.
Quando il server passerà a un kernel più recente, il profilo va rimosso e la
build riprovata senza. Le soluzioni di questo tipo hanno una scadenza, e
l'unico modo perché non diventino debito permanente è annotarla.
La lezione che vale oltre questo caso
Io ci ho perso tre ore. Ho svuotato la cache di build, controllato lo spazio
su disco, cambiato versione di PHP, messo SELinux in permissive. Tutti
tentativi sensati, tutti inutili.
L'errore di metodo era sempre lo stesso: cercavo la causa dentro il
container. Ma il container era la stessa identica immagine che in locale
funzionava. L'unica variabile cambiata era la macchina sotto.
Da lì una regola che ho adottato. Quando una build fallisce solo su una
macchina, la prima domanda non è "cos'ha di sbagliato il Dockerfile", ma:
Che cosa di questa macchina è visibile dall'interno del container?
La lista è corta — il kernel, che è condiviso e non fa parte dell'immagine;
i filtri sui syscall; i moduli di sicurezza della distribuzione;
l'architettura del processore. Percorrerla richiede dieci minuti.
E un corollario che vale ben oltre Docker: quando un programma rifiuta di
funzionare per motivi di sicurezza, la domanda giusta non è come zittirlo, ma
quale informazione sbagliata gli sta arrivando. Qui il programma era
corretto e la risposta che riceveva era falsa. Aggirare il controllo avrebbe
funzionato per caso; correggere la risposta ha funzionato per la ragione
giusta.
Questo è uno dei problemi che ho incontrato containerizzando 61 progetti PHP
legacy e 113 database, senza riscrivere una riga di applicazione. Ne ho fatto
un libro — Docker per PHP legacy — costruito tutto così: il sintomo,
i tentativi sbagliati, la causa vera, e cosa resta valido oltre il caso
specifico.
Top comments (0)