1. 程式人生 > >Intel CPU漏洞技術解讀:都是快取惹的禍!

Intel CPU漏洞技術解讀:都是快取惹的禍!

漏洞

背景

2017年6月1日,Google的安全團隊向Intel、AMD、ARM報了一個硬體級的漏洞,造成的危害是核心資料洩露,修復該漏洞的代價是至少30%的效能損失。2017年末Linux核心社群推出了KPTI「Kernel Page Table Isolation」補丁,Linus Torvalds在核心郵件列表上毫不留情地抨擊了Intel。2017年6月1日,Google的安全團隊向Intel、AMD、ARM報了一個硬體級的漏洞,造成的危害是核心資料洩露,修復該漏洞的代價是至少30%的效能損失。2017年末Linux核心社群推出了KPTI「Kernel Page Table Isolation」補丁,Linus Torvalds在核心郵件列表上毫不留情地抨擊了Intel。

安全人員將這兩個漏洞命名為MeltdownSpectre;Meltdown目前只存在於Intel的處理器和部分ARM處理器,Spectre存在於一切有亂序執行的現代處理器架構裡面,包括AMD。從原理上來說漏洞無法徹底修復。

本次的漏洞會對所有雲廠商造成較大影響,已經有跡象表明有黑客在利用漏洞攻擊雲系統。Microsoft Azure中國區已釋出公告稱,將於北京時間2018 年 1 月 4 日上午 11:30 開始自動重啟受影響的虛擬機器,並全部關閉向部分客戶開放的自助維護視窗;AWS也傳送了通知郵件聲稱本週五將進行重大安全更新。

原因

一切還是要從CPU指令執行的框架——流水線說起。Intel當然不至於明知你要用一個使用者態的程序讀取Kernel記憶體還會給你許可。但現代CPU流水線的設計,尤其是和效能優化相關的流水線的特性,讓這一切充滿了變數。

給所有還沒有看過雲杉網路連載的系列文章《x86高效能程式設計箋註系列》的讀者一點背景知識的介紹:

x86 CPU為了優化效能,在處理器架構方面做了很多努力。諸如“多級快取”這一類的特性,是大家都比較熟悉的概念。還有一些特性,比如分支預測和亂序執行,也都是一些可以從並行性等方面有效提升程式效能的特性,並且它們也都是組成流水線的幾個關鍵環節。即便你暫時還不能準確理解其含義,但望文生義,也能看出來這肯定是兩個熵增的過程。熵增帶來無序,無序就會帶來更多漏洞。

快取的困境

講快取,必然先掛一張memory hierarchy鎮樓:

快取

不過我要說的和這個沒太大關係。現在需要考慮的是,如果能讀取到核心地址的內容,那這部分內容最終肯定是跑到快取中去了,因為真正直接和CPU核心互動的儲存器,就是快取。這對一級快取(L1 Cache,業內也常用縮寫L1$,取cash之音)提出的要求就是,必須要非常快,唯有如此才能跟上CPU處理核心的速度。

Side Notes: 為什麼在不考慮成本的情況下快取不是越大越好,也是因為當快取規模越大,查詢某一特定資料就會越慢。而快取首先要滿足的要求就是快,其他的都是次要的。

根據核心的基本知識我們知道,程序執行時都有一個虛擬地址「Virtual address」和其所對應的實體地址「physical address」。

從虛擬地址到實體地址的翻譯轉換也由CPU通過page table完成。Page table並不儲存在CPU裡,但近期查詢到的Page table entry「PTE」都像資料一樣,快取在了CPU中的translation lookaside buffer「TLB」裡。為了不再過多堆砌術語和名詞,畫張圖說明一下:

CPU

當CPU根據程式要求需要讀取某個地址上的資料時,首先會在L1 Cache中查詢。為了適應CPU的速度,L1快取實現為Virtually indexed physically tagged「VIPT」的形式,即用虛擬地址即可直接讀取該虛擬地址對應的實體地址的內容,而不再需要多加一道轉換的工序。

