由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 beyond the current end of the underlying objects may result in SIGBUS signals being sent to the process. The reason for this is that the size of the object can be manipulated by other processes and can change at any moment. The implementation should tell the application that a memory reference is outside the object where this can be detected; otherwise, written data may be lost and read data may not reflect actual data in the object.

参阅mmap文档:http://www.opengroup.org/onlinepubs/000095399/functions/mmap.html


捕获异常的绝招

这个问题用户程序还有什么招吗?
发现还有一招,那就是使用异常处理的方法将这个错误catch住。在C下,我们可以使用sigsetjmp - siglongjmp来实现。(关于setjmp/longjmp,可参阅:http://www.yuanma.org/data/2007/0110/article_2084.htm

把之前的代码改造如下(类似的方法也可以用来捕获内存访问越界段错误等问题):

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/mman.h>
#include <unistd.h>
#include <setjmp.h>
#define FILESIZE 8192
sigjmp_buf env;
void handle_sigbus(int sig)
{
printf("SIGBUS!\n");
siglongjmp(env, 1);
}
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();
if (!sigsetjmp(env, 1)) {
for (i=0; i<FILESIZE; i++) {
tmp = p[i];
}
}
else {
printf("fault cached when i=%d\n", i);
}
printf("ok\n");
}

注意env参数的传递,看起来似乎有些奇怪。env是一个保存执行上下文(栈指针、指令指针、等)的结构,setjmp函数会在这个结构中填入当前的上下文信息。然而,调用setjmp时传递的居然不是&env,而是env!这是怎么回事呢?C可不支持引用传递的喔~
在libc里面,jmp_buf(env其类型)有个很奇怪的定义:typedef struct __jmp_buf_tag jmp_buf[1];
知道原因了吧,原来env是一个数组的名字呀~

按照同样的流程,先执行程序、再将文件缩小、再进行内存访问。得到的输出结果如下:

kouu@kouu-one:~/test$ ./a.out

SIGBUS!
fault cached when i=4096
ok

参阅一段邮件列表:http://lkml.indiana.edu/hypermail/linux/kernel/0205.1/0525.html


像可执行文件那样“text busy”?

