
Pesquisa de exploit para CVE-2026-98260 (UAF em exec de posix-cpu-timers): S9280 Android 14 GKI 6.1 — gatilho de UAF + primitiva de escrita zero + SELinux Permissive. Documento de transferência em HANDOFF.md
exec: Cleanup POSIX timers right after de_thread() (autor Hyunwoo Kim, upstream em 2026-09-11,
Fixes: 55e8c8eb2c7b = a partir da 5.7).
Um timer CLOCK_THREAD_CPUTIME_ID está anexado à thread não-leader E; em execve(),
após de_thread() trocar o tid, o timer falha ao localizar a task pelo PID e o nó permanece
no posix_cputimers de E. Se mmap_read_lock_killable() de exec_mmap() retornar -EINTR
(exec falha e o código de limpeza vem mais adiante), do_exit() → exit_itimers() libera o
k_itimer mas o nó não é removido; quando o processo pai o recolhe via wait4(),
cleanup_timerqueue() → timerqueue_del() opera sobre um objeto já liberado → UAF.
| Branch | posixtimer_exec | Estado |
|---|---|---|
| linux-6.1.y | ausente | não corrigido |
| linux-5.15.y | ausente | não corrigido |
| linux-6.6.y | presente | corrigido (6.6.158+) |
| linux-6.12.y | presente | corrigido (6.12.112+) |
→ Os alvos deste projeto (linhas 5.15 / 6.1, e 6.6 anterior à 6.6.158) estão todos na faixa afetada.
fragment_vmas() usa memfd + MAP_FIXED para dividir o
espaço de endereçamento em --vmas (padrão 50000) VMAs não mescláveis (o vm_pgoff de
cada uma é 0, e pgoffs de endereços adjacentes não são contíguos → vma_merge falha). A
thread locker repete mprotect sobre todo o trecho, e percorrer dezenas de milhares de VMAs
mantém o write lock de mmap retido por milissegundos.ESRCH de tgkill(tgid, tid antigo, 0)
para determinar que "de_thread() concluiu a troca de tid", e só então envia SIGKILL.mmap_read_lock_killable() no ponto de
exec_mmap() (o read lock é barrado pelo write lock), e o SIGKILL chega microssegundos depois
→ essa chamada retorna -EINTR → exec falha. A janela (ms) ≫ o tempo de reação (µs), portanto
não depende de sorte.⚠️ Atenção à distinção: o disparo é determinístico; a passagem do UAF para escrita
arbitrária/alteração de cred depende da orquestração de reutilização do slab de
posix_timers_cache (order, objects/slab, CPU partial, pertencimento a memcg), o que é
engenharia de heap mensurável e iterável, não uma corrida.
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=\$?"
Sinais de sucesso (basta um dos três):
[*] 开始触发, o dispositivo reinicia/tela fica preta (mais direto quando panic_on_oops=1)exit=137/139), e o sistema continua vivo depoiscleanup_timerqueue / timerqueue_del /
rb_erase / rb_nextEm caso de falha (não disparou), o programa tenta novamente sozinho --attempts vezes e imprime o resultado de wait4 de cada rodada,
por fim sugere ajuste de parâmetros: --vmas 120000 --lockers 4 --cpu 0.
panic_on_oops da Samsung pode ser 0: nesse caso a tarefa é morta e o kernel continua rodando com a rbtree corrompida;
recomenda-se executar 2~3 vezes seguidas para observar se ocorre um panic subsequente.Ambiente de teste: S9280ZHU6EZIF, kernel 6.1.162-android14-11-34343818-abS9280ZHU6EZIF
(build 2026-09-20), sem root, domínio shell (uid 2000), SELinux Enforcing.
| Rodada | Observação | Significado |
|---|---|---|
| Cada rodada | O processo pai tgkill(child_tgid, etid, 0) retorna ESRCH | de_thread() já concluiu a troca de tid |
| Cada rodada | Em seguida kill(child_tgid, SIGKILL), o processo filho retorna rc=-9 | exec foi abortado (não é rc=0 de exec bem-sucedido) |
Controle --no-killer | exec do estado armed até o fim leva 277 ms (com killer, 287 ms) | O write lock do mmap de fato bloqueia o mmap_read_lock_killable() de exec_mmap() |
5/5 rodadas acertaram "ESRCH → SIGKILL → falha do exec", nenhuma vez o exec teve sucesso.
Isso prova que a janela para abortar o exec após de_thread é controlável e estável; a janela vem do
mprotect do locker (60 mil VMAs, 200–700 ms por vez), e a reação do processo pai é em nível de µs.
de_thread() executa zap_other_threads() e mata todas as
outras threads do mesmo processo; um killer escrito no processo filho morre antes, no momento do de_thread,
e nunca vê o ESRCH (o sintoma é o exec ter sucesso sempre).SIGKILL é ineficaz — nesse momento o processo filho ainda não morreu e
o k_itimer ainda não foi liberado pelo RCU. A ordem correta é
waitid(WNOWAIT) aguardar o zumbi → aguardar o callback do RCU liberar → disputar a página → por fim wait4 disparar o caminho de reclaim.Atualmente a etapa "após a liberação, o nó ainda está na fila" já foi alcançada de forma estável, mas o rb_erase no momento do wait4
não gera oops — isso indica que a página do slab vítima ainda não foi capturada pelo nosso spray (o partial slab do posix_timers_cache
ainda está em cache, a página não voltou ao buddy). Isso pertence à orquestração de reclaim do exploit,
não é um problema de corrida; é preciso seguir a abordagem das notas do vivo: primeiro usar o próprio k_itimer para encher/esvaziar o slab
e forçar a devolução da página inteira, depois usar um portador order-0 (AIO ring etc.) para capturar com precisão a meia-página dividida.
Sugestão para o próximo passo: usar esta máquina como referência e fazer reclaim direcionado seguindo a ideia de docs/08-reclaim-choreography.md (notas do vivo);
ou validar com um kernel compilado com KASAN se o UAF realmente ocorre.
A cadeia pública (Honor uid0.c) depende dos eventos kmem do tracefs para duas coisas críticas:
| 用途 | Honor 611/6.12 用的 | 本机(Samsung 6.1.162) |
|---|---|---|
找受害 k_itimer 地址 V(算 payload 在页内偏移) | kmem:kmem_cache_alloc | 事件存在,但 events/kmem/*/enable 是 0644 root:root,shell 无法 enable(events/sched/*/enable 可写,kmem 不行) |
| 判断 order-1 slab 页已归还 buddy(精确抢页时刻) | kmem:mm_page_free | 同上,存在但无法 enable |
| 读原始 trace 缓冲 | per_cpu/cpuN/trace_pipe_raw | 可读(-r--r----- root readtracefs)✓ |
| KASLR slide | 各 trace 事件 static site | 可做:sched 事件可 enable,项目已有同类手法 ✓ |
结论:公开链不能原样移植。缺的不是内核能力(事件都在),而是 shell 没有 kmem 事件的 enable 权限。