
Exploit do kernel Android GKI 6.12 para CVE-2026-43499, encadeando um bug de rollback do rt_mutex com sobrescrita de pilha do pselect para obter root em dispositivos Samsung e Pixel.
Um exploit de kernel Linux para a linha GKI 6.12 do Android (dispositivos
Samsung e Pixel) visando CVE-2026-43499: um rollback defeituoso de
remove_waiter() em rt_mutex_start_proxy_lock() que usa current em vez de
waiter::task, deixando o pi_blocked_on de um waiter apontando para seu
rt_mutex_waiter na pilha do kernel (posteriormente removido). Combinado com
uma sobrescrita de pilha de fd_set via pselect() e uma travessia de rb_tree
falsa controlada por sched_setattr do consumidor, isso produz uma primitiva
de escrita determinística no kernel e root completo.
O exploit roda como uma biblioteca compartilhada LD_PRELOAD (preload.so) e
instala um daemon su root mais o wallpaper como artefatos pós-root.
Status: desenvolvimento ativo. O alvo m1q (ZF1) é o foco do bring-up. O bootstrap pipei tmp_page-uname é a rota ativa atual: a travessia está comprovadamente limpa no dispositivo (build #33: regiões rt_mutex semeadas por filho) e a varredura completa de 168 candidatos está em andamento. A rota configfs CFI é um beco sem saída no m1q (Rust ashmem — sem slot fops injetável) e permanece a rota para alvos C-ashmem. Os offsets por alvo variam por dispositivo — verifique contra o binário real do kernel antes de confiar neles.
Quando FUTEX_CMP_REQUEUE_PI reenfileira um waiter e a travessia da cadeia de
detecção de deadlock do requeue retorna -EDEADLK, __rt_mutex_start_proxy_lock()
faz rollback via remove_waiter() (rtmutex.c:1535). O rollback desenfileira
corretamente o waiter da árvore de espera, mas limpa o pi_blocked_on do
chamador do requeue em vez de waiter->task->pi_blocked_on. O pi_blocked_on
do WAITER permanece pendente em seu rt_mutex_waiter na pilha, que é removido
quando o futex expira.
Após o timeout, a região da pilha do kernel do waiter é reutilizada:
core_sys_select() copia os três fd_sets para esse buffer de pilha (o caminho
de pilha nfds < 344 no ZF1). Um array de palavras fd_set forjado
re-materializa um rt_mutex_waiter falso / task falsa / rt_mutex falso (com
uma raiz rb_tree cujos ponteiros são controlados). Uma thread consumidora então
chama sched_setattr_tid(waiter) → rt_mutex_adjust_pi() → rb_erase_cached,
o que produz uma escrita em endereço arbitrário de um valor controlado.
A primitiva inteira requer o ciclo da cadeia PI: o owner mantém
f_pi_target e também bloqueia em f_pi_chain (mantido pelo waiter), então a
travessia da cadeia atinge owner → chain → waiter → target → owner e falha com
-EDEADLK. Remover o ciclo faz o requeue retornar sucesso com efeito zero no
kernel.
sched_blocked_reason (primária no m1q, comprovada no
dispositivo): o PC de retorno salvo de um kworker bloqueado
(stack_trace_save_tsk) é lido do ring buffer e comparado contra o
offset worker_thread compilado. Roda antes de qualquer palavra da rota
boot_id ser usada, para que os alvos de escrita de data-alias
(data_addr() = alias p0 + slide_p0_offset) permaneçam corretos em um
slide diferente de zero.SLIDE_LOGGERS_0_1 nos dados do sysctl boot_id via um
alias de mapa linear; o valor vazado reconstrói stext. Estacionada no
m1q: seu alvo W1 (SLIDE_RANDOM_BOOT_ID_DATA_OFF, RAM física baixa)
tem gravabilidade dependente de página — o slide move o alvo através de
limites de página a cada boot, então ~6/7 execuções falham. Só é
exercitada explicitamente via SLIDE_FORCE_BOOTID (teste de mecanismo)
com tree_pc/tree_left redirecionados para a página sprayada.mm_struct e uso de uma
colisão de hash de futex para localizar um no heap, vazando um
endereço de página do heap do kernel usado como base do spray de objetos
falsos. Os waiters são desanexados (não unidos) na limpeza para evitar o OOM
da pilha do kernel que derrubou builds antes da #26.As palavras fd_set do pselect re-materializam um rt_mutex_waiter falso na
pilha do kernel do waiter; o sched_setattr_tid(waiter) de uma thread
consumidora o percorre via rt_mutex_adjust_pi(). Mapa de palavras do ZF1
(verificado por disasm): PSELECT_WAITER_WORD_SHIFT = 0, palavra 12 = task
(@+0x50), palavra 13 = lock (@+0x58), palavra 14 = wake_state (@+0x60 = 3).
Com um rt_mutex falso semeado, a travessia completa limpa: as W1
*(tree_left)=tree_pc / W2 *(tree_pc&~3+8)=tree_left do rb_erase de [7] são
as únicas escritas no kernel; [11] (setprio / dequeue_pi) e o wake de [9] são
ambos ignorados (waiter falso local em prio 100 mantém o nó da pilha fora do
top-waiter). Toda conclusão que entra na travessia desde a build #19 sobrevive
no dispositivo; os crashes determinísticos anteriores eram um erro de
posicionamento de payload de 8 bytes (SKB_DATA_DELTA), não uma falha do corpo
da travessia.
Fase opcional STAGE3=1 que faz fork de um filho ponte, troca seu
mm->pgd por uma tabela de páginas falsa preparada (3 níveis: PGD→L1→L2, folha
de loop RX + folha de pilha RW) e deixa o filho executar um blob de assembly
somente de registradores que faz patch de seu próprio cred através de uma
janela de varredura física de 2MB — phys R/W de toda a RAM sem as primitivas
configfs/pipe. Atualmente conectado apenas ao alvo m1q e não foi executado
no dispositivo.
41 alvos em src/targets/<codename>-<build>/, cada um exigindo no mínimo um
target.h (offsets de símbolos do kernel, KIMAGE_TEXT_BASE, base do
direct-map, offsets de structs). Alvos Pixel (comet, tokay, tegu,
caiman, komodo, frankel, mustang, rango, stallion, blazer)
sobrescrevem fontes compartilhadas; alvos Samsung (m1q-*) adicionam lógica
específica do dispositivo.
make list-projects # full list
Os layouts de structs do kernel diferem por dispositivo — os offsets devem ser verificados contra o binário real do kernel / kallsyms para cada alvo.
Saída: build/<PROJECT>/bin/preload.so (biblioteca compartilhada carregada via
LD_PRELOAD).
# Default project
CC=clang make
# Specific device target (default: blazer-CP2A.260605.012)
CC=clang make PROJECT=m1q-BP4A.251205.006
# Without CC=clang: uses NDK if found ($NDK_ROOT / $ANDROID_NDK_HOME /
# $ANDROID_NDK_ROOT), otherwise host clang + Android sysroot
make PROJECT=m1q-BP4A.251205.006
# Show build configuration
make info
# Clean
make clean
O build embute um binário PIE su_daemon (src/su_daemon.c, compilado para
build/embed/su_daemon_aarch64_pie) e assets/wallpaper.webp em preload.so
via src/su_blob.S / src/wallpaper_blob.S.
Envie as saídas do build para /data/local/tmp, então execute o exploit sob
LD_PRELOAD. Execute SEM tee em um alvo de bring-up ativo — tee faz buffer
de seu próprio stdio e perde o final do log em um panic do kernel (o stdout do
exploit é sem buffer, então redirecionar direto para um arquivo preserva cada
linha):
adb push build/m1q-BP4A.251205.006/bin/preload.so /data/local/tmp/preload.so
adb push build/embed/su_daemon_aarch64_pie /data/local/tmp/su_daemon_aarch64_pie
adb shell "chmod 755 /data/local/tmp/preload.so /data/local/tmp/su_daemon_aarch64_pie"
adb shell "LD_PRELOAD=/data/local/tmp/preload.so \
/data/local/tmp/su_daemon_aarch64_pie \
> /data/local/tmp/output.log 2>&1"
Em caso de sucesso, o daemon escuta em /data/local/tmp/temp_su.sock e su é
instalado em /apex/com.android.virt/bin.
tools/run_pipei.sh envia o binário, verifica seu sha256 no dispositivo
(um preload.so obsoleto produz vereditos silenciosamente ilegíveis — a
escada uma vez executou uma build < #22 cujo formato de log difere), então
executa a varredura completa de 168 candidatos em blocos de 3x56 para que um
panic perca no máximo um bloco (~10 min). O stdout do exploit transmite
diretamente pelo adb (sem redirecionamento no dispositivo — um tee do lado
do dispositivo perde o final em caso de panic); uma cópia de cada bloco no
lado do host é mantida em /tmp/m1q_pipei_chunk{1..3}_stream.log:
sh tools/run_pipei.sh
PASS = qualquer bloco mostra tmp_page uname changed + after uname='CatOS.
PIPEI_CHILD_REGIONS deve ser definido pelo menos com o tamanho do bloco, ou
as regiões rt_mutex por filho permanecem não semeadas e a varredura entra em
panic no candidato 0. A cobertura completa também requer
TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES=1 — sem isso, apenas uma página do bloco
mm liberado de 8 páginas é varrida. run_pipei_slim.sh é a variante de
diagnóstico de candidato único; run_m1q_ladder.sh é a bateria de
travessia/isolamento (oráculo B16-2, mecanismo de slide, etapas de varredura).
exploit/
Makefile
assets/ # wallpaper.webp (embedded post-root artifact)
src/
main.c # run_exploit() entry point (shared)
preload.c # LD_PRELOAD constructor, su/wallpaper install
util.c # addr translation, page prep, configfs helpers
pipe.c # pipe-based physical read/write
root.c # cred patching / task walk
slide.c # KASLR slide (tracefs + boot_id routes)
fops.c # pselect corruption + fops manipulation
faketables.c # stage-3 fake page-table builders (3-level)
stage3.c # pgd-swap bridge child (no-op unless configured)
stage3_loop.S, stage3_poll.S # bridge blob + enter trampoline
su_daemon.c # root daemon (compiled as separate PIE)
su_blob.S, wallpaper_blob.S # embedded binaries
offset.h # pulls per-target target.h via -DTARGET_CONFIG_H
kernelsnitch/ # mm_struct futex-hash leak library
targets/<codename>-<build>/ # per-device target.h + optional overrides
poc/poc.c # standalone POC (separate from the exploit)
Os alvos sobrescrevem fontes compartilhadas através do mecanismo pick_src do
Makefile: se src/targets/<PROJECT>/<file>.c existir, ele é usado em vez do
src/<file>.c compartilhado; caso contrário, a versão compartilhada é usada.
-EDEADLK (errno 35). Um ciclo ausente aparece como
success=1 com efeito zero no kernel.PSELECT_WAITER_WORD_SHIFT deve ser 0 (profundidades de pilha do
ZF1 verificadas a partir do ELF). Outros alvos ainda usam o padrão 1 —
verifique por dispositivo antes de confiar neles.p0_data_alias() usa DIRECT_MAP_BASE (o mapa linear), que difere de
P0_PAGE_OFFSET no ZF1. Aliases de dados para objetos do kernel
desreferenciados devem usar o mapa linear.pr_error() é fatal (exit(-1)), ao contrário das macros de
aviso/informação.SLIDE_LOGGERS_0_1, não SLIDE_NFULNL_LOGGER;
a palavra é um alias de mapa linear não randomizado, então a rota boot_id é
independente de slide (use-a apenas em slide=0 — execute primeiro o vazamento
tracefs).Este é código de exploit para dispositivos Android e destina-se a pesquisa de segurança, testes de vulnerabilidade autorizados e trabalho de defesa em hardware que você possui ou tem permissão explícita para testar. O uso indevido em dispositivos que você não possui é ilegal. O autor não é responsável por dispositivos brickados, kernels brickados ou garantias anuladas.
mm_structwrite_iter injetável e o objeto de heap ASHMEM_SET_NAME é
um KVec cujo layout não se alinha com o private_data de
configfs_bin_write_iter. O m1q pivota para uma escrita kmalloc: recupera o
bloco mm order-3 vazado como objetos pipe_inode_info e usa a escrita
pselect W1 (*(tree_left) = tree_pc) para sobrescrever o slot tmp_page de
um candidato (+0x90) com a página do namespace UTS (init_uts_ns). Uma
escrita fanout planta um nome marcador no offset sysname e uname()
reporta "CatOS" — uma escrita de kernel verificável sem dependência de
objeto estático. Alvos C-ashmem mantêm o caminho configfs.preload.c instala o daemon su embutido (montado em tmpfs em
/apex/com.android.virt/bin, mais variantes de namespace adbd e local) e
troca o wallpaper.| Variable | Default | Purpose |
|---|
PSELECT_ROUTE_SHIFT | compile-time | A/B fd_set word shift for the main route (m1q) |
SLIDE_SHIFT | compile-time | A/B fd_set word shift for the slide route |
PSELECT_ROUTE_DELAY_USEC | 50000 | Consumer delay before sched_setattr (must be > 0) |
SLIDE_CONSUME_USEC, SLIDE_ENTER_DELAY_USEC, SLIDE_CONSUMER_CORE | — | Slide route timing/pinning |
SKIP_SLIDE | 0 | Use direct-map fallback (slide = 0) |
SLIDE_ONLY | 0 | Run only the KASLR slide and exit |
SLIDE_P0_OFFSET, SLIDE_BOOTID_OFF | — | Override slide p0-alias / boot_id offsets |
SLIDE_FORCE_BOOTID | 0 | Force the boot_id pselect route after the tracefs slide (mechanism test) |
KSNITCH_COLLISIONS | 4 | KernelSnitch collision count |
STAGE3 | 0 | Enable the pgd-swap bridge child phase (m1q) |
STAGE3_DRYRUN | 0 | Stage/verify tables then abort before the swap |
PAGE_RECLAIM_SENDS, PSELECT_SIMPLE_LAYOUT | — | Spray/fd-set layout tweaks |
TMP_PAGE_UNAME | 0 | Run the m1q pipei tmp_page-uname bootstrap |
TMP_UNAME_PIPEI_SWEEP, TMP_UNAME_PIPEI_ALIGN_ORDER, TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES | — | Sweep mode; split_order sweeps all 8 pages of the freed mm block |
TMP_UNAME_PIPEI_SLOT, TMP_UNAME_PIPEI_SLOT_CANDIDATES | — | Sweep range (chunk start / count) |
PIPEI_CHILD_REGIONS | 0 | Per-child seeded rt_mutex regions; MUST be >= sweep chunk or unseeded regions panic at candidate 0 |
PIPEI_RECLAIM_COUNT, PIPEI_DRAIN_COUNT | — | Reclaim/drain pipe pairs; raise on a reclaim miss |
PIPEI_SPONGE_MB | 3072 | Order-0 flood sponge held across the reclaim (build #29) |
PSELECT_W0_PRIO_OVERRIDE | 130 | Top-waiter prio; inert with seeded regions (legacy stale-tree guard, A/B only) |
PSELECT_SKB_DELTA_OVERRIDE | 0 | A/B payload intra-page offset (only if a device differs from -0xe78) |
TMP_UNAME_NAME, TMP_UNAME_HOLD_SEC | "CatOS", 5 | Marker name / hold time after success |
SKB_DATA_DELTA é -0xe78 (medido no dispositivo, build #19) — o
verdadeiro skb frag head é 8 bytes menor que o -0xe80 derivado do disasm;
esse erro de 8 bytes causou todo crash determinístico de pselect das builds
#6-18 (entrou na travessia → deref de lixo). Valor compartilhado do GKI 6.12;
cópias de util.c por alvo carregam um botão A/B de ambiente
SKB_DATA_DELTA. NÃO empilhe PSELECT_SKB_DELTA_OVERRIDE sobre a constante
embutida.ASHMEM_SET_NAME não se alinha
com configfs_bin_write_iter. Use o bootstrap pipei tmp_page. As guardas
PSELECT_WRITE_RIGHT_SPRAYED / PSELECT_PI_TOP_TASK_SELF no target.h do
m1q redirecionam as escritas [11] da travessia para fora dos fops estáticos
RO.payload+0x6000 + idx*0x40) dentro da página recuperada. Varreduras com
fake_lock compartilhado entraram em panic no candidato ~9-12 (nós obsoletos
da pilha do kernel reciclados), e as regiões vazias da build #32 entraram em
panic no candidato 0 (wake [9] owner==NULL na fake_task preenchida com 0x41).
PIPEI_CHILD_REGIONS deve ser definido ou as regiões permanecem não
semeadas.