【原创】访问Linux进程文件表导致系统异常复位的排查记录
前提知识:
Linux内核、Linux 进程和文件数据结构、vmcore解析、汇编语言
问题背景:
这个问题出自项目的一个安全模块,主要功能是确定某进程是否有权限访问其正在访问的文件。
实现功能时,需要在内核里通过扫描该进程打开的文件表,获取文件的路径,和安全模块里配置的可访问文件的进程白名单进行匹配;
模块会一直到搜索到进程pid为1的进程,也就是init进程。在访问中间某个父进程的文件表时,出现struct task_struct的files指针为空的情况,
导致系统异常复位。
下面就是这次异常的分析和定位过程,希望对大家有所帮助,有什么想法我们可以交流讨论。
接到现场保障时,没想到又是这个模块导致的,因为这个模块刚升级版本,首先是确认系统有无crash vmcore文件生成,还好这次有
vmcore文件,先登录上去看异常堆栈吧。
确实是安全内核模块出了异常,为了保密,我省去了很多信息。
在dmesg.txt文件里,还有一句话:
从堆栈信息可以产出,异常进程comm名称是gzip,pid为10877,task指针为ffff88207f7f2380,异常原因是访问了NULL指针。
通过nm和addr2line命令,我们定位具体出异常的代码行:
include/linux/fdtable.h: 87

