BookinglyTech News
Software

Un CALL que apunta a la nada: crónica de un crash de Wine con MinGW-w64

Un programa compilado con MinGW-w64 se caía en Wine al llamar a png_read_info. El rastro acabó en el runtime de pseudo-relocaciones del propio toolchain reescribiendo una llamada dentro de libpng.

3 min de lecturaLobsters0 vistas

El 3 de diciembre de 2021 un conocido pidió ayuda: su programa se caía en Wine justo al llegar a png_read_info, de libpng. El binario salía del toolchain MinGW-w64 de MSys2 con GCC 10.3, y llevaba libpng y zlib enlazadas como DLL. No había ningún motivo evidente para que aquello fallara. La crónica de la depuración se publicó meses después, y el culpable no era ninguna función sin implementar en Wine.

Lo primero fue lanzarlo con WINEDEBUG=+all y generar un log enorme del que casi todo sobraba. Correlacionando líneas, la última llamada de la API antes del golpe era un msvcrt._read que devolvía sin error. Y entonces, el crash. El detalle que se había pasado por alto durante un rato: la violación de acceso era de ejecución, no de lectura ni escritura. Es decir, RIP aterrizaba en una página no ejecutable. Tiene pinta de corrupción de pila, así que tocaba descartarlo.

Wine se puede ejecutar bajo Valgrind con algunos flags y funciona más o menos bien. No apareció nada, y menos algo que sugiriera corrupción de pila. El siguiente paso fue rr, el depurador que graba y reproduce la ejecución. Con Wine encima es incómodo de arrancar y los símbolos de depuración nunca llegaron a mapearse, así que hubo que recorrer el espacio de direcciones a mano. El programa se estrellaba en 0x2'fe8f'2910, donde no había nada mapeado. Retrocediendo instrucción a instrucción con rr, en 0x3'6810'e3eb apareció el byte e8 20 45 7e 96: un CALL a esa dirección vacía. Al abrir el mismo punto en un desensamblador, la instrucción era e8 78 9e 03 00, una llamada a crc32. Alguien estaba cambiando la llamada en tiempo de ejecución.

El segmento de código no se toca, salvo que alguien lo marque

.text es de solo lectura. Para escribirlo hay que pedirlo explícitamente: mprotect en plataformas tipo UNIX, VirtualProtect de kernel32 en Windows. Y ahí estaba la pista: libpng llamaba a VirtualProtect. El rastro llevaba a sub_3680F1200, que no es más que el punto de entrada de la DLL, y cerca aparecía una cadena reveladora: "Unknown pseudo relocation bit size %d".

Para entender qué pinta ahí un mecanismo de reubicación hay que recordar cómo enlaza el linker. Al construir el módulo se elige una dirección base y los punteros estáticos se escriben con esa base. Si el cargador coloca el módulo en otro sitio, esos valores dejan de cuadrar y hay que corregirlos. Con ASLR eso pasa casi siempre. MinGW-w64 va un paso más allá con las pseudo-relocaciones, que el runtime aplica al cargar y que permiten parchear posiciones que en teoría no deberían tocarse. El código que las procesa vive en pseudo-reloc.c, dentro del CRT.

La historia es de 2021 y se publicó con retraso, pero le sigue tocando a cualquiera que empaquete DLL para Windows y las ejecute bajo Wine o sobre el toolchain de MSys2: un fallo que solo se manifiesta al cargar el módulo, en una instrucción que el desensamblador muestra correcta y que en memoria ya no lo está, es de los que se comen una tarde entera. Saber que el runtime de MinGW-w64 escribe sobre .text durante la carga ahorra bastante de ese tiempo.