能否从Kernel Module中读取user-space页面的内容
核心结论
编写合法的内核模块完全可以读取用户空间内存页面存储的具体内容。内核本身运行在最高特权级,具备访问所有物理内存的权限,但由于用户态和内核态地址空间隔离、内核安全保护机制(SMAP/SMEP等)、进程地址空间独立等特性,绝对不能直接解引用用户态虚拟地址,必须使用内核提供的标准访问API,否则会直接触发内核oops甚至系统崩溃。
基本前提约束
- 只有在进程上下文下才能访问用户空间内存,中断上下文、软中断上下文不属于任何特定进程,没有有效的用户地址空间映射,不能做用户内存读取操作。
- 读取当前运行进程的用户内存时,可以直接通过当前进程的
current->mm内存描述符完成访问;读取其他进程的用户内存时,需要先获取目标进程的任务描述符、内存描述符,必要时切换地址空间。 - 所有用户态传入的地址都属于不可信输入,必须经过合法性校验,不能假设地址一定映射了有效物理页。
常用实现方案
场景1:读取当前进程的用户空间内存
这是驱动开发最常见的场景,比如字符设备的ioctl、read/write回调里,用户态传入缓冲区指针需要读取内容,直接使用内核提供的带校验的拷贝接口即可:
- 通用长度读取用
copy_from_user(void *kernel_dst, const void __user *user_src, unsigned long len):函数会自动检查用户地址的合法性、处理页换入逻辑,把用户地址user_src处的len字节内容拷贝到内核缓冲区kernel_dst,返回值为拷贝失败的字节数,返回0代表读取成功。 - 小长度(单字节、单整型等)读取可以用轻量宏
get_user(kernel_var, user_ptr),效率比copy_from_user更高,同样自带地址合法性校验。
踩坑提醒:哪怕测试时直接强转解引用用户指针偶尔能读到内容,也绝对不要这么写。开启SMAP(内核 Supervisor Mode Access Prevention)特性的系统上,内核态直接访问用户地址会被硬件直接拦截触发panic;就算没开SMAP,遇到未映射的用户地址、换出到交换分区的页,直接解引用也会触发内核页错误导致崩溃。
最简实现示例:
#include <linux/uaccess.h> #include <linux/slab.h> /** * 读取当前进程用户空间地址的内容 * @user_addr: 用户空间待读取的起始地址 * @len: 待读取的长度 * 返回值: 成功返回存储读取内容的内核缓冲区(使用后需kfree),失败返回NULL */ char *read_current_process_user_mem(void __user *user_addr, size_t len) { char *kbuf; long copy_ret; if (unlikely(len == 0 || !user_addr)) return NULL; kbuf = kmalloc(len, GFP_KERNEL); if (unlikely(!kbuf)) return NULL; copy_ret = copy_from_user(kbuf, user_addr, len); if (unlikely(copy_ret != 0)) { pr_err("user mem read failed, %ld bytes not accessible\n", copy_ret); kfree(kbuf); return NULL; } return kbuf; }
场景2:跨进程读取其他用户进程的内存
如果要读取非当前运行进程的内存内容,不需要手动切换页表遍历,直接使用内核提供的access_process_vm接口即可,这个接口会自动处理地址空间切换、页表遍历、缺页换入、权限校验的全套逻辑,内核自带的/proc/<pid>/mem节点底层就是用这个接口实现跨进程内存访问的,可靠性有保证。
调用前需要先通过目标进程pid拿到对应的任务描述符、内存描述符,访问完成后记得释放引用计数,避免资源泄漏。
最简实现示例:
#include <linux/pid.h> #include <linux/sched.h> #include <linux/sched/mm.h> #include <linux/mm.h> /** * 读取指定pid进程的用户空间内存内容 * @pid: 目标进程ID * @user_addr: 目标进程内的虚拟地址 * @kbuf: 内核侧存储读取结果的缓冲区 * @len: 待读取长度 * 返回值: 0为成功,负数为错误码 */ int read_other_process_user_mem(pid_t pid, unsigned long user_addr, char *kbuf, size_t len) { struct pid *pid_struct; struct task_struct *tsk; struct mm_struct *mm; int read_bytes; if (unlikely(!kbuf || len == 0)) return -EINVAL; pid_struct = find_get_pid(pid); if (unlikely(!pid_struct)) return -ESRCH; tsk = get_pid_task(pid_struct, PIDTYPE_PID); put_pid(pid_struct); if (unlikely(!tsk)) return -ESRCH; mm = get_task_mm(tsk); if (unlikely(!mm)) { put_task_struct(tsk); return -EINVAL; } read_bytes = access_process_vm(tsk, user_addr, kbuf, len, 0); // 最后一个参数0代表读操作 mmput(mm); put_task_struct(tsk); return read_bytes == len ? 0 : -EFAULT; }
常见注意事项
- 不要在持有自旋锁、中断关闭的状态下调用上述用户内存访问接口,这类接口可能触发缺页异常导致睡眠,会引发死锁。
- 跨进程访问内存时,不要手动遍历目标进程的页表做物理地址映射,不仅逻辑复杂容易出错,还会绕过内核的安全校验,在开启内存保护特性的系统上直接失效。
- 所有拿到的进程、内存描述符引用计数必须配对释放,否则会造成内核资源泄漏,甚至导致目标进程退出后内存无法回收。
内容的提问来源于stack exchange,提问作者Hoang Do
相关产品推荐
相关产品推荐

