由mmap引发的SIGBUS

一直以来都觉得使用mmap读文件是非常高效、非常优雅的做法(参见《从"read"看系统调用的耗时》)。mmap之后,就可以通过内存访问的方式访问到文件里的内容,省去了read这样的系统调用。
却不曾想过,mmap以后,如果读文件出错会发生什么……

今晚看到一篇介绍apache bug的文章,里面说到,apache使用mmap来实现对静态文件的访问。在读文件之前,apache使用stat系统调用得知了文件的长度,然后按照此长度读取已经被映射在某个内存区间上的文件。
然而如果在读静态文件(内存访问)的过程中,文件被外部势力修改了,导致文件长度被减小。则apache可能访问到映射文件之外的内存(本来这块内存是在映射文件之内的,但是现在文件减小了),导致进程收到SIGBUS信号,然后崩溃。

内核代码追踪

真的会存在这样的情况吗?在好奇心驱使下,看了看相关的内核代码。(以下,关于内存管理方面的细节请参阅《linux内存管理浅析》。)

首先是mmap的调用过程,考虑最普遍的情况,一个vma会被分配,并且与对应的file建立联系。

mmap_region()
......
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
......
if (file) {
......
vma->vm_file = file;
get_file(file);
error = file->f_op->mmap(file, vma);
......
} else if (vm_flags & VM_SHARED) {
......

这里是通过file->f_op->mmap函数来“建立联系”的,而一般情况下,这个函数等于generic_file_mmap。

generic_file_mmap()
......
vma->vm_ops = &generic_file_vm_ops;
vma->vm_flags |= VM_CAN_NONLINEAR;
......

其中:

struct vm_operations_struct generic_file_vm_ops = {
.fault = filemap_fault,
};

注意这里对vma->vm_ops的赋值,下面会用到。然后,mmap就完成了,仅仅是建立了vma,及其与file的对应关系。没有分配内存、更没有读文件。

接下来,当对应的虚拟内存被访问时,将触发访存异常。内核捕捉到异常,再完成内存分配和读文件的事情。(其中细节还是详见《linux内存管理浅析》。)
do_page_fault就是内核用于捕捉访存异常的函数。其中内核会先确认引起异常的内存地址是合法的,并且找出它所对应的vma(如果找不到就是不合法)。然后分配内存、建立页表。对于本文中描述的mmap映射了某个文件的这种情况,内核还需要把文件对应位置上的数据读到新分配的内存上,这个工作主要是由vma->vm_ops->fault来完成的。前面我们看到vma->vm_ops是如何被赋值的了,而且这个vma->vm_ops->fault就等于filemap_fault。

filemap_fault()
......
size = (i_size_read(inode) + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT;
if (vmf->pgoff >= size)
return VM_FAULT_SIGBUS;
......

这个函数做的第一件事情就是检查要访问的地址偏移(相对于文件的)是否超过了文件大小,如果超过就返回VM_FAULT_SIGBUS,这将导致SIGBUS信号被发送给进程。

用户程序验证

虽然看到内核代码就是这么实现的了,写个用户程序来验证一下总会让人更信服。一个简单的测试程序如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/mman.h>
#include <unistd.h>
#define FILESIZE 8192
void handle_sigbus(int sig)
{
printf("SIGBUS!\n");
_exit(0);
}
void main()
{
int i;
char *p, tmp;
int fd = open("tmp.ttt", O_RDWR);
p = (char*)mmap(NULL,FILESIZE, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
signal(SIGBUS, handle_sigbus);
getchar();
for (i=0; i<FILESIZE; i++) {
tmp = p[i];
}
printf("ok\n");
}

在执行这个程序前:
kouu@kouu-one:~/test$ stat tmp.ttt
File: "tmp.ttt"
Size: 239104 Blocks: 480 IO Block: 4096 普通文件

把程序跑起来,显然8192大小的内存是可以映射的。然后程序会停在getchar()处。
kouu@kouu-one:~/test$ echo "" > tmp.ttt
kouu@kouu-one:~/test$ stat tmp.ttt
File: "tmp.ttt"
Size: 1 Blocks: 8 IO Block: 4096 普通文件

现在我们将 tmp.ttt弄成1字节的。然后给程序一个输入,让它从getchar()返回。
kouu@kouu-one:~/test$ ./a.out

SIGBUS!

立刻,程序就收到SIGBUS信号了。

解决办法

这样的问题在用户态有办法解决吗?我的理解是:没有!

或许你会说,为什么不在每次读之前都取一下文件大小,以确保不越界呢?读文件是通过读内存来进行的,那么应当每读一个字就检查一下吗?这样做的话效率将大打折扣,mmap还有什么意义?
即便如此,“检查通过”与“读操作”并不是原子的,这两个操作之间还是可能存在文件被缩小的问题(尽管可能性变小了)。

所以,目前使用mmap的程序是会存在这样的风险,而收到SIGBUS信号。

是否你异想天开,打算把SIGBUS给捕捉了,然后忽略掉呢?
可以试一下上面的程序,在handle_sigbus函数中把_exit一句注释掉,看看会有什么样的结果。
其结果就是,handle_sigbus会重复重复再重复地被调用,就像一个死循环。为什么会这样呢?因为如果在handle_sigbus函数中把收到SIGBUS的事情给忽略了,内核也就会从前面提到的访存异常中返回,回到用户态,然后CPU会重新执行引起异常的那条指令(这条指令因为异常而未被执行完,必须得重新执行)。
正常情况下,这个时候页面已经分配了、页表已经建立了、文件也读好了。重新执行引起异常的指令时,就不会再引起异常了。但是现在这些条件不满足,这条指令还会引起异常,于是又走到上面讲的那一套流程,然后又触发SIGBUS,然后又被忽略……于是就成了死循环。

所以,面对这种情况,用户程序是没招的。
或许在打开文件之后,mmap之前,先给文件加一个强制锁(百度一下?),这是一种解决办法。但是使用强制锁的限制很多(文件系统要支持、mount时要特殊处理,还要给文件加SGID),并且锁本身很黄很暴力(确实可以阻止别人写入,但是如果程序BUG、句柄泄漏,别人就真没法改了),据说移植性又不好。这一招不到万不得已还是别使……

那么内核程序呢?

如果用户程序通过read来读文件,则每次读文件都是通过系统调用来进行的。当发生错误的时候,read系统调用可以尽可能地返回失败(而不必武断地发出SIGBUS信号),然后让程序决定下一步该怎么办。
但是像mmap这样,是以读内存的方式来读文件。有什么机制让你选择读内存失败了该怎么办吗?没有,只能是“不成功,便成仁”。(好比你写下“i=j;”这么一句,不可能还有办法检查读取j或者写i是否成功。)

然而,我不知道内核为什么要在判断访问文件越界时抛出SIGBUS,或许有些东西我没能理解透彻。
在这个地方(filemap_fault函数中),如果发现访问越界,是否可以返回一个0页面,让它给映射上呢(也就是说,如果读越界,读到的内容就是0)。(这个0页面在内核中其实也是存在的,页面的内容全是0,当程序去读没有被映射的页面时,这个0页面就映射给它,而并不用分配新的页面。因为页面都没映射,显然没被写过,也就是说这些内存没有初值,所以默认都填0了。)
并且,这里的访问越界,我觉得,应该没有什么危害。因为再怎么越界,都不会越过mmap时创建的那个vma,这些地址应该说都是合法的。

时间: 2024-09-17 04:45:48

由mmap引发的SIGBUS的相关文章

由mmap引发的SIGBUS(续)

关于之前的<由mmap引发的SIGBUS>问题,这几天又做了一些较深入的探索,整理如下. SIGBUS的必要性 为什么内核在上述情况下要抛出SIGBUS信号呢? 原来这是POSIX的规定,引用一段: The mmap() function can be used to map a region of memory that is larger than the current size of the object. Memory access within the mapping but be

认真分析mmap:是什么 为什么 怎么用【转】

转自:http://www.cnblogs.com/huxiao-tee/p/4660352.html?utm_source=tuicool&utm_medium=referral   阅读目录 mmap基础概念 mmap内存映射原理 mmap和常规文件操作的区别 mmap优点总结 mmap相关函数 mmap使用细节 回到顶部 mmap基础概念 mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系.实现这样

Linux内存管理之mmap详解 【转】

转自:http://blog.chinaunix.net/uid-26669729-id-3077015.html 一. mmap系统调用 1. mmap系统调用         mmap将一个文件或者其它对象映射进内存.文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零.munmap执行相反的操作,删除特定地址区域的对象映射. 当使用mmap映射文件到进程后,就可以直接操作这段虚拟地址进行文件的读写等操作,不必再调用read,write等系统调用.但需注

UC编程03-内存分配与内存映射函数sbrk/brk/mmap/munmap

函数说明: #include<stdio.h> #include<unistd.h> int main() { void* empty=sbrk(0); void * p1=sbrk(4);//虚拟内存映最少射一个内存页4096字节 int *pi=p1; *(pi+2)=120;//给p3赋值 void* p2=sbrk(4); void* p3=sbrk(4); printf("p1=%p,p2=%p,p3=%p\n",p1,p2,p3); sbrk(-8)

linux mmap 详解【转】

转自:http://blog.chinaunix.net/uid-20321537-id-3483405.html 一.前言mmap的具体实现以前在学习内核时学习过,但是对于其中的很多函数是一知半解的,有些只能根据其函数名来猜测其具体的功能,在本文中,一起来重新深入理解其具体的实现. 二.mmap的用户层应用void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize); 具体参数含义start :  指

linux下共享内存mmap和DMA(直接访问内存)的使用 【转】

转自:http://blog.chinaunix.net/uid-7374279-id-4413316.html 介绍Linux内存管理和内存映射的奥秘.同时讲述设备驱动程序是如何使用"直接内存访问"(DMA)的.尽管你可能反对,认为DMA更属于硬件处理而不是软件接口,但我觉得与硬件控制比起来,它与内存管理更相关.这一章比较高级:大多数驱动程序的作者并不需要太深入到系统内部.不过理解内存如何工作可以帮助你在设计驱动程序时有效地利用系统的能力.       共 享内存可以说是最有用的进程

Linux系统调用--mmap/munmap函数详解【转】

转自:http://www.cnblogs.com/leaven/archive/2011/01/14/1935199.html http://linux.chinaunix.net/techdoc/develop/2008/07/25/1020292.shtml     功能描述: mmap将一个文件或者其它对象映射进内存.文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零.munmap执行相反的操作,删除特定地址区域的对象映射. 基于文件的映射,在mm

剖析一个由sendfile引发的linux内核BUG

在论坛上看到一个讲linux内核BUG的帖子,利用这个BUG,一个普通用户能够在运行某个程序之后,获得root权限. 示例的代码如下:http://www.securityfocus.com/data/vulnerabilities/exploits/36038-4.tgz 在ubuntu 9.04,内核版本2.6.28.12的机器上测试通过. (警告,此BUG已经是公开的秘密了.如果想用来干坏事,是很容易被发现的,并且将受到法律的制裁!) 也希望linux内核尽快修复该BUG. 那么,这究竟是

文件 提取 字-文件按字读取存储引发的一点问题

问题描述 文件按字读取存储引发的一点问题 问题比较复杂: 1.我需要将二进制文件提取出来,加密后存入另一文件 2.由于原始文件按字节(fgetc)提取加密后会变成很大的数(>255),因此不能用fputc来存加密后的数据,这就不可避免的用到putwc.但是putwc会使加密文件变大(由char变成了wchar),而我又想保持源文件大小. 3.我想到了按字提取fgetwc.按字存储fputwc,但是按字提取不能很好解决文件结尾的判定,如果源文件只有"abc"三个字符,那么按字提取只