Kto słyszał już o nowej funkcji systemowej Linuksa, prlimit? Wywołanie to pozwala na zmianę nałożonych wcześniej limitów (np. ograniczenie czasu procesora, otwartych plików) dla już działającego procesu. Niby nic takiego ale jednak. Umożliwia to tuningowanie parametrów pracy serwera bez potrzeby restartu serwisów. Administrator systemu nie musi już ostrzegać o niedostępności usług, lub wysłuchiwać skarg użytkowników, gdy chce przydzielić np więcej czasu procesora dla serwera Apache.
Wszystko ładnie pięknie, ale jest to rozwiązanie które jeszcze nie weszło na dobre do systemu i nie wiadomo kiedy zacznie się pojawiać w stabilnych wersjach Linuksa. Czy do tej pory nie można było zmieniać limitów bez prlimit i restartu? Oczywiście że tak!
Standardowo mamy w systemie dwa wywołania, jedno do odczytu limitów, a drugie do ich ustawienia - getrlimit i setrlimit. Wpisz teraz "man setrlimit" i zobacz jak wyglądają ich prototypy oraz jak należy się nimi posługiwać. Jest to ważne, jeżeli chcesz zrozumieć to co za chwile pokażę.
Jak już wspomniałem wcześniej, szkopuł w tym, że wywołanie setrlimit odnosi się tylko dla bieżącego procesu. Rozwiązanie tego problemu jest proste - trzeba przypiąć się na chwile debuggerem i wywołać setrlimit w kontekście procesu dla którego chcemy zmienić limit. Ten sam efekt można jeszcze osiągnąć na kilka innych sposbów, ale my jesteśmy administratorami i nie będziemy bawić się w programowanie :)
Na początek banalny programik, którego celem jest wywołanie sygnału SIGSEGV. Przy standardowych ustawieniach polecenie ulimit -c powinno pokazać zero - czyli nasz programik nie zrzuci core dump. Postaramy się zatem zmienić mu limit, w trakcie jego działania i zrzucić core dump.
# cat mkcore.c
#include
int main() {
printf("pid: %i\n",getpid());
sleep(60);
*((int *)0) = 0;
struct rlimit rl;
getrlimit(RLIMIT_CORE, &rl);
setrlimit(RLIMIT_CORE, &rl);
}
Programik jak widać wyświetla swój PID oraz wpada w stan uśpienia na 60 sekund. Funkcja sleep nie tylko da nam czas na uruchomienie gdb, ale również dzięki niej gdb będzie w stanie rozpocząć śledzenie programu.
# gcc -g mkcore.c -o mkcore
# ./mkcore &
[1] 8808
pid: 8808
# gdb -p 8808
(gdb) set unwindonsignal on
(gdb) call malloc(8)
$1 = 138133512
(gdb) set *(long *) $1 = -1
(gdb) set *(long *) ($1+4) = -1
(gdb) call setrlimit(4, $1)
$2 = 0
(gdb) call free( $1 )
$3 = 0
Kilka słów objaśnienia. Ustawienie "uwindonsignal" spowoduje, że jeżeli w skutek naszych działań coś pójdzie nie tak (np proces dostanie sygnał SIGSEGV), nie spowodujemy przerwania procesu. Dalej mamy malloc w celu zaalokowania pamięci którą przekażemy do setrlimit. Dobrze jest to zrobić w ten sposób gdyż wtedy nie musimy korzystać z pamięci już przydzielonej do procesu, dzięki czemu minimalizujemy ryzyko awarii. Następne dwa polecenia set ustawiają limit twardy i miękki w strukturze rlimit
struct rlimit {
rlim_t rlim_cur; /* Soft limit */
rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
};
Limity ustawiamy na RLIM_INFINITY (unlimited) korzystając z faktu, że typ rlim_t na maszynie 32 bitowej to 4 bajty, a sama wartość RLIM_INFINITY jest zdefiniowana jako -1
# grep -r "RLIM_INFINITY" /usr/include/*
/usr/include/asm-generic/resource.h:# define RLIM_INFINITY (~0UL)
Potem wykonujemy setrlimit z parametrem RLIMIT_CORE i sprzątamy po sobie pamięć.
# grep -r "RLIMIT_CORE" /usr/include/
/usr/include/asm-generic/resource.h:#define RLIMIT_CORE 4 /* max core file size */
Czemu nie używam symboli i nie wydaje polecenia np call setrlimit(RLIMIT_CORE, $1)? Ponieważ nie każdy program musi być skompilowany z symbolami, a chcemy mieć narzędzie jak najbardziej uniwersalne. Teraz wychodzimy z dbg i naszym oczom pojawia się komunikat o zrzuceniu pliku core - faktycznie zmieniliśmy limit w uruchomionym procesie :)
(gdb) q
A debugging session is active.
Inferior 1 [process 8808] will be detached.
Quit anyway? (y or n) y
Detaching from program: /root/rtcore/mkcore, process 8808
[1]+ Segmentation fault (core dumped) ./mkcore
Do tej pory wszystko poszło dobrze, ale niech ktoś spróbuje to wykonać dla serwera apache a dostanie błąd:
Program received signal SIGSEGV, Segmentation fault.
0xb77e4a38 in setrlimit@plt () from /usr/lib/libapr-1.so.0
O co chodzi? Apache najwyraźniej nie używa funkcji setrlimit i program nie był z nią linkowany podczas kompilacji. Wynika z tego, że nie jest również obecna w tablicy plt (procedure linkage table). Sprawdźmy zatem gdzie znajduje się funkcja setrlimit w bibliotece libc:
# ls -l /lib/libc*
-rwxr-xr-x 1 root root 1954497 Jan 25 23:48 /lib/libc-2.15.so
...
lrwxrwxrwx 1 root root 12 Jan 25 23:48 /lib/libc.so.6 -> libc-2.15.so
# nm /lib/libc-2.15.so | grep rlimit
000e0720 T getrlimit64@@GLIBC_2.2
001290d0 T getrlimit64@GLIBC_2.1
000e0680 T getrlimit@@GLIBC_2.2
000e96b0 T getrlimit@GLIBC_2.0
000e94d0 T prlimit
000e9750 T prlimit64
000e0810 T setrlimit64
000e06d0 T setrlimit@@GLIBC_2.2
000e9700 T setrlimit@GLIBC_2.0
Liczba w pierwszej kolumnie pokazuje nam informacje gdzie w bibliotece zaczyna się kod danej funkcji (offset 0xe06d0). Jest to bardzo ważna informacja gdyż dzięki niej będziemy w stanie znaleźć setrlimit w pamięci naszego programu:
# cat /proc/$(pidof -s httpd)/maps | grep "libc-2.15.so"
b761e000-b77ba000 r-xp 00000000 08:03 64163 /lib/libc-2.15.so
b77ba000-b77bc000 r--p 0019c000 08:03 64163 /lib/libc-2.15.so
b77bc000-b77bd000 rw-p 0019e000 08:03 64163 /lib/libc-2.15.so
Trudno byłoby nam wykonać pracę linkera samodzielnie, dlatego najprościej podmienić wywołanie jakiejś innej funkcji zlinkowanej do programu (najlepiej z tej samej biblioteki). Podmienić możemy jakąkolwiek funkcje, chodzi tylko o to że potrzebujemy jednego "slotu" w tablicy plt, i nie chcemy go tworzyć - im mniej ingerujemy w śledzony program tym lepiej.
Jak to zrobić? Warto wiedzieć w jaki sposób Linux linkuje dynamiczne biblioteki:
(gdb) info address getrlimit
Symbol "getrlimit" is at 0x8061390 in a file compiled without debugging.
(gdb) disassemble 0x8061390
Dump of assembler code for function getrlimit@plt:
0x08061390 <+0>: jmp *0x80a2484
0x08061396 <+6>: push $0x908
0x0806139b <+11>: jmp 0x8060170
End of assembler dump.
(gdb) print/x *0x80a2484
$8 = 0x08061396
Powyższy kod pokazuje co się kryje jako ciało funkcji getrlimit. Działa to w ten sposób, że gdy włączone jest linkowanie leniwe, linker przy uruchamianiu procesu nie robi... nic :) Dlatego instrukcja jmp wskazuje na 0x08061396 - czyli kod który dopiero przy pierwszym wywołaniu uruchomi linker. Gdy linker wykona prace, adres naszej funkcji znajdzie się pod 0x80a2484.
Co nam pozostaje? Po pierwsze znaleźć nasz slot w tablicy plt który przekierujemy do funkcji setrlimit. Możemy użyć jakiegokolwiek slotu, ale jeżeli nie chcemy sobie dokładać pracy, najlepiej by funkcja pochodziła z biblioteki libc, dzięki czemu nie będziemy musieli szukać adresu funkcji setrlmit. Zmodyfikujemy tylko addres już zlinkowanej funkcji o odpowiednie przesunięcie. Zobaczmy więc jakie funkcje libc używa mój serwer apache:
# readelf -s $(which httpd) | grep GLIBC
...
28: 00000000 0 FUNC GLOBAL DEFAULT UND getrlimit@GLIBC_2.2 (6)
...
Jak widać wybrałem funcke getrlimit. Szukamy więc jakie jest przesunięcie pomiędzy ciałem funkcji getrlimit a setrlimit w bibliotece libc
nm /lib/libc-2.15.so | egrep "getrlimit|setrlimit"
000e0720 T getrlimit64@@GLIBC_2.2
001290d0 T getrlimit64@GLIBC_2.1
000e0680 T getrlimit@@GLIBC_2.2
000e96b0 T getrlimit@GLIBC_2.0
000e0810 T setrlimit64
000e06d0 T setrlimit@@GLIBC_2.2
000e9700 T setrlimit@GLIBC_2.0
# printf "0x%x\n" $(( 0x000e06d0 - 0x000e0680 ))
0x50
Wszystko już mamy, pora więc połączyć to w całość i wykonać:
# gdb -p 29395
(gdb) print getrlimit
$1 = {
(gdb) print/x *(long*) ($1 + 2)
$2 = 0x80a2484
(gdb) print/x *(long*)$2
$3 = 0xb76fe630
(gdb) set *(long *) $2 = $3 + 0x50
(gdb) call malloc(16)
$4 = 136970216
(gdb) set *(long *) $4 = -1
(gdb) set *(long *) ($4+4) = -1
(gdb) call getrlimit(4, $4)
$5 = 0
(gdb) set *(long *) $2 = $3
(gdb) call free( $4 )
$6 = 0
(gdb) q
A debugging session is active.
Inferior 1 [process 29395] will be detached.
Quit anyway? (y or n) y
Detaching from program: /usr/sbin/httpd, process 29395
Kto pisze skrypt? :)