进程匿名内存保存系统调用中copy_from_user/memcpy复制失败求助
进程匿名内存上下文保存系统调用的Panic问题排查与修复
问题背景
我正在编写一个系统调用,用于保存进程的上下文(即进程动态分配的匿名内存)。当系统调用参数state为0时,需要将进程地址空间(mm->mmap)中的动态分配匿名内存复制到内核缓冲区,并添加到task_struct的list_head字段(context)中。
现有实现代码
struct page_context { unsigned long addr; unsigned char *buffer; struct list_head list; }; SYSCALL_DEFINE1(mmcontext, int, state) { struct task_struct *task; int res; struct mm_struct *mm; struct vm_area_struct *vma; unsigned long start_addr, end_addr, curr_addr; struct page_context *pctx; if (state == 0) { task = current; if(!list_empty(&task -> context)) { printk(KERN_INFO "Process already has its context saved\n"); return -EINVAL; } mm = task->mm; down_write(&mm->mmap_lock); // Traverse the VMA list and copy pages vma = mm->mmap; printk(KERN_INFO "PAGE_SIZE = %zu\n", PAGE_SIZE); while (vma) { if (vma->vm_flags & VM_WRITE) { start_addr = vma->vm_start; end_addr = vma->vm_end; curr_addr = start_addr; while (curr_addr < end_addr) { pctx = kmalloc(sizeof(struct page_context), GFP_KERNEL); if (!pctx) { printk(KERN_ERR "Failed to allocate memory for page context\n"); return -ENOMEM; } printk(KERN_INFO "Allocated process context\n"); pctx->addr = curr_addr; pctx->buffer = kmalloc(PAGE_SIZE, GFP_KERNEL); INIT_LIST_HEAD(&pctx->list); if (!pctx -> buffer) { printk(KERN_ERR "Failed to allocate memory for page context buffer\n"); kfree(pctx); return -ENOMEM; } printk(KERN_INFO "Before memcpy from userspace to kernel\n"); // memcpy((void *)pctx->buffer, (void *)curr_addr, PAGE_SIZE); res = copy_from_user((void *)pctx->buffer, (void *)curr_addr, PAGE_SIZE); if (res != 0) { printk(KERN_ERR "Failed to copy %d bytes from userspace to kernel space\n", res); } printk(KERN_INFO "Copied memory from curr_addr to buffer\n"); list_add(&pctx->list, &task -> context); num_pages++; curr_addr += PAGE_SIZE; } } vma = vma->vm_next; } up_write(&mm->mmap_lock); return 0; } }
遇到的问题
使用copy_from_user或memcpy复制VMA内存时触发内核panic,抛出“supervisor mode read access”异常,怀疑是内存块未对齐导致,但不确定具体修复方式,也想了解是否有其他实现思路。
问题根源分析
- 内存未实际分配:VMA只是地址空间的描述,其中部分地址可能尚未分配物理页(比如用户进程用
mmap匿名映射但未写入过),直接访问会触发页错误,内核态下未处理的页错误会导致panic。 - VMA类型过滤错误:仅通过
VM_WRITE判断会错误包含可写文件映射,而非目标匿名内存。 - 错误处理不完整:中途分配或复制失败时,已分配的内存未释放,造成内存泄漏。
- 边界地址未对齐:部分VMA的起始/结束地址可能未按页对齐,直接按
PAGE_SIZE遍历会访问无效区域。
修复方案与优化实现
1. 精准过滤匿名VMA
只处理带VM_ANON标志的可写VMA,确保目标是匿名内存:
if ((vma->vm_flags & VM_WRITE) && (vma->vm_flags & VM_ANON)) { // 处理该VMA }
2. 安全获取用户物理页
用get_user_pages_fast替代直接内存访问,该函数会自动处理页分配和权限检查,避免非法访问:
struct page *page; int ret = get_user_pages_fast(curr_addr, 1, 1, &page); if (ret != 1) { // 页未分配或无权限,跳过该页 curr_addr += PAGE_SIZE; continue; } // 复制页内容到内核缓冲区 copy_page(pctx->buffer, page_address(page)); // 释放页引用 put_page(page);
3. 确保地址页对齐
对VMA的起始/结束地址做页对齐处理,避免访问页内无效区域:
start_addr = PAGE_ALIGN(vma->vm_start); end_addr = vma->vm_end & PAGE_MASK; if (start_addr >= end_addr) { vma = vma->vm_next; continue; }
4. 完善错误清理逻辑
添加清理函数,在中途失败时释放已分配的所有资源:
static void cleanup_context(struct task_struct *task) { struct page_context *pctx, *tmp; list_for_each_entry_safe(pctx, tmp, &task->context, list) { list_del(&pctx->list); kfree(pctx->buffer); kfree(pctx); } } // 分配失败时调用示例 if (!pctx) { printk(KERN_ERR "Failed to allocate memory for page context\n"); cleanup_context(task); up_write(&mm->mmap_lock); return -ENOMEM; }
5. 优化后的核心遍历代码
while (vma) { if ((vma->vm_flags & VM_WRITE) && (vma->vm_flags & VM_ANON)) { start_addr = PAGE_ALIGN(vma->vm_start); end_addr = vma->vm_end & PAGE_MASK; if (start_addr >= end_addr) { vma = vma->vm_next; continue; } curr_addr = start_addr; while (curr_addr < end_addr) { pctx = kmalloc(sizeof(struct page_context), GFP_KERNEL); if (!pctx) { printk(KERN_ERR "Failed to allocate memory for page context\n"); cleanup_context(task); up_write(&mm->mmap_lock); return -ENOMEM; } pctx->addr = curr_addr; pctx->buffer = kmalloc(PAGE_SIZE, GFP_KERNEL); INIT_LIST_HEAD(&pctx->list); if (!pctx->buffer) { printk(KERN_ERR "Failed to allocate memory for page context buffer\n"); kfree(pctx); cleanup_context(task); up_write(&mm->mmap_lock); return -ENOMEM; } struct page *page; int ret = get_user_pages_fast(curr_addr, 1, 1, &page); if (ret != 1) { printk(KERN_WARNING "Skipping unallocated page at 0x%lx\n", curr_addr); kfree(pctx->buffer); kfree(pctx); curr_addr += PAGE_SIZE; continue; } copy_page(pctx->buffer, page_address(page)); put_page(page); list_add(&pctx->list, &task->context); num_pages++; curr_addr += PAGE_SIZE; } } vma = vma->vm_next; }
其他实现思路
- 利用进程快照机制:借助
ptrace的内存读取能力,或通过fork的写时复制特性间接获取内存内容,但需额外处理快照逻辑。 - 遍历进程页表:直接解析
mm->pgd多级页表,定位匿名页并复制,实现复杂度较高,但效率更优。 - 用户态预分配:让用户进程先写入所有匿名内存区域触发页分配,再调用系统调用,降低内核态处理复杂度,但会增加用户态开销。
内容的提问来源于stack exchange,提问作者m_here
相关产品推荐
相关产品推荐

