blog · git · desktop · images · contact & privacy · gopher


Shared Libraries updaten

Im Zuge der ganzen Heartbleed-Updaterei kam die Frage auf, ob man den Apache nach dem OpenSSL-Update neustarten muss. Ja, muss man. Aber warum eigentlich? Wenn zwei Prozesse eine Datei öffnen und beide darin rumschreiben, dann sehen sie gegenseitig ihre Änderungen. Wenn also nun der Apache die libssl.so geöffnet hat und gleichzeitig das Paketmanagement die Datei aktualisiert, warum stürzt der Apache dann nicht ab, weil sich alle möglichen Adressen geändert haben?

Zuerst einmal werden Shared Libraries per mmap() in den Adressbereich eines Prozesses eingebunden. Das kann man schön sehen, wenn man mal cat /proc/self/maps aufruft:

00400000-0040b000 r-xp 00000000 08:01 12467093                           /usr/bin/cat
0060a000-0060b000 r--p 0000a000 08:01 12467093                           /usr/bin/cat
0060b000-0060c000 rw-p 0000b000 08:01 12467093                           /usr/bin/cat
012eb000-0130c000 rw-p 00000000 00:00 0                                  [heap]
7fedc654f000-7fedc6847000 r--p 00000000 08:01 10232523                   /usr/lib/locale/locale-archive
7fedc6847000-7fedc69e5000 r-xp 00000000 08:01 10225057                   /usr/lib/libc-2.19.so
7fedc69e5000-7fedc6be5000 ---p 0019e000 08:01 10225057                   /usr/lib/libc-2.19.so
7fedc6be5000-7fedc6be9000 r--p 0019e000 08:01 10225057                   /usr/lib/libc-2.19.so
7fedc6be9000-7fedc6beb000 rw-p 001a2000 08:01 10225057                   /usr/lib/libc-2.19.so
7fedc6beb000-7fedc6bef000 rw-p 00000000 00:00 0 
7fedc6bef000-7fedc6c0f000 r-xp 00000000 08:01 10225033                   /usr/lib/ld-2.19.so
7fedc6de3000-7fedc6de6000 rw-p 00000000 00:00 0 
7fedc6e0f000-7fedc6e10000 r--p 00020000 08:01 10225033                   /usr/lib/ld-2.19.so
7fedc6e10000-7fedc6e11000 rw-p 00021000 08:01 10225033                   /usr/lib/ld-2.19.so
7fedc6e11000-7fedc6e12000 rw-p 00000000 00:00 0 
7ffff7d4e000-7ffff7d6f000 rw-p 00000000 00:00 0                          [stack]
7ffff7db6000-7ffff7db8000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

Bei cat ist es nicht so spektakulär, aber die beiden so-Files sieht man trotzdem.

Bauen wir das mmap()-Experiment erst einmal mit einer normalen Datei nach. In der obigen Ausgabe sieht man, dass die Shared Libraries mit „p“ gemappt werden, was so erklärt ist:

MAP_PRIVATE
           Create  a private copy-on-write mapping.  Updates to
           the mapping are not visible to other processes  map‐
           ping  the  same file, and are not carried through to
           the underlying  file.   It  is  unspecified  whether
           changes  made  to the file after the mmap() call are
           visible in the mapped region.

Das kann es also schon mal nicht sein. Würde mein Prozess in die gemappte Region schreiben, käme es nicht bei der tatsächlichen Datei an, aber dieser Fall ist für uns sowieso irrelevant. Und umgekehrt ist das Verhalten sogar gar nicht definiert. Trotzdem mal ausprobieren:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>

int
main()
{
    int fd;
    char *p;

    if ((fd = open("/tmp/test", O_RDONLY)) == -1)
        return 1;

    p = (char *)mmap(NULL, 512, PROT_READ, MAP_PRIVATE, fd, 0);
    if (p == MAP_FAILED)
        return 1;

    while (1)
    {
        printf("%c\n", p[0]);
        sleep(1);
    }
}

Testinhalt der Datei:

$ date >/tmp/test

Dann das Programm ausführen und schauen, wie der Speicher aussieht:

