Depurando um crash de use-after-free no meniOS

Há uns dois anos, escrevi aqui que estava escrevendo um sistema operacional chamado meniOS, quase do zero. (Leia aqui).

Novamente fiquei um bom tempo parado, com a vida e a correria engolindo nossos momentos de lazer, mas voltei a brincar com ele.

A …


This content originally appeared on DEV Community and was authored by Plínio Balduino

Há uns dois anos, escrevi aqui que estava escrevendo um sistema operacional chamado meniOS, quase do zero. (Leia aqui).

Novamente fiquei um bom tempo parado, com a vida e a correria engolindo nossos momentos de lazer, mas voltei a brincar com ele.

A coisa avançou bastante nos últimos tempos, e atualmente é possível executar pequenos binários fora do kernel, na área chamada user space, onde rodam quase todos os softwares que usamos no dia a dia.

Conforme o sistema vai crescendo, cada vez mais partes vão trabalhando juntas e dependendo umas das outras, e vai ficando cada vez mais difícil entender porque um erro acontece.

Para ajudar a navegar entre vários e vários logs cheios de números hexadecimais que eu mesmo mandei imprimir mas não entendia muito bem o que significavam, criei esse pequeno guia, que mostra, passo a passo, como diagnosticar um crash provocado por acesso a memória já liberada (use-after-free). O objetivo é destacar o processo e as ferramentas que uso no meniOS, como nm, objdump e um script em Python com pyelftools, para transformar um endereço de falha em uma linha de código. Ao final, teremos um roteiro reaproveitável para incidentes parecidos.

1. Prepare o cenário

O binário de exemplo é intencionalmente falho: ele libera um ponteiro e logo em seguida tenta escrever naquele endereço. Não por coincidência, essa é uma das principais causas de tela azul no Windows, ou seja lá que cor estejam usando hoje em dia.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    char *ptr = malloc(128);
    if (ptr == NULL) {
        perror("malloc");
        return EXIT_FAILURE;
    }

    free(ptr);
    printf("Dereferencing freed pointer...\n");

    *ptr = 'A';  // Use-after-free: provoca crash
    printf("Value: %c\n", *ptr);

    return EXIT_SUCCESS;
}

Compilar com símbolos de depuração deixa o código-fonte acessível na desmontagem e nas tabelas DWARF; esse formato armazena metadados de depuração no ELF, incluindo o mapeamento entre endereços de instruções, arquivos e números de linha, o que é essencial para relacionar endereços a trechos de código.

gcc -g -O0 -Wall -Wextra use_after_free.c -o use_after_free

O meniOS executa binários no formato ELF, sigla para Executable and Linkable Format. Esse contêiner descreve cabeçalhos, seções e tabelas necessárias para carregar o programa na memória, incluindo as tabelas de símbolos que nm lê e as seções de código que objdump desmonta. Como o ELF também embute as extensões de depuração DWARF geradas pelo compilador, conseguimos cruzar endereços de instruções com arquivos de origem e, a partir disso, chegar rapidamente à linha com defeito.

Enquanto não há suporte a bibliotecas compartilhadas (.so), cada aplicativo do meniOS é entregue como um executável ELF independente. Compilo esses binários na máquina host e copio o resultado para a mesma imagem de disco utilizada no boot do sistema, mantendo o fluxo de build simples e previsível.

2. Reproduza o crash e capture o endereço

Executando o programa no meniOS, ou no ambiente host caso esteja testando localmente, o kernel interrompe o processo assim que detecta o acesso inválido:

Dereferencing freed pointer...
  User page fault at 0x0000555555555000 (present=yes write=yes), terminating pid=17
proc_exit: Process init/use_after_free exited with status 11

Quando o crash é coletado via console serial do meniOS, o log inclui ainda o contexto de sistema de chamadas. Logo antes da falha, a saída típica de write(1, …) registra o ponto para o qual a execução retornaria:

syscall_dispatch: pid=17 number=1 rip=40112f cs=3b rsp=bfdfd0
syscall_dispatch: post-handler rip=401168 cs=3b rsp=bfdf10 rax=32
mosh: process terminated by signal 11

Anote dois dados essenciais: rip=0x401168, que aponta para a instrução que falhou, e o nome do binário (use_after_free), porque é nele que vou procurar a origem.

3. Descubra o símbolo com nm

Comece localizando qual função cobre o endereço problemático. O nm lista a tabela de símbolos do executável, indicando o intervalo de cada função.

nm -an use_after_free | grep -i main

Saída típica:

00000000004010f0 t _start
0000000000401120 T main

main começa em 0x401120. Como 0x401168 ainda está próximo, é razoável assumir que a falha ocorreu dentro dessa função, mas é prudente confirmar.

4. Navegue no assembly com objdump

O objdump permite correlacionar instruções com o código-fonte. Use as opções --source e --line-numbers para misturar C e assembly; limite a saída a um trecho curto com sed.

objdump -d --no-show-raw-insn --source --line-numbers use_after_free \
  | sed -n '35,60p'

Trecho relevante:

use_after_free.c:16
    *ptr = 'A';  // Use-after-free: provoca crash
  401164:  mov    -0x8(%rbp),%rax
  401168:  movb   $0x41,(%rax)          ; 0x41 == 'A'

use_after_free.c:17
    printf("Value: %c\n", *ptr);

Isso confirma visualmente: a instrução em 0x401168 é exatamente a escrita com 'A'. Se o log apontar um endereço alguns bytes adiante, use o mesmo procedimento para encontrar a linha correspondente.

5. Resolver linha e arquivo com Python + pyelftools

As tabelas DWARF do executável guardam, para cada unidade de compilação, a sequência de endereços e as respectivas linhas do código-fonte. Para automatizar o mapeamento de endereços, um script curto em Python consulta esse material. Esse procedimento economiza tempo quando coleto crashes em lote ou quando quero integrar a análise em pipelines de CI.

#!/usr/bin/env python3

import os
import sys
from elftools.elf.elffile import ELFFile

def lookup(addr, path):
    with open(path, "rb") as f:
        elf = ELFFile(f)
        dwarf = elf.get_dwarf_info()
        for cu in dwarf.iter_CUs():
            lineprog = dwarf.line_program_for_CU(cu)
            prev = None
            for entry in lineprog.get_entries():
                if entry.state is None:
                    continue
                state = entry.state
                if state.end_sequence:
                    prev = None
                    continue
                if prev and prev.address <= addr < state.address:
                    file_entry = lineprog["file_entry"][prev.file - 1]
                    comp_dir_attr = cu.get_top_DIE().attributes.get("DW_AT_comp_dir")
                    comp_dir = ""
                    if comp_dir_attr:
                        comp_dir = comp_dir_attr.value.decode()
                    directory = comp_dir
                    if file_entry.dir_index not in (0, None):
                        include_dirs = lineprog["include_directory"]
                        directory = os.path.join(
                            comp_dir,
                            include_dirs[file_entry.dir_index - 1].decode(),
                        )
                    filename = file_entry.name.decode()
                    full_path = os.path.join(directory, filename) if directory else filename
                    return full_path, prev.line
                prev = state
            if prev and prev.address <= addr:
                file_entry = lineprog["file_entry"][prev.file - 1]
                comp_dir_attr = cu.get_top_DIE().attributes.get("DW_AT_comp_dir")
                comp_dir = comp_dir_attr.value.decode() if comp_dir_attr else ""
                directory = comp_dir
                if file_entry.dir_index not in (0, None):
                    include_dirs = lineprog["include_directory"]
                    directory = os.path.join(
                        comp_dir,
                        include_dirs[file_entry.dir_index - 1].decode(),
                    )
                filename = file_entry.name.decode()
                full_path = os.path.join(directory, filename) if directory else filename
                return full_path, prev.line
    return None, None

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print(f"Uso: {sys.argv[0]} <ELF> <endereco_hex>", file=sys.stderr)
        sys.exit(1)
    binary, raw_addr = sys.argv[1], sys.argv[2]
    address = int(raw_addr, 16)
    src, line = lookup(address, binary)
    if src is None:
        print("Endereço não encontrado.")
        sys.exit(2)
    print(f"{src}:{line}")