在CU论坛上与网友讨论中(见: http://linux.chinaunix.net/bbs/thread-1162037-1-1.html),又引出一个问题:进程所执行的可执行文件也是通过mmap进行映射的(可以通过cat /proc/$pid/maps来看到这些映射)。那么如果我们在进程的执行期间将文件改小,是不是进程也会收到SIGBUS而崩溃呢?

如果你有办法将文件改小的话,的确会这样。但是你会发现,当你重写或者拷贝覆盖一个正在执行的文件时,控制台会给出“text busy”的提示。linux内核保证了这个文件不可写。

那么这是怎么做到的呢?mmap映射普通文件时是否可以借鉴?
这是通过建立映射时的MAP_DENYWRITE选项来实现的。这个选项在mmap的过程中会被处理:

mmap_region()
......
if (file) {
......
if (vm_flags & VM_DENYWRITE) {
error = deny_write_access(file);
......
}
......

MAP_DENYWRITE选项会被转成vma上的标记VM_DENYWRITE。mmap时遇到这个标记会调用deny_write_access。

deny_write_access()
int deny_write_access(struct file * file)
{
struct inode *inode = file->f_path.dentry->d_inode;

spin_lock(&inode->i_lock);
if (atomic_read(&inode->i_writecount) > 0) {
spin_unlock(&inode->i_lock);
return -ETXTBSY;
}
atomic_dec(&inode->i_writecount);
spin_unlock(&inode->i_lock);

return 0;
}

如果文件正在被写(inode->i_writecount大于0,可能存在多个写者),则映射失败。因为现在要做的是禁止别人写,但是别人先到一步,这就没办法了。
否则(inode->i_writecount小于等于0),让inode->i_writecount自减1。inode->i_writecount的值小于0时表示文件已被“deny write”。而inode->i_writecount还可能小于-1,因为有多个进程同时让它“deny write”。只有等它们都解除禁止时,文件才能够被写。

当一个文件被“deny write”之后,其他进程若想修改它,则在open这个文件的时候就会因为无法通过“deny write”的检查,而得到相应的错误码。
检查函数get_write_access跟deny_write_access正好是反过来的:

get_write_access()
int get_write_access(struct inode * inode)
{
spin_lock(&inode->i_lock);
if (atomic_read(&inode->i_writecount) < 0) {
spin_unlock(&inode->i_lock);
return -ETXTBSY;
}
atomic_inc(&inode->i_writecount);
spin_unlock(&inode->i_lock);

return 0;
}

这样,试图以写模式打开一个已经被“deny write”的文件,就将会被阻止。文件既然不能被打开,也就不能被写了。

然而,不幸的是,MAP_DENYWRITE选项在mmap系统调用里面是会被忽略的,只有在内核内部使用do_mmap时才能被使用(比如exec系列的系统调用中,在加载可执行文件时,就会调用do_mmap,并使用MAP_DENYWRITE选项)。
就连动态链接库也没法幸免(它们也是由库函数通过系统调用mmap来映射的。奇怪的是,为什么不用uselib系统调用呢?),搜到一篇康神的文章在说这个事情: http://blog.kangkang.org/index.php/archives/49

那么为什么要忽略mmap系统调用时传递的MAP_DENYWRITE选项呢?man mmap,可以看到这么一段:
MAP_DENYWRITE
This flag is ignored. (Long ago, it signaled that attempts to write to the underlying file should fail with ETXTBUSY. But this was a source of denial-of-service attacks.)

指定MAP_DENYWRITE选项可能引起一些Dos,这里指的是:一个普通用户可以使整个系统在某些方面拒绝服务。典型的做法是:用户以MAP_DENYWRITE选项mmap某个日志文件,于是需要写这个日志文件的应用程序将无法正常工作。
比如,login程序在用户登录时会写utmp日志(一般在/var/run/utmp),如果这个文件被某个用户“deny write”,那么其他用户就没法登录了。

目录
相关文章
|
运维 测试技术
6月27日阿里云故障说明
6月27日下午,我们在运维上的一个操作失误,导致一些客户访问阿里云官网控制台和使用部分产品功能出现问题。故障于北京时间2018年6月27日16:21左右开始,16:50分开始陆续恢复。对于这次故障,没有借口,我们不能也不该出现这样的失误!我们将认真复盘改进自动化运维技术和发布验证流程,敬畏每一行代码,敬畏每一份托付。
11338 2
|
存储 编译器 测试技术
交叉编译spdlpg 参数详解
交叉编译spdlpg 参数详解
452 0
element-plus:el-table自定义展开图标处于列的位置
element-plus:el-table自定义展开图标处于列的位置
1204 0
|
3月前
|
人工智能 API
LLMs.txt:AI时代网站的"智能身份证"
当AI模型因HTML冗余代码浪费50%上下文窗口时,LLMs.txt正成为网站与AI对话的新语言。这个轻量级标准已被Anthropic、Cursor等企业采用,让AI理解内容效率提升3倍。
137 0
|
编译器 C# Android开发
震惊!Uno Platform 与 C# 最新特性的完美融合,你不可不知的跨平台开发秘籍!
Uno Platform 是一个强大的跨平台应用开发框架,支持 Windows、macOS、iOS、Android 和 WebAssembly,采用 C# 和 XAML 进行编程。C# 作为其核心语言,持续推出新特性,如可空引用类型、异步流、记录类型和顶级语句等,极大地提升了开发效率。要在 Uno Platform 中使用最新 C# 特性,需确保开发环境支持相应版本,并正确配置编译器选项。通过示例展示了如何在 Uno Platform 中应用可空引用类型、异步流、记录类型及顶级语句等功能,帮助开发者更好地构建高效、优质的跨平台应用。
559 59
|
Java Maven
【异常】java: Internal error in the mapping processor: java.lang.NullPointerException
【异常】java: Internal error in the mapping processor: java.lang.NullPointerException
1744 0
|
存储 JSON JavaScript
Python教程:一文了解Python中的json库
JSON(JavaScript Object Notation)是一种轻量级数据交换格式,易于人类阅读和编写,也易于计算机解析和生成。在Python中,JSON通常用于数据交换和存储,因为它与Python的字典和列表类型相似。
1286 2
|
NoSQL 程序员 C语言
探秘Segmentation Fault错误:程序猿的噩梦
探秘Segmentation Fault错误:程序猿的噩梦
2619 0
|
Linux Windows 内存技术
PCIe初始化枚举和资源分配流程分析
本文主要是对PCIe的初始化枚举、资源分配流程进行分析,代码对应的是alikernel-4.19,平台是arm64 ## 1. PCIe architecture ### 1.1 pcie的拓扑结构 在分析PCIe初始化枚举流程之前,先描述下pcie的拓扑结构。 如下图所示: ![11.png](https://ata2-img.oss-cn-zhangjiakou.aliyuncs
8662 1
PCIe初始化枚举和资源分配流程分析
|
Linux 数据处理 Windows
探索Linux中的hexdump命令:数据处理的瑞士军刀
`hexdump`是Linux下的命令行工具,用于以十六进制格式显示和解析二进制文件内容,适用于分析文件结构、查找特定字节序列。它支持多种显示格式(如八进制、十进制)、数据分组和过滤功能。常用参数包括`-C`(混合十六进制和ASCII显示)、`-d`(十进制格式)、`-o`(八进制格式)、`-s`(跳过字节)、`-n`(显示字节数)。通过与`grep`等工具结合使用,可实现更复杂的任务。注意文件大小和选择合适显示格式,对于大文件使用`-n`限制输出。
下一篇
oss云网关配置