Skip to main content

KPTI Support

Introduction​

CPU の脆弱性である Meltdown 脆弱性への対策手法として Linux では Kernel Page Table Isolation が行われています.このページでは Linux v5.8.13 上の KPTI によるページテーブルの切り替えがどのように行われるかを実際のコードを追って説明していきます.

Page Table Switching at System Call Entry​

User to Kernel​

KPTI では kernel 用のページテーブルと user 用のページテーブルを分けているため,CPU モードが切り替わるタイミングでページテーブルを切り替える必要があります.ここではシステムコールを発行したときにページテーブルのルートポインタが格納されている CR3 が切り替えられる処理を見ていきます.

ユーザプロセスがシステムコールを発行すると,arch/x86/entry/entry_64.S にある entry_SYSCALL_64 というエントリーポイントから実行が始まります.システムコールを実行するためにページテーブルの切り替えやレジスタの退避を行ったあとに call do_syscall_64 を実行することでシステムコール本体が呼ばれます.

このページではページテーブルの切り替えについて見ていくので,SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp に注目します.

SYM_CODE_START(entry_SYSCALL_64)
UNWIND_HINT_EMPTY

swapgs
/* tss.sp2 is scratch space. */
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
pushq %rax /* pt_regs->orig_ax */

PUSH_AND_CLEAR_REGS rax=$-ENOSYS

/* IRQs are off. */
movq %rax, %rdi
movq %rsp, %rsi
call do_syscall_64 /* returns with IRQs disabled */
...

SWITCH_TO_KERNEL_CR3 は arch/x86/entry/calling.h で次のように定義されています.

.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

KPTI が有効な場合は ADJUST_KERNEL_CR3 が実行されます.これも arch/x86/entry/calling.h で定義されており,下記のようになっています.

/*
#define PAGE_SHIFT 12
*/
#define PTI_USER_PGTABLE_BIT PAGE_SHIFT
#define PTI_USER_PGTABLE_MASK (1 << PTI_USER_PGTABLE_BIT)

/*
#ifdef CONFIG_PAGE_TABLE_ISOLATION
# define X86_CR3_PTI_PCID_USER_BIT 11
#endif
*/
#define PTI_USER_PCID_BIT X86_CR3_PTI_PCID_USER_BIT
#define PTI_USER_PCID_MASK (1 << PTI_USER_PCID_BIT)

#define PTI_USER_PGTABLE_AND_PCID_MASK (PTI_USER_PCID_MASK | PTI_USER_PGTABLE_MASK)

...

.macro ADJUST_KERNEL_CR3 reg:req
ALTERNATIVE "", "SET_NOFLUSH_BIT \reg", X86_FEATURE_PCID
/* Clear PCID and "PAGE_TABLE_ISOLATION bit", point CR3 at kernel pagetables: */
andq $(~PTI_USER_PGTABLE_AND_PCID_MASK), \reg
/* andq ~(0x1800), \reg */
.endm

上記のコードによれば,11 bit 目と 12 bit 目をクリアすることでカーネル用のページテーブルのルートポインタに変換できます.

Kernel to User​

システムコールの実行が終了してユーザプロセスに戻る際の切り替えは SWITCH_TO_USER_CR3_STACK で行われます.

...
SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi

popq %rdi
popq %rsp
USERGS_SYSRET64
SYM_CODE_END(entry_SYSCALL_64)

一見すると長いコードに見えますが,カーネルに切り替えるときと逆の処理を行うだけなので CR3 の 11 bit 目と 12 bit 目を適宜 1 にしているだけです.

.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

/*
SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
*/
.macro SWITCH_TO_USER_CR3_STACK scratch_reg:req
pushq %rax
SWITCH_TO_USER_CR3_NOSTACK scratch_reg=\scratch_reg scratch_reg2=%rax
popq %rax
.endm

実験​

実際にプロセスの CR3 をユーザプロセス実行中とカーネル実行中に出力したときの実験結果を見てみます.CR3 は QEMU/KVM で KVM 側から kvm_cr3_read() によって取得しました.

実験結果は下記のようになっており,ソースコードで確認したとおりの変換が行われていることがわかりました.

kernel cr3 = 0x273abe003
user cr3 = 0x273abf803