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
-
處理器
+關注
關注
68文章
19312瀏覽量
230033 -
Linux
+關注
關注
87文章
11312瀏覽量
209701
原文標題:KPTI補丁分析
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論