當前位置:
首頁 > 科技 > 這個 CPU bug 的鍋「Intel or 阿里雲」誰背?

這個 CPU bug 的鍋「Intel or 阿里雲」誰背?

以下內容整理自微博用戶@agentzh,個人資料:OpenResty Inc. 創始人兼 CEO;OpenResty 開源項目創始人;Nginx 貢獻者。

我們 OpenResty Inc 的一家付費客戶所使用的少數幾台阿里雲的機器的 Xeon E5 的 CPU 存在 bug。進程崩潰在了不可能崩潰的地方,即當 CPU 的執行流跳轉到實際有執行許可權的內存頁時,會拋內存頁沒能執行許可權的異常。我們使用自己的隨機請求壓測工具,在有問題的阿里雲主機上,每天可以復現好幾個這樣的 core dump。在網上看最早在 luajit 列表列表有用戶報告:

鏈接:https://www.freelists.org/post/luajit/SIGSEGV-in-LuaJIT-21-VM

然後前不久 Cloudflare 也遇到了完全相同的 CPU bug:

(已經在文末翻譯供大家參考)

Cloudflare 確認了更新 Intel CPU 的微碼之後可以解決這個問題。

根據 Cloudflare 的試驗結果,應該是一個和超線程有關的微碼更新修復了這個問題。於是我們建了工單,找阿里雲的工程師去升級這些機器的微碼到最新版本。阿里雲說已經更新了微碼,卻說不需要重啟物理機。這就搞笑了。我們分明每天還能看到這個 bug 導致的 core dump。我心裡說你們重啟一下會死啊……

我們的客戶已經開始嘗試換用其他公有雲了。

軟體 bug 我們都能修,如果是 CPU 的 bug,公有雲廠商不配合,我們就真沒轍了。阿里雲的工程師非要我們提供一個最小化的很簡單的程序來複現,你當這是軟體 bug 啊。這種 Intel 都沒說清楚的硬體 bug 我們能用 fuzzer 和隨機假流量一天一台機器復現好幾次就已經不錯了。Cloudflare 直接拿生產流量試的。

另一方面,Intel 也確實不靠譜,CPU 體系結構設計得越來越複雜,不出問題才怪。現在報出那麼嚴重的安全問題,居然也死撐著說自己沒有問題,然後逼著 Linux 這些操作系統引入一堆極噁心的補丁。也難怪 Intel 提補丁的工程師會被 Linus 老大罵得狗血淋頭了。看到一堆的 bullshit,fucking insane 的字眼。

CPU 異常讓進程崩掉了,肯定會影響當前進程正在處理中的所有那些請求的。整個服務肯定不會都掉線,nginx master 肯定會自動重啟新的 worker 頂上去的。你說的那種忽略崩潰的做法,正是之前導致 Cloudflare Cloudbleeding 安全事故的原因(好在那個不是我的鍋,也不是 OpenResty 的)。

以下是Cloudflare 也遇到 CPU bug 的詳細文章,雲頭條翻譯如下,供大家參考~

由於Meltdown和Spectre這兩個漏洞,處理器問題最近見諸報章。但是編寫軟體的工程師通常想當然地認為,計算機硬體以一種可靠的、易於理解的方式運行;任何問題出在軟體/硬體這道鴻溝的軟體這邊。現代處理器晶元經常一秒鐘執行數十億條指令,所以任何不穩定的行為肯定極難觸發,否則這種行為很快就會顯露無遺。

但有時候,認為處理器硬體很可靠的這種想法並不成立。去年,我們Cloudflare受到了英特爾的一款處理器中一個bug的影響。本文介紹我們如何發現自己遇到了神秘的問題,以及我們如何查明原因。

序言

早在2017年2月,Cloudflare就披露了一個名為Cloudbleed的安全問題:

鏈接:https://blog.cloudflare.com/incident-report-on-memory-leak-caused-by-cloudflare-parser-bug/

