Depurar la memoria slab: redzones, poison y kmemleak
Los bugs de memoria son silenciosos hasta que los haces gritar. SLUB_DEBUG con redzones y poison para cazar desbordamientos y use-after-free, SLAB_STORE_USER para las pilas de asignación, kmemleak para las fugas, y leer /proc/slabinfo, slabtop y /sys/kernel/slab. KASAN y KFENCE como sucesores.
Un desbordamiento de un byte o un use-after-free pueden vivir latentes durante meses, corrompiendo memoria ajena sin dar la cara, hasta que un día tumban una máquina lejos del lugar del crimen. La depuración slab existe para lo contrario: hacer que esos errores griten en el instante y en el sitio exactos en que ocurren. Redzones, poison, rastreo de pilas y detección de fugas convierten un Heisenbug en un informe reproducible.
- Activar la depuración SLUB por arranque y por caché.
- Leer redzones y poison para cazar desbordamientos y use-after-free.
- Encontrar fugas de memoria con
kmemleak. - Interpretar
/proc/slabinfo,slabtopy/sys/kernel/slab.
SLUB_DEBUG: redzones, poison y rastreo
CONFIG_SLUB_DEBUG viene compilado por defecto y no cuesta nada hasta que lo activas en tiempo de ejecución. Se enciende por línea de comandos del kernel, para todas las cachés o para una sola:
# todas las caches: consistencia, redzone, poison y rastreo de usuario
slub_debug=FZPU
# solo la cache 'conexion' (ademas desactiva su fusion)
slub_debug=FZPU,conexion
Cada letra activa un mecanismo: F comprobaciones de consistencia, Z redzones, P poison, U guardar las pilas de asignación y liberación, T trazar cada operación. En kernels recientes el parámetro también se llama slab_debug=, unificado al desaparecer SLAB; slub_debug= sigue funcionando como alias. Muchas de estas opciones se pueden alternar en caliente escribiendo en /sys/kernel/slab/<cache>/.
Escribir slub_debug a secas, sin letras, enciende un conjunto por defecto en todas las cachés; una letra en minúscula desactiva su mecanismo. Y como la depuración cambia la disposición de los objetos —inserta redzones y cabeceras de rastreo alrededor de cada uno—, obliga a desactivar la fusión de las cachés afectadas: por eso una caché que en producción se fundiría con kmalloc-64 aparece con su nombre propio en cuanto la depuras, lo que de paso la vuelve visible y aislable.
Los patrones no son arbitrarios: son valores reconocibles de un vistazo en un volcado hexadecimal.
Redzone
Bytes 0xbb (inactivo) y 0xcc (activo) rodeando el objeto. Si cambian, hubo una escritura fuera de límites.
Poison
0x6b relleno tras liberar, 0x5a en uso, 0xa5 el último byte. Leer ‘kkkk’ es un use-after-free.
Store user
Con U, cada objeto guarda la pila de quién lo asignó y quién lo liberó. El informe te lleva al culpable.
De las cuatro, U es la más valiosa para depurar y la más cara en memoria: añade a cada objeto dos registros de pila, el de quién lo asignó y el de quién lo liberó. Sin ella sabes que hubo corrupción; con ella sabes quién tocó ese objeto y cuándo, que suele ser la mitad del camino hasta la causa raíz.
flowchart LR RZ1[redzone 0xbb antes] --> OBJ[objeto o poison 0x6b si libre] OBJ --> RZ2[redzone 0xcc despues] RZ2 --> TRK[track alloc y free con U] style RZ1 fill:#f38ba8,color:#11111b style RZ2 fill:#f38ba8,color:#11111b style OBJ fill:#f9e2af,color:#11111b
Cuando algo rompe una redzone o toca un objeto envenenado, SLUB imprime en dmesg un informe que señala el objeto, el desplazamiento y —con U— las pilas culpables:
=============================================================================
BUG conexion (Tainted: G B): Redzone overwritten
-----------------------------------------------------------------------------
INFO: 0xffff888103a4c840-0xffff888103a4c847 @offset=64 size=8
INFO: Allocated in conn_open+0x3a/0xc0 age=42 cpu=1 pid=1043
INFO: Freed in conn_release+0x18/0x60 age=12 cpu=3 pid=1200
Object ffff888103a4c800: 6b 6b 6b 6b 6b 6b 6b 6b kkkkkkkk
Redzone ffff888103a4c840: cc cc cc 00 00 00 00 00 ........
Léelo de arriba abajo. La cabecera nombra la caché (conexion) y la clase de fallo. La línea @offset=64 size=8 localiza los ocho bytes exactos que se corrompieron dentro del objeto; age cuenta los jiffies desde la asignación o la liberación, y cpu con pid sitúan el contexto. Debajo va el volcado hexadecimal del cuerpo y de la redzone, con la columna ASCII a la derecha donde los patrones cantan.
La cc cc cc 00 de la redzone delata tres bytes intactos y luego una escritura que se salió del objeto por el final; el 6b 6b del cuerpo, que se liberó y alguien lo leyó después. Las líneas Allocated in y Freed in te dan las dos pilas: el diagnóstico casi se escribe solo.
No siempre hace falta reiniciar para acotar el foco. Con una caché ya bajo depuración, sus ficheros en /sys/kernel/slab/conexion/ dejan leer —y a veces ajustar— el comportamiento: red_zone, poison, store_user, sanity_checks, trace. Y escribir en validate fuerza un barrido inmediato de todas sus slabs comprobando redzones y freelist de golpe, útil para sorprender una corrupción latente en el momento que tú elijas, y no cuando el azar la destape.
slub_debug cambia el tamaño y la disposición de cada objeto y ralentiza cada asignación. Eso tiene dos consecuencias: algunos bugs sensibles al timing o a la disposición exacta de memoria pueden esfumarse al activarlo —el clásico Heisenbug—, y jamás debes medir rendimiento con la depuración puesta. Actívala para cazar, reproduce en una VM, y quítala para medir.
kmemleak: cazar fugas
Redzones y poison cazan corrupción; las fugas son otra bestia. CONFIG_DEBUG_KMEMLEAK implementa un detector conservador de marcado y barrido, un recolector de basura de mentira: rastrea toda asignación de kmalloc, kmem_cache_alloc y vmalloc, escanea la memoria del kernel en busca de punteros que las alcancen, y lo que queda inalcanzable es un candidato a fuga.
# forzar un escaneo y leer el informe de sospechosos
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak
# limpiar la lista actual de sospechosos (tras revisarlos)
echo clear > /sys/kernel/debug/kmemleak
Cada entrada del informe trae el tamaño, la CPU, el PID y la pila de la asignación fugada, que suele apuntar directo a la línea que olvidó su kfree. Es conservador: como escanea sin saber qué es un puntero y qué no, puede dar falsos positivos si guardas una dirección de forma rara o desplazada. Para esos casos hay anotaciones: kmemleak_not_leak() marca un objeto como vivo aunque no se le vea referencia, y kmemleak_ignore() lo excluye del rastreo. Su coste en tiempo y memoria es alto, así que es una herramienta de kernel de laboratorio, no de producción.
Por dentro, kmemleak guarda cada asignación en un árbol rojo-negro indexado por su rango de direcciones, junto con la pila que la creó. En cada escaneo recorre la memoria del kernel tratando cada palabra como un posible puntero: si cae dentro de un rango registrado, marca ese objeto como alcanzable. Lo que sobrevive sin marca a dos escaneos consecutivos se reporta como sospechoso. Arranca con el parámetro kmemleak=on, y su primer barrido automático llega minutos después del arranque; los siguientes los disparas tú con scan.
/proc/slabinfo, slabtop y sysfs
Para ver el estado agregado de las cachés en vivo, tres ventanas. slabtop da una foto ordenada por uso, como un top de slabs:
sudo slabtop -o # una instantanea ordenada por consumo
sudo head -3 /proc/slabinfo
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : ...
conexion 1280 1408 64 64 1 : slabdata ...
Las columnas dicen objetos activos y totales, tamaño de objeto, objetos por slab y páginas por slab; las columnas de tunables que SLAB usaba salen a cero bajo SLUB, que no las tiene. Para el detalle por caché está /sys/kernel/slab/<cache>/: ficheros como objects, slabs, object_size, order, partial, cpu_slabs, y —con U activado— alloc_calls y free_calls con las pilas agregadas. La herramienta tools/mm/slabinfo.c del árbol parsea todo eso; slabinfo -a revela qué cachés se han fusionado en una sola.
Para husmear una fuga sin kmemleak, /proc/slabinfo ya da pistas: si los active_objs de una caché crecen de forma monótona y no bajan nunca, alguien asigna y no libera. slabtop -s c ordena por consumo de memoria y sube arriba a la caché que más engorda; vigilar su tendencia en el tiempo es un diagnóstico de primer nivel antes de sacar la artillería pesada de KASAN o kmemleak.
Y si arrancaste con U, los ficheros alloc_calls y free_calls en el directorio de la caché bajo /sys/kernel/slab agregan las pilas con un contador por sitio de llamada: una sola línea te dice qué función pidió la mayoría de los objetos vivos. Es la vía más directa para atribuir consumo de memoria a código concreto sin instrumentar nada más.
SLUB_DEBUG es la primera línea, sin dependencias y siempre disponible. Por encima hay dos herederos. KASAN (CONFIG_KASAN) instrumenta cada acceso a memoria con el compilador y una shadow memory, y caza el desbordamiento o el use-after-free en la instrucción exacta, con pila completa: es el estándar de oro para pruebas, a cambio de un coste de memoria y CPU considerable. KFENCE (CONFIG_KFENCE) es un detector por muestreo, tan barato que puede correr en producción: intercepta una fracción minúscula de asignaciones y las rodea de páginas guarda. Redzones de SLUB para el día a día, KASAN para la caza fina, KFENCE para el campo.
En la práctica, estos mecanismos se encadenan en un flujo. Ante una corrupción intermitente: arranca la VM con slub_debug=FZPU y reproduce; si el redzone o el poison saltan, las pilas de Allocated in y Freed in suelen bastar para el diagnóstico. Si no reproduces con SLUB_DEBUG pero sospechas un acceso fino fuera de límites, sube a un kernel con CONFIG_KASAN, que instrumenta cada carga y cada almacenamiento. Y si el síntoma es memoria que solo crece, kmemleak y la tendencia de /proc/slabinfo apuntan a la asignación huérfana. Son tres redes de distinta malla para tres formas del mismo pez.
SLUB_DEBUG no tiene dependencias y apenas cuesta hasta que lo enciendes: que sea siempre tu primer intento. KASAN es preciso pero pesado en memoria y CPU; resérvalo para cuando SLUB_DEBUG no reproduzca el fallo. KFENCE es tan barato que puede vivir en producción muestreando, la red que atrapa lo que se escapó del laboratorio. Escalar de menos a más intrusivo ahorra horas de máquina y de espera.
Toda la depuración slab persigue una sola idea, y es profunda: transformar errores no deterministas y diferidos en fallos deterministas e inmediatos. Un use-after-free en un kernel normal no hace nada visible hasta que, mucho después y en otro subsistema, esa memoria reutilizada produce corrupción incoherente imposible de rastrear hasta su origen. El poison 0x6b lo vuelve determinista: leer un objeto liberado devuelve al instante un patrón inconfundible, y si además intentas usarlo como puntero, saltas a 0x6b6b6b6b y el crash apunta al lugar del crimen. La redzone hace lo mismo con los desbordamientos: convierte un byte que se salió —que quizá cae en padding inocuo y nunca se nota— en una violación detectada y reportada. kmemleak hace deterministas las fugas, que por naturaleza no producen ningún síntoma inmediato. El patrón común es sembrar la memoria con valores conocidos e imposibles, de modo que cualquier uso indebido choque contra ellos y se delate. No hay observabilidad que rescate un bug que no da síntomas; la única salida es diseñar el sistema para que el error se manifieste al ocurrir, no meses después. Esa inversión —de rezar por reproducir a garantizar que el fallo grite— es la misma que viste en lockdep con la concurrencia, y es la marca de la ingeniería de sistemas madura.
- Arranca un kernel con
slub_debug=FZPUen QEMU y confirma endmesgque la depuración está activa. - Escribe un módulo que desborde un objeto por un byte y localiza en el informe la redzone alterada y su desplazamiento.
- Provoca un use-after-free y reconoce el patrón 0x6b en el volcado; explica por qué delata el error.
- Fuga memoria a propósito, fuerza un
echo scanenkmemleaky encuentra la pila de la asignación fugada. - Lee
/proc/slabinfoyslabtop, identifica tu caché y contrasta sus contadores con/sys/kernel/slab.