Saltar a contenido

Última actualización:2026-10-12

Investigación práctica de fugas de memoria en C++

Al investigar un problema de memoria en un kernel de base de datos (familia Greenplum/PostgreSQL), me encontré con una cadena de WARNING muy clásica: problem in alloc set ExecutorState: bad single-chunk. El significado de este registro: el pool de memoria (alloc set) descubrió, al validar, que la cabecera de un chunk estaba corrupta — nueve de cada diez veces, algún código escribió fuera de rango y pisó los metadatos del chunk vecino.

La dificultad de este tipo de problemas: el lugar donde salta el error no es el lugar del fallo. El WARNING se imprime cuando la memoria se libera y se valida, pero la escritura fuera de rango pudo ocurrir en cualquier trozo de código mucho antes. Abajo dejo el proceso completo de aquella investigación, para ver juntos cómo recuperar la «escena del crimen» con gdb.

Primero, leer la información de la escena:

regression=# SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_e t1, prt2_e t2 WHERE (t1.a + t1.b)/2 = (t2.b + t2.a)/2 AND t1.c = 0 ORDER BY t1.a, t2.b;
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x7cfdd48 in block 0x7cfc580  (seg1 slice1 127.0.1.1:7003 pid=4295)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x7cfc580  (seg1 slice1 127.0.1.1:7003 pid=4295)
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x7cfdd48 in block 0x7cfc580  (seg1 slice1 127.0.1.1:7003 pid=4295)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x7cfc580  (seg1 slice1 127.0.1.1:7003 pid=4295)
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x87f85c8 in block 0x87f6e00  (seg0 slice1 127.0.1.1:7002 pid=4294)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x87f6e00  (seg0 slice1 127.0.1.1:7002 pid=4294)
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x7d3c5c8 in block 0x7d3ae00  (seg2 slice1 127.0.1.1:7004 pid=4296)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x7d3ae00  (seg2 slice1 127.0.1.1:7004 pid=4296)
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x87f85c8 in block 0x87f6e00  (seg0 slice1 127.0.1.1:7002 pid=4294)
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x7d3c5c8 in block 0x7d3ae00  (seg2 slice1 127.0.1.1:7004 pid=4296)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x87f6e00  (seg0 slice1 127.0.1.1:7002 pid=4294)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x7d3ae00  (seg2 slice1 127.0.1.1:7004 pid=4296)

De los WARNING se extraen varios datos clave:

(1) la dirección del chunk corrupto es conocida: 0x7cfdd48 (bad single-chunk la señala);

(2) el block que lo contiene es 0x7cfc580, y el «found inconsistent memory block» del mismo block indica que la validación en cadena de todo el block también falló;

(3) que varios segmentos (seg0/seg1/seg2) lo reporten a la vez descarta las fluctuaciones del entorno: es un bug lógico determinista.

Con una dirección exacta en la mano, el plan es muy directo: poner un breakpoint condicional en el asignador de memoria (AllocSetAlloc de aset.c) para interceptar la llamada que «devuelve esta dirección al asignar», y luego poner un watchpoint por hardware sobre esa dirección: quien la escriba es el culpable.

La sesión de gdb:

Depuración:

(gdb) b aset.c:1109 if chunk=0x7cfdd48
Breakpoint 1 at 0x105d420: file aset.c, line 1109.
(gdb) c
Continuing.

Thread 1 "postgres" hit Breakpoint 1, AllocSetAlloc (context=0x787ac50, size=4096) at aset.c:1109
1109        block->freeptr += (chunk_size + ALLOC_CHUNKHDRSZ);
(gdb) p chunk
$1 = (AllocChunk) 0x7cfdd48
(gdb) p chunk->size
$2 = 9187201950435737471
(gdb) p &chunk->size
$3 = (Size *) 0x7cfdd48
(gdb) watch (*((Size *) 0x7cfdd48))
Hardware watchpoint 2: (*((Size *) 0x7cfdd48))
(gdb) i b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000105d420 in AllocSetAlloc at aset.c:1109
    stop only if chunk=0x7cfdd48
    breakpoint already hit 1 time
2       hw watchpoint  keep y                      (*((Size *) 0x7cfdd48))
(gdb) d 1
(gdb) c

Retrospectiva

El guion de este caso se traslada tal cual a la investigación de corrupción de memoria en cualquier proyecto C/C++:

(1) primero extraer del registro de errores la dirección exacta: es el único ancla fiable;

(2) el breakpoint condicional b aset.c:1109 if chunk=0x7cfdd48 atrapa el momento de la asignación y confirma a quién fue a parar esta dirección;

(3) watch (*(Size*)0x7cfdd48), un watchpoint por hardware, espera a que aparezca el culpable — los breakpoints por hardware no reescriben memoria ni afectan al comportamiento del programa, y son lo más fiable para los problemas de corrupción de memoria;

(4) una vez en ejecución, la pila de llamadas en el momento del hit del watchpoint es la escena completa de la escritura fuera de rango.

Cabe señalar que en el primer paso p chunk->size imprimió 9187201950435737471 (0x7F7F7F7F7F7F7F7F) — un montón de 0x7F, la firma típica de memoria sobrescrita: el código que pisó la memoria estaba escribiendo un puñado de flags o valores de relleno. Buscar ese valor globalmente en el código muchas veces encuentra al culpable directamente.

Dejo una pregunta: si la escritura fuera de rango ocurre en un proceso hijo nacido de un fork y tú depuras el proceso padre, ¿cómo habría que ajustar el flujo anterior? Bienvenido a discutirlo en los comentarios.

Temas relacionados

Comentarios