核心電子圍欄 (KFENCE)

核心電子圍欄 (KFENCE) 是一種低開銷的基於取樣的記憶體安全錯誤檢測器。KFENCE 檢測堆越界訪問、釋放後使用和無效釋放錯誤。

KFENCE 設計為在生產核心中啟用,並且效能開銷幾乎為零。與 KASAN 相比,KFENCE 用精度換取效能。KFENCE 設計背後的主要動機是,如果總執行時間足夠長,KFENCE 將檢測到非生產測試工作負載通常不會執行的程式碼路徑中的錯誤。快速實現足夠長的總執行時間的一種方法是在大型機器群中部署該工具。

用法

要啟用 KFENCE,請使用以下配置核心:

CONFIG_KFENCE=y

要構建支援 KFENCE 的核心,但預設情況下停用(要啟用,請將 kfence.sample_interval 設定為非零值),請使用以下配置核心:

CONFIG_KFENCE=y
CONFIG_KFENCE_SAMPLE_INTERVAL=0

KFENCE 提供了幾個其他配置選項來自定義行為(有關更多資訊,請參閱 lib/Kconfig.kfence 中的相應幫助文字)。

調整效能

最重要的引數是 KFENCE 的取樣間隔,可以透過核心啟動引數 kfence.sample_interval 以毫秒為單位設定。取樣間隔確定堆分配受到 KFENCE 保護的頻率。預設值可以透過 Kconfig 選項 CONFIG_KFENCE_SAMPLE_INTERVAL 配置。設定 kfence.sample_interval=0 會停用 KFENCE。

取樣間隔控制設定 KFENCE 分配的計時器。預設情況下,為了保持實際取樣間隔的可預測性,當系統完全空閒時,普通計時器也會導致 CPU 喚醒。這在功率受限的系統上可能是不希望的。啟動引數 kfence.deferrable=1 而是切換到“可延遲”計時器,該計時器不會在空閒系統上強制 CPU 喚醒,但存在取樣間隔不可預測的風險。預設值可以透過 Kconfig 選項 CONFIG_KFENCE_DEFERRABLE 配置。

警告

由於它目前會導致非常不可預測的取樣間隔,因此在使用可延遲計時器時,KUnit 測試套件很可能會失敗。

預設情況下,KFENCE 僅在每個取樣間隔內取樣 1 個堆分配。 *Burst mode* 允許取樣連續的堆分配,其中核心啟動引數 kfence.burst 可以設定為非零值,該值表示取樣間隔內的 *additional* 連續分配;設定 kfence.burst=N 意味著每個取樣間隔都嘗試透過 KFENCE 進行 1 + N 個連續分配。

KFENCE 記憶體池的大小是固定的,如果池耗盡,則不會發生進一步的 KFENCE 分配。使用 CONFIG_KFENCE_NUM_OBJECTS(預設值為 255),可以控制可用的受保護物件數量。每個物件需要 2 個頁面,一個用於物件本身,另一個用作保護頁面;物件頁面與保護頁面交錯,因此每個物件頁面都被兩個保護頁面包圍。

專用於 KFENCE 記憶體池的總記憶體可以計算為

