LD_PRELOAD, interposición de funciones y parcheo de Thunderbird

En mi artículo "fsync" y LD_PRELOAD explico cómo utilizar LD_PRELOAD para interceptar las llamadas a una función determinada cuando esta función reside en una biblioteca dinámica (una de las muchísimas ventajas de emplear bibliotecas dinámicas).

Lo que no expliqué es cómo poder llamar a la función original. Esto puede ser necesario cuando lo que queremos es extender la funcionalidad original, no reemplazarla por completo.

En pocas palabras, queremos que el programa llame a nuestra función pero que nuestra función pueda llamar a la función original.

Simplificando mucho podríamos decir que las bibliotecas dinámicas que se van cargando forman un árbol de dependencias y de resolución de nombres. El orden en el que se resuelven los símbolos viene dado por la posición en el árbol. Al emplear LD_PRELOAD colocamos una biblioteca propia al principio de este árbol y sus símbolos serán los primeros que se encuentren. Pero queremos que las funciones de esa biblioteca busquen los símbolos en el resto del árbol, el fragmento que está detrás.

Veamos un ejemplo. El bug 541130 de Mozilla Thunderbird:

El bug es el siguiente: si usas una versión antigua de GNOME y visualizas un mensaje en Thunderbird en el que aparezca texto del estilo GMT+02:00 (típico en los correos electrónicos de muchos dispositivos móviles), nos aparecerá una ventana pop-up indicando que ha ocurrido un error. Pulsamos sobre el botón de OK o pulsamos la tecla ESC y veremos el mensaje con normalidad. Un bug inocuo, pero molesto.

El bug 541130 explica de forma bastante clara cuál es el problema, su causa y su solución.

Una solución de compromiso si no podemos actualizar GNOME (ver los detalles en el bug 541130) es usar LD_PRELOAD y técnicas de interposición para alterar el comportamiento de la biblioteca problemática:

 /*
 ** https://bugzilla.mozilla.org/show_bug.cgi?id=541130
 ** (c) 2015 jcea@jcea.es
 **
 ** Compile as:
 ** gcc -o patch_thunderbird.so -shared -fPIC \
 **        patch_thunderbird.c /usr/lib/libgconf-2.so.4
 **
 ** Alternative:
 ** http://git.auf.org/?p=auf-poste-client.git;a=blob;f=lucid/auf-thunderbird-hack/auf-thunderbird-hack.c
 */

 #define _GNU_SOURCE
 #include <string.h>
 #include <dlfcn.h>

 char* gconf_client_get_string(void* client, const char* key, void** err)
 {
     static char* (*original_gconf_client_get_string)(void*, const char*,
             void**) = NULL;

     if (strchr(key, '+')) return NULL;

     if (!original_gconf_client_get_string) {
         original_gconf_client_get_string = dlsym(RTLD_NEXT,
                 "gconf_client_get_string");
     }

     return original_gconf_client_get_string(client, key, err);
 }

Este código intercepta la función gconf_client_get_string de la biblioteca gconf. Especificamos _GNU_SOURCE en la línea 13 para que se defina el símbolo RTLD_NEXT al incluir la cabecera dlfcn.h. Si la clave que pide Thunderbird contiene un carácter + regresamos (línea 22). Esta es la funcionalidad nueva de nuestra rutina, la que soluciona el bug (al menos en cuanto a su impacto en Thunderbird). Si no es así, queremos llamar a la función original. Si no la hemos llamado nunca aún (línea 24), la buscamos (línea 25-26). Una vez que la tenemos, la llamamos (línea 29).

Obsérvese el uso de dlsym() y RTLD_NEXT para localizar el símbolo en el árbol de resolución tras la biblioteca actual.

Compilamos la biblioteca como:

$ gcc -o patch_thunderbird.so -shared -fPIC \
         patch_thunderbird.c /usr/lib/libgconf-2.so.4

Nótese que hemos añadido la dependencia /usr/lib/libgconf-2.so.4. Esto hace que dicha biblioteca (y sus propias dependencias) formen un árbol de resolución tras nuestra biblioteca. Si no, no encontraría el símbolo que queremos [1].

Lanzamos Thunderbird con el siguiente script:

 #!/bin/sh

 eval $(gpg-agent --daemon)

 export LC_TIME=es_ES.utf-8
 [ "$LC_ALL" != "$LC_TIME" ] && unset LC_ALL

 export LD_PRELOAD=/home/jcea/patch_thunderbird.so

 exec /home/jcea/thunderbird/thunderbird "$@"

Como puede verse, tengo mi sistema muy, muy, muy personalizado. Hasta tengo mi zona horaria propia :-p.

Por supuesto estas técnicas funcionan en cualquier Unix, no es algo exclusivo de Linux ni mucho menos. Es una práctica estándar.

[1]

Obsérvese que empleamos la función dlsym() directamente y nos colocamos en el lugar correcto del árbol de resolución añadiendo la referencia a la hora de compilar la biblioteca. Una alternativa es no modificar el árbol de resolución, sino buscar el símbolo donde sea que esté. A mí me parece un enfoque más mágico y que no permite cosas como ver las dependencias de la biblioteca ya compilada mediante el comando ldd:

jcea@ubuntu:~$ ldd patch_thunderbird.so
    linux-vdso.so.1 =>  (0x00007fff05066000)
    libgconf-2.so.4 => /usr/lib/libgconf-2.so.4 (0x00007f64a02cf000)
    libc.so.6 => /lib/libc.so.6 (0x00007f649ff46000)
    libgmodule-2.0.so.0 => /usr/lib/libgmodule-2.0.so.0 (0x00007f649fd41000)
    libORBit-2.so.0 => /usr/lib/libORBit-2.so.0 (0x00007f649fad3000)
    libdbus-glib-1.so.2 => /usr/lib/libdbus-glib-1.so.2 (0x00007f649f8b0000)
    libdbus-1.so.3 => /lib/libdbus-1.so.3 (0x00007f649f670000)
    libpthread.so.0 => /lib/libpthread.so.0 (0x00007f649f453000)
    libgobject-2.0.so.0 => /usr/lib/libgobject-2.0.so.0 (0x00007f649f20b000)
    libgthread-2.0.so.0 => /usr/lib/libgthread-2.0.so.0 (0x00007f649f005000)
    librt.so.1 => /lib/librt.so.1 (0x00007f649edfd000)
    libglib-2.0.so.0 => /lib/libglib-2.0.so.0 (0x00007f649eb1f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f64a0741000)
    libdl.so.2 => /lib/libdl.so.2 (0x00007f649e91a000)
    libpcre.so.3 => /lib/libpcre.so.3 (0x00007f649e6ec000)

Todo ese montón de dependencias es el árbol de resolución que colocamos tras nuestra biblioteca por el hecho de compilar con la dependencia explícita de /usr/lib/libgconf-2.so.4. Una ventaja es que si actualizamos el sistema y nos desaparece esa versión de la biblioteca, tendremos un error automático, explícito y claro.

El enfoque alternativo tiene otras ventajas, como poder saltar a puntos arbitrarios en el árbol de resolución.