$ grep test /proc/$(pgrep mmap)/maps
7f854e696000-7f854e697000 r--p 00000000 00:1f 35602                      /tmp/test

Gut. Wenn ich jetzt den Dateiinhalt ändere, während das Programm noch läuft, sieht man, dass sich die Ausgabe verändert. „MAP_PRIVATE“ hilft also sicher nicht bei den Updates für Shared Libraries.

Aber vielleicht macht der Loader ja etwas anderes als ein simples mmap()? Könnte man im Quellcode nachlesen. Viel Spaß bei der glibc. Auch in Fefes dietlibc ist das noch ganz schön komplex. Probieren wir’s daher doch einfach mal aus, was passiert.

Angelehnt hieran folgender Aufbau:

$ tree lib prog
lib
├── ctest1.c
└── out/
prog
└── prog.c

Und noch ein abgeändertes prog.c:

#include <stdio.h>

void ctest1(int *);

int main()
{
    int x;
    while (1)
    {
        ctest1(&x);
        printf("Valx=%d\n",x);
    }

    return 0;
}

Ich habe hier bewusst auf das „sleep(1)“ verzichtet, damit möglichst häufig in den Code der Shared Library gesprungen wird.

Ich erzeuge jetzt zuerst die Shared Library:

$ pwd
/tmp/tmp/lib
$ rm -f *.o libc* ; gcc -Wall -fPIC -c *.c ; gcc -shared -Wl,-soname,libctest.so -o libctest.so *.o ; cat libctest.so >out/libctest.so

(Wer clever ist, sieht hier schon den Knackpunkt.)

Man öffne ein zweites Terminal. Das Programm übersetzen, dynamisch linken und ausführen:

$ pwd
/tmp/tmp/prog
$ gcc -Wall -L/tmp/tmp/lib/out prog.c -lctest -o prog ; LD_LIBRARY_PATH=/tmp/tmp/lib/out:$LD_LIBRARY_PATH ./prog
Valx=5
Valx=5
Valx=5
Valx=5
Valx=5
...

Gut, es spammt nun wie gewünscht die Konsole voll.

Wenn ich nun, während es läuft, den Code in ctest1.c ändere und die Library neu erstelle (mit obigem Befehl im ersten Terminal), dann kann man Glück haben und es geht gut. Oder es passiert das hier:

...
Valx=5
Valx=5
Valx=5
Valx=5
Valx=5
Valx=5
Valx=5
Bus error (core dumped)

Schau’ an! Ein „inplace update“ einer Shared Library kann tatsächlich zum Absturz führen.

Der Knackpunkt ist, wie das Update durchgeführt wird. Ich habe es oben bewusst und explizit so gebaut, dass die neue Library dieselbe Inode benutzt wie die alte. Deswegen habe ich sie mit cat und einer Shell-Umleitung „kopiert“. Ein korrektes Update muss hingegen so vorgehen, dass die neue Bibliothek an einer neuen Inode abgelegt wird, damit laufende Prozesse weiterhin die alte Inode benutzen können. Das können sie tun, wenn sie die Datei an einer Inode schon geöffnet hatten, bevor sie gelöscht wurde.

Kurz zurück zum ersten Code-Beispiel mit der /tmp/test: Wenn ich hier die Datei lösche, während das Programm noch läuft, dann sieht man folgende Ausgabe:

$ rm /tmp/test
$ grep test /proc/$(pgrep mmap)/maps
7feeede11000-7feeede12000 r--p 00000000 00:1f 35602                      /tmp/test (deleted)

Derweil läuft aber das Programm weiter, stürzt nicht ab und gibt weiterhin das erste Zeichen der Datei aus. Die Datei liegt auch noch auf der Platte, sie ist bloß nicht mehr über die Verzeichnishierarchie erreichbar. (Das passiert beim Löschen bei den gängigen Dateisystemen immer.)

Das Erzeugen und Updaten meiner Shared Library müsste ich also so machen:

$ rm *.o libc* ; gcc -Wall -fPIC -c *.c ; gcc -shared -Wl,-soname,libctest.so -o libctest.so *.o ; rm out/libctest.so ; cp libctest.so out/

Ta-tah, das Programm stürzt nicht mehr ab.

