一個由進程內存佈局異常引發的問題

前段時間業務反映某類服務器上更新了 bash 以後,ssh 連上去偶發登錄失敗,客戶端吐出錯誤信息以下所示:

圖 - 0html

該版本 bash 爲部門這邊所定製,可是實現上與原生版並無不一樣,那麼這些錯誤從哪裏來?linux

是 bash 的鍋嗎

從上面的錯誤信息能夠猜想,異常是 bash 在啓動過程當中分配內存失敗所致使,看起來像是某些狀況下該進程錯誤地進行了大量內存分配,最後致使內存不足,要確認這個事情比較簡單,動態內存分配到系統調用這一層上主要就兩種方式: brk() 和 mmap(), 因此只要統計一下這二者的調用就能夠大概估算出是否有大內存分配了。git

bash 是由 sshd 啓動的,因而 strace 跟蹤了一下 sshd 進程,結果發現異常發生時,bash 分配的內存很是地少,少到有時甚至只有幾十字節也會失敗,幾乎能夠判定 bash 在內存使用上沒有異常,但在這期間發現一個詭異的現象,Bash 一直只用 brk 在分配小內存,brk() 失敗後就直接退出了,通常程序使用的 libc 中的 malloc (或其它相似的 malloc) 會結合 brk 和 mmap 一塊兒使用【0】,不至於 brk 一失敗就分配不到內存,順手查看了下 bash 的源碼,發現它確實基於 brk 作了本身的內存管理,並無使用 malloc 或 mmap。github

但那並非重點,重點是即便是隻使用 brk,也不至於只能分配幾十字節的內存。c#

進程的內存佈局

進程的內存佈局在結構上是有規律的,具體來講對於 linux 系統上的進程,其內存空間通常能夠粗略地分爲如下幾大段【1】,從高內存到低內存排列:
一、內核態內存空間,其大小通常比較固定(能夠編譯時調整),但 32 位系統和 64 位系統的值不同。
二、用戶態的堆棧,大小不固定,能夠用 ulimit -s 進行調整,默認通常爲 8M,從高地址向低地址增加。
三、mmap 區域,進程茫茫內存空間裏的主要部分,既能夠從高地址到低地址延伸(所謂 flexible layout),也能夠從低到高延伸(所謂 legacy layout),看進程具體狀況【2】【3】。
四、brk 區域,緊鄰數據段(甚至貼着),從低位向高位伸展,但它的大小主要取決於 mmap 如何增加,通常來講,即便是 32 位的進程以傳統方式延伸,也有差很少 1 GB 的空間(準確地說是 TASK_SIZE/3 - 代碼段數據段,參看 arch/x86/include/asm/processor.h 裏宏 TASK_UNMAPPED_BASE 的定義)【4】
五、數據段,主要是進程裏初始化和未初始化的全局數據總和,固然還有編譯器生成一些輔助數據結構等等),大小取決於具體進程,其位置緊貼着代碼段。
六、代碼段,主要是進程的指令,包括用戶代碼和編譯器生成的輔助代碼,其大小取決於具體程序,但起始位置根據 32 位仍是 64 位通常固定(-fPIC, -fPIE等除外【5】)。centos

以上各段(除了代碼段數據段)其起始位置根據系統是否起用 randomize_va_space 通常稍有變化,各段之間所以可能有隨機大小的間隔,千言萬語不如一幅圖:安全


圖 - 1bash

因此如今的問題歸結爲:爲何目標進程的 brk 的區域忽然那麼小了,先檢查一下 bash 的內存佈局:服務器


圖 - 2數據結構

這個進程的內存佈局和通常理解上有很大出入,從上往下是低內存到高內存:
#1 處爲進程的代碼段和數據段,這兩個區域通常處於進程內存空間的最低處,但如今在更低處明顯有動態庫被映射了進來。
#2 處爲 brk 的區域,該區域還算緊臨着數據段,可是,brk 與代碼段之間也被插入了動態庫,並且更要命的是,brk 區域向高處伸展的方向上,動態庫映射的區域貼的很近,致使 brk 的區域事實上只有很小一個空間(0x886000 - 0x7ac000)。

這並非咱們想要的內存佈局,咱們想要的應該是長成下面這樣的:


圖 - 3

看出來不一樣了沒有,兩個 bash 進程都是 64 位的,不一樣在於前者是 sshd 起的進程後者是我手動在終端上起起來的,手動 cat /proc/self/maps 看了下 64 位的 cat 的進程的內存佈局也是正常的:


圖 - 4

那 sshd 進程呢?

圖 - 5

sshd 進程也不正常,並且意外發現 sshd 是 32 位的,因而寫了個測試程序:


圖 - 6

該程序編譯爲 32 位在目標機器上能夠重現問題,而若是編譯爲 64 位則一切正常,另外一個發現是隻要是 32 位的進程,它們的內存佈局都"不正常"。

操做系統的鍋嗎?

要搞清楚這個問題得先搞明白進程在內核裏啓動的流程,對用戶態的進程來講,任何進程都是從母進程 fork 出來後再執行 execve, execv 則主要調用對應的加載器(主要是 elf loader)來把代碼段、數據段以及動態鏈接器(ld.so,若是須要)加載進內存空間的各個相應位置,完成以後直接跳到動態鏈接器的入口(這裏先忽略靜態連接的程序),其它的動態庫都由動態庫鏈接器負責加載,須要注意的是,不管是內核加載 ld.so 仍是 ld.so 加載其它動態庫,都須要 mmap 的協助,這是用來在內存空間裏找位置用的。

如今咱們來看看內核出了什麼問題,目標系統版本以下,通過諮詢系統組的人確認,該系統基於 centos 6.5: http://vault.centos.org/6.5/centosplus/Source/SPackages/kernel-2.6.32-431.el6.centos.plus.src.rpm


圖 - 7

首先看看 arch/x86/mm/mmap.c: arch_pick_mmap_layout() 這個函數,它的做用是根據進程和當前系統的設置初化 mmap 相關的入口:


圖 - 8

Exec-shield 是一類安全功能的開關,由紅帽在不少年前主導搞的對 buffer overflow 攻擊的一系列加強,具體能夠參看這幾個鏈接 1234,exec shield 在實現和使用上一直有問題,也破壞了有些舊程序的兼容性【6】,所以一直沒進主幹,只在 redhat 家族 6.x 及其派生系統上使用。

這個功能有一個開關 /proc/sys/kernel/exec-shield,根據連接【6】上的說明,exec-shield 能夠設置爲 0、一、二、3,分別表示:強制關閉/默認關閉除非可執行程序指定打開/默認打開除非可執行程序指定關閉/強制打開。

mm->get_unmapped_area 是進程須要進行 mmap 時調用的最終函數, arch_get_unmap_area() 用來以傳統方式從低位開始搜索合適的位置,arch_get_unmapped_area_topdown() 則以 flexible layout 的方式從高位開始搜索合適的位置,關鍵點在於 125 ~ 129 行,exec-shield 引進了另外一種專門針對 32 位進程的內存分配方式,這種方式指定若是要分配的內存須要可執行權限,那麼應該從 mm->shlib_base 這裏開始搜索合適的位置,shlib_base 的值爲 SHLIB_BASE 加上一個小的隨機偏移,而 SHLIB_BASE 的值爲【7】:

圖 - 9

注意到該地址位於 32 位進程的代碼段以前(0x8048000),因此這就解釋了爲何 32 位的進程,它的動態庫被加載到了低位甚至穿插進了 brk 和數據段之間的空隙,原本這個特殊的搜索內存空間的方式是隻針對須要可執行權限的內存,但因爲 elf 加載器在加載動態庫時是分段(PT_LOAD)進行加載【8】,第一個段的位置由 mm->get_unmap_area() 搜索合適的位置分配,後續的段則使用 MAP_FIXED 強制放在了第一個段的後面,因此致使數據段也映射到了低位.【9】

下圖 1641 行展現了 mmap 時怎樣從 mm 結構裏獲取 get_area 函數,能夠看到,只要 mm->get_unmmapped_exec_area 不爲空,且要分配的內存須要可執行權限,就優先使用 mm->get_unmmapped_exec_area 進行搜索。


圖 - 10

上面這種針對 exec 內存的分配方式實際上很容易引發衝突,redhat 在這裏也是打了很多補丁,參看123

問題並無解決

上面的解釋說明了爲何 32 位進程的內存佈局會異常,可是這裏的問題是,爲何用 32 位進程起 64 位進程時,64 位的進程也一樣受到了影響。要搞清楚這裏的問題,就得看看 fs/binfmt_elf.c: load_elf_binary() 這個函數,它用來在當前進程中加載 elf 格式可執行文件並跳過去執行,此函數被 32 位的 elf 與 64 位 elf 所共用(藉助了比較隱蔽的宏),它作的事情總結起來包括以下:
一、讀取和解析 elf 文件裏包含的各類信息,關鍵信息如代碼段,數據段,動態連接器等。
二、flush_old_exec(): 中止當前進程內的全部線程,清空當前內存空間,重置各類狀態等。
三、設置新進程的狀態,如分配內存空間,初始化等。
四、加載動態鏈接器並跳過去執行。


圖 - 11

如今回到咱們問題,當前進程是 32 位的,在 64 位的系統上執行 32 位的進程須要內核支持,當內核發現 elf 是 32 位的程序時,會在 task 內部置一個標誌,這個標誌在上圖 load_elf_binary() 函數裏 740 行調用 SET_PERSONALITY() 纔會被清除,因此在 721 行時,當前進程仍認爲本身是 32 位的,flush_old_exec() 作了什麼事情呢,參看:fs/exec.c: flush_old_exec()


圖 - 12

注意其中 1039 行,bprm->mm 表示新的內存空間(舊的還在,但立刻就要釋放並切換新的),這裏須要對新的內存空間進行設置,參看: fs/exec.c: exec_mmap()


圖 - 13

咱們能夠看到在當前進程仍是 32 位的時候,內核對新的內存空間進行了初始化,致使 arch_pick_mmap_layout() 錯誤地將 arch_get_unmaped_exec_area 賦值給了 bprm->mm->get_unmapped_exec_area 這個成員變量,雖然圖-11中 load_elf_binary() 函數在 748 行,32 位的標誌被清空以後再次調用 set_up_new_exec() -> arch_get_unmapped_exec_area(),但 arch_get_unmaped_exec_area() 並無清空 mm->get_unmapped_exec_area 這個變量,致使 execv 後雖然進程是 64 位的,但仍然以 mm->shlib_base 這裏做爲起始地址搜索內存空間給動態庫使用, oops.

解決方案

最直接可靠的作法是在進入 arch_pick_mmap_layout() 時,先把 mm->get_unmapped_exec_area 置爲 NULL,但這就要修改內核了,用戶態要規避的話有如下方式:
一、設置 ulimit -s unlimited,並設置 exec-shield 爲 0 或 1,再起進程,這樣一來,由於用戶態的棧是無限長的,內核只能以傳統的方式來對 32 位進程分配內存,不會掉進 exec-shield 的坑裏。
二、把 randomize_va_space 禁掉,但這個作法只是把頭埋進了沙子裏。

總的來講,上面兩種用戶態的規避方案基本是哪裏疼往哪貼膏藥,並不是解決問題之道(且有安全隱患),退一步來講,不要用 32 位的進程來起動 64 位進程還相對穩妥點.

參考

【0】https://en.wikipedia.org/wiki/C_dynamic_memory_allocation
【1】https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/5/html/Tuning_and_Optimizing_Red_Hat_Enterprise_Linux_for_Oracle_9i_and_10g_Databases/sect-Oracle_9i_and_10g_Tuning_Guide-Growing_the_Oracle_SGA_to_2.7_GB_in_x86_Red_Hat_Enterprise_Linux_2.1_Without_VLM-Linux_Memory_Layout.html
【2】understanding the linux kernel, page 819, flexible memory region layout: https://books.google.com.hk/books?id=h0lltXyJ8aIC&pg=PT925&lpg=PT925&dq=linux+flexible+memory&source=bl&ots=gO7rIYb8HR&sig=pirB5pswdHFHSljy57EksxS3ABw&hl=en&sa=X&ved=0ahUKEwjpkfa-2_rRAhVGFJQKHcETDSUQ6AEITDAH#v=onepage&q=linux%20flexible%20memory&f=false
【3】https://gist.github.com/CMCDragonkai/10ab53654b2aa6ce55c11cfc5b2432a4
【4】http://lxr.free-electrons.com/source/arch/x86/include/asm/processor.h#L770
【5】 https://access.redhat.com/blogs/766093/posts/1975793
【6】https://lwn.net/Articles/31032/
【7】https://lwn.net/Articles/454949/
【8】http://lxr.free-electrons.com/source/fs/binfmt_elf.c#L549
【9】http://lxr.free-electrons.com/source/fs/binfmt_elf.c#L563
【10】相似問題: https://bugzilla.redhat.com/show_bug.cgi?id=870914 https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=522849

相關文章
相關標籤/搜索