紧接着查看files_fdtable代码实现,是对files->fdt的访问:
#define files_fdtable(files) \
分析到这里,初步判断是访问files执行了异常。我们结合vmcore信息进一步确认。
vmcore文件分析
查找异常进程的files成员变量值是正常的,如下所示:
## crash> struct task_struct.files ffff88207f7f2380
## files = 0xffff881f97cff380
异常进程的进程名称:
## crash> struct task_struct.comm ffff88207f7f2380
## comm = "gzip\000\000\000\000\000\000\000\000\000\000\000"
问题不是访问当前进程files导致的,联想到此模块会向上遍历parent进程,并获取相关files中打开的文件信息,问题可能出自中间过程。
但是究竟是访问哪个进程出的问题呢?这就需要查看调用函数的堆栈信息和寄存器信息。
查找异常进程的父子进程关系
通过crash的ps命令,我们可以得到异常时所有的进程信息,我们摘出与gzip相关的进行信息:
从上图我们可以看到与gzip进程(pid=10877)相关的父子进程关系,我们上溯到pid=10875的进程是,发现其VSZ和RSS都是0,比较可疑。
通过crash该进程信息,可以看到其files,mm变量都为NULL。
从这里可以推断可能是访问该进程的异常files成员变量,导致了系统异常。
到底是不是这个访问引起的,我们还要从当时的堆栈信息做最终的确认。
通过crash dis 命令,可以得到 堆栈中显示的异常函数的汇编代码,截取代码片段如下:
异常堆栈显示异常代码是fdtable.h line 87,其上面一段代码: mov 0x730(%rdi),%rax就是装载task->files变量到rax寄存器。
结合堆栈信息,寄存器rdi值正是访问的task结构体指针ffff881f1d3ce280,而当前rax寄存器值为0。所以,会引起访问NULL指针
的异常。
另外,struct task_struct结构体中,files成员的偏移是1840,也就是0x730。
crash> struct task_struct.files
struct task_struct {
[1840] struct files_struct *files;
}
现在我们完全可以确定,安全模块函数访问了进程的files空指针,引起了系统异常。
但是,为什么父进程的files成员变量会为NULL呢?一般fork出来的子进程都会copy父进程的files等变量的呀。
关于这个问题,还是要从业务的源代码分析。业务中做文件压缩的模拟代码如下:
pid_t pid;
if ( (pid = vfork())< )
{
debug(("fork first process error.") );
} if (pid == )
{
if ( (pid = vfork())< )
{
debug(("fork second process error.") );
} if (pid == )
{
if(execlp("/XXX/mygzip.sh", "-f", ttemp.c_str(), t.c_str(), (char *) ) < )
{
debug(("execlp gzip error.") );
}
}
_exit(0);
}
else
{
if ( waitpid(pid, NULL, 0) < )
{
debug( ("wait error.") );
}
}
业务代码里通过vfork出来子进程调用execlp执行mygzip.sh脚本来做文件压缩。
查找vfork函数说明,有如下描述:
vfork() differs from fork(2) in that the parent is suspended until the child terminates (either normally, by calling exit(2), or abnormally, after
delivery of a fatal signal), or it makes a call to execve(2). Until that point, the child shares all memory with its parent, including the stack. The
child must not return from the current function or call exit(3), but may call _exit(2).
翻译一下,就是: 调用vfork的父进程会一直阻塞到子进程终结。
分析vfork的内核源码,也可以得到相应的印证:do_fork会调用copy_process函数,拷贝files,mm,fs等信息;由于vfork调用do_fork是带有
CLONE_VFORK标记,会等待子进程返回。
int sys_vfork(struct pt_regs *regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, , NULL, NULL);
}
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
......
9 /* copy files,mm,fs,namespace等信息 */
p = copy_process(clone_flags, stack_start, regs, stack_size,
child_tidptr, NULL, trace);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork; ...... wake_up_new_task(p); tracehook_report_clone_complete(trace, regs,
clone_flags, nr, p); if (clone_flags & CLONE_VFORK) {
freezer_do_not_count();
wait_for_completion(&vfork); /* 等待子进程返回 */
freezer_count();
tracehook_report_vfork_done(p, nr);
}
} else {
nr = PTR_ERR(p);
}
return nr;
}
以上说明,正常情况,子进程执行完成后,父进程才继续执行,其files,mm等成员不应该为空才对。
关键是,vfork说明里还有关键的一句:
(either normally, by calling exit(2), or abnormally, after delivery of a fatal signal), or it makes a call to execve(2).
说明子进程返回有三种情况:调用exit返回,或发送致命信号异常返回,或调用execve函数族返回。
业务代码调用了二次vfork函数,第二次vfork后,子进程2调用了execlp函数,该函数启动mygzip.sh脚本,而子进程1(第一次vfork后产生的子进程)
立即返回了(调用_exit(0)函数),父进程等到了子进程1退出。而子进程1是执行mygzip.sh脚本的父进程。
接着分析exit函数实现,会发现do_exit函数会释放父进程的mm,files等数据:
NORET_TYPE void do_exit(long code)
{
struct task_struct *tsk = current;
......
exit_signals(tsk); /* sets PF_EXITING */
......
exit_mm(tsk); /* 释放mm数据 */
......
exit_sem(tsk);
exit_files(tsk); /* 释放打开的文件表 */
exit_fs(tsk);
check_stack_usage();
exit_thread(); ......
}
exit_files函数实现:
void exit_files(struct task_struct *tsk)
{
struct files_struct * files = tsk->files; if (files) {
task_lock(tsk);
tsk->files = NULL; /* 进程files赋值为NULL */
task_unlock(tsk);
put_files_struct(files); /* 会调用close_files函数,接着看下面的代码 */
}
}
put_files_struct函数:
void put_files_struct(struct files_struct *files)
{
struct fdtable *fdt; if (atomic_dec_and_test(&files->count)) {
close_files(files); /* 会调用 cond_resched(); */
...... }
}
closes_files函数:
static void close_files(struct files_struct * files)
{
......
rcu_read_lock();
fdt = files_fdtable(files);
rcu_read_unlock();
for (;;) {
unsigned long set;
i = j * __NFDBITS;
if (i >= fdt->max_fds)
break;
set = fdt->open_fds->fds_bits[j++];
while (set) {
if (set & ) {
struct file * file = xchg(&fdt->fd[i], NULL);
if (file) {
filp_close(file, files);
cond_resched(); /* 正式这一句代码,让gzip进程有了执行的机会,父进程此时还未完全退出,但是其files已经是NULL */
}
}
i++;
set >>= ;
}
}
}
cond_resched();
正式这一句代码,让gzip进程有了执行的机会,父进程此时还未完全退出,但是其files已经是NULL。当gzip访问父进程的files变量时,
就会出现NULL访问异常,系统异常复位。
经过以上的分析,可以得出如下结论:
1.由于子进程访问了父进程的空files,导致了系统异常;
2.由于vfork和execlp函数的特性,以及_exit函数调用,共同决定了父进程files值为NULL的可能;
3.子进程通过parent访问父进程的成员变量是不安全的。
最后一个问题:如果才能安全访问进程的parent及其成员变量呢?这又是一个课题了,有待后续分析。
PS:您的支持是对博主最大的鼓励
【原创】访问Linux进程文件表导致系统异常复位的排查记录的更多相关文章
- Linux中生成Core Dump系统异常信息记录文件的教程
Linux中生成Core Dump系统异常信息记录文件的教程 http://www.jb51.net/LINUXjishu/473351.html
- 【进程/作业管理】篇章一:Linux进程及其管理(系统监控类工具)----glances、dstat
glances dstat glances命令详解 相对于htop工具的使用,这里介绍一下glances工具的使用,我个人是比较喜欢这款工具的,主要就是由于glances这款工具可以将系统状态 ...
- Linux 进程必知必会
上一篇文章只是简单的描述了一下 Linux 基本概念,通过几个例子来说明 Linux 基本应用程序,然后以 Linux 基本内核构造来结尾.那么本篇文章我们就深入理解一下 Linux 内核来理解 Li ...
- Linux大文件已删除,但df查看已使用的空间并未减少解决
在我的生活当中遇到磁盘快满了,这时候准备去删除一些大文件 于是我使用ncdu 查看了一下当前系统占用资源比较多的是那些文件,结果一看是elasticsearch的日志文件,好吧,竟然找到源头了,那就把 ...
- Linux各个文件及其含义
树状目录结构: 以下是对这些目录的解释: /bin:bin是Binary的缩写, 这个目录存放着最经常使用的命令. /boot:这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像 ...
- linux初学者-文件权限
linux初学者-文件权限 lunix系统都是以文件的形式存在,自然而然的就会要求不同的用户拥有不同的权限,这也是系统能够运行的根本保证,下文将对文件的权限管理进行简要的介绍. 1.文件属性的查看 - ...
- 【Linux 进程】孤儿进程、僵尸进程和守护进程
1.孤儿进程: 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程.孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作.孤儿进程是 ...
- 代码中会话同步(同步redis)导致的异常问题
背景: 第一天拷贝了一份tomcat(配置了redis会话同步),部署了erp-rocketmq应用(用作给顾客发送消息). 第二天早晨,整个erp系统出现异常情况: 1> ...
- MySQL实例多库某张表数据文件损坏导致xxx库无法访问故障恢复
一.问题发现 命令行进入数据库实例手动给某张表进行alter操作,发现如下报错. mysql> use xx_xxx; No connection. Trying to reconnect... ...
随机推荐
- Share Today
当问[一生中最大的错误是什么?]时,佛陀回答: 最大的错误就是你以为你还有时间 时间是免费的也是无价的 你无法拥有 但可以花费 而一旦失去 就无法挽回 一般人一生有78年 我们有28.3年在睡觉 几乎 ...
- su: Authentication failure问题
问题: su命令不能切换root,提示错误su: Authentication failure 解决: 使用命令 sudo passwd root 下次再su的时候只要输入密码就可以成功登录了.
- note 11 字典
字典 Dictionary +什么是字典? +一系列的"键-值(key-value)"对 +通过"键"查找对应的"值" +类似纸质字典,通过 ...
- 禅知Pro 1.6 前台任意文件读取 | 代码审计
禅知 Pro v1.6 前台任意文件读取 | 代码审计 蝉知专业版是基于蝉知企业门户系统开源版开发,继承了蝉知本身的优秀功能.相对于蝉知开源版增强了商品的属性自定义.属性价格定制.物流跟踪.微信支付. ...
- 并发之痛 Thread,Goroutine,Actor
转自:http://jolestar.com/parallel-programming-model-thread-goroutine-actor/ 先梳理下两个概念,几乎所有讲并发的文章都要先讲这两个 ...
- oracle存储过程和存储函数&触发器
oracle存储过程和存储函数 指存储在数据库中供所有用户程序调用的子程序叫存储过程,存储函数 存储过程和存储函数的相同点:完成特定功能的程序 存储过程和存储函数的区别:是否用return语句返回值 ...
- WordPress版微信小程序2.6版发布
WordPress版微信小程序的完善和升级的工作一直都在进行中,我争取保证一个月可以出一个版本,希望通过一点点的改进,让这个开源产品日趋完美. 同时,pro版WordPress微信小程序也在紧锣密鼓的 ...
- scikit-learn框架学习笔记(一)
sklearn于2006年问世于Google,是使用python语言编写的.基于numpy.scipy和matplotlib的一个机器学习算法库,设计的非常优雅,它让我们能够使用同样的接口来实现所有不 ...
- [STM32F103]串口UART配置
l 串口时钟使能,GPIO时钟使能: RCC_APB2PeriphClockCmd(); l 串口复位: USART_DeInit(); 这一步不是必须的 l GPIO端口模式设置: GPIO_Ini ...
- express+websocket+exec+spawn=webshell
var child_process = require('child_process'); var ws = require("nodejs-websocket"); consol ...