Execute assim:

./resolve_addr.py use_after_free 0x401168

Saída:

/path/para/use_after_free.c:16

Integre o script ao seu fluxo de crash dumps no meniOS: basta salvar o endereço de rip e, depois, usar a ferramenta para apontar diretamente para a linha culpada.

6. Confirme a correção

Depois de identificar a origem, ajuste o código para evitar o uso após free. Um fix simples é atrasar a chamada de free ou zerar o ponteiro antes de qualquer acesso:

printf("Dereferencing freed pointer...\n");
ptr[0] = 'A';
printf("Value: %c\n", ptr[0]);
free(ptr);

Recompile, execute e confirme que o crash desapareceu. Aproveite para rodar o binário antigo novamente e validar que seu processo de depuração continua funcionando.

Dicas extras

Garanta que o console serial do meniOS esteja ligado durante os testes. Como o sistema roda hoje em emuladores QEMU ou Bochs, redireciono a porta serial primária para com1.log (ou com1bochs.log, no caso do Bochs); esse console funciona como uma janela de logs do kernel, exibindo tudo o que o sistema imprime por kprintf, incluindo o endereço de instrução, o PID e a syscall ativa no momento do crash; como cada serviço roda em um executável ELF independente, repita o procedimento com o binário correspondente na imagem do meniOS; se quiser detectar problemas dessa classe automaticamente em paralelo no host, compile versões instrumentadas com AddressSanitizer e execute-as no Linux ou no macOS, onde o runtime já está disponível.

Seguindo esses passos você transforma um crash misterioso em uma linha específica do código-fonte.

Achei importante documentar isso aqui para, caso eu pare novamente de brincar com isso, ao voltar eu consiga me virar com certa rapidez. E também para que você, leitor(a) se sinta convidado(a) a participar desse desenvolvimento.


This content originally appeared on DEV Community and was authored by Plínio Balduino


Print Share Comment Cite Upload Translate Updates
APA

Plínio Balduino | Sciencx (2025-10-14T22:32:52+00:00) Depurando um crash de use-after-free no meniOS. Retrieved from https://www.scien.cx/2025/10/14/depurando-um-crash-de-use-after-free-no-menios/

MLA
" » Depurando um crash de use-after-free no meniOS." Plínio Balduino | Sciencx - Tuesday October 14, 2025, https://www.scien.cx/2025/10/14/depurando-um-crash-de-use-after-free-no-menios/
HARVARD
Plínio Balduino | Sciencx Tuesday October 14, 2025 » Depurando um crash de use-after-free no meniOS., viewed ,<https://www.scien.cx/2025/10/14/depurando-um-crash-de-use-after-free-no-menios/>
VANCOUVER
Plínio Balduino | Sciencx - » Depurando um crash de use-after-free no meniOS. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/10/14/depurando-um-crash-de-use-after-free-no-menios/
CHICAGO
" » Depurando um crash de use-after-free no meniOS." Plínio Balduino | Sciencx - Accessed . https://www.scien.cx/2025/10/14/depurando-um-crash-de-use-after-free-no-menios/
IEEE
" » Depurando um crash de use-after-free no meniOS." Plínio Balduino | Sciencx [Online]. Available: https://www.scien.cx/2025/10/14/depurando-um-crash-de-use-after-free-no-menios/. [Accessed: ]
rf:citation
» Depurando um crash de use-after-free no meniOS | Plínio Balduino | Sciencx | https://www.scien.cx/2025/10/14/depurando-um-crash-de-use-after-free-no-menios/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.