
Ricerca sull'exploit di CVE-2026-98260 (UAF exec di posix-cpu-timers): S9280 Android 14 GKI 6.1 — trigger UAF + primitiva di scrittura zero + SELinux Permissive. Documento di handoff in HANDOFF.md
exec: Cleanup POSIX timers right after de_thread() (autore Hyunwoo Kim, upstream 2026-09-11,
Fixes: 55e8c8eb2c7b = da 5.7).
Sul thread non leader E è agganciato un timer CLOCK_THREAD_CPUTIME_ID; in execve(),
dopo che de_thread() scambia il tid, la ricerca inversa del task tramite PID del timer fallisce
e il nodo rimane nei posix_cputimers di E. Se mmap_read_lock_killable() di exec_mmap()
restituisce -EINTR (exec fallisce e il codice di pulizia è più avanti), do_exit() →
exit_itimers() libera il k_itimer ma il nodo non viene rimosso; quando il processo padre
lo recupera con wait4(), cleanup_timerqueue() → timerqueue_del() opera su un oggetto
già liberato → UAF.
| Ramo | posixtimer_exec | Stato |
|---|---|---|
| linux-6.1.y | assente | non corretta |
| linux-5.15.y | assente | non corretta |
| linux-6.6.y | presente | corretta (6.6.158+) |
| linux-6.12.y | presente | corretta (6.12.112+) |
→ Gli obiettivi di questo progetto (linee 5.15 / 6.1, e 6.6 precedenti alla 6.6.158) rientrano tutti nell'ambito interessato.
fragment_vmas() usa memfd + MAP_FIXED per suddividere
lo spazio di indirizzi in --vmas (default 50000) VMA non unibili (il vm_pgoff di
ciascuna è 0, i pgoff di indirizzi adiacenti non sono contigui → vma_merge fallisce).
Il thread locker esegue ripetutamente mprotect sull'intero intervallo, e l'attraversamento
di decine di migliaia di VMA mantiene il write lock di mmap per millisecondi.tgkill(tgid, vecchio tid, 0) che
restituisce ESRCH per stabilire che "de_thread() ha completato lo scambio di tid", e solo
in quel momento invia SIGKILL.exec_mmap() su mmap_read_lock_killable()
(il read lock è bloccato dal write lock), e il SIGKILL arriva microsecondi dopo → quella
chiamata restituisce -EINTR → exec fallisce. La finestra (ms) ≫ tempo di reazione (µs),
quindi non dipende dalla fortuna.⚠️ Nota bene la distinzione: il trigger è deterministico; il passaggio da UAF a
arbitrary write / modifica delle cred dipende dall'orchestrazione del riuso dello slab di
posix_timers_cache (order, objects/slab, CPU partial, appartenenza memcg), che è heap
engineering misurabile e iterabile, non una race.
export ANDROID_NDK_HOME=/home/nt/项目/samsung_root_research/06_工具链/android-ndk-r29
make # 产物 trigger(aarch64 / Android 21+ / 仅依赖 libc、libdl)
adb push trigger /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/trigger"
adb shell "/data/local/tmp/trigger --check" # 先看环境
adb shell "/data/local/tmp/trigger; echo exit=\$?"
Indicatori di successo (ne basta uno dei tre):
[*] 开始触发 il dispositivo si riavvia/schermo nero (il caso più diretto con panic_on_oops=1)exit=137/139) e il sistema resta vivocleanup_timerqueue / timerqueue_del /
rb_erase / rb_nextIn caso di fallimento (mancata attivazione) il programma ritenta da solo --attempts volte e stampa il risultato di wait4 per ogni ciclo,
infine suggerisce la regolazione dei parametri: --vmas 120000 --lockers 4 --cpu 0.
panic_on_oops potrebbe essere 0: in tal caso il task viene terminato e il kernel continua a funzionare con un rbtree corrotto;
si consiglia di eseguire 2~3 volte di seguito per osservare se compare un panic successivo.Ambiente di test: S9280ZHU6EZIF, kernel 6.1.162-android14-11-34343818-abS9280ZHU6EZIF
(build 2026-09-20), non root, dominio shell (uid 2000), SELinux Enforcing.
| Ciclo | Osservazione | Significato |
|---|---|---|
| Ogni ciclo | Il processo padre tgkill(child_tgid, etid, 0) restituisce ESRCH | de_thread() ha completato lo scambio dei tid |
| Ogni ciclo | Subito dopo kill(child_tgid, SIGKILL), il processo figlio ha rc=-9 | exec è stato interrotto (non rc=0 di un exec riuscito) |
Controllo --no-killer | exec da armed a fine impiega 277 ms (287 ms con il killer) | il write lock di mmap ha effettivamente bloccato mmap_read_lock_killable() di exec_mmap() |
5/5 cicli hanno tutti centrato "ESRCH → SIGKILL → exec fallito", nessuna volta exec è riuscito.
Ciò dimostra che la finestra di interruzione di exec dopo de_thread è controllabile e stabile; la finestra deriva dal
mprotect del locker (60.000 VMA, 200–700 ms per singola chiamata), mentre la reazione del processo padre è a livello di µs.
de_thread() esegue zap_other_threads() e uccide tutti gli
altri thread dello stesso processo; un killer scritto nel processo figlio morirebbe da solo al momento di
de_thread, senza mai vedere ESRCH (il che si manifesta come exec riuscito ogni volta).SIGKILL è inefficace — a quel punto il processo figlio non è ancora morto e
k_itimer non è ancora stato liberato da RCU. L'ordine corretto è
waitid(WNOWAIT) attendere lo zombie → attendere che il callback RCU liberi → conquistare la pagina → infine wait4 per attivare il percorso di reclaim.Al momento il passo "nodo ancora in coda dopo il rilascio" è stabilmente raggiunto, ma l'rb_erase al momento di wait4
non ha prodotto oops — il che indica che la pagina slab vittima non è ancora stata conquistata dal nostro spray (la partial slab di posix_timers_cache
è ancora in cache, la pagina non è tornata al buddy). Questo riguarda l'orchestrazione del reclaim dell'exploit,
non è un problema di race; occorre procedere come nelle note vivo: prima saturare/svuotare la slab con k_itimer stesso
per favorire la restituzione dell'intera pagina, poi intercettare con precisione la metà di pagina splittata usando un carrier order-0 (AIO ring, ecc.).
Suggerimento per il passo successivo: usare questa macchina come riferimento e fare un reclaim mirato seguendo
l'approccio di docs/08-reclaim-choreography.md (note vivo); oppure verificare con un kernel compilato da sé con KASAN se la UAF si verifica davvero.
La catena pubblica (uid0.c di Honor) si basa sugli eventi kmem di tracefs per due cose cruciali: