mark_rodata_ro 是 x86-64 架构中用于将内核的只读数据段(.rodata)和代码段(.text)在页表级别设置为只读的初始化函数。它的核心目的是利用硬件的内存保护机制,防止内核启动后意外或恶意的修改。
核心作用:硬件级写保护
在内核启动早期,所有内存页都是可写的,因为内核需要修改页表、初始化数据结构。当初始化基本完成、即将启动第一个用户进程之前,内核会调用 mark_rodata_ro,将那些“本不该被修改”的内存区域(内核代码和只读数据)在页表项中清除写权限。
这个函数由 mark_readonly() 在 CONFIG_STRICT_KERNEL_RWX=y 时调用。值得注意的是,CONFIG_STRICT_KERNEL_RWX 在 x86、arm64 和 s390 上是强制启用的,无法通过 make menuconfig关闭。
实现逻辑
该函数在 x86-64 上的典型实现位于 arch/x86/mm/init_64.c,核心步骤如下:
-
确定保护范围:通常从
_stext(内核代码段起始)开始,到__end_rodata(只读数据段结束)为止。在某些实现中,起始地址会跳过第一页(_stext + PAGE_SIZE),因为内核页全局目录(kernel_pg_dir)位于该页中。 -
设置页表属性:通过
set_memory_ro()或类似机制,将指定范围内的页面标记为只读。在早期版本中,使用的是change_page_attr_addr()配合PAGE_KERNEL_RO。 -
刷新 TLB:修改页表后,必须刷新 TLB(转换检测缓冲区),以确保 CPU 使用新的权限。通常通过
global_flush_tlb()或flush_tlb_all()完成。 -
非可执行标记:对于
.rodata段之后的数据区域,还会调用set_memory_nx()将其标记为不可执行,防止数据被当作代码执行。
与其他机制的关系
mark_rodata_ro 与内核的动态代码修改机制存在交互:
-
ftrace/kprobes:这些机制需要临时修改内核代码。因此,内核引入了
kernel_set_to_readonly标志和set_kernel_text_rw()等函数,允许在需要时临时将代码段恢复为可写,完成修改后再重新保护。 -
CONFIG_DEBUG_RODATA:在较新的内核中,这个配置选项已被CONFIG_STRICT_KERNEL_RWX取代。旧版的mark_rodata_ro就是由CONFIG_DEBUG_RODATA控制的。
安全意义
这个函数是内核自我加固(Self-Hardening) 的关键一步。通过将代码和只读数据标记为只读,它显著增加了利用内核漏洞的难度——攻击者无法直接覆写内核代码或关键数据结构来执行恶意代码。这也是现代内核安全基线的核心组成部分。
调用时机与调用者
mark_rodata_ro() 的调用关系非常清晰,它位于 free_initmem() 之后、启动第一个用户进程之前,是内核从“初始化阶段”切换到“运行阶段”的标志性步骤之一。
这个函数由 kernel_init()(PID 1,即 init 进程的内核线程部分)在启动流程中调用。
典型的调用顺序可以简化为:
rest_init() 创建 kernel_init (PID 1)
↓
kernel_init() 等待 kthreadd (PID 2) 完成
↓
kernel_init_freeable() 执行 do_basic_setup() 等初始化
↓
free_initmem() 释放 __init 代码和数据段
↓
mark_rodata_ro() 将 .text 和 .rodata 设为只读
↓
run_init_process() 启动第一个用户态进程 (/sbin/init)
内核文档注释明确写道:“Linux calls mark_rodata_ro() after freeing .init code and prior to calling the first init userspace process”。这句话精炼地概括了它的位置。
为什么必须在这个时机调用?
这个时机是精心选择的,有三个关键原因:
-
__init代码已释放:在free_initmem()之后,所有标记为__init的函数(仅在初始化期间运行一次)所占用的内存已经被归还给伙伴系统。mark_rodata_ro()此时再将.text和.rodata设为只读,不会影响任何还在运行的初始化代码。 -
__ro_after_init数据已定型:内核中有一类特殊数据(如phys_base),它们在初始化期间需要被写入,但初始化完成后就永不改变。这些数据被放在专门的段中,在mark_rodata_ro()执行时一并被设为只读,从而在运行期间受到硬件写保护。 -
第一个用户进程尚未启动:此时系统仍处于纯粹的内核态,没有用户态代码运行。在用户态程序开始执行前完成只读保护,可以确保内核的攻击面在最早的时刻就被最小化。
架构相关的实现
mark_rodata_ro() 是一个架构相关的函数,每个支持严格内核内存保护(CONFIG_STRICT_KERNEL_RWX)的架构都需要实现它。在 x86 上,它的实现位于 arch/x86/mm/init_64.c,通过 set_memory_ro() 和 set_memory_nx() 修改页表项,然后刷新 TLB 使新的权限生效。

315

被折叠的 条评论
为什么被折叠?



