blog · git · desktop · images · contact
2014-04-12
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.