三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

PCIe DMA 驱动越界:用 kdump、crash 与 KASAN 复核

PCIe DMA 驱动越界:用 kdump、crash 与 KASAN 复核

PCIe DMA 驱动越界:用 kdump、crash 与 KASAN 复核

PCIe DMA 驱动出现 Kernel Panic 时,可信证据来自对应内核的 vmcore、vmlinux、模块版本和触发输入。下面展示 kdump、crash 与 KASAN 的排查顺序;时间戳、并发数和“运行多久必现”都应由实际复现记录提供。


结合 Crash 工具解析 Vmcore 提取现场证据

如果系统已正确配置 kdump,可使用crash配合匹配的vmlinux分析保存下来的内存页。转储范围取决于 kdump 配置,不应默认认为包含全部物理内存:

crash vmlinux /var/crash/<dump-id>/vmcore

进入 crash 交互环境后,使用bt查看调用栈和寄存器。输出必须来自当前 vmcore,这里只保留命令:

crash> bt -r

若调用栈涉及 Slab,再根据实际地址查看对应kmem_cache

crash> struct kmem_cache <address-from-current-dump>

将寄存器值、异常指令和对象地址放在一起检查。近零指针可能说明指针损坏,但还需通过 KASAN、写入路径和触发输入确认是谁破坏了对象。


崩溃定位证据链与 Slab 污染溯源流程

通过结合kdump静态分析与KASAN(Kernel Address Sanitizer)动态检测,推导 Slab 内存越界写的物理证据链:

如果 KASAN 报告指向 SGL 填充函数,就继续核对sg_count与数组容量,并确认越界写地址是否落入相邻对象。只有地址范围和写入长度吻合,才能将它确认为直接原因。


带边界校验与 KASAN 友好型的 DMA 驱动修复代码

下面代码演示容量检查和部分映射失败时的回滚。MAX_SG_ENTRIES应来自设备与驱动设计,不是通用上限:

#include <linux/module.h> #include <linux/kernel.h> #include <linux/dma-mapping.h> #include <linux/slab.h> #define MAX_SG_ENTRIES 32 // 硬性约束单次 DMA 最大的 SGL 项数 struct pcie_dma_desc { dma_addr_t dma_phy_addr; uint32_t length; uint32_t flags; }; struct pcie_dma_task { struct pcie_dma_desc sg_list[MAX_SG_ENTRIES]; // 静态固定数组,防止越界 uint32_t sg_count; struct dma_pool *pool; }; static struct kmem_cache *g_dma_desc_cache = NULL; // 经过防御性重构的描述符分配函数 struct pcie_dma_task* pcie_dma_alloc_task(struct device *dev) { struct pcie_dma_task *task; if (!g_dma_desc_cache) return NULL; // 采用 GFP_KERNEL 标志分配,确保在内存紧缺时能适当等待 task = kmem_cache_alloc(g_dma_desc_cache, GFP_KERNEL | __GFP_ZERO); if (!task) { dev_err(dev, "Failed to allocate dma task from kmem_cache\n"); return NULL; } task->sg_count = 0; return task; } // 带严格边界校验的 SGL 填充函数 int pcie_dma_fill_sg_list(struct device *dev, struct pcie_dma_task *task, struct page **pages, int page_count) { int i; dma_addr_t dma_handle; // 1. 严格防线:校验页数是否超出 SGL 物理数组容量 if (page_count > MAX_SG_ENTRIES) { dev_warn(dev, "Requested page count %d exceeds MAX_SG_ENTRIES (%d)\n", page_count, MAX_SG_ENTRIES); return -EINVAL; // 拒绝越界请求,返回参数错误 } for (i = 0; i < page_count; i++) { // 2. 执行 DMA 单页映射 dma_handle = dma_map_page(dev, pages[i], 0, PAGE_SIZE, DMA_BIDIRECTIONAL); if (dma_mapping_error(dev, dma_handle)) { dev_err(dev, "DMA mapping failed at page index %d\n", i); // 发生错误时,回滚已映射的页,防止 DMA 地址泄漏 while (--i >= 0) { dma_unmap_page(dev, task->sg_list[i].dma_phy_addr, PAGE_SIZE, DMA_BIDIRECTIONAL); } return -EIO; } // 3. 安全写入数组,避免任何溢出可能性 task->sg_list[i].dma_phy_addr = dma_handle; task->sg_list[i].length = PAGE_SIZE; task->sg_list[i].flags = 0; } task->sg_count = page_count; return 0; }

修复后不能只看“没有崩溃”

先用原始触发输入复现 KASAN 告警,再验证长度上限、映射失败和部分映射等分支。修复后的测试至少保存:

证据核对内容
KASAN / kmemleak是否仍出现越界、释放后使用或泄漏
DMA API 调试映射与解除映射是否成对,方向是否一致
错误返回超限与映射失败是否释放已分配资源
并发回归负载、持续时间、内核配置和结果是否完整记录

没有这些原始记录,就不写“零崩溃”或“无内存泄漏”的结论。


每次 Panic 都是改进架构的契机

Kernel Panic 不能只靠末尾日志归因。寄存器、vmcore、符号文件和 KASAN 报告需要与触发输入相互印证;有些问题还要配合 DMA API 调试或硬件记录。把已证实的边界写入代码和回归用例,未确认的部分继续保留为假设。

← 返回列表