glibc free 死锁
生活随笔
收集整理的這篇文章主要介紹了
glibc free 死锁
小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
通常我們認(rèn)為一旦內(nèi)存寫溢出,程序就很容易崩潰。所以服務(wù)器上通常會(huì)對(duì)一些重要進(jìn)程做腳本保護(hù),一旦崩潰立即重新拉起。
最近發(fā)現(xiàn)我們一個(gè)公共服務(wù)內(nèi)存寫溢出時(shí)程序沒(méi)有崩潰,而是卡死了。
為了深入分析原因,我們仔細(xì)review了glibc的代碼,并發(fā)現(xiàn)一個(gè)較為隱蔽的bug。
????
先來(lái)看這個(gè)卡死的程序堆棧(64位環(huán)境,下同): #0 0x00002b059302ac38 in __lll_mutex_lock_wait () from /lib64/libc.so.6 #1 0x00002b0592fcee5f in _L_lock_4026 () from /lib64/libc.so.6 #2 0x00002b0592fcbdf1 in free () from /lib64/libc.so.6 #3 0x00002b0592fe4148 in tzset_internal () from /lib64/libc.so.6 #4 0x00002b0592fe49d0 in tzset () from /lib64/libc.so.6 #5 0x00002b0592fe8e44 in strftime_l () from /lib64/libc.so.6 #6 0x00002b059301c701 in __vsyslog_chk () from /lib64/libc.so.6 #7 0x00002b0592fc56d0 in __libc_message () from /lib64/libc.so.6 #8 0x00002b0592fca77e in malloc_printerr () from /lib64/libc.so.6 #9 0x00002b0592fcbdfc in free () from /lib64/libc.so.6 #10 0x00002b05929ed657 in deflateEnd () from /lib64/libz.so.1 #11 0x00000000004884b8 in CHttpResp::GetOutput (this=0x2b059dd414f8, #12 ……
這應(yīng)該屬于glibc的bug了,雖然這個(gè)bug首先要由程序員的bug來(lái)觸發(fā)。 為了進(jìn)一步確認(rèn)glibc的這個(gè)問(wèn)題,我們繼續(xù)深入閱讀glibc的代碼以重現(xiàn)之。
首先,為什么內(nèi)存寫越界會(huì)導(dǎo)致free出錯(cuò)?解答這個(gè)問(wèn)題前我們先簡(jiǎn)單說(shuō)說(shuō)一些相關(guān)的malloc分配內(nèi)存原理。
跟一些人想象不同的是,并不是每次malloc調(diào)用一定導(dǎo)致內(nèi)存分配,因?yàn)楫?dāng)內(nèi)存釋放時(shí)glibc會(huì)將內(nèi)存先保留到空閑隊(duì)列當(dāng)中,下次有malloc調(diào)用時(shí)可以找一個(gè)合適的內(nèi)存塊直接返回,這樣就避免了真正從系統(tǒng)分配內(nèi)存的系統(tǒng)調(diào)用開(kāi)銷。glibc需要管理這些空閑內(nèi)存塊,那么就需要一個(gè)相應(yīng)的數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)定義如下: struct malloc_chunk {INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */struct malloc_chunk* fd; /* double links -- used only if free. */struct malloc_chunk* bk;/* Only used for large blocks: pointer to next larger size. */struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */struct malloc_chunk* bk_nextsize; };
size被寫壞,有兩種結(jié)果。一種是free函數(shù)能檢查出這個(gè)錯(cuò)誤,程序就會(huì)先輸出一些錯(cuò)誤信息然后abort;一種是free函數(shù)無(wú)法檢查出這個(gè)錯(cuò)誤,程序便往往會(huì)直接crash。
根據(jù)最上面的堆棧推測(cè),誘發(fā)bug的是前一種情況。我們的測(cè)試程序?qū)?huì)直接分配兩塊內(nèi)存,并對(duì)第二塊內(nèi)存chunk的size參數(shù)做簡(jiǎn)單修改,以觸發(fā)情況一。
順便說(shuō)一句,windows內(nèi)存分配跟linux比較類似,也是將內(nèi)存塊大小存放在malloc返回的指針位置之前。DEBUG模式下,編譯器還會(huì)在實(shí)際分配內(nèi)存的兩端放兩個(gè)特殊值,這樣在內(nèi)存回收時(shí)就可以檢測(cè)到內(nèi)存寫溢出的問(wèn)題。
????
其次,當(dāng)free函數(shù)檢查到size異常以后,會(huì)調(diào)用malloc_printerr輸出一些錯(cuò)誤信息,但它并不一定會(huì)寫syslog。
查看__libc_message的代碼可以發(fā)現(xiàn),出現(xiàn)錯(cuò)誤以后,glibc會(huì)先嘗試將錯(cuò)誤信息寫入到stderr或tty,如果寫入失敗,才會(huì)去寫syslog(代碼有點(diǎn)啰嗦就不貼了)。
要模擬這個(gè)情況,只需將環(huán)境變量"LIBC_FATAL_STDERR_"設(shè)為1迫使出錯(cuò)時(shí)寫stderr,然后將stderr關(guān)閉即可。通常daemon程序很容易處在這樣的狀態(tài)。
????
再次,查看tzset_internal的代碼,我們發(fā)現(xiàn)導(dǎo)致free操作的原因是靜態(tài)變量static char* old_tz釋放導(dǎo)致的。
old_tz存放了上一次調(diào)用tzset_internal時(shí)使用的時(shí)區(qū)字符串。如果再次調(diào)用tzset_internal時(shí),時(shí)區(qū)不變就復(fù)用;如果不同,則free掉舊的字符串,strdup新的字符串,而strdup里面malloc了新字符串所需的內(nèi)存塊。
要模擬這個(gè)情況只需先設(shè)法給old_tz一個(gè)初值,然后再做內(nèi)存釋放觸發(fā)free(old_tz)即可。要給old_tz設(shè)初值只需先調(diào)用相關(guān)的時(shí)間函數(shù)即可,例如localtime這個(gè)函數(shù)經(jīng)常就被用到,old_tz會(huì)初始化為默認(rèn)值"/etc/localtime"。當(dāng)malloc_printerr一步步調(diào)用到tzset_internal時(shí),glibc會(huì)從環(huán)境變量"TZ"讀取新的時(shí)區(qū)字符串,通常大多數(shù)服務(wù)器是沒(méi)設(shè)置這個(gè)環(huán)境變量的,所以新tz就是空,從而導(dǎo)致"free(old_tz); old_tz = NULL;"這樣的操作。
????
所以我們的簡(jiǎn)單演示代碼如下: // file: test.cpp #include <time.h> #include <unistd.h> #include <stdlib.h>int main(int argc, char** argv) {// 設(shè)置環(huán)境變量,強(qiáng)制錯(cuò)誤輸出到stderr,而不是ttysetenv("LIBC_FATAL_STDERR_", "1", 1);close(STDERR_FILENO); // 關(guān)閉stderrtime_t now = time(NULL);tm *t = localtime(&now); // 觸發(fā)old_tz初始化char *p1 = new char[102400];char *p2 = new char[4096];p1[102400 + sizeof(size_t)] = 1;// 模擬內(nèi)存寫溢出delete [] p2; // 程序在這里死鎖delete [] p1;return 0; }
查看堆棧如下: (gdb) bt #0 0x00002ba6519a4c38 in __lll_mutex_lock_wait () from /lib64/libc.so.6 #1 0x00002ba651948e5f in _L_lock_4026 () from /lib64/libc.so.6 #2 0x00002ba651945df1 in free () from /lib64/libc.so.6 #3 0x00002ba65195e148 in tzset_internal () from /lib64/libc.so.6 #4 0x00002ba65195e9d0 in tzset () from /lib64/libc.so.6 #5 0x00002ba651962e44 in strftime_l () from /lib64/libc.so.6 #6 0x00002ba651996701 in __vsyslog_chk () from /lib64/libc.so.6 #7 0x00002ba65193f6d0 in __libc_message () from /lib64/libc.so.6 #8 0x00002ba65194477e in malloc_printerr () from /lib64/libc.so.6 #9 0x00002ba651945dfc in free () from /lib64/libc.so.6 #10 0x000000000040094e in main (argc=1, argv=0x7fff5974c828) at test1.cpp:18
我簡(jiǎn)單查看了一下glibc的歷史版本代碼,這個(gè)bug在2.4到2.8的版本上都存在。當(dāng)然這個(gè)bug首先需要程序員犯了內(nèi)存寫溢出錯(cuò)誤才會(huì)誘發(fā),所以這并不是嚴(yán)重bug,大家只要知道了自然也可結(jié)合實(shí)際情況做防范。比如檢查進(jìn)程是否正常不能光看進(jìn)程是否存在,還需用工具做收發(fā)包檢測(cè),或者查看定時(shí)日志是否一直有輸出之類。
就這個(gè)問(wèn)題本身來(lái)看,glibc確實(shí)還可以做得更好,例如應(yīng)該進(jìn)一步縮小鎖的作用域,既提升并發(fā)性能,又可降低作用域內(nèi)其他函數(shù)交叉調(diào)用引發(fā)的死鎖風(fēng)險(xiǎn);另外,個(gè)人認(rèn)為tzset_internal中完全沒(méi)必要?jiǎng)討B(tài)分配內(nèi)存,給old_tz一個(gè)固定大小的內(nèi)存比如256byte應(yīng)該基本上就可以了。
最近發(fā)現(xiàn)我們一個(gè)公共服務(wù)內(nèi)存寫溢出時(shí)程序沒(méi)有崩潰,而是卡死了。
為了深入分析原因,我們仔細(xì)review了glibc的代碼,并發(fā)現(xiàn)一個(gè)較為隱蔽的bug。
????
先來(lái)看這個(gè)卡死的程序堆棧(64位環(huán)境,下同): #0 0x00002b059302ac38 in __lll_mutex_lock_wait () from /lib64/libc.so.6 #1 0x00002b0592fcee5f in _L_lock_4026 () from /lib64/libc.so.6 #2 0x00002b0592fcbdf1 in free () from /lib64/libc.so.6 #3 0x00002b0592fe4148 in tzset_internal () from /lib64/libc.so.6 #4 0x00002b0592fe49d0 in tzset () from /lib64/libc.so.6 #5 0x00002b0592fe8e44 in strftime_l () from /lib64/libc.so.6 #6 0x00002b059301c701 in __vsyslog_chk () from /lib64/libc.so.6 #7 0x00002b0592fc56d0 in __libc_message () from /lib64/libc.so.6 #8 0x00002b0592fca77e in malloc_printerr () from /lib64/libc.so.6 #9 0x00002b0592fcbdfc in free () from /lib64/libc.so.6 #10 0x00002b05929ed657 in deflateEnd () from /lib64/libz.so.1 #11 0x00000000004884b8 in CHttpResp::GetOutput (this=0x2b059dd414f8, #12 ……
可以看到在free函數(shù)中使用了鎖。
那么再來(lái)看看glibc中free函數(shù)的主要代碼: #define public_fREe free void public_fREe(Void_t* mem) {mchunkptr p = mem2chunk(mem);mstate ar_ptr = arena_for_chunk(p);……(void)mutex_lock(&ar_ptr->mutex);_int_free(ar_ptr, mem);(void)mutex_unlock(&ar_ptr->mutex); }這段代碼相當(dāng)簡(jiǎn)單,不用過(guò)多解釋。
再對(duì)比上面的堆棧,可以推測(cè)流程大概是這樣的:frame 9釋放內(nèi)存時(shí)發(fā)現(xiàn)內(nèi)存數(shù)據(jù)校驗(yàn)有誤所以進(jìn)行出錯(cuò)輸出,當(dāng)寫syslog時(shí)需要取本地時(shí)間,而在取時(shí)區(qū)信息的函數(shù)里面也有free函數(shù)調(diào)用,所以到frame 2釋放內(nèi)存想要再次獲取鎖的時(shí)候程序死鎖了。這應(yīng)該屬于glibc的bug了,雖然這個(gè)bug首先要由程序員的bug來(lái)觸發(fā)。 為了進(jìn)一步確認(rèn)glibc的這個(gè)問(wèn)題,我們繼續(xù)深入閱讀glibc的代碼以重現(xiàn)之。
首先,為什么內(nèi)存寫越界會(huì)導(dǎo)致free出錯(cuò)?解答這個(gè)問(wèn)題前我們先簡(jiǎn)單說(shuō)說(shuō)一些相關(guān)的malloc分配內(nèi)存原理。
跟一些人想象不同的是,并不是每次malloc調(diào)用一定導(dǎo)致內(nèi)存分配,因?yàn)楫?dāng)內(nèi)存釋放時(shí)glibc會(huì)將內(nèi)存先保留到空閑隊(duì)列當(dāng)中,下次有malloc調(diào)用時(shí)可以找一個(gè)合適的內(nèi)存塊直接返回,這樣就避免了真正從系統(tǒng)分配內(nèi)存的系統(tǒng)調(diào)用開(kāi)銷。glibc需要管理這些空閑內(nèi)存塊,那么就需要一個(gè)相應(yīng)的數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)定義如下: struct malloc_chunk {INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */struct malloc_chunk* fd; /* double links -- used only if free. */struct malloc_chunk* bk;/* Only used for large blocks: pointer to next larger size. */struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */struct malloc_chunk* bk_nextsize; };
映射到內(nèi)存示意圖上如下圖所示:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ <--真正的chunk首指針 | prev_size, 前一個(gè)chunk的大小 | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | size, 低位作標(biāo)志位,高位存放chunk的大小 |M|P| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ <--malloc成功返回的首指針 | 正常時(shí)存放用戶數(shù)據(jù); . . 空閑時(shí)存放malloc_chunk結(jié)構(gòu)后續(xù)成員變量。 . . . . | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ <--下一個(gè)chunk的首指針 | prev_size …… | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+可以看到,我們每次malloc返回的指針并不是內(nèi)存塊的首指針,前面還有兩個(gè)size_t大小的參數(shù),對(duì)于非空閑內(nèi)存而言size參數(shù)最為重要。size參數(shù)存放著整個(gè)chunk的大小,由于物理內(nèi)存的分配是要做字節(jié)對(duì)齊的,所以size參數(shù)的低位用不上,便作為flag使用。
內(nèi)存寫溢出,通常就是把后一個(gè)chunk的size參數(shù)寫壞了。size被寫壞,有兩種結(jié)果。一種是free函數(shù)能檢查出這個(gè)錯(cuò)誤,程序就會(huì)先輸出一些錯(cuò)誤信息然后abort;一種是free函數(shù)無(wú)法檢查出這個(gè)錯(cuò)誤,程序便往往會(huì)直接crash。
根據(jù)最上面的堆棧推測(cè),誘發(fā)bug的是前一種情況。我們的測(cè)試程序?qū)?huì)直接分配兩塊內(nèi)存,并對(duì)第二塊內(nèi)存chunk的size參數(shù)做簡(jiǎn)單修改,以觸發(fā)情況一。
順便說(shuō)一句,windows內(nèi)存分配跟linux比較類似,也是將內(nèi)存塊大小存放在malloc返回的指針位置之前。DEBUG模式下,編譯器還會(huì)在實(shí)際分配內(nèi)存的兩端放兩個(gè)特殊值,這樣在內(nèi)存回收時(shí)就可以檢測(cè)到內(nèi)存寫溢出的問(wèn)題。
????
其次,當(dāng)free函數(shù)檢查到size異常以后,會(huì)調(diào)用malloc_printerr輸出一些錯(cuò)誤信息,但它并不一定會(huì)寫syslog。
查看__libc_message的代碼可以發(fā)現(xiàn),出現(xiàn)錯(cuò)誤以后,glibc會(huì)先嘗試將錯(cuò)誤信息寫入到stderr或tty,如果寫入失敗,才會(huì)去寫syslog(代碼有點(diǎn)啰嗦就不貼了)。
要模擬這個(gè)情況,只需將環(huán)境變量"LIBC_FATAL_STDERR_"設(shè)為1迫使出錯(cuò)時(shí)寫stderr,然后將stderr關(guān)閉即可。通常daemon程序很容易處在這樣的狀態(tài)。
????
再次,查看tzset_internal的代碼,我們發(fā)現(xiàn)導(dǎo)致free操作的原因是靜態(tài)變量static char* old_tz釋放導(dǎo)致的。
old_tz存放了上一次調(diào)用tzset_internal時(shí)使用的時(shí)區(qū)字符串。如果再次調(diào)用tzset_internal時(shí),時(shí)區(qū)不變就復(fù)用;如果不同,則free掉舊的字符串,strdup新的字符串,而strdup里面malloc了新字符串所需的內(nèi)存塊。
要模擬這個(gè)情況只需先設(shè)法給old_tz一個(gè)初值,然后再做內(nèi)存釋放觸發(fā)free(old_tz)即可。要給old_tz設(shè)初值只需先調(diào)用相關(guān)的時(shí)間函數(shù)即可,例如localtime這個(gè)函數(shù)經(jīng)常就被用到,old_tz會(huì)初始化為默認(rèn)值"/etc/localtime"。當(dāng)malloc_printerr一步步調(diào)用到tzset_internal時(shí),glibc會(huì)從環(huán)境變量"TZ"讀取新的時(shí)區(qū)字符串,通常大多數(shù)服務(wù)器是沒(méi)設(shè)置這個(gè)環(huán)境變量的,所以新tz就是空,從而導(dǎo)致"free(old_tz); old_tz = NULL;"這樣的操作。
????
所以我們的簡(jiǎn)單演示代碼如下: // file: test.cpp #include <time.h> #include <unistd.h> #include <stdlib.h>int main(int argc, char** argv) {// 設(shè)置環(huán)境變量,強(qiáng)制錯(cuò)誤輸出到stderr,而不是ttysetenv("LIBC_FATAL_STDERR_", "1", 1);close(STDERR_FILENO); // 關(guān)閉stderrtime_t now = time(NULL);tm *t = localtime(&now); // 觸發(fā)old_tz初始化char *p1 = new char[102400];char *p2 = new char[4096];p1[102400 + sizeof(size_t)] = 1;// 模擬內(nèi)存寫溢出delete [] p2; // 程序在這里死鎖delete [] p1;return 0; }
g++ -pg -g test.cpp編譯得到可執(zhí)行程序a.out。
使用gdb運(yùn)行此程序,如預(yù)期般的死鎖。查看堆棧如下: (gdb) bt #0 0x00002ba6519a4c38 in __lll_mutex_lock_wait () from /lib64/libc.so.6 #1 0x00002ba651948e5f in _L_lock_4026 () from /lib64/libc.so.6 #2 0x00002ba651945df1 in free () from /lib64/libc.so.6 #3 0x00002ba65195e148 in tzset_internal () from /lib64/libc.so.6 #4 0x00002ba65195e9d0 in tzset () from /lib64/libc.so.6 #5 0x00002ba651962e44 in strftime_l () from /lib64/libc.so.6 #6 0x00002ba651996701 in __vsyslog_chk () from /lib64/libc.so.6 #7 0x00002ba65193f6d0 in __libc_message () from /lib64/libc.so.6 #8 0x00002ba65194477e in malloc_printerr () from /lib64/libc.so.6 #9 0x00002ba651945dfc in free () from /lib64/libc.so.6 #10 0x000000000040094e in main (argc=1, argv=0x7fff5974c828) at test1.cpp:18
程序堆棧跟文首的完全相同。至此問(wèn)題得到確認(rèn)。
????我簡(jiǎn)單查看了一下glibc的歷史版本代碼,這個(gè)bug在2.4到2.8的版本上都存在。當(dāng)然這個(gè)bug首先需要程序員犯了內(nèi)存寫溢出錯(cuò)誤才會(huì)誘發(fā),所以這并不是嚴(yán)重bug,大家只要知道了自然也可結(jié)合實(shí)際情況做防范。比如檢查進(jìn)程是否正常不能光看進(jìn)程是否存在,還需用工具做收發(fā)包檢測(cè),或者查看定時(shí)日志是否一直有輸出之類。
就這個(gè)問(wèn)題本身來(lái)看,glibc確實(shí)還可以做得更好,例如應(yīng)該進(jìn)一步縮小鎖的作用域,既提升并發(fā)性能,又可降低作用域內(nèi)其他函數(shù)交叉調(diào)用引發(fā)的死鎖風(fēng)險(xiǎn);另外,個(gè)人認(rèn)為tzset_internal中完全沒(méi)必要?jiǎng)討B(tài)分配內(nèi)存,給old_tz一個(gè)固定大小的內(nèi)存比如256byte應(yīng)該基本上就可以了。
總結(jié)
以上是生活随笔為你收集整理的glibc free 死锁的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: sql server 提示无法彻底删除_
- 下一篇: 线段树--codevs 1690 开关灯