谷歌給內核提的這個補丁,
共 8791字,需瀏覽 18分鐘
·
2024-05-28 08:12
作者:J.Zhou
Linux內核中內存損壞一直是極難定位但又較為常見的一類問題。在內核中已經有較多的機制來攔截此類問題。比如Kasan/Kfence等等。而內核自5.17版本起又引入了Page Table Check機制,用來檢測某些page計數(shù)異常導致的內存損壞問題。
一、 為何引入Page Table Check機制:
Google 的工程師在分析一個進程的dump時,無意間發(fā)現(xiàn)了一頁不屬于該進程的內存。進一步研究發(fā)現(xiàn)了內核自4.14起就存在的內存page引用計數(shù)的bug。為化解此類內存缺陷,Google 提出了一個全新的“頁表檢查”(Page Table Check)解決方案。
我們看看 Google 的修復patch及問題發(fā)生的原因:
---kernel/events/core.c | 10 +++++-----1 file changed, 5 insertions(+), 5 deletions(-)diff --git a/kernel/events/core.c b/kernel/events/core.cindex 236e7900e3fc..0736508d595b 100644--- a/kernel/events/core.c+++ b/kernel/events/core.c@@ -6110,7 +6110,6 @@ void perf_output_sample(struct perf_output_handle *handle,static u64 perf_virt_to_phys(u64 virt){u64 phys_addr = 0;- struct page *p = NULL;if (!virt)return 0;@@ -6129,14 +6128,15 @@ static u64 perf_virt_to_phys(u64 virt)* If failed, leave phys_addr as 0.*/if (current->mm != NULL) {+ struct page *p;+pagefault_disable();- if (__get_user_pages_fast(virt, 1, 0, &p) == 1)+ if (__get_user_pages_fast(virt, 1, 0, &p) == 1) {phys_addr = page_to_phys(p) + virt % PAGE_SIZE;+ put_page(p);+ }pagefault_enable();}-- if (p)- put_page(p);}return phys_addr;--
問題發(fā)生的根因就在于__get_user_pages_fast函數(shù)可能存在先對page指針p賦值了,但是后續(xù)因為某個錯誤直接返回。在此場景下__get_user_pages_fast中是沒有調用get_page來增加引用計數(shù)的,因此后續(xù)的put_page是多余的,會導致引用計數(shù)下溢。修復方式其實比較簡單,只有在__get_user_pages_fast成功時,才put_page。
此問題是很隱秘的,在內核中存在了很長時間。正因為如此,google才推出了Page Table Check機制,希望在第一時間攔截此類問題。
二、Page Table Check 機制的實現(xiàn):
新增一個page_ext記錄當前page的映射是匿名或者文件映射。在每次映射關系改變時,會判斷當前的映射標記,如果出現(xiàn)不允許的情況就會主動panic,保留第一現(xiàn)場。
具體規(guī)則如下:
當前映射 |
新映射 |
映射權限 |
規(guī)則 |
匿名 |
匿名 |
讀 |
允許 |
匿名 |
匿名 |
讀/寫 |
禁止 |
匿名 |
文件 |
任何 |
禁止 |
文件 |
匿名 |
任何 |
禁止 |
文件 |
文件 |
任何 |
允許 |
我們來看看這個規(guī)則的代碼實現(xiàn)。在有新的映射發(fā)生時,會根據(jù)page現(xiàn)有的file_map_count/anon_map_count的標志來判斷映射是否合法,并且會修改標志值。
代碼的調用邏輯如下:
缺頁中斷→ handle_mm_fault→ handle_pte_fault→ do_anonymous_page / do_fault→ set_pte_at→ page_table_check_set? 判斷是否合法
同樣在unmap時,也會調用page_table_check_clear來判斷當前的標志位。
static void page_table_check_set(struct mm_struct *mm, unsigned long addr, unsigned long pfn, unsigned long pgcnt,bool rw){struct page_ext *page_ext;struct page *page;unsigned long i;bool anon;if (!pfn_valid(pfn))return;page = pfn_to_page(pfn);page_ext = page_ext_get(page);anon = PageAnon(page);for (i = 0; i < pgcnt; i++) {struct page_table_check *ptc = get_page_table_check(page_ext);if (anon) {BUG_ON(atomic_read(&ptc->file_map_count));BUG_ON(atomic_inc_return(&ptc->anon_map_count) > 1 && rw);} else {BUG_ON(atomic_read(&ptc->anon_map_count));BUG_ON(atomic_inc_return(&ptc->file_map_count) < 0);}page_ext = page_ext_next(page_ext);}page_ext_put(page_ext);}
而在分配 alloc_pages() 和釋放 free_pages_prepare() 內存的時候也會調用__page_table_check_zero,保證當前內存沒有被映射。
void __page_table_check_zero(struct page *page, unsigned int order){struct page_ext *page_ext;unsigned long i;page_ext = page_ext_get(page);BUG_ON(!page_ext);for (i = 0; i < (1ul << order); i++) {struct page_table_check *ptc = get_page_table_check(page_ext);BUG_ON(atomic_read(&ptc->anon_map_count));BUG_ON(atomic_read(&ptc->file_map_count));page_ext = page_ext_next(page_ext);}page_ext_put(page_ext);}
三、Page Table Check 機制的配置:
要使用Page Table Check機制,需要在編譯的時候使能PAGE_TABLE_CHECK=y。并且需要在內核的cmdline中增加'page_table_check=on'或者在kconfig中使能CONFIG_PAGE_TABLE_CHECK_ENFORCED=y。
比如,我們的配置如下:
CONFIG_PAGE_TABLE_CHECK=yCONFIG_PAGE_TABLE_CHECK_ENFORCED=y
四、測試驗證:
前面提到引入page table check的起因是因為異常調用put_page導致的。那么我們人為構建一個多次調用put_page的測試程序來看看page table check如何生效的吧。
測試程序的代碼如下:
void test_page_table_check(void){unsigned long addr;struct task_struct *task = find_task_by_vpid(1);struct vm_area_struct *vma;struct mm_struct *mm = task->mm;struct page *page;addr = mm->mmap_base - PAGE_SIZE;mmap_read_lock(mm);vma = find_vma(mm, addr);page = follow_page(vma, addr, FOLL_GET);put_page(page);mmap_read_unlock(mm);put_page(page);}
首先獲取init進程中mmap分配的一頁page。我們故意在最后多操作了一遍put_page。從而導致page的引用計數(shù)為0,因此會釋放掉此內存,而在釋放內存時,__page_table_check_zero檢查到anon_map_count不為0,因此panic了。
具體的調用過程可以參考下面的堆棧:
[ 132.032451][2:3990:sh] ------------[ cut here ]------------[ 132.032453][2:3990:sh] kernel BUG at mm/page_table_check.c:143![ 132.032458][2:3990:sh] Internal error: Oops - BUG: 00000000f2000800 [#1] PREEMPT SMP[ 132.032822][2:3990:sh] CPU: 2 PID: 3990 Comm: sh Tainted: G S W OE 6.1.25-android14-11-maybe-dirty-qki-consolidate #1[ 132.032829][2:3990:sh] pstate: 82400005 (Nzcv daif +PAN -UAO +TCO -DIT -SSBS BTYPE=--)[ 132.032833][2:3990:sh] pc : __page_table_check_zero+0xcc/0xdc[ 132.032843][2:3990:sh] lr : __page_table_check_zero+0x30/0xdc[ 132.032846][2:3990:sh] sp : ffffffc03094bb10[ 132.032848][2:3990:sh] x29: ffffffc03094bb10 x28: ffffff88c4a00000 x27: 0000000000000000[ 132.032854][2:3990:sh] x26: ffffffe31e256000 x25: ffffffe31e256000 x24: 0000000000000001[ 132.032858][2:3990:sh] x23: 0000000000000000 x22: ffffffe31d36b523 x21: ffffffe31d3cac1c[ 132.032862][2:3990:sh] x20: ffffff8023a81760 x19: fffffffe01baba40 x18: ffffffe31e18b240[ 132.032866][2:3990:sh] x17: 00000000ad6b63b6 x16: 00000000ad6b63b6 x15: ffffffe31c2ad328[ 132.032870][2:3990:sh] x14: ffffffe31b7466fc x13: ffffffc030948000 x12: ffffffc03094c000[ 132.032874][2:3990:sh] x11: 0000000000000060 x10: ffffffe31e178720 x9 : 0000000000000001[ 132.032878][2:3990:sh] x8 : ffffff8023a817b8 x7 : ffffffe31c18cda4 x6 : ffffffe31c1e5ee8[ 132.032882][2:3990:sh] x5 : 0000000000000000 x4 : 0000000000000000 x3 : 0000000000000002[ 132.032886][2:3990:sh] x2 : 0000000000000000 x1 : ffffffe31d3bf383 x0 : ffffff8023a81760[ 132.032890][2:3990:sh] Call trace:[ 132.032892][2:3990:sh] __page_table_check_zero+0xcc/0xdc[ 132.032896][2:3990:sh] free_unref_page_prepare+0x36c/0x42c[ 132.032903][2:3990:sh] free_unref_page+0x58/0x268[ 132.032907][2:3990:sh] __folio_put+0x54/0x80[ 132.032917][2:3990:sh] test_page_table_check+0x114/0x1f8 [mz_stability_test][ 132.032930][2:3990:sh] proc_generate_oops_write+0x960/0xa18 [mz_stability_test][ 132.032939][2:3990:sh] proc_reg_write+0xfc/0x170[ 132.032949][2:3990:sh] vfs_write+0x110/0x2d0[ 132.032956][2:3990:sh] ksys_write+0x80/0xf0[ 132.032960][2:3990:sh] __arm64_sys_write+0x24/0x34[ 132.032965][2:3990:sh] invoke_syscall+0x60/0x124[ 132.032975][2:3990:sh] el0_svc_common+0xcc/0x118[ 132.032980][2:3990:sh] do_el0_svc+0x34/0xb8[ 132.032984][2:3990:sh] el0_svc+0x30/0xb0[ 132.032992][2:3990:sh] el0t_64_sync_handler+0x68/0xb4[ 132.032996][2:3990:sh] el0t_64_sync+0x1a0/0x1a4
五、小結
在page table操作時增加校驗,從而檢查是否存在非法共享等人為軟件漏洞,提前發(fā)現(xiàn)問題,確保防止某些內存損壞。在生產環(huán)境和研發(fā)階段,對硬件和工藝原因導致的隨機內存跳變問題,也會有所幫助。
六、參考
https://lwn.net/Articles/876264/
https://lore.kernel.org/all/[email protected]/