如果L1 Cache miss,則會在下級快取中查詢。但越過L1 Cache之後,對L2$和L3$的速度要求就不再這麼嚴苛。此時CPU core給出的虛擬地址請求會先通過TLB轉換為實體地址,再送入下級快取中查詢。而檢查程序有沒有許可權讀取某一地址這一過程,僅在地址轉換的時候發生,而這種轉換和檢查是需要時間的,所以有意地安排在了L1 Cache之後。

L1快取這種必須求“快”的特性,成了整個事件的楔子。

分支預測

分支預測是一種提高流水線執行效率的手段。在遇到if..else..這種程式執行的分支時,可以通過以往的歷史記錄判斷哪一分支是最可能被執行的分支,並在分支判斷條件真正返回判斷結果之前提前執行分支的程式碼。詳情可以在上面提到的連載文章中閱讀。

需要強調的是,提前執行的分支程式碼,即便事後證明不是正確的分支,其執行過程中所讀取的資料也可以進入L1快取。在Intel的官網文件《Intel® 64 and IA-32 Architectures Optimization Reference Manual》第2.3.5.2節中指:

L1 DCache Loads:

– Be carried out speculatively, before preceding branches are resolved.

– Take cache misses out of order and in an overlapped manner.

Show you the [偽] code:

if (likely(A < B)) {    value = *(kernel_address_pointer);}

當分支判斷條件A < B被預測為真時,CPU會去提前執行對核心地址的讀取。當實際條件為A > B時,雖然核心的值不會真正寫入暫存器(沒有retire),但會存入L1 Cache,再加之上一節介紹的,獲取L1 Cache的值毋須地址轉換,毋須許可權檢查,這就為核心資訊的洩漏創造了可能。

從理論上來講,如果可以控制程式的分支判斷,並且可以獲取L1快取中的資料(這個沒有直接方法,但可以通過其他間接手法)的話,就完全可以獲取核心資訊。而分支預測這種特性是不能隨隨便便就關閉的,這也就是這次問題會如此棘手的原因。

亂序執行

還有一個原因是亂序執行,但原理大致類似。亂序執行是Intel在1995年首次引入Pentium Pro處理器的機制。其過程首先是將我們在彙編程式碼中看到的指令“打散”,成為更細粒度的微指令「micro-operations」,更小的指令粒度將會帶來更多的亂序排列的組合,CPU真正執行的是這些微指令。

沒有資料依賴的微指令在有相應執行資源的情況下亂序並行執行,進而提升程式的並行程度,提高程式效能。但引入的問題是,讀取核心資料的微指令可能會在流水線發出exception之前將核心資料寫入L1 Cache。與分支選擇一樣,為通過使用者態程序獲取核心程式碼提供了可能。

限於篇幅,更詳細的內容讀者可以在國外安全團隊釋出的訊息中獲取。

後續

剛剛查閱之前連載中的一些細節的時候,看到在“流水線”那一章裡寫過這樣一段話:

在面對問題的時候,人總是會傾向於引入一個更復雜的機制來解決問題,多級流水線就是一個例子。複雜可以反映出技術的改良,但“複雜”本身就是一個新的問題。這也許就是矛盾永遠不會消失,技術也不會停止進步的原因。但“為學日益,為道日損”,愈發複雜的機制總會在某個時機之下發生大破大立,但可能現在時機還沒有到來:D

很難講現在是不是就是所謂的那個“時機”。雖然對整個行業都產生了負面影響,但我對此仍保持樂觀。因為這就是事物自然發展的一個正常過程。效能損失並不是一件壞事,尤其是對牙膏廠的使用者來說。

作者:

一個不耽誤碼字的網工

張攀,雲杉網路工程師,專注於x86網路軟體的開發與效能優化,深度參與ONF/OPNFV/ONOS等組織及社群,曾任ONF測試工作組副主席。

原文來自微信公眾號:雲杉網路