這個事件背後的bug存在於在我們的伺服器上運行的用來解析HTML的代碼中。在碰到無效HTML的某些情況下,解析代碼從被解析的緩衝區末尾之外的內存區域讀取數據。相鄰內存可能含有其他客戶的數據,這些數據隨後在HTTP響應中返回,結果出現了Cloudbleed問題。

但這不是這個bug的唯一後果。有時它會導致無效的內存讀取,導致NGINX進程崩潰,而我們的度量指標表明在發現Cloudbleed之前的幾周出現了這些崩潰。於是我們採取諸多措施,防止這類問題再次發生,其中一項措施是要求對每次崩潰進行詳細的調查。

我們迅速採取了行動來應對Cloudbleed,因此消除了這個bug引起的崩潰,但是這並沒有阻止所有崩潰。我們開始著力研究另外這些崩潰。

崩潰不是一個技術術語

但在這種情況下,「崩潰」究竟是什麼意思?處理器發現企圖訪問無效內存(更準確地說,訪問頁表中沒有有效頁面的地址)時,它會向操作系統的內核發出頁面錯誤信號。以Linux為例,這些頁面錯誤導致SIGSEGV信號傳送給相關進程(SIGSEGV這個名稱源自於以前的Unix術語「segmentation violation」,又叫內存段錯誤或segfault)。SIGSEGV的默認行為是終止進程。這種突然終止正是Cloudbleed缺陷的一個癥狀。

