
Race reproducer e stress toolkit per CVE-2026-52910, un use-after-free nel kernel Linux nei programmi selettori cBPF di reuseport, con controlli dmesg e leak.
Un riproduttore di race e toolkit di stress per CVE-2026-52910, un use-after-free (UAF) nella gestione da parte del kernel Linux dei programmi selettore classic BPF (cBPF) reuseport, corretto upstream dal commit "bpf: Free reuseport cBPF prog after RCU grace period".
| CVE | CVE-2026-52910 |
| Tipo | Use-after-free / lettura out-of-bounds (CWE-125), CVSS 3.1 7.8 HIGH AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Introdotto | v4.5 (con il supporto cBPF reuseport) |
| Corretto in | 5.10.259, 5.15.210, 6.1.176, 6.6.143, 6.12.94, 6.18.36, 7.0.13 (stable); mainline v7.1 |
| Splat upstream | BUG: KASAN: vmalloc-out-of-bounds in reuseport_select_sock (net/core/sock_reuseport.c:596) |
| Segnalato da | Eulgyu Kim |
[!WARNING] Questo è uno strumento di stress per il kernel. Su un kernel vulnerabile amplia deliberatamente una finestra di race use-after-free; un colpo può mandare in crash o corrompere la macchina. Eseguilo solo su macchine di tua proprietà o per cui sei esplicitamente autorizzato a testare (VM di test, macchine CI usa e getta), mai su sistemi di produzione.
SO_REUSEPORT consente a molti socket di fare bind sulla stessa porta UDP; per ogni
pacchetto in ingresso il kernel sceglie un socket del gruppo in
reuseport_select_sock() (net/core/sock_reuseport.c). Un gruppo può installare un
programma selettore — un programma BPF classico collegato con
setsockopt(SO_ATTACH_REUSEPORT_CBPF) — che decide per ogni pacchetto quale socket
del gruppo lo riceve. Il programma viene eseguito nella softirq RX (elaborazione
di ricezione di rete) all'interno di una sezione critica RCU read-side.
Il bug: quando il programma viene sostituito o rimosso tramite setsockopt()
(reuseport_attach_prog() / reuseport_detach_prog()), il vecchio programma cBPF
viene liberato immediatamente da sk_reuseport_prog_free(), senza attendere
i lettori RCU in corso. Una CPU che sta ancora percorrendo le istruzioni del programma
liberato legge memoria vmalloc liberata:
sequenceDiagram
autonumber
participant C as CPU0 — churner thread
participant K as setsockopt() path
participant R as CPU1 — RX softirq
R->>R: rcu_read_lock()
R->>R: prog = rcu_dereference(reuse->prog)
C->>K: setsockopt(SO_ATTACH_REUSEPORT_CBPF, progB)
K->>K: swap progA → progB
K->>K: sk_reuseport_prog_free(progA)
Note right of K: unfixed kernels: bpf_prog_free()<br/>runs NOW — no RCU grace period
R->>R: execute progA->insns (run_bpf_filter)
Note right of R: progA was already freed<br/>KASAN: vmalloc-out-of-bounds
Note over K: fix: call_rcu(sk_reuseport_prog_free_rcu) —<br/>free deferred by one RCU grace periodIl percorso del selettore eBPF (SO_ATTACH_REUSEPORT_EBPF) non è interessato:
rilascia già i programmi attraverso fasi differite di bpf_prog_put(). La correzione
riserva al percorso cBPF lo stesso trattamento — un periodo di grazia RCU prima che il vecchio
programma venga liberato.
Il report KASAN upstream (su un kernel di debug 7.0):
BUG: KASAN: vmalloc-out-of-bounds in reuseport_select_sock+0xedc/0x1220
Read of size 4 at addr ffffc9000051e004 by task slowme/10208
net/core/sock_reuseport.c:596
Su una macchina Linux di test:
$ make
$ sudo ./run_hammer.sh 600 # 10-minute run
...
== result: RC=0 (0 clean / 1 setup / 2 splat / 3 leak / 4 integrity) ==
Requisiti:
gcc e bash.dmesg, /proc/vmallocinfo, sysctl);
l'hammer stesso viene eseguito senza privilegi (il repro upstream girava come UID 1000).Risultati attesi:
RC=2; occasionalmente il
controllo di integrità dell'hammer scatta per primo → RC=4.RC=0, conteggio vmalloc bpf_prog stabile.La finestra di race è minuscola (free vs. esecuzione RX in corso), quindi considera una singola esecuzione pulita come non conclusiva. Per un test reale esegui per ore, ad esempio:
$ sudo ./run_hammer.sh 86400 512 8 16 4 127.0.0.1 0
reuseport_race_hammer$ ./reuseport_race_hammer [dur_sec] [insns] [nports] [nsocks] [nsenders] [ip] [ebpf]
Ogni gruppo esegue un thread churner che scambia e rimuove il selettore
cBPF tramite setsockopt() in un ciclo stretto, e thread sender che inondano
di datagrammi UDP da 64 byte il gruppo mentre i ricevitori contano la consegna per socket.
Fasi di esecuzione (T = dur_sec):
time ──────────────────────────────────────────────────────────────►
[0 ──────────── T-15s) [T-15s ── T-10s) [T-10s ─────────── T]
CHURN + FLOOD SETTLE MEASURE
churner swaps/detaches churn frozen, deterministic program
the selector prog at final program (selects the LAST
max rate under full attached socket): EVERY packet
UDP flood — THE (selects LAST must land on the LAST
race window open socket) socket; snapshot A →
run → snapshot B
Controllo di integrità: durante la fase di misura il programma finale seleziona in modo deterministico l'ultimo socket del gruppo. Se i pacchetti ricevuti dal gruppo durante quella finestra non sono finiti tutti su quel socket, la selezione è andata storta (un possibile effetto UAF anche senza KASAN) → codice di uscita 2.
Codici di uscita dell'hammer: 0 PASS · 1 errore di setup/runtime · 2 WARN di integrità.
run_hammer.sh — wrapper per una singola esecuzioneEsegue l'hammer e aggiunge i controlli che rendono significativa una singola esecuzione:
bpf_prog in /proc/vmallocinfo;net.core.optmem_max così i programmi cBPF da diversi KB si collegano senza problemi;DRAIN (default 30s) per il completamento delle free differite di RCU/workqueue;BUG:, Oops:, WARNING:, RIP:,
leaked, stuck);bpf_prog prima/dopo (controllo leak), e
opzionalmente scansiona kmemleak se esiste /sys/kernel/debug/kmemleak.Codici di uscita: 0 pulito · 1 errore di setup (incluso fallimento del setup dell'hammer) ·
2 splat del kernel rilevato · 3 possibile leak di bpf_prog · 4 WARN di integrità
dell'hammer.
livepatch_cycle.sh — test del ciclo di vita della livepatchLa correzione libera il vecchio programma cBPF da una callback call_rcu(). Se distribuisci
la correzione come livepatch (kernel live patching — codice applicato a un
kernel in esecuzione), la funzione di callback stessa risiede nel modulo della patch: un
revert/unload mentre le callback sono ancora in sospeso libera il testo del modulo sotto
i piedi della callback. Questo script esercita cicli di apply/revert mentre l'
hammer mantiene calda la finestra di race, e osserva
/sys/kernel/livepatch/*/transition e dmesg.
$ MODE=rcu ./livepatch_cycle.sh 20 120 # 20 cycles × 120s hammer each
| Modalità | Comportamento |
|---|---|
cycle (default) | apply → revert, entrambi sotto carico continuo dell'hammer |
Codici di uscita: 0 pulito · 1 fallimento del comando · 2 splat o transizione bloccata ·
4 WARN di integrità dall'hammer.
La CI compila l'hammer con due set di flag ed esegue shellcheck sugli script. I test runtime del kernel non vengono intenzionalmente eseguiti su runner CI condivisi: il riproduttore necessita del controllo sulla versione del kernel del runner (e su un kernel vulnerabile potrebbe mandare in oops il runner). Esegui quelli su macchine di test reali.
GPL-2.0-only — vedi LICENSE.
| File | Scopo |
|---|
reuseport_race_hammer.c | Il riproduttore: hammer multithread che agita il selettore cBPF sotto pieno carico UDP, poi verifica l'integrità della consegna. |
run_hammer.sh | Wrapper per una singola esecuzione: alza net.core.optmem_max, esegue l'hammer, poi controlla dmesg per splat, /proc/vmallocinfo per allocazioni bpf_prog perse, e opzionalmente kmemleak. |
livepatch_cycle.sh | Applica/ripristina una livepatch che contiene la correzione mentre l'hammer è in esecuzione — caccia ai rischi del ciclo di vita della livepatch della correzione basata su call_rcu(). |
Makefile | Compila l'hammer. |
.github/workflows/ci.yml | CI: build + shellcheck (nessun test runtime del kernel; vedi CI). |
| Argomento | Default | Significato |
|---|
dur_sec | 300 (min 45) | secondi totali di esecuzione |
insns | 256 | istruzioni di riempimento nel programma cBPF agitato; un programma più grande è una regione liberata più grande da colpire. Se l'attach fallisce con ENOMEM, alza net.core.optmem_max (il wrapper lo fa per te). |
nports | 4 (max 64) | gruppi reuseport (una porta UDP ciascuno, da 21000) |
nsocks | 8 (max 512) | socket per gruppo |
nsenders | 4 (max 32) | thread mittenti UDP per gruppo |
ip | 127.0.0.1 | indirizzo di destinazione; usa l'IP di una NIC fisica per distribuire le softirq RX tra le CPU (RSS) |
ebpf | 0 | 1 = agita anche l'attach/detach di SO_ATTACH_REUSEPORT_EBPF (richiede CAP_BPF/CAP_NET_ADMIN); quel percorso non è vulnerabile, questo è per confronto/copertura |
| Env | Default | Significato |
|---|
HAMMER | ./reuseport_race_hammer | binario dell'hammer |
DRAIN | 30 | secondi di attesa dopo l'esecuzione prima del controllo |
OPTMEM_MAX | 131072 | valore per net.core.optmem_max; 0 = non toccare |
rcu |
| revert immediatamente al massimo churn — il caso di rischio sopra |
safe | ferma l'hammer → sleep GRACE (default 30s, un periodo di grazia) → revert |
| Env | Default | Significato |
|---|
APPLY_CMD / REVERT_CMD | kpatch load $PATCH / kpatch unload $PATCH | comandi livepatch |
PATCH | ./livepatch-reuseport.ko | modulo della patch |
HAMMER | ./reuseport_race_hammer | binario dell'hammer |
HAMMER_ARGS | 256 4 8 4 127.0.0.1 0 | argomenti dell'hammer |
TRANSITION_TIMEOUT | 60 | secondi massimi di attesa per una transizione livepatch |
GRACE | 30 | sleep del periodo di grazia per MODE=safe |
FORCE | 0 | 1 = esegui anche se non viene rilevata alcuna transizione livepatch |