mmap 缺页异常引起的条件竞争

TOC

  1. 1. mmap基础
  2. 2. mmap内存映射原理
    1. 2.1. (一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
    2. 2.2. (二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
    3. 2.3. (三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
  3. 3. 利用
  4. 4. 举例

写这篇文章的灵感来自:xz.aliyun.com

mmap基础

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问。

vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_ops指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用要的信息,都可以从vm_area_struct中获得。mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。

mmap内存映射原理

mmap内存映射的实现过程,总的来说可以分为三个阶段:

(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域

1、进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址

3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化

4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中

(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。

6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。

7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。

8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。

(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。

9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。

10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。

11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。

12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

利用

第一次对mmap的地址进行读写操作的时候,由于要进行缺页中断,则这里的时间差可以被我们利用来控制条件竞争。

原理是 copy_to_user 和 copy_from_user 都有可能因为缺页中断引起阻塞,当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。这时如果驱动内部的全局变量没有上锁的话,则我们就能利用多个线程对其进行非法操作。

举例

下面的例子仅对于多核系统有效,单核的系统似乎在缺页中时并不会切换线程。

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <pthread.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <memory.h>

int lock = 0;
char *buf1 = NULL;

void *func1(void *p)
{
if (lock == 0)
{
lock = 1;
puts("func1 get the lock!");
}
}

#define TEST_FILE "./mmap"

int main(int argc, char **argv)
{
pthread_t th1;
char local[8];
int fd1, choice, i;

if(access(TEST_FILE, R_OK|W_OK) == -1)
{
fprintf(stderr, "Please run the command: echo > %s\n", TEST_FILE);
exit(1);
}

if(argc < 2)
{
printf("Usage: %s 0\nOr: %s 1\n", argv[0], argv[0]);
exit(0);
}

choice = atoi(argv[1]);

for (i = 0; i < 0x10; i++)
{
lock = 0;

fd1 = open(TEST_FILE, O_RDWR);

buf1 = mmap(NULL, 1, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);

pthread_create(&th1, NULL, func1, NULL);

if(choice)
{
buf1[0] = 'a';
}
else
{
local[0] = 'a';
}

if (lock == 0)
{
lock = 0xff;
puts("main get the lock!");
}

pthread_join(th1, NULL);

munmap(buf1, 1);
close(fd1);
}

return 0;
}

上面的程序功能就是主线程和子线程争夺锁,由于子线程启动需要一定时间,所以在一般情况下主线程有很大几率抢到锁,这里我给主线程加了一个额外操作:

if(choice)
{
buf1[0] = 'a';
}
else
{
local[0] = 'a';
}

choice为0时,则编辑的是就是栈内存,基本对主线程的速度不会有影响,但是当choice为1时,由于是第一次写mmap映射的文件内存,所以要有缺页中断处理,这就使得子线程抢到锁的几率大大增加。

运行实例:

ex@Ex:~/test$ gcc -pthread main.c
ex@Ex:~/test$ echo > mmap
ex@Ex:~/test$ ./a.out 0
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
ex@Ex:~/test$ ./a.out 1
main get the lock!
main get the lock!
main get the lock!
func1 get the lock!
main get the lock!
main get the lock!
main get the lock!
func1 get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
main get the lock!
func1 get the lock!
func1 get the lock!