可能出現無效內存訪問以及因而終止這種情況,主要與用C或C++編寫的進程密切相關。較高級的編譯語言(比如Go和基於JVM的語言)使用類型系統來防止可能導致訪問無效內存的那種低級編程錯誤。此外,這類語言有複雜的運行時環境,充分利用頁面錯誤,以獲得提高效率的實現技巧(https://pdos.csail.mit.edu/6.828/2017/readings/appel-li.pdf,進程可以為SIGSEGV安裝信號處理程序,那樣它不會被終止,而是可以在遇到這種情形後恢復)。

針對Python之類的解釋型語言,解釋器檢查以確保導致無效內存訪問的情況不會出現。所以,未處理的SIGSEGV信號往往僅限於用C和C++編寫的代碼。

SIGSEGV不是表明進程出錯,導致終止的唯一信號。我們還發現了進程因SIGABRT和SIGILL被終止的情況,這表明我們的代碼存在其他類型的bug。

如果我們擁有的關於這些被終止的NGINX程序的唯一信息是相關信號,調查原因將會很難。但是Linux(以及Unix衍生的其他操作系統)的另一項特性提供了一條前進道路,那就是核心轉儲(core dump)。核心轉儲是進程被突然終止時,操作系統寫入的一個文件。它記錄下進程被終止時的完整狀態,以便事後調試。記錄的狀態包括如下:

進程中所有線程的處理器寄存器值(一些程序變數的值將保存在寄存器中)

過程的常規內存區域中的內容(給出其他程序變數和堆數據的值)

關於作為文件只讀映射的內存區域的描述,比如可執行文件和共享庫

與導致終止的信號有關的信息,比如導致SIGSEGV的企圖訪問內存的地址

由於核心轉儲記錄了所有這些狀態,它們的大小取決於涉及的程序,但它們可能相當大。我們的NGINX核心轉儲常常有數GB大小。

一旦核心轉儲被記錄下來,可以使用gdb之類的調試工具來檢查它。這樣就可以從原始程序源代碼的角度來探究核心轉儲的狀態,因而你就能相當方便地查詢程序堆棧、變數和堆的內容。

捎帶提一下:為什麼core dump被稱為核心轉儲?這個歷史術語起源於20世紀60年代,當時磁芯存儲器還是隨機存取存儲器的一種主要形式。當時,core這個詞用作內存的簡寫,所以「核心轉儲」意味著內存內容的轉儲。

圖片來源:Konstantin Lanzet

好戲在上演

我們在研究分析核心轉儲時,能夠查明其中一些核心轉儲歸咎於代碼中的更多bug。它們沒有一個像Cloudbleed那樣泄露數據,也沒有給我們的客戶帶來其他安全方面的影響。有一些可能讓攻擊者得以企圖影響我們的服務,不過核心轉儲表明,這些bug是在無害情況下,而不是在攻擊情況下被觸發的。在生成的核心轉儲數量大幅下降之前,我們沒必要修復許多這類bug。

但是仍有一些核心轉儲在我們的伺服器上生成――我們的整批伺服器上每天大概生成一個核心轉儲。事實證明找到這些剩餘核心轉儲的根源來得更難。

我們逐漸開始懷疑這些剩餘的核心轉儲並不歸咎於代碼中的bug。之所以有這樣的懷疑,是由於我們發現在一些情況下,從程序代碼來看,核心轉儲中記錄的狀態似乎不可能(而在分析這些情況時,我們沒有依賴C代碼,而是查看編譯器生成的機器碼,萬一我們處理編譯器bug也說不定)。起初,我們在Cloudflare的工程師當中討論這些核心轉儲時,一些人對於根源可能不在代碼這個想法抱有合理的懷疑,甚至有人開玩笑說是宇宙射線引起的。但是隨著我們積累的例子越來越多,很顯然出現了不同尋常的情況。正如我們所說的那樣,找到另一個「神秘核心轉儲」成了例行事務,不過這種核心轉儲的細節多種多樣,觸發它們的代碼分布在我們的代碼庫當中。一個共同的特點是,它們顯然不可能。

生成這些神秘核心轉儲的伺服器當中沒有明顯的模式。我們從整批伺服器平均每天獲得大概一個核心轉儲。所以樣本數不是很大,但是似乎在我們所有的伺服器和數據中心當中均勻分布,沒有一台伺服器發生過兩次。單台伺服器獲得神秘核心轉儲的概率似乎非常低(伺服器每正常運行10年才有大概一個核心轉儲,假設它們對我們的所有伺服器而言的確有同樣的可能性)。但由於我們擁有大量伺服器,我們看到持續不斷的零星的轉儲現象。

尋求解決方案

生成神秘核心轉儲的速度足夠慢,並沒有顯著影響我們為客戶提供的服務。但是我們仍然致力於探究出現的每次核心轉儲。雖然我們越來越擅長識別這些神秘核心轉儲,但是對它們進行調查和分類很耗費技術資源。我們想要找到根源,並加以解決。於是我們開始考慮似乎有點合理的根源:

我們分析了硬體問題。尤其是內存錯誤的可能性很大。但我們的伺服器使用ECC(糾錯碼)內存,這種內存可以檢測(在大多數情況下糾正)出現的任何內存錯誤。此外,任何內存錯誤都會記錄在伺服器的IPMI日誌中。我們確實在自己的整批伺服器上看到了一些內存錯誤,但是它們與核心轉儲無關。

如果不是內存錯誤,那麼會不會是處理器硬體存在問題?我們主要使用各種型號的英特爾至強處理器。這種處理器一貫很可靠,雖然生成核心轉儲的速度很慢,但似乎太高了,不可能歸咎於處理器錯誤。我們上網搜索類似問題的報道,打聽內幕消息,但是沒有聽到似乎與我們的問題相符合的消息。

我們在調查過程中,英特爾Skylake處理器的一個問題(https://lists.debian.org/debian-devel/2017/06/msg00308.html)浮出了水面。但當時我們的生產環境中沒有基於Skylake的伺服器;此外,這個問題與並不是我們神秘核心轉儲的常見特徵的特定代碼模式有關。

也許核心轉儲被Linux內核錯誤地記錄下來,因而我們代碼中的bug引起的普通崩潰最後看起來很神秘?但我們在核心轉儲中並沒有看到表明這種現象的任何模式。此外,一遇到未處理的SIGSEGV,內核會在日誌中生成一行,附有關於原因的少量信息,就像這樣:

我們對照核心轉儲檢查了這些日誌行,它們始終保持一致。

內核扮演的一個角色是,控制處理器的內存管理單元,為應用程序提供虛擬內存。因此,這個方面的內核錯誤會導致令人驚訝的結果(我們Cloudflare在不同的環境中遇到了這類bug)。但是我們檢查了內核代碼,搜索Linux中相關bug的報道,卻一無所獲。

幾周下來,雖然我們努力查找原因,但毫無成效。由於從每台伺服器來考慮,神秘核心轉儲的頻率非常低,因此我們無法採用通常解決問題的最後一招:改變各種可能的誘發因素,希望它們讓問題更有可能發生或更不可能發生。我們需要另一條思路。

解決方案

但是最終,我們注意到了在此之前一直忽視的重要方面:所有神秘核心轉儲都來自含有英特爾至強E5-2650 v4的伺服器。這個型號屬於代號為「Broadwell」的這一代英特爾處理器,它也是我們在邊緣伺服器中使用的那一代處理器的唯一型號,於是我們簡單地稱這些伺服器為Broadwell。Broadwell當時佔了我們伺服器總數的三分之一,它們用在我們的許多數據中心。這解釋為什麼這個模式沒有一眼看出來。

明白了這點後,我們立即將調查重心轉回到處理器硬體問題這種可能性上。我們下載了針對這個型號的英特爾規格更新(Specification Update,https://www.intel.co.uk/content/www/uk/en/processors/xeon/xeon-e5-v4-spec-update.html)。

在這些規格更新文檔中,英特爾披露了其處理器偏離已發布規格的所有方式,無論是由於良性差異,還是硬體中的bug――英特爾有意稱之為「errata」(勘誤)。

規格更新描述了85個問題,大部分問題是主要該由BIOS和操作系統的開發者關注的模糊問題。但是有一個問題引起了我們的注意:「BDF76支持英特爾超線程技術的處理器可能會出現內部奇偶校驗錯誤或不可預測的系統行為」。為這個問題描述的癥狀非常籠統(「不可預測的系統行為可能會發生」),但是我們觀察到的現象似乎與這個問題的描述再貼合不過了。

此外,規格更新聲明BDF76在微碼更新中已得到了修復。微碼是控制處理器最低級別操作的固件,可以由BIOS(來自系統供應商)或操作系統來更新。微碼更新可以在一定程度上改變處理器的行為(具體改變多少是英特爾嚴加保守的秘密,不過最近的微碼更新以解決Spectre漏洞多少表明了英特爾能夠大幅重新調整處理器的行為)。

當時,我們對Broadwell伺服器打上微碼更新的最便捷方式就是,通過伺服器供應商提供的BIOS更新。但是向那麼多數據中心中的那麼多伺服器部署BIOS更新需要一些規劃和時間才能進行。由於生成神秘核心轉儲的速度很慢,除非我們的一大部分Broadwell伺服器被更新,否則不知道BDF76是不是真的是我們問題的根源。

一旦更新完畢,神秘核心轉儲就停止了,這讓我們大鬆了一口氣。該圖顯示了2017年相關月份我們每天獲得的核心轉儲數量:

核心轉儲

正如你所見,微碼更新後,生成核心轉儲的速度顯著降低。但是我們仍獲得一些核心轉儲。這些不是神秘核心轉儲,而是代表我們軟體中的傳統問題。我們繼續調查並解決這些轉儲,確保它們不是我們服務中的安全問題。

結論

消除神秘核心轉儲後,更容易專註於我們的代碼導致的任何其餘的崩潰。因而避免了這種情況:就因為原因不明確,不認真考慮核心轉儲。

至於我們現在看到的一些核心轉儲,了解原因可能非常困難。它們對應不太可能的情形,而且常常涉及與觸發核心轉儲的直接問題相差甚遠的根源。比如說,我們在LuaJIT(我們通過OpenResty嵌入到NGINX中)中看到段錯誤,它們不是歸咎於LuaJIT的問題,而是由於LuaJIT特別容易受無關的C代碼中的bug對其數據結構造成的破壞的影響。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 雲頭條 的精彩文章:

DeepMind:詳解生成式對抗網路

TAG:雲頭條 |