mmap 失败并返回 -EPERM 错误问题

问题现象

在我们的应用系统初始化过程中,用户态程序会读取 /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/memmmap 操作定义为 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 选项关闭并重新编译内核,这也是我们的应用系统所采用的方法。

以上。

Comments are Disabled