LWN:新增ioctl來檢測(cè)內(nèi)存被寫入過!
關(guān)注了就能看到更多這么棒的文章哦~
An ioctl() call to detect memory writes
By Jonathan Corbet
August 10, 2023
ChatGPT assisted translation
https://lwn.net/Articles/940704/
kernel 需要知道一個(gè)進(jìn)程的內(nèi)存是何時(shí)被寫入的,這個(gè)用處很多,其中之一就是可以用來確定哪些頁面(page)可以立即回收或者該要將臟頁(dirty page)寫入后備存儲(chǔ)(backing store)。然而,有時(shí)用戶空間也需要以可靠且快速的方式訪問這些信息。這個(gè)來自 Muhammad Usama Anjum 的補(bǔ)丁就為此目的添加了一個(gè)新的 ioctl()調(diào)用;盡管使用它時(shí)需要以不尋常的方式重用一個(gè)現(xiàn)有的系統(tǒng)調(diào)用。
這個(gè)特性的目的似乎是為了高效地實(shí)現(xiàn)對(duì) Windows GetWriteWatch() 系統(tǒng)調(diào)用的模擬,這顯然對(duì)那些希望防范某些類型作弊的游戲開發(fā)人員非常有用。一個(gè)能夠訪問(并修改)游戲內(nèi)存的玩家可以以不受開發(fā)人員或其他玩家歡迎的方式來改變游戲功能。通過使用 GetWriteWatch(),游戲能夠檢測(cè)到關(guān)鍵數(shù)據(jù)結(jié)構(gòu)被外部參與者修改的情況,展示一個(gè)時(shí)尚的“Tilt”提示,并終止游戲會(huì)話。
實(shí)際上,Linux 現(xiàn)在通過/proc 中的 pagemap 文件提供了這個(gè)功能。可以從這個(gè)文件中讀取到一系列頁面的當(dāng)前是否 dirty 的狀態(tài),并寫入關(guān)聯(lián)的 clear_refs 文件來將 dirty 狀態(tài)重置(例如,在游戲自身寫入所需內(nèi)存后非常有用)。然而,從用戶空間訪問此文件時(shí)速度較慢,這與大多數(shù)游戲的需求背道而馳。新的 ioctl()調(diào)用旨在更高效地實(shí)現(xiàn)這個(gè)功能。在用戶空間中使用 Checkpoint/Restore In Userspace (CRIU)項(xiàng)目也可以利用更高效的機(jī)制來檢測(cè)寫入;在這種情況下,目標(biāo)是識(shí)別在開始了 checkpoint 過程后已被修改的頁面。
Soft dirty deemed insufficient
內(nèi)核的“soft dirty”機(jī)制,提供了 pagemap 文件,看起來是實(shí)現(xiàn)這個(gè)特性的最合適的基礎(chǔ)。只需要一個(gè)更高效的機(jī)制來查詢數(shù)據(jù)并為特定范圍的 page 重置 soft dirty 信息。然而,根據(jù) patch 封面郵件中的描述,這個(gè)方法最終效果不佳。還有其他各種操作比如虛擬內(nèi)存區(qū)域合并或 mprotect()調(diào)用,也都可能會(huì)導(dǎo)致頁面被報(bào)告為 dirty,哪怕它們沒有被真正寫入。這反過來可能會(huì)導(dǎo)致游戲錯(cuò)誤地得出結(jié)論,以為自己內(nèi)存已被篡改。
這可能會(huì)導(dǎo)致不良的結(jié)果。當(dāng)游戲玩家在玩到一個(gè)任務(wù)的關(guān)鍵點(diǎn)時(shí)被告知檢測(cè)到了作弊行為并且游戲立即結(jié)束了,那么可想而知玩家會(huì)有多么憤怒。
顯然,修復(fù)這個(gè)誤報(bào)問題不是一個(gè)可有可無的選項(xiàng),所以人們決定要通過一個(gè)意想不到的路徑來解決它。userfaultfd()系統(tǒng)調(diào)用允許一個(gè)進(jìn)程負(fù)責(zé)給定內(nèi)存范圍的自己的 page fault 的處理工作。相關(guān) patch set 為 userfaultfd()添加了一個(gè)新的操作(UFFD_FEATURE_WP_ASYNC),以更改如何處理 write-protect fault。內(nèi)核將不會(huì)把這些錯(cuò)誤傳遞給用戶空間,而是會(huì)簡(jiǎn)單地為相關(guān) page 恢復(fù)寫權(quán)限,讓出現(xiàn) fault 的進(jìn)程繼續(xù)運(yùn)行。
因此,userfaultfd()原本設(shè)計(jì)用于在用戶空間處理錯(cuò)誤,現(xiàn)在被用于直接在內(nèi)核內(nèi)部來修改 fault 處理邏輯,而無需用戶空間參與。然而,對(duì)于這個(gè)用例來說,這種方法確實(shí)有一些優(yōu)勢(shì):它可以針對(duì)特定內(nèi)存范圍使用寫保護(hù)方式來捕獲寫操作,從而更可靠地報(bào)告內(nèi)存寫入動(dòng)作。要查看哪些頁面已被寫入了的話,只需要查詢寫保護(hù)狀態(tài);如果頁面已被設(shè)置為可寫,那么它就已被寫入過了。
The ioctl() interface
有了這個(gè)功能之后,就可以創(chuàng)建一個(gè)用于查詢結(jié)果的接口。這是通過打開與所關(guān)注的進(jìn)程相關(guān)的 pagemap 文件,然后發(fā)起新增的 PAGEMAP_SCAN ioctl()調(diào)用來實(shí)現(xiàn)的。這個(gè)調(diào)用可以接受一個(gè)相對(duì)復(fù)雜的結(jié)構(gòu)作為參數(shù):
struct pm_scan_arg {
__u64 size;
__u64 flags;
__u64 start;
__u64 end;
__u64 walk_end;
__u64 vec;
__u64 vec_len;
__u64 max_pages;
__u64 category_inverted;
__u64 category_mask;
__u64 category_anyof_mask;
__u64 return_mask;
};
size 參數(shù)包含結(jié)構(gòu)本身的大小;它存在的目的是為了實(shí)現(xiàn)對(duì)未來添加更多字段時(shí)的向后兼容。有兩個(gè) flags 值將在下面描述。要查詢的地址范圍由 start 和 end 指定;walk_end 字段將由內(nèi)核更新,從而指示 page scan 實(shí)際結(jié)束的位置。vec 指向一個(gè)數(shù)組,其中包含了 vec_len 個(gè)結(jié)構(gòu)(如下所述),用于填充想要的信息。
最后四個(gè)字段描述了調(diào)用者要查找的信息。有六個(gè) page “類別(category)”可以報(bào)告出來:
PAGE_IS_WPALLOWED:頁面保護(hù)允許寫入。
PAGE_IS_WRITTEN:頁面已被寫入。
PAGE_IS_FILE:頁面由文件支持(is backed by a file)。
PAGE_IS_PRESENT:頁面存在于 RAM 中。
PAGE_IS_SWAPPED:(匿名)頁面已被寫入交換空間。
PAGE_IS_PFNZERO:頁表項(xiàng)指向零頁(zero page)。
每個(gè)頁面都屬于一些組合類別;調(diào)用這個(gè) API 的程序會(huì)希望知曉這些范圍內(nèi)屬于一些(或不屬于)這些類別的子集的頁面。mask 的用法在相關(guān) patch 中有所描述,盡管讀起來挺困難。代碼本身通過以下方式來確定根據(jù) categories 所描述的特定頁面是否關(guān)注:
categories ^= p->arg.category_inverted;
if ((categories & p->arg.category_mask) != p->arg.category_mask)
return false;
if (p->arg.category_anyof_mask && !(categories & p->arg.category_anyof_mask))
return false;
return true;
或者換成文字描述:首先,使用 category_inverted 這個(gè) mask 來翻轉(zhuǎn)所選擇的任何 category 的含義;這相當(dāng)于是選擇了不是指定 category set 的 page。然后,得到的就是所有具有由 category_mask 描述的 category 集合了,并且如果 category_anyof_mask 不為零,則還必須設(shè)置其中至少一個(gè) category。如果所有這些檢測(cè)都成功了,這個(gè) page 就是感興趣的;否則將被跳過。
在掃描之后,這些頁面會(huì)在 vec 中返回給用戶空間,vec 是一個(gè)包含這種結(jié)構(gòu)的數(shù)組:
struct page_region {
__u64 start;
__u64 end;
__u64 categories;
};
這個(gè)請(qǐng)求中的 return_mask 字段用于將感興趣的 page 來合并(collapse)成具有相同 category 的區(qū)域;對(duì)于每個(gè)這樣的區(qū)域,所返回的結(jié)構(gòu)都描述了其包含的地址范圍和實(shí)際的 category set。
最后,回到 flags 參數(shù),它有兩種取值。如果設(shè)置了 PM_SCAN_WP_MATCHING,會(huì)在注意到它們的狀態(tài)后,對(duì)所有選擇的頁面進(jìn)行寫保護(hù);這是為了允許檢查已被寫入的頁面,并為下次檢查重置狀態(tài)。如果設(shè)置了 PM_SCAN_CHECK_WPASYNC,如果內(nèi)存區(qū)域沒有像上面描述的那樣通過 userfaultfd()進(jìn)行設(shè)置,整個(gè)操作將被中止。
Checking for cheaters
為了實(shí)現(xiàn)最初的目標(biāo),也就是確定指定范圍內(nèi)的頁面是否已被修改,第一步就是調(diào)用 userfaultfd()來設(shè)置寫保護(hù)處理。然后,應(yīng)用程序可以偶爾使用上述兩個(gè) flag 來發(fā)起這個(gè)新增的 ioctl()調(diào)用,并且將 category_mask 和 return_mask 都設(shè)置為 PAGE_IS_WRITTEN。如果沒有頁面被寫入的話,將不會(huì)返回任何結(jié)果;否則,返回的結(jié)構(gòu)會(huì)指向修改發(fā)生的位置。與此同時(shí),已被寫入的 page 將重置其寫保護(hù)狀態(tài),以備下一次掃描。
截至撰寫本文時(shí),這個(gè)系列已經(jīng)有令人印象深刻的第 27(28)個(gè)修訂版本。它最初于 2022 年中期提出,作為一個(gè)名為 process_memwatch()的新系統(tǒng)調(diào)用,最終變成了一個(gè) ioctl()調(diào)用。顯然有人有動(dòng)力要將這個(gè)功能合并到內(nèi)核中,但內(nèi)存管理社區(qū)有多感興趣就不完全清楚了。這項(xiàng)工作尚未進(jìn)入 linux-next,最近的帖子也沒有引出很多 review 意見。然而,似乎確實(shí)存在這個(gè)功能的使用案例,因此總需要在某個(gè)時(shí)候做出決定。
全文完
LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。
長按下面二維碼關(guān)注,關(guān)注 LWN 深度文章以及開源社區(qū)的各種新近言論~