Eigentlich unnötig, sich das nun anzuschauen, aber ja, pacman verhält sich richtig. ;-) 646 ist hier die PID von einem Firefox-Prozess:

$ grep nss /proc/646/maps
7ff8d00f8000-7ff8d00fd000 r-xp 00000000 08:01 10225095                   /usr/lib/libnss_dns-2.19.so
7ff8d00fd000-7ff8d02fc000 ---p 00005000 08:01 10225095                   /usr/lib/libnss_dns-2.19.so
7ff8d02fc000-7ff8d02fd000 r--p 00004000 08:01 10225095                   /usr/lib/libnss_dns-2.19.so
7ff8d02fd000-7ff8d02fe000 rw-p 00005000 08:01 10225095                   /usr/lib/libnss_dns-2.19.so
7ff8de377000-7ff8de3f6000 r-xp 00000000 08:01 10231895                   /usr/lib/libnssckbi.so
7ff8de3f6000-7ff8de5f6000 ---p 0007f000 08:01 10231895                   /usr/lib/libnssckbi.so
7ff8de5f6000-7ff8de60a000 r--p 0007f000 08:01 10231895                   /usr/lib/libnssckbi.so
7ff8de60a000-7ff8de616000 rw-p 00093000 08:01 10231895                   /usr/lib/libnssckbi.so
7ff8de893000-7ff8de8bc000 r-xp 00000000 08:01 10231897                   /usr/lib/libnssdbm3.so
7ff8de8bc000-7ff8deabb000 ---p 00029000 08:01 10231897                   /usr/lib/libnssdbm3.so
7ff8deabb000-7ff8deabc000 r--p 00028000 08:01 10231897                   /usr/lib/libnssdbm3.so
7ff8deabc000-7ff8deabd000 rw-p 00029000 08:01 10231897                   /usr/lib/libnssdbm3.so
7ff8e1201000-7ff8e1204000 r-xp 00000000 08:01 10224442                   /usr/lib/libnss_myhostname.so.2
7ff8e1204000-7ff8e1205000 r--p 00002000 08:01 10224442                   /usr/lib/libnss_myhostname.so.2
7ff8e1205000-7ff8e1206000 rw-p 00003000 08:01 10224442                   /usr/lib/libnss_myhostname.so.2
7ff8e5af4000-7ff8e5aff000 r-xp 00000000 08:01 10225049                   /usr/lib/libnss_files-2.19.so
7ff8e5aff000-7ff8e5cfe000 ---p 0000b000 08:01 10225049                   /usr/lib/libnss_files-2.19.so
7ff8e5cfe000-7ff8e5cff000 r--p 0000a000 08:01 10225049                   /usr/lib/libnss_files-2.19.so
7ff8e5cff000-7ff8e5d00000 rw-p 0000b000 08:01 10225049                   /usr/lib/libnss_files-2.19.so
7ff8f146a000-7ff8f148f000 r-xp 00000000 08:01 10231985                   /usr/lib/libnssutil3.so
7ff8f148f000-7ff8f168f000 ---p 00025000 08:01 10231985                   /usr/lib/libnssutil3.so
7ff8f168f000-7ff8f1695000 r--p 00025000 08:01 10231985                   /usr/lib/libnssutil3.so
7ff8f1695000-7ff8f1696000 rw-p 0002b000 08:01 10231985                   /usr/lib/libnssutil3.so
7ff8f1696000-7ff8f17cb000 r-xp 00000000 08:01 10231894                   /usr/lib/libnss3.so
7ff8f17cb000-7ff8f19cb000 ---p 00135000 08:01 10231894                   /usr/lib/libnss3.so
7ff8f19cb000-7ff8f19cf000 r--p 00135000 08:01 10231894                   /usr/lib/libnss3.so
7ff8f19cf000-7ff8f19d2000 rw-p 00139000 08:01 10231894                   /usr/lib/libnss3.so

Und jetzt nach einem Downgrade des „nss“-Pakets ohne zwischenzeitlichen Firefox-Neustart:

$ grep nss /proc/646/maps
7ff8d00f8000-7ff8d00fd000 r-xp 00000000 08:01 10225095                   /usr/lib/libnss_dns-2.19.so
7ff8d00fd000-7ff8d02fc000 ---p 00005000 08:01 10225095                   /usr/lib/libnss_dns-2.19.so
7ff8d02fc000-7ff8d02fd000 r--p 00004000 08:01 10225095                   /usr/lib/libnss_dns-2.19.so
7ff8d02fd000-7ff8d02fe000 rw-p 00005000 08:01 10225095                   /usr/lib/libnss_dns-2.19.so
7ff8de377000-7ff8de3f6000 r-xp 00000000 08:01 10231895                   /usr/lib/libnssckbi.so (deleted)
7ff8de3f6000-7ff8de5f6000 ---p 0007f000 08:01 10231895                   /usr/lib/libnssckbi.so (deleted)
7ff8de5f6000-7ff8de60a000 r--p 0007f000 08:01 10231895                   /usr/lib/libnssckbi.so (deleted)
7ff8de60a000-7ff8de616000 rw-p 00093000 08:01 10231895                   /usr/lib/libnssckbi.so (deleted)
7ff8de893000-7ff8de8bc000 r-xp 00000000 08:01 10231897                   /usr/lib/libnssdbm3.so (deleted)
7ff8de8bc000-7ff8deabb000 ---p 00029000 08:01 10231897                   /usr/lib/libnssdbm3.so (deleted)
7ff8deabb000-7ff8deabc000 r--p 00028000 08:01 10231897                   /usr/lib/libnssdbm3.so (deleted)
7ff8deabc000-7ff8deabd000 rw-p 00029000 08:01 10231897                   /usr/lib/libnssdbm3.so (deleted)
7ff8e1201000-7ff8e1204000 r-xp 00000000 08:01 10224442                   /usr/lib/libnss_myhostname.so.2
7ff8e1204000-7ff8e1205000 r--p 00002000 08:01 10224442                   /usr/lib/libnss_myhostname.so.2
7ff8e1205000-7ff8e1206000 rw-p 00003000 08:01 10224442                   /usr/lib/libnss_myhostname.so.2
7ff8e5af4000-7ff8e5aff000 r-xp 00000000 08:01 10225049                   /usr/lib/libnss_files-2.19.so
7ff8e5aff000-7ff8e5cfe000 ---p 0000b000 08:01 10225049                   /usr/lib/libnss_files-2.19.so
7ff8e5cfe000-7ff8e5cff000 r--p 0000a000 08:01 10225049                   /usr/lib/libnss_files-2.19.so
7ff8e5cff000-7ff8e5d00000 rw-p 0000b000 08:01 10225049                   /usr/lib/libnss_files-2.19.so
7ff8f146a000-7ff8f148f000 r-xp 00000000 08:01 10231985                   /usr/lib/libnssutil3.so (deleted)
7ff8f148f000-7ff8f168f000 ---p 00025000 08:01 10231985                   /usr/lib/libnssutil3.so (deleted)
7ff8f168f000-7ff8f1695000 r--p 00025000 08:01 10231985                   /usr/lib/libnssutil3.so (deleted)
7ff8f1695000-7ff8f1696000 rw-p 0002b000 08:01 10231985                   /usr/lib/libnssutil3.so (deleted)
7ff8f1696000-7ff8f17cb000 r-xp 00000000 08:01 10231894                   /usr/lib/libnss3.so (deleted)
7ff8f17cb000-7ff8f19cb000 ---p 00135000 08:01 10231894                   /usr/lib/libnss3.so (deleted)
7ff8f19cb000-7ff8f19cf000 r--p 00135000 08:01 10231894                   /usr/lib/libnss3.so (deleted)
7ff8f19cf000-7ff8f19d2000 rw-p 00139000 08:01 10231894                   /usr/lib/libnss3.so (deleted)

Bei allen Libraries, die aus dem „nss“-Paket kommen, steht „deleted“. Er benutzt weiterhin die alten Dateien.

Schlussendlich: Wenn ich jetzt den Firefox neustarten würde, dann würde er die Datei „/usr/lib/libnssckbi.so“ über das Dateisystem neu öffnen und käme dabei bei der neuen Inode heraus. Deswegen ist erst dann die neue Library tatsächlich in Betrieb.