Spinlocks no eBPF e a arte de depurar o kernel
Como bugs de spinlock no eBPF aparecem no kernel Linux, por que são difíceis de encontrar e o que o processo de depuração revela sobre o modelo de segurança do eBPF.

Um bug de spinlock no eBPF não se anuncia com um stack trace ou uma mensagem de erro. Ele se anuncia com uma máquina que para de responder. Sem logs, sem avisos, sem degradação graciosa: apenas um núcleo de CPU girando para sempre em um lock que nunca será liberado. Se você tiver sorte, o watchdog dispara e você recebe um kernel panic com um trace. Se não tiver, a máquina simplesmente trava e você precisa desligá-la e ligá-la de novo na força bruta.
O eBPF deveria ser seguro. Ele roda no kernel, mas passa por um verifier que comprova que os programas terminam, não acessam memória inválida e não corrompem o estado do kernel. Então como bugs de spinlock passam por ali? Porque o verifier analisa programas individualmente, enquanto bugs de spinlock surgem da interação entre programas, mapas e as decisões de escalonamento do kernel, coisas que nenhum verifier estático consegue prever por completo.
Para que servem os spinlocks no eBPF
Programas eBPF rodam em contexto de kernel, frequentemente em várias CPUs ao mesmo tempo. Quando dois programas eBPF precisam atualizar o mesmo valor em um mapa, seja um contador, uma estrutura de dados ou uma máquina de estados, eles precisam de sincronização. Para isso o eBPF oferece bpf_spin_lock e bpf_spin_unlock.
Um spinlock é o lock mais simples que existe: tenta adquirir e, se estiver ocupado, fica girando em um loop apertado até ser liberado. Sem dormir, sem ceder a vez, sem fila. Isso faz sentido no eBPF porque os programas rodam em contextos onde dormir é proibido: handlers de interrupção, softirqs, seções com preempção desabilitada. Um mutex que colocasse o chamador para dormir derrubaria o kernel. O spinlock apenas queima ciclos de CPU até o lock ficar livre.
struct map_value {
struct bpf_spin_lock lock;
__u64 counter;
__u32 last_pid;
};
SEC("tp/sched/sched_switch")
int handle_sched_switch(struct trace_event_raw_sched_switch *ctx) {
__u32 key = 0;
struct map_value *val;
val = bpf_map_lookup_elem(&my_map, &key);
if (!val)
return 0;
bpf_spin_lock(&val->lock);
val->counter++;
val->last_pid = ctx->next_pid;
bpf_spin_unlock(&val->lock);
return 0;
}
Parece direto. E, para casos simples como esse (lock, atualiza um valor, unlock), funciona bem. Os problemas começam quando os padrões de locking ficam mais complexos, ou quando a interação entre programas eBPF e o próprio sistema de locks do kernel cria dependências inesperadas.
Como os bugs de spinlock se manifestam
O bug clássico de spinlock é o deadlock: o programa A segura o lock 1 e tenta pegar o lock 2, enquanto o programa B segura o lock 2 e tenta pegar o lock 1. Os dois ficam girando para sempre, esperando um pelo outro. Em contexto de kernel, isso significa que esses núcleos de CPU estão perdidos: nunca vão executar mais nada.
O verifier do eBPF previne alguns desses casos. Ele rejeita programas que seguram mais de um spinlock ao mesmo tempo, o que elimina o deadlock AB-BA clássico. Mas não consegue impedir todos os cenários de deadlock, porque alguns surgem da relação entre spinlocks do eBPF e os locks próprios do kernel.
Considere este cenário: um programa eBPF anexado a um tracepoint adquire um spinlock em um mapa. Enquanto o lock está seguro, uma interrupção de hardware dispara na mesma CPU. O handler da interrupção executa outro programa eBPF que tenta pegar o mesmo spinlock. Deadlock: a interrupção não pode retornar sem o lock, mas o lock não pode ser liberado até o programa interrompido continuar, o que não acontece antes da interrupção retornar.
Deadlock scenario (same CPU):
CPU 0 running eBPF program A
→ acquires spinlock on map_value
→ hardware interrupt fires
→ CPU 0 runs interrupt handler
→ interrupt handler runs eBPF program B
→ program B tries to acquire same spinlock
→ spinlock is held by program A
→ program B spins... forever
→ interrupt handler never returns
→ program A never resumes
→ program A never releases lock
→ CPU 0 is permanently stuck
Os spinlocks do próprio kernel resolvem isso desabilitando interrupções enquanto o lock está seguro (spin_lock_irqsave). Os spinlocks do eBPF fazem algo parecido: bpf_spin_lock desabilita a preempção e, em kernels 5.1+, desabilita softirqs. Mas nem todos os contextos de interrupção são cobertos, e nem todas as versões do kernel tratam isso de forma idêntica. É aí que moram os bugs sutis.
O problema da depuração
Depurar problemas de spinlock no eBPF é difícil por motivos específicos.
A reprodução é não determinística. Bugs de spinlock dependem de timing: qual CPU executa qual programa, quando as interrupções disparam, quanto tempo a seção crítica leva. Um teste que roda bem 999 vezes pode dar deadlock na milésima. Você não consegue reproduzir isso de forma confiável em desenvolvimento, porque sua máquina tem topologia de CPU, frequência de interrupções e padrões de carga diferentes da produção.
A falha destrói as evidências. Quando ocorre um deadlock de spinlock, as CPUs afetadas param de executar qualquer coisa, incluindo a infraestrutura de logs e monitoramento que diria o que aconteceu. Se o deadlock estiver em uma única CPU, a máquina pode continuar se arrastando. Se várias CPUs travarem (ou se a CPU presa segura outros recursos que outras CPUs precisam), a máquina inteira trava.
As ferramentas tradicionais de depuração não ajudam. Você não consegue anexar um debugger ao kernel para inspecionar o estado travado (bem, dá para usar o KGDB, mas precisa ter uma conexão serial configurada antes do deadlock). O dmesg é inútil porque ninguém está escrevendo nele. A única informação confiável vem da saída do lockup detector, se ele disparar antes de a máquina morrer de vez.
Estratégias que realmente funcionam
Apesar desses desafios, as pessoas encontram e corrigem bugs de spinlock no eBPF. Veja como.
Análise de ordem de locks. Antes de rodar qualquer coisa, analise o comportamento de locking de cada programa eBPF. Quais mapas cada programa acessa? Quais locks ele adquire? Dois programas que tocam o mesmo lock podem rodar na mesma CPU (mesmo tracepoint, ou tracepoint versus interrupção)? É uma análise manual e cansativa, mas pega os padrões de deadlock mais comuns antes que eles aconteçam.
Monitoramento do tempo de retenção do lock. Instrumente seus programas eBPF para medir por quanto tempo os spinlocks ficam seguros. Um spinlock retido por mais de alguns microssegundos é um sinal de alerta: aumenta a janela para deadlocks causados por interrupções e faz outras CPUs desperdiçarem ciclos girando. Seções críticas curtas são mais rápidas e mais seguras.
// Monitoring lock hold time in eBPF
SEC("tp/sched/sched_switch")
int handle_switch(void *ctx) {
struct map_value *val;
__u64 start, elapsed;
__u32 key = 0;
val = bpf_map_lookup_elem(&my_map, &key);
if (!val)
return 0;
start = bpf_ktime_get_ns();
bpf_spin_lock(&val->lock);
// Critical section — keep this minimal
val->counter++;
bpf_spin_unlock(&val->lock);
elapsed = bpf_ktime_get_ns() - start;
// Report if lock was held too long
if (elapsed > 1000) { // > 1 microsecond
bpf_printk("lock held for %llu ns", elapsed);
}
return 0;
}
Estruturas de dados por CPU. O melhor bug de spinlock é aquele que não pode acontecer. Se cada CPU trabalha na sua própria cópia dos dados e você agrega depois, não há contenção e não são necessários locks. Os tipos BPF_MAP_TYPE_PERCPU_HASH e BPF_MAP_TYPE_PERCPU_ARRAY foram feitos exatamente para isso. Contadores, histogramas e acumuladores quase nunca precisam de spinlocks de estado compartilhado.
Operações atômicas no lugar de locks. Para operações simples, como incrementar um contador ou comparar e trocar um valor, __sync_fetch_and_add e outras atômicas são mais rápidas e livres de deadlock. Você não precisa de spinlock para incrementar um contador. Reserve os spinlocks para operações que realmente precisam atualizar vários campos de forma atômica.
O que isso revela sobre o modelo de segurança do eBPF
A história de segurança do eBPF é impressionante, mas tem nuances. O verifier garante que programas individuais são seguros: eles terminam, não acessam memória inválida e não corrompem o estado do kernel. Mas programas 'seguros' podem se combinar em sistemas inseguros quando suas interações criam dependências de timing que o verifier não consegue analisar.
Essa é uma limitação fundamental da análise estática. O verifier olha cada programa isoladamente. Ele não sabe quais outros programas estão carregados, quais mapas eles compartilham ou em quais contextos de kernel rodam. Um programa que adquire um spinlock é 'seguro': ele adquire e libera corretamente. Mas se ele é seguro em combinação com todos os outros programas eBPF carregados depende de condições de runtime.
A comunidade do kernel vem lidando com isso aos poucos. Patches recentes adicionaram restrições sobre quais tipos de programa eBPF podem usar spinlocks, quanto tempo as seções críticas podem durar e quais operações são proibidas enquanto um spinlock está seguro. Cada restrição estreita a janela para bugs, mas também limita o que os programas eBPF podem fazer.
Regras práticas para locking no eBPF
Depois de depurar problemas de spinlock suficientes, surgem padrões para evitá-los.
- Prefira mapas por CPU. Se seus dados podem ser particionados por CPU e agregados no user space, faça isso. Sem locks, sem contenção, sem deadlocks. Isso resolve 80% dos casos de uso, como contadores, logs de eventos e histogramas.
- Prefira atômicas a spinlocks. Se você precisa de um contador compartilhado ou de um compare-and-swap, use operações atômicas. Elas são livres de lock e não podem dar deadlock.
- Se você precisar de spinlocks, mantenha as seções críticas minúsculas. Incremente um contador, atualize um timestamp, troque um ponteiro. Nada além disso. Nunca chame funções auxiliares (helpers) enquanto segura um lock, porque você não sabe que locking elas fazem por dentro.
- Nunca segure o mesmo lock a partir de dois tipos de programa eBPF. Se um programa kprobe e um programa de tracepoint travam o mesmo valor de mapa, você está a uma interrupção de distância de um deadlock. Use mapas separados ou dados por CPU.
- Teste sob carga. Bugs de spinlock dependem de timing. Eles aparecem sob contenção: alta utilização de CPU, interrupções frequentes, muitos programas eBPF rodando ao mesmo tempo. Teste com carga realista de produção, não em máquinas de desenvolvimento ociosas.
O eBPF mudou de forma profunda como interagimos com o kernel Linux, dando a programas em user space acesso seguro à observabilidade e à rede em nível de kernel. Mas a 'segurança' tem limites. O verifier torna o eBPF muito mais seguro do que escrever módulos de kernel na mão, e de forma dramática. Ainda assim, não o torna seguro da mesma forma que a programação em user space é segura. Entender onde estão esses limites, especialmente em torno de estado compartilhado e locking, é o que separa programas eBPF que funcionam em produção daqueles que só funcionam em testes.