( #objects + 1 ) * 2 * PAGE_SIZE

使用預設配置,並假設頁面大小為 4 KiB,則將 2 MiB 專用於 KFENCE 記憶體池。

注意:在支援大頁面的架構上,KFENCE 將確保池使用大小為 PAGE_SIZE 的頁面。這將導致分配額外的頁表。

錯誤報告

典型的越界訪問如下所示

==================================================================
BUG: KFENCE: out-of-bounds read in test_out_of_bounds_read+0xa6/0x234

Out-of-bounds read at 0xffff8c3f2e291fff (1B left of kfence-#72):
 test_out_of_bounds_read+0xa6/0x234
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

kfence-#72: 0xffff8c3f2e292000-0xffff8c3f2e29201f, size=32, cache=kmalloc-32

allocated by task 484 on cpu 0 at 32.919330s:
 test_alloc+0xfe/0x738
 test_out_of_bounds_read+0x9b/0x234
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

CPU: 0 PID: 484 Comm: kunit_try_catch Not tainted 5.13.0-rc3+ #7
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

報告的標題提供了訪問中涉及的函式的簡短摘要。 緊隨其後的是有關訪問及其來源的更詳細資訊。 請注意,只有在使用核心命令列選項 no_hash_pointers 時才會顯示真實的核心地址。

釋放後使用訪問報告為

==================================================================
BUG: KFENCE: use-after-free read in test_use_after_free_read+0xb3/0x143

Use-after-free read at 0xffff8c3f2e2a0000 (in kfence-#79):
 test_use_after_free_read+0xb3/0x143
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

kfence-#79: 0xffff8c3f2e2a0000-0xffff8c3f2e2a001f, size=32, cache=kmalloc-32

allocated by task 488 on cpu 2 at 33.871326s:
 test_alloc+0xfe/0x738
 test_use_after_free_read+0x76/0x143
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

freed by task 488 on cpu 2 at 33.871358s:
 test_use_after_free_read+0xa8/0x143
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

CPU: 2 PID: 488 Comm: kunit_try_catch Tainted: G    B             5.13.0-rc3+ #7
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

KFENCE 還會報告無效的釋放,例如雙重釋放

==================================================================
BUG: KFENCE: invalid free in test_double_free+0xdc/0x171

Invalid free of 0xffff8c3f2e2a4000 (in kfence-#81):
 test_double_free+0xdc/0x171
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

kfence-#81: 0xffff8c3f2e2a4000-0xffff8c3f2e2a401f, size=32, cache=kmalloc-32

allocated by task 490 on cpu 1 at 34.175321s:
 test_alloc+0xfe/0x738
 test_double_free+0x76/0x171
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

freed by task 490 on cpu 1 at 34.175348s:
 test_double_free+0xa8/0x171
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

CPU: 1 PID: 490 Comm: kunit_try_catch Tainted: G    B             5.13.0-rc3+ #7
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

KFENCE 還在物件保護頁面的另一側使用基於模式的紅色區域,以檢測物件未保護側的越界寫入。 這些在釋放時報告

==================================================================
BUG: KFENCE: memory corruption in test_kmalloc_aligned_oob_write+0xef/0x184

Corrupted memory at 0xffff8c3f2e33aff9 [ 0xac . . . . . . ] (in kfence-#156):
 test_kmalloc_aligned_oob_write+0xef/0x184
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

kfence-#156: 0xffff8c3f2e33afb0-0xffff8c3f2e33aff8, size=73, cache=kmalloc-96

allocated by task 502 on cpu 7 at 42.159302s:
 test_alloc+0xfe/0x738
 test_kmalloc_aligned_oob_write+0x57/0x184
 kunit_try_run_case+0x61/0xa0
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x176/0x1b0
 ret_from_fork+0x22/0x30

CPU: 7 PID: 502 Comm: kunit_try_catch Tainted: G    B             5.13.0-rc3+ #7
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

對於此類錯誤,將顯示發生損壞的地址以及無效寫入的位元組(從地址偏移);在此表示中,“.” 表示未觸及的位元組。 在上面的示例中,0xac 是寫入偏移量 0 處的無效地址的值,其餘的“.” 表示沒有觸及以下位元組。 請注意,只有在核心使用 no_hash_pointers 啟動時才會顯示真實值;為了避免資訊洩露,否則,使用“!”代替表示無效寫入的位元組。

最後,KFENCE 還可能報告對任何受保護頁面的無效訪問,其中無法確定關聯的物件,例如,如果相鄰的物件頁面尚未分配

==================================================================
BUG: KFENCE: invalid read in test_invalid_access+0x26/0xe0

Invalid read at 0xffffffffb670b00a:
 test_invalid_access+0x26/0xe0
 kunit_try_run_case+0x51/0x85
 kunit_generic_run_threadfn_adapter+0x16/0x30
 kthread+0x137/0x160
 ret_from_fork+0x22/0x30

CPU: 4 PID: 124 Comm: kunit_try_catch Tainted: G        W         5.8.0-rc6+ #7
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1 04/01/2014
==================================================================

DebugFS 介面

一些除錯資訊透過 debugfs 公開

  • 檔案 /sys/kernel/debug/kfence/stats 提供執行時統計資訊。

  • 檔案 /sys/kernel/debug/kfence/objects 提供透過 KFENCE 分配的物件的列表,包括已釋放但受保護的物件。

實現細節

保護分配是根據取樣間隔設定的。 取樣間隔到期後,透過主分配器(SLAB 或 SLUB)的下一個分配將從 KFENCE 物件池返回保護分配(支援高達 PAGE_SIZE 的分配大小)。 此時,計時器被重置,並在間隔到期後設置下一個分配。

當使用 CONFIG_KFENCE_STATIC_KEYS=y 時,KFENCE 分配透過靜態鍵基礎設施依靠靜態分支透過主分配器的快速路徑進行“門控”。 切換靜態分支以將分配重定向到 KFENCE。 根據取樣間隔、目標工作負載和系統架構,這可能比簡單的動態分支執行得更好。 建議進行仔細的基準測試。

KFENCE 物件每個都位於專用頁面上,位於隨機選擇的左側或右側頁面邊界。 物件頁面左側和右側的頁面是“保護頁面”,其屬性更改為受保護狀態,並在任何嘗試訪問時導致頁面錯誤。 然後,此類頁面錯誤被 KFENCE 攔截,KFENCE 透過報告越界訪問並標記頁面為可訪問來優雅地處理該錯誤,以便發生錯誤的的程式碼可以(錯誤地)繼續執行(設定 panic_on_warn 以改為 panic)。

為了檢測物件頁面本身內的記憶體的越界寫入,KFENCE 還使用基於模式的紅色區域。 對於每個物件頁面,為所有非物件記憶體設定一個紅色區域。 對於典型的對齊方式,紅色區域僅需要在物件的未保護側。 因為 KFENCE 必須遵守快取請求的對齊方式,所以特殊的對齊方式可能會導致物件任一側出現未受保護的間隙,所有這些間隙都是紅色區域。

下圖說明了頁面佈局

---+-----------+-----------+-----------+-----------+-----------+---
   | xxxxxxxxx | O :       | xxxxxxxxx |       : O | xxxxxxxxx |
   | xxxxxxxxx | B :       | xxxxxxxxx |       : B | xxxxxxxxx |
   | x GUARD x | J : RED-  | x GUARD x | RED-  : J | x GUARD x |
   | xxxxxxxxx | E :  ZONE | xxxxxxxxx |  ZONE : E | xxxxxxxxx |
   | xxxxxxxxx | C :       | xxxxxxxxx |       : C | xxxxxxxxx |
   | xxxxxxxxx | T :       | xxxxxxxxx |       : T | xxxxxxxxx |
---+-----------+-----------+-----------+-----------+-----------+---

在取消分配 KFENCE 物件後,物件的頁面再次受到保護,並且該物件被標記為已釋放。 任何進一步訪問該物件都會導致錯誤,並且 KFENCE 報告釋放後使用訪問。 釋放的物件被插入到 KFENCE 空閒列表的尾部,以便首先重用最近釋放的物件,並增加檢測到最近釋放的物件釋放後使用的機會。

如果池利用率達到 75%(預設值)或以上,為了降低池最終被已分配的物件完全佔用的風險,同時確保分配的多樣化覆蓋,KFENCE 限制同一來源的當前覆蓋分配進一步填滿池。“分配的來源”基於其部分分配堆疊跟蹤。 一個副作用是,這也限制了同一來源的頻繁的長壽命分配(例如頁面快取)永久地填滿池,這是池變得已滿且取樣分配率降至零的最常見風險。 可以透過啟動引數 kfence.skip_covered_thresh(池使用率 %)配置開始限制當前覆蓋分配的閾值。

介面

以下描述了分配器以及頁面處理程式碼用於設定和處理 KFENCE 分配的函式。

bool is_kfence_address(const void *addr)

檢查地址是否屬於 KFENCE 池

引數

const void *addr

要檢查的地址

返回

true 或 false,取決於地址是否在 KFENCE 物件範圍內。

描述

KFENCE 物件位於單獨的頁面範圍內,不得與常規堆物件混合(例如,KFENCE 物件絕不能新增到分配器空閒列表中)。 否則可能會並且將會導致堆損壞,因此必須使用 is_kfence_address() 來檢查物件是否需要特定的處理。

注意

此函式可以在快速路徑中使用,並且對效能至關重要。 未來的更改應考慮到這一點;例如,我們希望避免引入另一個負載,因此需要保持 KFENCE_POOL_SIZE 不變(直到向核心新增即時修補支援)。

void kfence_shutdown_cache(struct kmem_cache *s)

處理 KFENCE 物件的 shutdown_cache()

引數

struct kmem_cache *s

正在關閉的快取

描述

在關閉快取之前,必須確保沒有剩餘的從中分配的物件。 因為 KFENCE 物件不是直接從快取引用的,所以我們需要在此處檢查它們。

請注意,shutdown_cache() 是 SL*B 的內部函式,如果分配的物件仍然存在,則 kmem_cache_destroy() 不會返回:它會列印一條錯誤訊息,並簡單地中止快取的銷燬,從而導致記憶體洩漏。

如果唯一此類物件是 KFENCE 物件,我們將不會洩漏整個快取,而是嘗試透過使分配的物件成為“殭屍分配”來提供更有用的除錯資訊。 然後,物件仍然可以使用或釋放(這將得到優雅的處理),但使用將導致顯示 KFENCE 錯誤報告,其中包括物件使用者的堆疊跟蹤、原始分配站點和 shutdown_cache() 的呼叫方。

void *kfence_alloc(struct kmem_cache *s, size_t size, gfp_t flags)

以低機率分配 KFENCE 物件

引數

struct kmem_cache *s

struct kmem_cache 具有物件要求

size_t size

要分配的物件的精確大小(可以小於 s->size 例如,對於 kmalloc 快取)

gfp_t flags

GFP 標誌

返回

  • NULL - 必須像往常一樣繼續分配,

  • 非 NULL - 指向 KFENCE 物件的指標。

描述

kfence_alloc() 應該插入到堆分配快速路徑中,允許它使用靜態分支以低機率透明地返回 KFENCE 分配的物件(機率由 kfence.sample_interval 啟動引數控制)。

size_t kfence_ksize(const void *addr)

獲取為 KFENCE 物件分配的實際記憶體量

引數

const void *addr

指向堆物件的指標

返回

  • 0 - 不是 KFENCE 物件,必須改為呼叫 __ksize(),

  • 非 0 - 可以訪問這麼多位元組,而不會導致記憶體錯誤。

描述

kfence_ksize() 返回在分配時為 KFENCE 物件請求的位元組數。 此數字可能小於相應 struct kmem_cache 的物件大小。

void *kfence_object_start(const void *addr)

查詢 KFENCE 物件的開頭

引數

const void *addr

KFENCE 分配的物件中的地址

返回

物件開頭的地址。

描述

SL[AU]B 分配的物件一個接一個地佈置在頁面中,因此給定其中的指標和物件大小,很容易計算出物件的開頭。 對於 KFENCE 來說,情況並非如此,它將單個物件放置在頁面的任一端。 此幫助函式用於查詢 KFENCE 分配的物件的開頭。

void __kfence_free(void *addr)

將 KFENCE 堆物件釋放到 KFENCE 池

引數

void *addr

要釋放的物件

描述

要求:is_kfence_address(addr)

釋放 KFENCE 物件並將其標記為已釋放。

bool kfence_free(void *addr)

嘗試將任意堆物件釋放到 KFENCE 池

引數

void *addr

要釋放的物件

返回

  • false - 物件不屬於 KFENCE 池,因此被忽略,

  • true - 物件已釋放到 KFENCE 池。

描述

釋放 KFENCE 物件並將其標記為已釋放。 可以在任何物件上呼叫,甚至是非 KFENCE 物件,以簡化將鉤子整合到分配器的空閒程式碼路徑中。 分配器必須檢查返回值以確定它是否是 KFENCE 物件。

bool kfence_handle_page_fault(unsigned long addr, bool is_write, struct pt_regs *regs)

對 KFENCE 頁面執行頁面錯誤處理

引數

unsigned long addr

發生錯誤的地址

bool is_write

訪問是否為寫入

struct pt_regs *regs

當前 struct pt_regs(可以為 NULL,但顯示完整的堆疊跟蹤)

返回

  • false - 地址在 KFENCE 池之外,

  • true - 頁面錯誤由 KFENCE 處理,不需要額外的處理。

描述

KFENCE 池內的頁面錯誤表示記憶體錯誤,例如越界訪問、釋放後使用或無效的記憶體訪問。 在這些情況下,KFENCE 會列印一條錯誤訊息,並將有問題的頁面標記為存在,以便核心可以繼續執行。