In poche parole: c’è un nuovo bug critico per Linux in circolazione
‘Copy Fail’ è un nuovo e fastidioso bug di local privilege escalation, CVE-2026-31431, trovato dal team di analisti Xint Code.
Questo bug in pratica permette all’attaccante di ottenere i permessi di root su molti sistemi operativi Linux (Ubuntu, Debian, RHEL, Suse, Alma, Amazon Linux), con uno script Python di soli 732 bytes.
‘Copy Fail’ richiede solo un account utente locale senza privilegi: nessun accesso alla rete, nessuna funzionalità di debug del kernel, nessuna libreria preinstallata. L’API di crittografia del kernel (AF_ALG) è abilitata di default praticamente in tutte le distribuzioni principali, quindi l’intero intervallo di patch dal 2017 in poi è vulnerabile.
Capire bene la vulnerabilità ‘Copy Fail’
In poche parole, immaginate il kernel Linux che per evitare di leggere sempre da disco i file, ne tiene una copia in memoria: la cosiddetta page cache. Questa cache viene usata normalmente per velocizzare alcune operazioni da parte del kernel. Tutto in regola.
Poi però, entra in gioco algif_aead, uno dei moduli del kernel Linux che espone il sottosistema crittografico allo userspace mediante l’interfaccia AF_ALG. In parole povere? Una API per i servizi crittografici, usata per esempio in protocolli come IPsec.
Nel lontano 2017 è stato modificato questo modulo del kernel per renderlo più “efficiente” e gli hanno detto “Invece di fare una fotocopia del documento (il file) su cui devi lavorare, scrivi direttamente sull’originale che il kernel ti passa nella page cache” grazie a una chiamata di sistema chiamata splice() che passa i dati per riferimento invece di copiarli.
Questa ottimizzazione in-place è la radice del problema, perché ha permesso a un utente di scrivere dati dove non avrebbe assolutamente dovuto.
E quali dati può scrivere un attaccante? Può andare a riscrivere una piccola parte di /usr/bin/su per fare in modo di poter eseguire su e diventare quindi l’utente root.
L’aspetto ‘fantasma’
Una delle cose più subdole di questa vulnerabilità è che la modifica avviene solo in RAM. Il file su disco rimane intatto.
Ciò significa che tutti i classici sistemi di controllo dell’integrità dei file (come Tripwire, AIDE, Wazuh, ecc.) non vedono nulla di strano. Questo è molto pericoloso perché non lascia tracce su disco.
La fuga dai container
Prima parlavamo della page cache, quella parte di memoria che serve a velocizzare le operazioni del kernel. Ebbene, la page cache è condivisa tra tutti i container su uno stesso host (Podman, Docker, Kubernetes, etc.).
Questo significa che un container vulnerabile o infettato da un attacco di tipo supply chain, può fare container escape in una maniera spaventosamente semplice.
Il playbook Ansible per la mitigazione immediata
Traendo spunto dalla KB di Morrolinux e da questo gist su Github dell’utente m3nu ho creato un playbook Ansible disponibile a questo link copy-fail-CVE-2026-31431-mitigation-ansible-playbook che permette di applicare la mitigazione e di fare rollback in seguito all’aggiornamento del kernel.
tasks:
- name: Apply Mitigation
tags: [mitigation]
when: not (rollback | bool)
block:
# --- Debian family modprobe blacklist + unload ---
- name: Apply modprobe blacklist (Debian family)
when: ansible_facts['os_family'] == 'Debian'
block:
- name: Create modprobe blacklist for {{ target_module }}
ansible.builtin.copy:
dest: "{{ modprobe_conf_path }}"
content: |
# Generated by Ansible — CVE-2026-31431 mitigation
install {{ target_module }} /bin/false
owner: root
group: root
mode: "0644"
- name: Unload module {{ target_module }} if currently loaded
community.general.modprobe:
name: "{{ target_module }}"
state: absent
register: rmmod_result
failed_when:
- rmmod_result is failed
- >-
'not currently loaded' not in (rmmod_result.msg | default(''))
and 'not currently loaded' not in (rmmod_result.stderr | default(''))
- >-
'is in use' not in (rmmod_result.msg | default(''))
and 'is in use' not in (rmmod_result.stderr | default(''))
- name: Remind operator to reboot if module was in use (Debian)
when: >-
'is in use' in (rmmod_result.msg | default(''))
or 'is in use' in (rmmod_result.stderr | default(''))
ansible.builtin.debug:
msg: "REMINDER: Module was in use and could not be unloaded. Reboot required to complete mitigation."
# --- kernel initcall blacklist (RHEL/Alma 9 and 10 only) ---
- name: Apply kernel initcall blacklist (Red Hat family)
when:
- ansible_facts['os_family'] == 'RedHat'
- ansible_facts['distribution_major_version'] in ['9', '10']
block:
- name: Check current grubby kernel args (pre-change)
ansible.builtin.command: "grubby --info=ALL"
register: grubby_info_pre
changed_when: false
check_mode: false
- name: Add '{{ target_kernel_arg }}' to all kernels via grubby
ansible.builtin.command:
cmd: "grubby --update-kernel=ALL --args='{{ target_kernel_arg }}'"
register: grubby_add_result
changed_when: target_kernel_arg not in grubby_info_pre.stdout
- name: Remind operator to reboot (RHEL)
when: grubby_add_result.changed
ansible.builtin.debug:
msg: "REMINDER: Kernel args changed. Reboot required to activate mitigation."
Analizziamo il playbook
Il playbook è compatibile con sistemi operativi basati su RHEL (CentOS, Rocky, Alma) e basati su Debian (Ubuntu e Mint).
Di base utilizza la funzione serial per eseguire il playbook solo su un numero limitato di host simultaneamente, di default il 25%. Ad esempio, avendo 100 host nell’inventory, la mitigazione sarà applicata a 25 host nel primo batch, 25 nel secondo e così via.
La funzione max_fail_percentage invece, tollera un tasso di errori del 20%. Ad esempio, se ci sono 25 host nel batch, se 6 o più host falliscono, il job sarà abortito.
Il Pre Task mostra alcune informazioni sul sistema operativo e sul kernel per ogni host e raccoglie informazioni sullo stato del sistema.
Il Task per le distribuzioni Debian-based disabilita il modulo kernel e lo mette in blacklist, e se il modulo è in uso, ricorda all’utente di riavviare il sistema.
Per le distribuzioni RHEL-based invece, usa il comando grubby per disabilitare il modulo e ricorda sempre all’utente di riavviare il sistema.
Il Post Task raccoglie ulteriori informazioni e presenta all’utente un sommario delle azioni eseguite.
Come eseguirlo
Innanzitutto è necessario preparare il file di configurazione di Ansible e l’inventory come in ogni playbook Ansible. Successivamente lanciare un ping per controllare la raggiungibilità dei server.
Infine, usare ansible-playbook mitigation-playbook.yaml per lanciare la mitigazione su tutti i server, o usare l’opzione --limit webservers per limitare il playbook solo a un determinato gruppo di host.
Per il rollback, come sopra, ma si userà l’opzione --tags rollback per eseguire solo i task di rollback.
Verificare che la mitigazione funzioni
In seguito all’esecuzione del playbook, è necessario verificare che la mitigazione abbia avuto effetto. Per fare ciò usare questi due metodi, in base alla distribuzione dei vostri host:
- Sulle distribuzioni Debian-based usare
lsmod | grep algif_aead. - Sulle distribuzioni RHEL-based usare
grep initcall_blacklist /proc/cmdline.
La mia opinione: un passo in più per i container
Personalmente, la mia opinione è che fermarsi alla configurazione dell’host non sia sufficiente se fai girare dei container. Poiché i container condividono il kernel della macchina host, un riavvio o un errore di configurazione che ricarica il modulo vulnerabile per sbaglio lascerebbe i tuoi workload di nuovo esposti.
Sappiamo che questo exploit ha bisogno di aprire un socket della famiglia AF_ALG per poter comunicare con la Crypto API del kernel. Per questo motivo, vi consiglio caldamente di implementare un approccio “defense in depth” (difesa su più livelli) utilizzando seccomp. Se configurate un profilo personalizzato per il vostro container runtime, potete dire al sistema di bloccare del tutto le syscall di tipo socket dirette verso AF_ALG. In questo modo, anche se il modulo algif_aead dovesse magicamente riattivarsi sull’host, il kernel “segherà” in tronco qualsiasi tentativo del container di sfruttarlo.
Un’esempio di policy seccomp.
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["socket"],
"action": "SCMP_ACT_ERRNO",
"args": [
{
"index": 0,
"value": 38,
"op": "SCMP_CMP_EQ"
}
],
"comment": "Blocca i socket AF_ALG (famiglia 38) per mitigare Copy Fail"
}
]
}
Non abbassate la guardia!
Eccoci arrivati alla fine. C’è una cosa fondamentale che ci tengo a precisare: la soluzione basata su Ansible e modprobe che abbiamo appena visto è un’ottima “pezza” per proteggersi nell’immediato, ma ricordatevi che non è la cura definitiva.
La mia opinione personale è che i workaround di sicurezza non dovrebbero mai essere dimenticati sui server a prendere polvere. Tenete d’occhio i bollettini di sicurezza dei vostri vendor (che sia Canonical, Red Hat, SUSE o altri). Non appena rilasceranno i pacchetti del kernel aggiornati con la patch ufficiale per la CVE-2026-31431, programmate l’installazione e un bel riavvio delle macchine. Una volta che i sistemi avranno il kernel patchato, potrete fare pulizia e rimuovere i file creati da questo playbook.
Spero che questa guida vi abbia fatto risparmiare qualche mal di testa! Se avete dubbi, domande o se volete condividere come state gestendo questa CVE nei vostri ambienti, contattatemi pure su LinkedIn o se avete bisogno di una consulenza personalizzata, potete prenotare una call nel footer di questa pagina.
