1 KPTI概述
KPTI(Kernel PageTable Isolation)全稱核心頁表隔離。KPTI是由KAISER補丁修改而來。之前,行程地址空間被分成了核心地址空間和使用者地址空間。其中核心地址空間對映到了整個物理地址空間,而使用者地址空間只能對映到指定的物理地址空間。核心地址空間和使用者地址空間共用一個頁全域性目錄表(PGD表示行程的整個地址空間),meltdown漏洞就恰恰利用了這一點。攻擊者在非法訪問核心地址和CPU處理異常的時間視窗,透過訪存微指令獲取核心資料。為了徹底防止使用者程式獲取核心資料,可以令核心地址空間和使用者地址空間使用兩組頁表集(也就是使用兩個PGD)。
圖1 修改後的行程地址空間
2 問題
當然事情並沒有那麼簡單,有兩個問題:
問題1: X86架構中,在背景關係切換的間隙(註意是間隙)記憶體中的一部分需要對核心空間和使用者空間都是有效的,也就是說在切換CR3之前核心就要開始工作了。
問題2:修改CR3時,CPU會沖刷TLB,從而帶來很大的效能問題
3 KPTI實現機制
在KAISER的論文中針對這兩個問題,提出了以下解決方案
3.1 影子地址空間(Shadow Address Spaces)
KPTI中每個行程有兩個地址空間,第一個地址空間只能在核心態下訪問,可以建立到內核和使用者的對映(不過使用者空間受SMAP和SMEP保護,具體可查詢Intel手冊)。第二個地址空間被稱為影子地址空間,只包含使用者空間。不過由於涉及到背景關係切換,所以在影子地址空間中必須包含部分核心地址,用來建立到中斷入口和出口的對映。
當中斷在使用者態發生時,就涉及到切換CR3暫存器,從影子地址空間切換到使用者態的地址空間。中斷上半部的要求是盡可能的快,從而切換CR3這個操作也要求盡可能的快。為了達到這個目的,KAISER中將核心空間的PGD和使用者空間的PGD連續的放置在一個8KB的記憶體空間中。這段空間必須是8K對齊的,這樣將CR3的切換操作轉換為將CR3值的第13位(由低到高)的置位或清零操作,提高了CR3切換的速度。
使用者空間和核心空間的PGD分佈示意圖
3.2 核心空間的最小對映
上文提到,在從影子地址空間切換到核心地址空間的過程中,為了使得核心在CR3切換之前就能夠開始工作,影子地址空間必須包含部分核心地址空間。
如下圖所示,陰影處就是在陷入核心態過程中,需要對映的核心資料和程式碼。圖a 是常規OS的行程的地址空間。圖b和圖c是頁表隔離後的行程地址空間,兩者的區別再與是否使用了SMAP和SMEP機制。
那麼如何確定影子地址空間應該對映那些核心資料呢?由於中斷可能發生在使用者態,所以應該包含中斷向量表(IDT),中斷棧,中斷向量。另外核心棧,GDT和TSS也應該對映到影子地址空間。
4 效能與開銷(performance and overhead)
4.1 TLB
在intel手冊中提到,線性地址的高位被稱為頁號(page number),低位被稱為頁偏移(page offset, 如果頁大小是4K則是低12位)。物理地址的高位被稱為頁框(page frame)。
TLB用於加速從線性地址到物理地址的轉換,本質上還是一種快取。TLB使用頁號來獲取線性地址所對應的頁的基地址。TLB中的每一項包含以下內容:
-
頁號對應頁的物理地址
-
頁的訪問許可權(R/W,U/S )
-
頁屬性(dirty flag,memory type)
圖4-1 基於TLB的訪存過程
一個處理器可能包含不同型別的TLB,比如專用於取指令的TLB和用於資料訪問的TLB
切換CR3時,CPU會隱式的沖刷TLB。TLB的miss penalty可以達到10 – 100 個 時鐘週期(clock cycles)。記憶體中的一些頁(比如共享庫)的一些頁是由所有的行程共享的。這些頁由頁表項的全域性位(G)來標示。共享頁並不會參與TLB的隱式沖刷。
有兩種方法防止資料的洩露,第一種需要衝刷整個TLB,而第二種則是禁用頁表項的全域性位。
透過PCID的使用可以緩解由於沖刷TLB帶來的效能問題。
4.2 Process-Context Identifiers(PCID)
PCID全稱行程背景關係標示符,CR4暫存器的PCIDE位表示是否啟用CPU的PCID功能。PCIDE=1表示啟用PCID。啟用之後,CR3(頁目基址暫存器)的低12位用來儲存PCID。每個行程都有一個PCID,當未啟用PCID時,CR3的低12位為全0(000H)。
Intel手冊對於TLB失效的行為作出了很詳細的解釋,在使用mov指令修改CR3時會使TLB失效(mov to CR3),具體行為如下:
-
如果CR4.PCIDE = 0(表示未啟用PCID),CPU會使所有與PCID 000H關聯的TLB快取項(TLB entry)失效,除了全域性頁。
-
如果CR4.PCIDE = 1(啟用PCID),並且源運算元的第63位=0,源運算元的0-11位為指定的PCID。那麼CPU會使所有與指定PCID關聯的TLB快取項失效。TLB中與其他PCID關聯的TLB快取項並不會失效。
-
如果CR4.PCIDE=1,並且源運算元的第63位=1,CPU不會對TLB做任何的失效操作。
5 程式碼分析
我們選取linux4.15版本作為演示,說明KPTI補丁的核心中的分佈
這是4.15版本和PTI(pagetable isolation)有關的diff stat. 可以看到共涉及到45個檔案的修改,插入了1636行程式碼,刪除202行程式碼。
增加程式碼行數的前三名是
-
mm/pti.c
-
arch/x86/include/asm/tlbflush.h
-
arch/x86/entry/calling.h
5.1 arch/x86/mm/pti.c
pti.c是補丁新增的檔案. 其中的入口函式是pti_init(), 該函式在init/main.c中的mm_init()函式中呼叫。這個檔案中的函式總共分為兩種,第一種類似pti_clone_user_shared(),將內核的頁表項複製到使用者空間。第二種類似pti_user_pagetable_walk_p4d(unsigned long address),根據引數中的虛擬地址,得到該地址相應的頁表項指標。
void __init pti_init(void)
{
if (!static_cpu_has(X86_FEATURE_PTI))
return;
pr_info("enabled\n");
pti_clone_user_shared();
pti_clone_entry_text();
pti_setup_espfix64();
pti_setup_vsyscall();
}
5.2 arch/x86/include/asm/tlbflush.h
該檔案包含一系列的有關TLB flush的函式
在KPTI中並不僅僅使用PCID,由於核心中的行程地址空間標示符必須從0開始。所以ASID是地址空間真正的標示符。又因為補丁中行程的地址空間有兩個部分,所以我們需要兩個PCID。kPCID核心空間使用的標示符。uPCID使用者空間使用的標示符。
* ASID - [0, TLB_NR_DYN_ASIDS-1]
* the canonical identifier for an mm
*
* kPCID - [1, TLB_NR_DYN_ASIDS]
* the value we write into the PCID part of CR3; corresponds to the
* ASID+1, because PCID 0 is special.
*
* uPCID - [2048 + 1, 2048 + TLB_NR_DYN_ASIDS]
* for KPTI each mm has two address spaces and thus needs two
* PCID values, but we can still do with a single ASID denomination
* for each mm. Corresponds to kPCID + 2048.
#define CR3_HW_ASID_BITS 12
# define PTI_CONSUMED_PCID_BITS 1
/*
* 6 because 6 should be plenty and struct tlb_state will fit in two cache
* lines.
*/
#define TLB_NR_DYN_ASIDS 6
5.3 /arch/x86/entry/calling.h
calling.h 是系統呼叫的入口函式,用於處理系統呼叫時的暫存器儲存操作。系統呼叫涉及到由使用者態到核心態的切換。所以calling.h需要修改。
以下一系列的彙編宏指令涉及到使用者PGD和核心PGD的切換. 下麵我們挑選幾個宏進行說明:
1. SWITCH_TO_KERNEL_CR3
該宏的任務是清楚CR3儲存的PCID,並將CR3的第13置1,從而使其指向核心PGD
.macro SWITCH_TO_KERNEL_CR3 scratch_reg:req
ALTERNATIVE "jmp .Lend_\@", "", X86_FEATURE_PTI
mov %cr3, \scratch_reg
ADJUST_KERNEL_CR3 \scratch_reg
mov \scratch_reg, %cr3
.Lend_\@:
.endm
2. SWITCH_TO_USER_CR3_NOSTACK
該宏的任務是根據行程的ASID判斷其TLB是否需要flush, 如果不需要就在CR3中標記為no_flush。隨後將kPCID轉換為uPCID,並使CR3指向使用者PGD。
這一切都在很短的時間內發生,因為它們只是對CR3暫存器的置位操作。
.macro SWITCH_TO_USER_CR3_NOSTACK scratch_reg:req scratch_reg2:req
ALTERNATIVE "jmp .Lend_\@", "", X86_FEATURE_PTI
mov %cr3, \scratch_reg
ALTERNATIVE "jmp .Lwrcr3_\@", "", X86_FEATURE_PCID
/*
* Test if the ASID needs a flush.
*/
movq \scratch_reg, \scratch_reg2
andq $(0x7FF), \scratch_reg /* mask ASID */
bt \scratch_reg, THIS_CPU_user_pcid_flush_mask
jnc .Lnoflush_\@
/* Flush needed, clear the bit */
btr \scratch_reg, THIS_CPU_user_pcid_flush_mask
movq \scratch_reg2, \scratch_reg
jmp .Lwrcr3_pcid_\@
.Lnoflush_\@:
movq \scratch_reg2, \scratch_reg
SET_NOFLUSH_BIT \scratch_reg
.Lwrcr3_pcid_\@:
/* Flip the ASID to the user version */
orq $(PTI_USER_PCID_MASK), \scratch_reg
.Lwrcr3_\@:
/* Flip the PGD to the user version */
orq $(PTI_USER_PGTABLE_MASK), \scratch_reg
mov \scratch_reg, %cr3
.Lend_\@:
.endm
參考
http://www.wowotech.net/memory_management/tlb-flush.html
Intel® 64 and IA-32 Architectures Software Developer Manuals
linux 4.15原始碼
[1] D. Gruss, M. Lipp, M. Schwarz, R. Fellner, C. Maurice, and S. Mangard, “KASLR is dead: Long live KASLR,” Lect. Notes Comput. Sci. (including Subser. Lect. Notes Artif. Intell. Lect. Notes Bioinformatics), vol. 10379 LNCS, pp. 161–176, 2017.
後記
由於作者水平有限,可能某些方面敘述的不夠準確,望各位讀者海涵。也希望大家能夠來踴躍拍磚。