核心地址消毒器 (KASAN)¶
概述¶
核心地址消毒器 (KASAN) 是一種動態記憶體安全錯誤檢測器,旨在查詢越界和釋放後使用錯誤。
KASAN 具有三種模式
通用 KASAN
基於軟體標籤的 KASAN
基於硬體標籤的 KASAN
通用 KASAN,透過 CONFIG_KASAN_GENERIC 啟用,是用於除錯的模式,類似於使用者空間 ASan。此模式在許多 CPU 架構上受支援,但它具有顯著的效能和記憶體開銷。
基於軟體標籤的 KASAN 或 SW_TAGS KASAN,透過 CONFIG_KASAN_SW_TAGS 啟用,可用於除錯和內部測試,類似於使用者空間 HWASan。此模式僅支援 arm64,但其適度的記憶體開銷允許在記憶體受限的裝置上使用真實工作負載進行測試。
基於硬體標籤的 KASAN 或 HW_TAGS KASAN,透過 CONFIG_KASAN_HW_TAGS 啟用,旨在用作現場記憶體錯誤檢測器或安全緩解措施。此模式僅適用於支援 MTE (記憶體標記擴充套件) 的 arm64 CPU,但其記憶體和效能開銷較低,因此可在生產中使用。
有關每種 KASAN 模式的記憶體和效能影響的詳細資訊,請參閱相應 Kconfig 選項的說明。
通用模式和基於軟體標籤的模式通常稱為軟體模式。基於軟體標籤的模式和基於硬體標籤的模式稱為基於標籤的模式。
支援¶
架構¶
通用 KASAN 在 x86_64、arm、arm64、powerpc、riscv、s390、xtensa 和 loongarch 上受支援,基於標籤的 KASAN 模式僅在 arm64 上受支援。
編譯器¶
軟體 KASAN 模式使用編譯時插樁在每次記憶體訪問之前插入有效性檢查,因此需要提供支援的編譯器版本。 基於硬體標籤的模式依賴於硬體來執行這些檢查,但仍然需要支援記憶體標記指令的編譯器版本。
通用 KASAN 需要 GCC 8.3.0 或更高版本,或者核心支援的任何 Clang 版本。
基於軟體標籤的 KASAN 需要 GCC 11+ 或核心支援的任何 Clang 版本。
基於硬體標籤的 KASAN 需要 GCC 10+ 或 Clang 12+。
記憶體型別¶
通用 KASAN 支援查詢 slab、page_alloc、vmap、vmalloc、stack 和全域性記憶體中的錯誤。
基於軟體標籤的 KASAN 支援 slab、page_alloc、vmalloc 和 stack 記憶體。
基於硬體標籤的 KASAN 支援 slab、page_alloc 和不可執行的 vmalloc 記憶體。
對於 slab,軟體 KASAN 模式都支援 SLUB 和 SLAB 分配器,而基於硬體標籤的 KASAN 僅支援 SLUB。
用法¶
要啟用 KASAN,請使用以下命令配置核心
CONFIG_KASAN=y
並在 CONFIG_KASAN_GENERIC (啟用通用 KASAN)、CONFIG_KASAN_SW_TAGS (啟用基於軟體標籤的 KASAN) 和 CONFIG_KASAN_HW_TAGS (啟用基於硬體標籤的 KASAN) 之間進行選擇。
對於軟體模式,還可以在 CONFIG_KASAN_OUTLINE 和 CONFIG_KASAN_INLINE 之間進行選擇。 Outline 和 inline 是編譯器插樁型別。 前者產生較小的二進位制檔案,而後者速度最多快 2 倍。
要將受影響的 slab 物件的 alloc 和 free 堆疊跟蹤包含到報告中,請啟用 CONFIG_STACKTRACE。 要將受影響的物理頁面的 alloc 和 free 堆疊跟蹤包含到報告中,請啟用 CONFIG_PAGE_OWNER 並使用 page_owner=on 引導。
啟動引數¶
KASAN 受通用 panic_on_warn 命令列引數的影響。 啟用後,KASAN 會在列印錯誤報告後使核心崩潰。
預設情況下,KASAN 僅列印第一個無效記憶體訪問的錯誤報告。 使用 kasan_multi_shot,KASAN 會列印每個無效訪問的報告。 這實際上為 KASAN 報告停用了 panic_on_warn。
或者,獨立於 panic_on_warn,kasan.fault= 啟動引數可用於控制崩潰和報告行為
kasan.fault=report、=panic或=panic_on_write控制是僅列印 KASAN 報告、使核心崩潰還是僅在無效寫入時使核心崩潰 (預設值:report)。 即使啟用了kasan_multi_shot,也會發生崩潰。 請注意,當使用基於硬體標籤的 KASAN 的非同步模式時,kasan.fault=panic_on_write始終會因非同步檢查的訪問 (包括讀取) 而崩潰。
基於軟體和硬體標籤的 KASAN 模式 (請參閱下面有關各種模式的部分) 支援更改堆疊跟蹤收集行為
kasan.stacktrace=off或=on停用或啟用 alloc 和 free 堆疊跟蹤收集 (預設值:on)。kasan.stack_ring_size=<條目數>指定堆疊環中的條目數 (預設值:32768)。
基於硬體標籤的 KASAN 模式旨在用於生產中,作為一種安全緩解措施。 因此,它支援其他啟動引數,允許完全停用 KASAN 或控制其功能
kasan=off或=on控制是否啟用 KASAN (預設值:on)。kasan.mode=sync、=async或=asymm控制是否在同步、非同步或非對稱執行模式下配置 KASAN (預設值:sync)。 同步模式:當發生標籤檢查故障時,會立即檢測到錯誤的訪問。 非同步模式:會延遲錯誤的訪問檢測。 當發生標籤檢查故障時,資訊儲存在硬體中 (對於 arm64,儲存在 TFSR_EL1 暫存器中)。 核心定期檢查硬體,並且僅在這些檢查期間報告標籤故障。 非對稱模式:在讀取時同步檢測到錯誤的訪問,而在寫入時非同步檢測到錯誤的訪問。kasan.vmalloc=off或=on停用或啟用 vmalloc 分配的標記 (預設值:on)。kasan.page_alloc.sample=<取樣間隔>使 KASAN 僅標記每第 N 個 page_alloc 分配,其中順序等於或大於kasan.page_alloc.sample.order,其中 N 是sample引數的值 (預設值:1,或者標記每個此類分配)。 此引數旨在緩解 KASAN 引入的效能開銷。 請注意,啟用此引數會使基於硬體標籤的 KASAN 跳過抽樣選擇的分配的檢查,從而錯過對這些分配的錯誤訪問。 使用預設值進行準確的錯誤檢測。kasan.page_alloc.sample.order=<最小頁面順序>指定受抽樣影響的分配的最小順序 (預設值:3)。 僅當kasan.page_alloc.sample設定為大於1的值時才適用。 此引數旨在僅允許抽樣大型 page_alloc 分配,這是效能開銷的最大來源。
錯誤報告¶
典型的 KASAN 報告如下所示
==================================================================
BUG: KASAN: slab-out-of-bounds in kmalloc_oob_right+0xa8/0xbc [kasan_test]
Write of size 1 at addr ffff8801f44ec37b by task insmod/2760
CPU: 1 PID: 2760 Comm: insmod Not tainted 4.19.0-rc3+ #698
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1 04/01/2014
Call Trace:
dump_stack+0x94/0xd8
print_address_description+0x73/0x280
kasan_report+0x144/0x187
__asan_report_store1_noabort+0x17/0x20
kmalloc_oob_right+0xa8/0xbc [kasan_test]
kmalloc_tests_init+0x16/0x700 [kasan_test]
do_one_initcall+0xa5/0x3ae
do_init_module+0x1b6/0x547
load_module+0x75df/0x8070
__do_sys_init_module+0x1c6/0x200
__x64_sys_init_module+0x6e/0xb0
do_syscall_64+0x9f/0x2c0
entry_SYSCALL_64_after_hwframe+0x44/0xa9
RIP: 0033:0x7f96443109da
RSP: 002b:00007ffcf0b51b08 EFLAGS: 00000202 ORIG_RAX: 00000000000000af
RAX: ffffffffffffffda RBX: 000055dc3ee521a0 RCX: 00007f96443109da
RDX: 00007f96445cff88 RSI: 0000000000057a50 RDI: 00007f9644992000
RBP: 000055dc3ee510b0 R08: 0000000000000003 R09: 0000000000000000
R10: 00007f964430cd0a R11: 0000000000000202 R12: 00007f96445cff88
R13: 000055dc3ee51090 R14: 0000000000000000 R15: 0000000000000000
Allocated by task 2760:
save_stack+0x43/0xd0
kasan_kmalloc+0xa7/0xd0
kmem_cache_alloc_trace+0xe1/0x1b0
kmalloc_oob_right+0x56/0xbc [kasan_test]
kmalloc_tests_init+0x16/0x700 [kasan_test]
do_one_initcall+0xa5/0x3ae
do_init_module+0x1b6/0x547
load_module+0x75df/0x8070
__do_sys_init_module+0x1c6/0x200
__x64_sys_init_module+0x6e/0xb0
do_syscall_64+0x9f/0x2c0
entry_SYSCALL_64_after_hwframe+0x44/0xa9
Freed by task 815:
save_stack+0x43/0xd0
__kasan_slab_free+0x135/0x190
kasan_slab_free+0xe/0x10
kfree+0x93/0x1a0
umh_complete+0x6a/0xa0
call_usermodehelper_exec_async+0x4c3/0x640
ret_from_fork+0x35/0x40
The buggy address belongs to the object at ffff8801f44ec300
which belongs to the cache kmalloc-128 of size 128
The buggy address is located 123 bytes inside of
128-byte region [ffff8801f44ec300, ffff8801f44ec380)
The buggy address belongs to the page:
page:ffffea0007d13b00 count:1 mapcount:0 mapping:ffff8801f7001640 index:0x0
flags: 0x200000000000100(slab)
raw: 0200000000000100 ffffea0007d11dc0 0000001a0000001a ffff8801f7001640
raw: 0000000000000000 0000000080150015 00000001ffffffff 0000000000000000
page dumped because: kasan: bad access detected
Memory state around the buggy address:
ffff8801f44ec200: fc fc fc fc fc fc fc fc fb fb fb fb fb fb fb fb
ffff8801f44ec280: fb fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
>ffff8801f44ec300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03
^
ffff8801f44ec380: fc fc fc fc fc fc fc fc fb fb fb fb fb fb fb fb
ffff8801f44ec400: fb fb fb fb fb fb fb fb fc fc fc fc fc fc fc fc
==================================================================
報告標頭總結了發生的錯誤型別以及導致錯誤的訪問型別。 後面是錯誤訪問的堆疊跟蹤、訪問的記憶體的分配位置的堆疊跟蹤 (如果訪問了 slab 物件) 以及物件釋放位置的堆疊跟蹤 (如果是釋放後使用錯誤報告)。 接下來是訪問的 slab 物件的描述以及有關訪問的記憶體頁面的資訊。
最後,報告顯示了訪問地址周圍的記憶體狀態。 在內部,KASAN 分別跟蹤每個記憶體粒度的記憶體狀態,這取決於 KASAN 模式是 8 個或 16 個對齊的位元組。 報告的記憶體狀態部分中的每個數字都顯示了圍繞訪問地址的記憶體粒度之一的狀態。
對於通用 KASAN,每個記憶體粒度的大小為 8。 每個粒度的狀態都編碼在一個影子位元組中。 這 8 個位元組可以是可訪問的、部分可訪問的、已釋放的或屬於 redzone 的一部分。 KASAN 使用以下編碼用於每個影子位元組:00 表示相應記憶體區域的所有 8 個位元組都可以訪問; 數字 N (1 <= N <= 7) 表示前 N 個位元組可以訪問,其他 (8 - N) 個位元組不可訪問; 任何負值都表示整個 8 位元組字都不可訪問。 KASAN 使用不同的負值來區分不同型別的不可訪問記憶體,例如 redzone 或釋放的記憶體 (請參閱 mm/kasan/kasan.h)。
在上面的報告中,箭頭指向影子位元組 03,這意味著訪問的地址是部分可訪問的。
對於基於標籤的 KASAN 模式,此最後的報告部分顯示了訪問地址周圍的記憶體標籤 (請參閱實現細節部分)。
請注意,KASAN 錯誤標題 (如 slab-out-of-bounds 或 use-after-free) 是盡力而為:KASAN 根據它擁有的有限資訊列印最可能的錯誤型別。 錯誤的實際型別可能不同。
通用 KASAN 還會報告最多兩個輔助呼叫堆疊跟蹤。 這些堆疊跟蹤指向程式碼中與物件互動但在錯誤訪問堆疊跟蹤中未直接顯示的位置。 目前,這包括 call_rcu() 和工作佇列排隊。
CONFIG_KASAN_EXTRA_INFO¶
啟用 CONFIG_KASAN_EXTRA_INFO 允許 KASAN 記錄和報告更多資訊。 當前支援的額外資訊是在分配和釋放時的 CPU 編號和時間戳。 更多資訊有助於查詢錯誤原因並將錯誤與其他系統事件相關聯,代價是使用額外的記憶體來記錄更多資訊 (更多成本詳細資訊請參閱 CONFIG_KASAN_EXTRA_INFO 的幫助文字)。
這是啟用 CONFIG_KASAN_EXTRA_INFO 的報告 (僅顯示不同的部分)
==================================================================
...
Allocated by task 134 on cpu 5 at 229.133855s:
...
Freed by task 136 on cpu 3 at 230.199335s:
...
==================================================================
實現細節¶
通用 KASAN¶
軟體 KASAN 模式使用影子記憶體來記錄記憶體的每個位元組是否可以安全訪問,並使用編譯時插樁在每次記憶體訪問之前插入影子記憶體檢查。
通用 KASAN 將核心記憶體的 1/8 專用於其影子記憶體 (16TB 以覆蓋 x86_64 上的 128TB),並使用帶有比例和偏移量的直接對映將記憶體地址轉換為其相應的影子地址。
這是將地址轉換為其相應影子地址的函式
static inline void *kasan_mem_to_shadow(const void *addr)
{
return (void *)((unsigned long)addr >> KASAN_SHADOW_SCALE_SHIFT)
+ KASAN_SHADOW_OFFSET;
}
其中 KASAN_SHADOW_SCALE_SHIFT = 3。
編譯時插樁用於插入記憶體訪問檢查。 編譯器在每次大小為 1、2、4、8 或 16 的記憶體訪問之前插入函式呼叫 (__asan_load*(addr), __asan_store*(addr))。 這些函式透過檢查相應的影子記憶體來檢查記憶體訪問是否有效。
使用內聯插樁,編譯器直接插入程式碼以檢查影子記憶體,而不是進行函式呼叫。 此選項顯著擴大了核心,但與輪廓插樁核心相比,它提供了 x1.1-x2 的效能提升。
通用 KASAN 是唯一透過隔離區延遲重用已釋放物件的模式 (有關實現,請參閱 mm/kasan/quarantine.c)。
基於軟體標籤的 KASAN¶
基於軟體標籤的 KASAN 使用軟體記憶體標記方法來檢查訪問有效性。 目前僅為 arm64 架構實現了該方法。
基於軟體標籤的 KASAN 使用 arm64 CPU 的頂部位元組忽略 (TBI) 功能在核心指標的頂部位元組中儲存指標標籤。 它使用影子記憶體來儲存與每個 16 位元組記憶體單元關聯的記憶體標籤 (因此,它將核心記憶體的 1/16 專用於影子記憶體)。
在每次記憶體分配時,基於軟體標籤的 KASAN 都會生成一個隨機標籤,使用此標籤標記分配的記憶體,並將相同的標籤嵌入到返回的指標中。
基於軟體標籤的 KASAN 使用編譯時插樁在每次記憶體訪問之前插入檢查。 這些檢查確保被訪問的記憶體的標籤等於用於訪問此記憶體的指標的標籤。 如果標籤不匹配,則基於軟體標籤的 KASAN 會列印錯誤報告。
基於軟體標籤的 KASAN 也有兩種插樁模式 (outline,它發出回撥以檢查記憶體訪問;以及 inline,它內聯執行影子記憶體檢查)。 使用 outline 插樁模式,錯誤報告從執行訪問檢查的函式中列印。 使用 inline 插樁,編譯器發出 brk 指令,並且使用專用的 brk 處理程式來列印錯誤報告。
基於軟體標籤的 KASAN 使用 0xFF 作為匹配所有指標標籤 (不檢查透過帶有 0xFF 指標標籤的指標進行的訪問)。 值 0xFE 當前保留用於標記釋放的記憶體區域。
基於硬體標籤的 KASAN¶
基於硬體標籤的 KASAN 在概念上與軟體模式相似,但使用硬體記憶體標記支援而不是編譯器插樁和影子記憶體。
基於硬體標籤的 KASAN 目前僅為 arm64 架構實現,並且基於 ARMv8.5 指令集架構中引入的 arm64 記憶體標記擴充套件 (MTE) 和頂部位元組忽略 (TBI)。
專用 arm64 指令用於為每次分配分配記憶體標籤。 相同的標籤分配給指向這些分配的指標。 在每次記憶體訪問時,硬體都會確保被訪問的記憶體的標籤等於用於訪問此記憶體的指標的標籤。 如果標籤不匹配,則會生成故障並列印報告。
基於硬體標籤的 KASAN 使用 0xFF 作為匹配所有指標標籤 (不檢查透過帶有 0xFF 指標標籤的指標進行的訪問)。 值 0xFE 當前保留用於標記釋放的記憶體區域。
如果硬體不支援 MTE (pre ARMv8.5),則不會啟用基於硬體標籤的 KASAN。 在這種情況下,所有 KASAN 啟動引數都將被忽略。
請注意,啟用 CONFIG_KASAN_HW_TAGS 始終會導致啟用核心內 TBI。 即使提供了 kasan.mode=off 或當硬體不支援 MTE (但支援 TBI) 時。
基於硬體標籤的 KASAN 僅報告第一個找到的錯誤。 之後,MTE 標籤檢查將被停用。
影子記憶體¶
本節的內容僅適用於軟體 KASAN 模式。
核心在地址空間的幾個不同部分中對映記憶體。 核心虛擬地址的範圍很大:沒有足夠的真實記憶體來支援每個可由核心訪問的地址的真實影子區域。 因此,KASAN 僅為地址的某些部分對映真實影子。
預設行為¶
預設情況下,架構僅將真實記憶體對映到線性對映的影子區域上 (以及可能其他小區域)。 對於所有其他區域 (例如 vmalloc 和 vmemmap 空間),單個只讀頁面對映到影子區域上。 此只讀影子頁面宣告所有記憶體訪問都已允許。
這給模組帶來了問題:它們不線上性對映中,而是在專用的模組空間中。 透過掛鉤到模組分配器,KASAN 臨時對映真實影子記憶體以覆蓋它們。 這允許檢測到對模組全域性變數的無效訪問,例如。
這也建立了與 VMAP_STACK 的不相容性:如果堆疊位於 vmalloc 空間中,它將被只讀頁面隱藏,並且核心在嘗試為堆疊變數設定影子資料時將出現故障。
CONFIG_KASAN_VMALLOC¶
使用 CONFIG_KASAN_VMALLOC,KASAN 可以覆蓋 vmalloc 空間,但代價是更大的記憶體使用量。 目前,x86、arm64、riscv、s390 和 powerpc 支援此功能。
這透過掛鉤到 vmalloc 和 vmap 並動態分配真實影子記憶體來支援對映來實現。
vmalloc 空間中的大多數對映都很小,需要小於整個頁面的影子空間。 因此,為每個對映分配整個影子頁面將是浪費的。 此外,為了確保不同的對映使用不同的影子頁面,對映必須與 KASAN_GRANULE_SIZE * PAGE_SIZE 對齊。
相反,KASAN 在多個對映之間共享後備空間。 當 vmalloc 空間中的對映使用影子區域的特定頁面時,它會分配一個後備頁面。 此頁面可以稍後由其他 vmalloc 對映共享。
KASAN 掛鉤到 vmap 基礎結構以延遲清理未使用的影子記憶體。
為了避免圍繞交換對映的困難,KASAN 希望覆蓋 vmalloc 空間的那部分影子區域不會被早期的影子頁面覆蓋,而是保持未對映狀態。 這將需要在特定於架構的程式碼中進行更改。
這允許 x86 上支援 VMAP_STACK,並可以簡化對沒有固定模組區域的架構的支援。
面向開發者¶
忽略訪問¶
軟體 KASAN 模式使用編譯器插樁來插入有效性檢查。 此類插樁可能與核心的某些部分不相容,因此需要停用。
核心的其他部分可能會訪問已分配物件的元資料。 通常,KASAN 會檢測並報告此類訪問,但在某些情況下 (例如,在記憶體分配器中),這些訪問是有效的。
對於軟體 KASAN 模式,要停用特定檔案或目錄的插樁,請將 KASAN_SANITIZE 註釋新增到相應的核心 Makefile
對於單個檔案 (例如 main.o)
KASAN_SANITIZE_main.o := n
對於一個目錄中的所有檔案
KASAN_SANITIZE := n
對於軟體 KASAN 模式,要在每個函式的基礎上停用插樁,請使用 KASAN 特定的 __no_sanitize_address 函式屬性或通用的 noinstr 函式屬性。
請注意,停用編譯器插樁 (無論是按檔案還是按函式) 會使 KASAN 忽略軟體 KASAN 模式下直接在該程式碼中發生的訪問。 當訪問間接發生 (透過呼叫插樁函式) 或使用不使用編譯器插樁的基於硬體標籤的 KASAN 時,它無濟於事。
對於軟體 KASAN 模式,要在核心程式碼的一部分中為當前任務停用 KASAN 報告,請使用 kasan_disable_current()/kasan_enable_current() 部分註釋程式碼的這一部分。 這也會停用透過函式呼叫發生的間接訪問的報告。
對於基於標籤的 KASAN 模式,要停用訪問檢查,請使用 kasan_reset_tag() 或 page_kasan_tag_reset()。 請注意,透過 page_kasan_tag_reset() 臨時停用訪問檢查需要透過 page_kasan_tag/page_kasan_tag_set 儲存和恢復每個頁面的 KASAN 標籤。
測試¶
有一些 KASAN 測試允許驗證 KASAN 是否工作以及是否可以檢測某些型別的記憶體損壞。
所有 KASAN 測試都與 KUnit 測試框架整合,可以透過 CONFIG_KASAN_KUNIT_TEST 啟用。 這些測試可以以幾種不同的方式執行和部分自動驗證; 請參閱下面的說明。
如果檢測到錯誤,每個 KASAN 測試都會列印多個 KASAN 報告之一。 然後,測試會列印其編號和狀態。
當測試透過時
ok 28 - kmalloc_double_kzfree
當測試因 kmalloc 失敗而失敗時
# kmalloc_large_oob_right: ASSERTION FAILED at mm/kasan/kasan_test.c:245
Expected ptr is not null, but is
not ok 5 - kmalloc_large_oob_right
當測試因缺少 KASAN 報告而失敗時
# kmalloc_double_kzfree: EXPECTATION FAILED at mm/kasan/kasan_test.c:709
KASAN failure expected in "kfree_sensitive(ptr)", but none occurred
not ok 28 - kmalloc_double_kzfree
最後,列印所有 KASAN 測試的累積狀態。 成功時
ok 1 - kasan
或者,如果其中一個測試失敗
not ok 1 - kasan
有幾種方法可以執行 KASAN 測試。
可載入模組
啟用
CONFIG_KUNIT後,可以將測試構建為可載入模組,並透過使用insmod或modprobe載入kasan_test.ko來執行它們。內建
如果內建了
CONFIG_KUNIT,測試也可以內建。在這種情況下,測試將在啟動時作為晚期初始化呼叫執行。使用 kunit_tool
如果內建了
CONFIG_KUNIT和CONFIG_KASAN_KUNIT_TEST,也可以使用kunit_tool以更易讀的方式檢視 KUnit 測試的結果。 這將不會列印已透過測試的 KASAN 報告。 有關kunit_tool的最新資訊,請參閱 KUnit 文件。