Heartbleed 是來自OpenSSL的緊急安全警告:OpenSSL出現“Heartbleed”安全漏洞。這一漏洞讓任何人都能讀取系統的運行內存,文名稱叫做“心髒出血”、““擊穿心髒””等。
為什麼固定大小緩沖區這麼流行
心髒出血漏洞是最新發現的安全問題,由長字符串導致緩沖區越界。最常見的緩沖區越界發生在如下兩種條件同時滿足中:
程序中一個組件A向另外一個組件B傳遞了一個指針,也可能同時傳遞長度信息
組件B忽略了,或者沒有正確使用這個長度信息。此信息規定了指針所指向的內存區域能夠存儲多少數據。
上述條件都滿足的程序結構之所以能引起緩沖區越界,一個重要原因是,調用者A分配了一塊內存,但是只有當數據被真正讀取的時候,才能知道程序到底需要分配多大的內存空間,因為不會被讀取的數據,我們完全可以不保存它。 換句話說,一個函數負責分配空間,然後調用另外一個函數向該空間填充數據的結構,都會有點不安全。
即使這點危險能夠通過正確的檢查內存邊界的方式成功避免,但是邊界檢查也會引入其自身存在的負面效果。 比如,我的一位前同事, 他創建了一個文本文件, 此文本文件壓縮了數萬個字符構成的單行字符串。 然後他又將這個文件作為輸入,傳遞給了許多其他部件,比如編譯器,文本處理程序等等。 幾乎所有的這些程序都會出現這樣那樣的異常行為,例如,直接崩潰,或者悄無聲息的忽略掉輸入字符串的最後一截。
應對該問題的簡單解決方案是:如果程序中任何部分涉及讀入長度不確定的輸入,那就應該負責分配足夠大的內存來保存這些輸入。當然,在C++語言中使用STL標准庫就能輕松實現。但是在C語言中,卻沒有簡單有效的實現代碼,可以從輸入讀入一個單行字符串,返回包含該輸入的內存指針,無視輸入的長度。 任何在C語言中實現此功能的嘗試,都或多或少的存在一些副作用。
我也曾靜下心來在當時工作的部門,嘗試在C語言庫中增加一個針對上述問題的解決方案。 如果有人想要將使用了我寫的函數的代碼分享到別的地方,我想讓他們也能將我寫的函數作為其中一部分發布出去。 我所增加的函數的名稱是readline,且為方便使用而設計:只需要傳入一個文件指針(例如 stdin)作為輸入,此函數就能讀入一整行的輸入,返回一個指向以NULL結尾的此字符串的第一個字符,無需考慮輸入的長度。 如果讀到了文件結束符(EOF),就返回一個NULL指針。
顯然,任何分配內存,並返回指向該內存指針的函數都存在一個問題:該內存何時被釋放? 我考慮過讓readline函數的調用者負責釋放,但是覺得很多調用函數可能會忘記釋放內存。那麼此時的緩沖區越界問題又變成了內存洩漏問題。
最後,我決定采取在別的地方看到的策略:readline將會返回一個指向內存空間的指針,並且保證其中的內容在下一次調用readline函數之前都會保持不變。這種策略不僅可以減少用戶的擔憂,而且也能讓實現更簡單:程序將存儲一個靜態指針(static pointer) 指向(動態分配的)緩沖區。緩沖區的大小將隨讀入的行的長度需要增減。 這種機制能讓readline函數在最常用的場景中簡單好用,並且安全。
當然,這種機制也有他自身存在的問題。比如,在同一個表達式中,兩次調用readline函數將導致未定義行為(undefined behavior)。因為當程序員計劃在第二次調用readline()函數之後,試圖保存兩次調用readline所讀入的全部數據時,第一次調用所創建的內存空間,將在第二次調用時被釋放掉。 此外,該代碼會在讀入輸入的最後一行後,因為不再被調用,會一直占用內存空間。實際上,它所浪費的內存空間是整個輸入中最長的那一行的長度。在實現該函數時,雖然我在緩沖區小於輸入行長度時,都會重新分配更大的緩沖區,但是卻沒有允許緩沖區變小。因為我覺得反復分配釋放內存的所導致的性能下降,相比於在少數清醒下浪費一點點內存空間來說不值得。
很顯然,我高估了人們所能忍受的內存分配延遲開銷:當我幾個月後回頭看這些代碼時,發現有人已經將我所寫的readline版本完全修改為固定的4096-字符緩沖區。據我所了解,他的動機是完全避免運行時存儲分配的開銷。換句話說,為了避免只有在少數情況下才存在的多次內存分配器調用,他悄悄的讓所有使用readline函數的程序,在行的長度大於4096個字符時,出現了很大的安全隱患。
之所以花了大量的篇幅講這樣一個故事,是因為它透露出我覺得非常重要的幾點:
緩沖區越界通常發生在程序中某個部分A分配內存,而實際需要的存儲空間大小只有另一個部分B知道。
在程序中的同一個函數內部分配內存,並將其填充。這種方式解決了緩沖區分配的問題,而付出的代價是必須要讓程序的另一個函數負責內存的釋放。內存的分配和釋放在程序的兩個不同的函數中。
這種分配和釋放在兩個不同的函數將會導致程序的可用性問題,除非在編程語言上有系統的支持,否則很難繞開。
即使用戶為了安全和通用性,需要接收這個現實,但是他們可能也無法接受動態分配內存引入的開銷。
我想,程序員不願為了安全而引入運行時開銷,是很多安全性問題之所以普遍存在的原因。 我們將在下周詳細聊聊這種現象。