如何在Kernel >=6.3的内核模块中通过get_free_page分配只读页?
在内核模块(Kernel ≥6.3)中标记页面为只读的可行方案
由于set_memory_ro未导出且kallsyms_lookup禁用,可通过以下两种合规方式实现需求:
方案一:使用导出的set_memory_prot函数
set_memory_prot是内核导出的GPL符号,可直接用于修改内存页的保护属性,适合遵循GPL许可的模块。
代码示例
#include <linux/mm.h> #include <linux/gfp.h> void setup_readonly_page(void) { // 分配1个物理页(返回内核虚拟地址,已页对齐) void *page = get_free_page(GFP_KERNEL); if (!page) { // 处理分配失败 return; } // 将该页设置为只读+不可执行 int ret = set_memory_prot((unsigned long)page, 1, PAGE_KERNEL_RO); if (ret) { // 处理设置失败,释放页 free_page((unsigned long)page); return; } // 后续对page的写入/执行操作会触发页错误 // ... }
关键说明
PAGE_KERNEL_RO是内核定义的页保护标志,包含「只读」「非执行」属性,恰好满足需求。set_memory_prot会自动处理页表修改、TLB刷新等底层操作,无需手动干预。- 该函数要求模块遵循GPL许可(因符号为
EXPORT_SYMBOL_GPL导出)。
方案二:直接操作页表(非GPL模块可选)
如果模块无法遵循GPL许可,可直接修改对应页的页表项(PTE),手动设置权限并刷新TLB。
代码示例
#include <linux/mm.h> #include <linux/gfp.h> #include <asm/pgtable.h> void setup_readonly_page(void) { void *page = get_free_page(GFP_KERNEL); if (!page) { return; } // 获取内核虚拟地址对应的页表项(PTE) pte_t *pte = virt_to_kpte(page); if (!pte) { free_page((unsigned long)page); return; } // 清除页表项的「可写」和「可执行」位 pgprot_t new_prot = __pgprot(pte_val(*pte) & ~(_PAGE_RW | _PAGE_X)); // 更新页表项 set_pte_at(&init_mm, (unsigned long)page, pte, pte_modify(*pte, new_prot)); // 刷新TLB,确保权限变更生效 flush_tlb_kernel_range((unsigned long)page, (unsigned long)page + PAGE_SIZE); // 后续对page的写入/执行操作会触发页错误 // ... }
关键说明
virt_to_kpte是x86架构特有的函数,用于获取内核虚拟地址对应的PTE指针。- 修改页表后必须调用
flush_tlb_kernel_range刷新TLB,否则CPU可能仍使用旧的权限缓存。 - 此方法需注意页表层级:
get_free_page分配的是4KB普通页,操作PTE层级即可;若使用大页,需调整为操作PMD/PUD项。
注意事项
- 页面设置为只读后,内核自身对该页的写入操作会触发页错误,需确保后续逻辑不会意外写入;若需临时写入,可先调用
set_memory_prot改回PAGE_KERNEL,操作完成后再恢复只读。 - 分配的页使用完毕后,需调用
free_page释放,释放前建议先恢复可写权限(避免释放时内核操作页表出错)。
内容的提问来源于stack exchange,提问作者cryptobeginner
相关产品推荐
相关产品推荐

