- GhostLock (CVE-2026-43499) é uma vulnerabilidade do kernel introduzida no Linux 2.6.39 e corrigida no 7.1; ela permite que um atacante local sem privilégios cause um UAF na stack usando apenas chamadas de sistema comuns de threading, podendo ser explorada para obter privilégios de root e escapar de contêineres
- No caminho de proxy Requeue-PI,
remove_waiter()limpa opi_blocked_ondecurrentem vez da tarefa que está realmente aguardando, deixando na tarefa que retornou ao espaço de usuário um ponteiro para um stack frame já liberado - Com três futexes e três threads, cria-se um ciclo de dependência PI para induzir o rollback com
-EDEADLK; em seguida, constrói-se umrt_mutex_waiterfalso em um buffer de stack controlável dePR_SET_MM_MAP, obtendo uma escrita de ponteiro restrita - O exploit usa
prefetchpara encontrar os endereços-base de KASLR e do physmap, posiciona estruturas falsas e uma stack ROP na CPU entry area (CEA) e então sobrescreveinet6_protos[IPPROTO_UDP]para sequestrar o fluxo de controle com um pacote IPv6 UDP de loopback - Os pesquisadores receberam US$ 92.337 no Google kernelCTF por um exploit de elevação de privilégios e escape de contêiner 97% estável; todas as distribuições Linux não corrigidas devem atualizar para a versão LTS mais recente
Escopo de impacto e visão geral da vulnerabilidade
- GhostLock é uma vulnerabilidade do kernel Linux descoberta pela VEGA, que pode ser acionada por um usuário local sem privilégios, sem permissões especiais nem namespaces de usuário
- Foi introduzida pela reformulação do rtmutex em
8161239a8bcc, e o escopo afetado vai dev2.6.39-rc1atév7.1-rc1 - Foi corrigida em abril de 2026 em
3bfdc63936dd, e a única configuração de kernel necessária éCONFIG_FUTEX_PI=y - Um atacante pode elevar privilégios seguindo este processo
- Obter, usando apenas chamadas de sistema comuns de threading, um ponteiro de kernel pendente que aponta para memória da stack do kernel
- Criar uma primitiva restrita que permite escrever um ponteiro ou 8 bytes zero em um endereço quase arbitrário
- Sequestrar uma tabela de funções para assumir o controle do fluxo de execução e obter privilégios de root
- Como todas as distribuições Linux sem patch são afetadas, é necessário atualizar para a versão LTS mais recente
Por que remove_waiter() limpa a tarefa errada
remove_waiter(), emkernel/locking/rtmutex.c, foi originalmente escrito para o caminho em que a própria thread bloqueada limpa diretamente seu estado de espera- No slow path normal, o
currentem execução é a tarefa dona do waiter, portanto limparcurrent->pi_blocked_oné o comportamento correto - No caminho de proxy Requeue-PI,
rt_mutex_start_proxy_lock()enfileira umrt_mutex_waiterem nome de outra tarefa adormecida e, se ocorre um erro, faz rollback disso- Nesse momento,
currenté o requeuer que chamouFUTEX_CMP_REQUEUE_PI - O waiter real é uma tarefa separada adormecida em
FUTEX_WAIT_REQUEUE_PI
- Nesse momento,
- Quando
__rt_mutex_start_proxy_lock()retorna-EDEADLK,remove_waiter()remove o waiter do lock, mas define apenascurrent->pi_blocked_oncomoNULL - O
pi_blocked_ondo waiter real continua apontando para ort_mutex_waiterem sua própria stack do kernel; quando o waiter retorna ao espaço de usuário, esse stack frame passa a ser considerado liberado - Depois, no momento em que a varredura da cadeia PI passa por essa tarefa, ela desreferencia um objeto de stack já liberado
- O lockdep só verifica qual
pi_lockestá retido e não confere de quem é o lock, por isso não captura esse erro
Ciclo com três futexes que cria o rollback -EDEADLK
- Para alcançar o caminho de erro, constrói-se um ciclo de dependência PI com três futexes e três threads
f_pi_chain: o futex PI que o waiter bloqueia primeirof_pi_target: o futex PI que o owner bloqueia primeiro e que se torna o alvo do requeuef_wait: o futex comum no qual o waiter aguarda comFUTEX_WAIT_REQUEUE_PI
- A sequência de acionamento é a seguinte
- O waiter bloqueia
f_pi_chaine depois fica bloqueado emFUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target), com ort_mutex_waitercolocado em sua própria stack do kernel - O owner bloqueia
f_pi_targete depois fica bloqueado emf_pi_chain, que é mantido pelo waiter - A thread main chama
FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target)
- O waiter bloqueia
- Quando o proxy requeue tenta conectar o waiter a
f_pi_target, fecha-se o ciclowaiter → f_pi_target → owner → f_pi_chain → waiter - A varredura da cadeia PI retorna
-EDEADLKe executa o rollback incorreto, enquanto o waiter acorda com umpi_blocked_onpendente - A condição importante é que o requeuer faça rollback enquanto o waiter ainda mantém o objeto na stack; depois que o ciclo é concluído, o processo segue por conta própria
- Quando o waiter volta ao espaço de usuário, não há mais pressão de tempo, e a varredura da cadeia pode ser acionada depois a qualquer momento com
sched_setattr() - A configuração usa três threads, mas a corrida de UAF em si pode ser acionada mesmo em um único núcleo de CPU
Primitiva inicial fornecida pelo UAF na stack
- O ponteiro pendente aponta para o
rt_mutex_waiterque estava no frame anterior deFUTEX_WAIT_REQUEUE_PI - Ao reposicionar bytes controláveis na mesma profundidade de stack da mesma tarefa, é possível fazer o kernel desreferenciá-los como um
rt_mutex_waiterfalso - Dependendo de como a estrutura falsa é posicionada, uma única tentativa de acesso fornece duas primitivas principais
- É possível escrever um ponteiro em um endereço quase arbitrário, com restrições
- É possível escrever 8 bytes zero em um endereço quase arbitrário, com restrições
- Antes da escrita, várias desreferenciações de ponteiros e verificações de integridade são realizadas, mas, se as condições forem atendidas, o kernel retorna normalmente após a escrita sem travar
- Para completar o exploit, é necessário reutilizar o stack frame, passar nas verificações estruturais do waiter falso e escolher alvos que satisfaçam as restrições de escrita
Reutilização de um stack frame liberado com PR_SET_MM_MAP
- Assim que o waiter retorna da chamada de sistema futex, ele chama
prctl(PR_SET_MM, PR_SET_MM_MAP, ...) prctl_set_mm_map()copia o auxv fornecido pelo usuário para um buffer de pilha de tamanho fixo,unsigned long user_auxv[AT_VECTOR_SIZE]- Como esse buffer fica em uma profundidade de pilha semelhante à do waiter liberado, um bloco grande, alinhado e controlável de qwords se sobrepõe ao
rt_mutex_waiteranterior - A área sobreposta do auxv é configurada da seguinte forma
tree: transforma o ponteiro de filho escolhido para a remoção,W0_BASE, em um nó rb que é promovido à raiz da árvoretask: definido como&init_taskpara atravessar com segurança as desreferenciações da busca pela cadeialock: definido como&inet6_protos[IPPROTO_UDP] - 8para alinhar o alvo da escritawake_state: definido como0
- O auxv é colocado em um memfd e disposto para que a cópia atravesse uma fronteira de página; então uma thread irmã disputa, durante a execução do
prctl, comfallocate(PUNCH_HOLE)na página traseira para aumentar o tempo decopy_from_user - Uma thread consumer em outra CPU chama
sched_setattr()no waiter enquanto o waiter falso ainda permanece na pilha, fazendo a busca pela cadeia de PI - Outras chamadas de sistema que usam variáveis locais grandes e controláveis na pilha, como
clone,setsockopt,pselectekeyctl, também podem cumprir o mesmo papel prctlfoi escolhido porque o buffer é grande, alinhado e não exige namespace; candidatos adicionais estão incluídos no código PoC público
Criando uma escrita de ponteiro restrita por meio da remoção em rb-tree
- Mesmo controlando o waiter falso, isso não gera imediatamente uma escrita arbitrária completa; a busca pela cadeia executa o seguinte caminho
- encontra o waiter falso em
task->pi_blocked_on - encontra o
rt_mutex_basefalso emfake waiter->lock rt_mutex_dequeue(lock, waiter)realiza a remoção da rb-tree emlock->waiters
- encontra o waiter falso em
- A exploração usa a propriedade de que, ao remover um nó raiz com apenas um filho, esse filho é gravado no slot da raiz
- Ao definir
lockcomotarget - 8, os dados ao redor são interpretados como os seguintes campos dert_mutex_basetarget - 8:wait_lock, que precisa ser lido como não bloqueadotarget:waiters.rb_root.rb_node, que será sobrescritotarget + 8:waiters.rb_leftmosttarget + 16:owner
- Como resultado, a única escrita executada é
*(uint64_t *)target = W0_BASE - O endereço alvo deve satisfazer, em linhas gerais, as seguintes condições
- os 32 bits inferiores de
target - 0x08devem ser0 - o valor de 64 bits em
target + 0x08deve ser0 - o valor no ponteiro
owneremtarget + 0x10, excluídos os flags inferiores, deve ser0
- os 32 bits inferiores de
- Se o qword anterior parecer um spinlock travado, o trylock falha e a execução termina sem escrever nada
- Se o valor posterior apontar para um top waiter ou owner não controlado, ou para um valor não mapeado, pode ocorrer um kernel panic
- Como
W0_BASEprecisa permanecer válido até o fim das comparações, do requeue, da atualização de prioridade e do wakeup sem owner, é usado o alias de direct-map da CEA
Vazamento por prefetch e CPU entry area
-
Encontrando os endereços-base de KASLR e physmap
- O tempo de execução de
prefetchpara um endereço específico varia conforme esse endereço está mapeado na tabela de páginas atual - Quando um processo sem privilégios mede o tempo de execução na faixa de endereços do kernel, ele pode estimar as posições mapeadas; o princípio detalhado está descrito no artigo sobre prefetch
- Como a entropia do endereço-base da imagem padrão do kernel Linux é de cerca de 9 bits, medições repetidas recuperam o endereço-base do KASLR com quase 100% de confiabilidade
- Em teoria, CPUs com
prefetche sem KPTI adequado são afetadas, mas, na prática, a técnica é usada principalmente em x86 com KPTI desativado - A imagem do kernelCTF está com KPTI desativado; mesmo com KPTI ativado, combinar
prefetche EntryBleed permite recuperar o endereço-base da imagem do kernel por meio do trampoline
- O tempo de execução de
-
Contornando a randomização de endereços da CEA
- A CPU entry area (CEA) é uma estrutura por CPU no x86 que armazena pilhas para entrada e tratamento de exceções, além do contexto de registradores
- Quando um programa sem privilégios provoca uma exceção de software, seu próprio contexto de registradores é gravado em
pt_regsna pilha de exceção da CEA, criando cerca de 120 bytes de memória contígua controlável - Antes do Linux 6.2, o endereço virtual da CEA era totalmente fixo, podendo ser usado diretamente para estruturas falsas, para absorver efeitos colaterais de desreferenciações de ponteiros e para compor uma pilha ROP
- Após a divulgação do Bringing back the stack attack pelo Project Zero, a partir do Linux 6.2 o endereço virtual da CEA passou a ser fortemente randomizado
- O endereço virtual da CEA de cada CPU é randomizado de forma diferente, mas o endereço físico é fixo; portanto, conhecendo o endereço-base do physmap, é possível calcular o alias de direct-map
- Combinando
prefetch, normalização de limites candidatos e verificação das páginas CEA esperadas, aliases adjacentes são eliminados e obtém-secea_direct = physmap_base + CPU1_CEA_BASE - No ambiente de boot de 3,5 GB do kernelCTF LTS
6.12.80, o offset relacionado é0x11c517000(+0x1f58)
Reutilização da CEA como waiter falso e objetos subsequentes
- Antes da primeira escrita, são colocados em
W0da CEA um waiter falso e um lock com consistência internataské definido como&init_taskpriorecebe um valor válido- o
wait_lockdo lock é feito para parecer destravado - o owner é configurado para atravessar com segurança dequeue, requeue, atualização de prioridade e wakeup
- Após a escrita da rb-tree,
W0não precisa mais ser um waiter; portanto, a CEA pode ser preenchida novamente com a estrutura exigida pelo alvo sobrescrito - A CEA é pequena, com cerca de 120 bytes, mas é eficiente porque permite posicionar dados em um endereço fixo do kernel calculável
- NPerm, kernelsnitch e outros também podem cumprir o mesmo papel em um espaço maior
- O exploit usa uma única área CEA, sequencial ou simultaneamente, como
rt_mutex_waiterfalso, lock falso,inet6_protocol, slots de JOP e pivot de pilha, e pilha ROP final
Sequestro do fluxo de controle via inet6_protos[IPPROTO_UDP]
- Em um Linux x86_64 típico, depois de obter o endereço-base do KASLR, é possível escolher um caminho curto que sobrescreva uma tabela de funções adequada ou um objeto que a contenha
- A região ao redor de
inet6_protos[IPPROTO_UDP]na área de dados gravável satisfaz naturalmente as restrições necessáriasinet6_protos[16] == NULLvira o estado desbloqueado dowait_lockfalsoinet6_protos[17] == &udpv6_protocolé o alvo real a ser sobrescritoinet6_protos[18] == NULLvira orb_leftmostfalsoinet6_protos[19] == NULLvira o owner falso
- Quando a escrita termina,
inet6_protos[IPPROTO_UDP]passa a apontar para oinet6_protocolfalso dentro da página CEA - A CEA é pulverizada novamente para compor a struct da seguinte forma
handler: definido como o primeiro gadget de pivôerr_handler: não usadoflags: definido comoINET6_PROTO_NOPOLICY | INET6_PROTO_FINAL
- Ao enviar um pacote IPv6 UDP de loopback que faz
connectem::1e depois escreve dados, o kernel chama ohandlerfalso, permitindo controlar o contador de programa
Pivô curto e escalonamento de privilégios com DirtyMode
- No alvo
lts-6.12.80do Google kernelCTF, não foi encontrado um gadget adequado de pivô de pilha único; por isso, com um load/call adicional, o endereço da CEA foi colocado emrbpe então o pivô foi feito commov rsp, rbp; pop rbp; ret ret2usrou a sobrescrita completa de/proc/%P/fd/xexigem cerca de 10 qwords de gadgets, o que é grande demais para o espaço limitado da CEA- Na etapa final, foi usado o DirtyMode, que altera os bits de permissão com uma única escrita e executa o restante do processo no espaço de usuário
- O alvo da escrita é
coredump_sysctls[1].modenos dados do kernel, isto é, o modo de acesso do sysctlcore_pattern - Como ele compartilha o mesmo slide de KASLR da imagem do kernel, o endereço pode ser calculado; basta que o segundo bit menos significativo, o bit de escrita, esteja definido
- Com uma cadeia curta
pop reg; mov [reg], reg; ret, o valor de modo é alterado e a thread sequestrada é parada com segurança usandomsleep - Quando
/proc/sys/kernel/core_patternse torna gravável por todos os usuários, um processo sem privilégios grava|/proc/%P/fd/666 %Pe derruba o helper, fazendo o kernel executar o binário do atacante com privilégios de root - Devido às restrições de posicionamento em lote, a escrita inicial na rb-tree não consegue alcançar diretamente
coredump_sysctls[1].mode, então a alteração do modo é feita na etapa curta de ROP
Fluxo completo do exploit e resultados
- O ataque prossegue na seguinte ordem
- Vaza o slide da imagem do kernel e o endereço-base do physmap com
prefetch - Usa o GhostLock para deixar um
rt_mutex_waiterpendente empi_blocked_ondo waiter - Reutiliza o mesmo frame da pilha do kernel com
PR_SET_MM_MAPpara criar um waiter falso - Usa a remoção da rb-tree do rtmutex para gravar o ponteiro da CEA em
inet6_protos[IPPROTO_UDP] - Posiciona o
inet6_protocolfalso, o slot de pivô e a pilha ROP na CEA - Chama o handler sobrescrito com um pacote IPv6 UDP de loopback
- Altera os bits de modo de
core_patterncom DirtyMode e conclui o escalonamento de privilégios no espaço de usuário
- Vaza o slide da imagem do kernel e o endereço-base do physmap com
- No ambiente remoto do kernelCTF, o caminho que combina CEA e DirtyMode obteve a flag em cerca de 5 segundos
- O exploit completo está publicado no projeto CyberMeowfia
- No Android, a reutilização de frames da pilha e os métodos para contornar ASLR e CFI são diferentes, e serão tratados em um post separado futuro
Caminhos alternativos e mitigações
-
Espaço maior para ROP
- Memória baseada em NPerm pode ser usada como uma grande pilha falsa depois de sequestrar o fluxo de controle
- Caminhos mais pesados também são possíveis, como o vazamento de heap-KASLR de Lukas Maar, mas eles adicionam etapas e aumentam o tempo de execução
- No kernelCTF, a cadeia mais curta e confiável é vantajosa, por isso foi usada a combinação de CEA e DirtyMode
-
Patch do kernel
- O patch final pega o
pi_locke limpapi_blocked_oncom base emwaiter->task, em vez decurrent remove_waiter()salvawaiter_task = waiter->taske então processa na seguinte ordem- Bloqueia
waiter_task->pi_lock - Remove o waiter da fila do rtmutex
- Define
waiter_task->pi_blocked_on = NULL - Também passa
waiter_task, em vez decurrent, para ort_mutex_adjust_prio_chain()subsequente
- Bloqueia
- A correção separada enviada pelos pesquisadores antes da v1 foi estruturada para que o chamador passasse explicitamente a task proprietária
- Nos caminhos em que a própria task fica bloqueada, passa
current - No rollback de proxy, passa a
taskalvo do proxy - Limpa apenas quando
pi_blocked_onainda aponta para o waiter correspondente, e protege isso com opi_lockda task
- Nos caminhos em que a própria task fica bloqueada, passa
- O patch final pega o
-
RANDOMIZE_KSTACK_OFFSET- O exploit depende de o frame liberado do waiter e o frame
user_auxvsubsequente se sobreporem de forma determinística - Ao ativar
RANDOMIZE_KSTACK_OFFSET, o offset da pilha muda, transformando essa etapa em uma adivinhação de 5 bits, com cerca de 1/32 de chance - Nos dois alvos gerais submetidos, essa configuração estava desativada por padrão; no alvo com mitigação, ela estava ativada, então esse caminho de exploit não foi usado
- O exploit depende de o frame liberado do waiter e o frame
-
STATIC_USERMODE_HELPERSTATIC_USERMODE_HELPERbloqueia este caminho específico do DirtyMode- Porém, a mesma abordagem pode ser generalizada para outras configurações de
/proc/syscujo controle de acesso seja feito porctl_table::modee cuja tabela esteja em dados graváveis previsíveis do kernel
Cronograma de divulgação
- 18 de abril de 2026: a vulnerabilidade e o patch preliminar foram enviados para
security@kernel.org - 20 de abril de 2026: a vulnerabilidade foi corrigida por outro patch
- 4 de maio de 2026: a correção v1 foi retroportada
- 30 de junho de 2026: o Google confirmou a submissão ao kernelCTF
- 7 de julho de 2026: a análise técnica foi publicada
- A vulnerabilidade descoberta pela VEGA segue a política padrão de divulgação de 90+30 dias
1 comentários
Comentários do Hacker News
Testei em 3 dispositivos usando Android 9, 13 e 16 com versões diferentes do Firefox abaixo da 150; 2 entraram em loop de boot e precisaram ir para o modo de recuperação, e o outro simplesmente desligou. A demonstração troca o papel de parede em dispositivos Pixel compatíveis, e a página de teste pode ser vista em IonStack
Ao abrir blogs ou sites aleatórios em um aparelho pessoal, parece mais seguro instalar, além do navegador principal, um navegador baseado em Chromium como o Chromite, desativar nas flags o JavaScript e os decodificadores de vídeo com aceleração de hardware frequentemente atacados, e usar modo de leitura em sites quebrados. Outra opção é ter um tablet dedicado
adbe verificar, vou divulgar o resultado completoAo acessar a página de teste, apareceu saída na aba do Firefox, então parecia que o código de prova de conceito tinha rodado, mas depois disso o telefone travou e recusou toda entrada. Só reiniciar funcionava, e fiquei curioso sobre como ele ainda conseguia responder ao evento de reinicialização mesmo numa situação em que o kernel parecia travado. A tela ficou ligada mostrando parte do resultado da execução até o protetor de tela entrar
Grande elogio aos pesquisadores de segurança que não apenas descobriram o exploit, mas também, ao contrário do copyfail, não publicaram um script de elevação local de privilégio zero-day que qualquer um pudesse usar imediatamente
Passei algumas horas tentando fazer elevação local de privilégio (LPE) no Rocky Linux 9, mas felizmente não consegui. A menos que alguém tenha muito tempo livre ou habilidade excepcional, parece difícil transformar isso em ataque real contra distribuições corporativas
Fico curioso se essa falha permitiria desbloquear o bootloader até em telefones cujo bootloader normalmente não pode ser destravado. Se sim, pode ser uma das maiores coisas que já aconteceram no ecossistema Android
O título deveria ter incluído LPE, para que a maioria pudesse voltar tranquila para o fim de semana
Mas este ataque pode ser acionado até de dentro de processos fortemente sandboxados, como o processo isolado do navegador Firefox. Então basta o atacante encadear um bug de JavaScript para executar código local dentro do sandbox isolado e depois usar esta falha para subir até modo kernel, em um ataque de duas etapas; por isso é preciso atualizar tanto o Firefox quanto o kernel Linux
Fiquei bem interessado ao ler que “o Google pagou US$ 92.337 como recompensa do kernelCTF”
Isso quer dizer que um app Android pode executar código nativo via NDK e obter privilégios de root? E o SELinux ajuda em algo na defesa?
Dá para fazer backport de patches até para kernels antigos, mas changelogs de atualização de smartphone raramente citam CVEs, então ferramentas de verificação de vulnerabilidade acabam sendo praticamente o único meio de confirmar. Se um app da Play Store ou obtido por fora estiver comprometido, ele pode ganhar root imediatamente, então o princípio de verificar confiança e auditoria na hora de instalar continua importante
No futuro, essa verificação pode acabar sendo adicionada a todos os níveis do Google Play Integrity, impedindo a instalação de vários apps em telefones sem patch. Em navegadores, onde é difícil evitar sites arbitrários e anúncios, um escape do sandbox ainda contorna o isolamento entre apps, então é mais grave; lembra o JailbreakMe do iOS
Em qualquer Linux lançado nos últimos 15 anos, um app capaz de executar código nativo pode obter root no dispositivo até a atualização do kernel chegar
É chocante que o GhostLock tenha entrado no Linux 2.6.39 e só tenha sido corrigido no Linux 7.1
Acho que já tinha lido os comentários no dia anterior, mas todos aparecem com horário de menos de 10 horas; fiquei me perguntando se a marcação de tempo do HN estava errada
Na lista “underwater” que eu olho todo dia — ou seja, posts que receberam muitos upvotes mas, por algum motivo, não chegaram à página inicial — este estava no topo, então foi exposto de novo. Parece estranho, mas ainda não apareceu uma alternativa menos confusa do que isso