问题现象
在我们的应用系统初始化过程中,用户态程序会读取 /proc/task_info
第一个字段的值作为物理地址传入 mmap
对 /dev/mem
做内存映射。/proc/task_info
的内容如下:
$ cat /proc/task_info
0x81b0000 0xd555555555555555 0x5
也就是将 0x81b0000 (每次运行值可能不同)作为物理地址传入 mmap
,此时返回 errno
为 -1,表示无权限:
#define EPERM 1 /* Operation not permitted */
源码分析
内存设备节点 /dev/mem
作为可随机读写的字符设备,在内核源码的 drivers/char/mem.c
中初始化(/dev/kmem
, /dev/null
, /dev/zero
, /dev/random
, /dev/urandom
等设备节点也都在这个文件中定义),对 /dev/mem
的 mmap
操作定义为 mmap_mem()
。在 mmap_mem()
中返回 -EPERM
的代码片段如下:
if (!range_is_allowed(vma->vm_pgoff, size))
return -EPERM;
当定义了 CONFIG_STRICT_DEVMEM
时,在 range_is_allowed()
中会逐页调用 devmem_is_allowed()
检查是否可访问:
static inline int range_is_allowed(unsigned long pfn, unsigned long size)
{
u64 from = ((u64)pfn) << PAGE_SHIFT;
u64 to = from + size;
u64 cursor = from;
while (cursor < to) {
if (!devmem_is_allowed(pfn)) {
printk(KERN_INFO
"Program %s tried to access /dev/mem between %Lx->%Lx.\n",
current->comm, from, to);
return 0;
}
cursor += PAGE_SIZE;
pfn++;
}
return 1;
}
devmem_is_allowed()
由各个体系结构实现,以 PowerPC 为例,在 arch/powerpc/mm/mem.c
中,同样也是定义了 CONFIG_STRICT_DEVMEM
才有实现:
int devmem_is_allowed(unsigned long pfn)
{
if (iomem_is_exclusive(pfn << PAGE_SHIFT))
return 0;
if (!page_is_ram(pfn))
return 1;
if (page_is_rtas_user_buf(pfn))
return 1;
return 0;
}
devmem_is_allowed()
返回 1 表示允许访问,0 表示不允许访问。其中第一个 if
语句调用的 iomem_is_exclusive()
是用于判断 PCI 的内存空间(PCI mem)是否互斥访问的。由于我们访问的是内核生成的 /proc/task_info
,与 PCI 无关,因此 mmap
失败是在 devmem_is_allowed()
最后的 return 0
返回了不允许访问。
解决方法
方法一:修改 devmem_is_allowed()
函数
在 devmem_is_allowed()
函数中,当访问的是 /proc/task_info
对应的页号时,返回 1 表示允许访问。
这种方法实现复杂,需要在生成 /proc/task_info
时保存其对应的页号(在 fs/proc/taskinfo.c
文件的 task_info_init()
函数中分配的页号);同时这个方法污染了内核代码,不具有通用性。
方法二:关闭 CONFIG_STRICT_DEVMEM
配置
从前面的说明中可以看出,几个检查函数都是在定义了 CONFIG_STRICT_DEVMEM
才生效的,因此最简单的方法,就是将内核配置中 CONFIG_STRICT_DEVMEM
选项关闭并重新编译内核,这也是我们的应用系统所采用的方法。
以上。