記憶體保護鍵

記憶體保護鍵提供了一種強制實施基於頁的保護機制,而無需在應用程式更改保護域時修改頁表。

Pkeys 使用者空間 (PKU) 是一項可在以下處理器上找到的功能:
  • 英特爾伺服器 CPU,Skylake 及更新版本

  • 英特爾客戶端 CPU,Tiger Lake(第 11 代酷睿)及更新版本

  • 未來的 AMD CPU

  • 實現許可權覆蓋擴充套件 (FEAT_S1POE) 的 arm64 CPU

x86_64

Pkeys 的工作原理是:在每個頁表中,將 4 個先前保留的位專門用於一個“保護鍵”,從而提供 16 個可能的鍵。

每個鍵的保護由一個 per-CPU 使用者可訪問暫存器 (PKRU) 定義。每個 PKRU 都是一個 32 位暫存器,為 16 個鍵中的每個鍵儲存兩位(訪問停用和寫入停用)。

作為 CPU 暫存器,PKRU 本質上是執行緒區域性的,可能使每個執行緒擁有與其他所有執行緒不同的保護集。

有兩條指令(RDPKRU/WRPKRU)用於讀寫暫存器。即使理論上 PAE PTE 中有空間,該功能也僅在 64 位模式下可用。這些許可權僅在資料訪問時強制執行,對指令獲取沒有影響。

arm64

Pkeys 在每個頁表項中使用 3 位來編碼一個“保護鍵索引”,從而提供 8 個可能的鍵。

每個鍵的保護由一個 per-CPU 使用者可寫系統暫存器 (POR_EL0) 定義。這是一個 64 位暫存器,用於編碼每個保護鍵索引的讀、寫和執行覆蓋許可權。

作為 CPU 暫存器,POR_EL0 本質上是執行緒區域性的,可能使每個執行緒擁有與其他所有執行緒不同的保護集。

與 x86_64 不同,保護鍵許可權也適用於指令獲取。

系統呼叫

有 3 個系統呼叫直接與 pkeys 互動

int pkey_alloc(unsigned long flags, unsigned long init_access_rights)
int pkey_free(int pkey);
int pkey_mprotect(unsigned long start, size_t len,
                  unsigned long prot, int pkey);

在使用 pkey 之前,必須先用 pkey_alloc() 分配它。應用程式直接向特定於架構的 CPU 暫存器寫入,以更改由鍵覆蓋的記憶體的訪問許可權。在此示例中,這被一個名為 pkey_set() 的 C 函式封裝。

int real_prot = PROT_READ|PROT_WRITE;
pkey = pkey_alloc(0, PKEY_DISABLE_WRITE);
ptr = mmap(NULL, PAGE_SIZE, PROT_NONE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
ret = pkey_mprotect(ptr, PAGE_SIZE, real_prot, pkey);
... application runs here

現在,如果應用程式需要更新“ptr”處的資料,它可以獲得訪問許可權,進行更新,然後移除其寫訪問許可權

pkey_set(pkey, 0); // clear PKEY_DISABLE_WRITE
*ptr = foo; // assign something
pkey_set(pkey, PKEY_DISABLE_WRITE); // set PKEY_DISABLE_WRITE again

現在當它釋放記憶體時,它也會釋放 pkey,因為它不再被使用

munmap(ptr, PAGE_SIZE);
pkey_free(pkey);

注意

pkey_set() 是對寫入 CPU 暫存器的封裝。示例實現可以在 tools/testing/selftests/mm/pkey-{arm64,powerpc,x86}.h 中找到

行為

核心嘗試使保護鍵的行為與普通的 mprotect() 一致。例如,如果你這樣做

mprotect(ptr, size, PROT_NONE);
something(ptr);

你可以預期使用保護鍵時也會有相同的效果

pkey = pkey_alloc(0, PKEY_DISABLE_WRITE | PKEY_DISABLE_READ);
pkey_mprotect(ptr, size, PROT_READ|PROT_WRITE, pkey);
something(ptr);

無論 something() 是對“ptr”的直接訪問,例如

*ptr = foo;

還是核心代表應用程式進行訪問,例如使用 read() 時,都應該如此

read(fd, ptr, 1);

在這兩種情況下,核心都會發送 SIGSEGV,但是當違反保護鍵時 si_code 將設定為 SEGV_PKERR,而當違反普通 mprotect() 許可權時則設定為 SEGV_ACCERR。

請注意,kthread 的核心訪問(例如 io_uring)將使用保護鍵暫存器的預設值,因此與使用者空間中暫存器的值或 mprotect() 不一致。