【原创】访问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... ...
随机推荐
- VS重新生成项目失败,但是不报错
相信很多同行可能都遇到这种情况: 使用SVN或者其他项目管理工具checkout一个项目后,重新生成项目后,生成失败了,但是没有显示任何错误: 其实,这个大多数是因为类库里面的引用失效导致的,我们可以 ...
- debian shell脚本关联
懒得命令行一个个的输 设置,MIME类型编辑,搜索x-shellscript,默认的改成/bin/bash即可
- SecureCRT 6.7 vim高亮
cp /etc/vimrc ~/.vimrc set nocompatible "去掉有关vi一致性模式,避免以前版本的bug和局限 set nu! ...
- RAM和Flash区别
都是随机存储器,断电数据消失,但Flash有点不一样,它在消失数据之前,添加了一个""性质",这个性质能上电后再识别,且把这个信号返回到ram中,这样近似的把flash当 ...
- 流程帮App风险评估
一. 存在风险 此处罗列出了我们开发小组可能遇到8种的风险. 编号 风险名称 内容 发生概率 损失(人周) 危险度(周) 1 计划编制风险 对所要使用技术不熟悉,可能导致无法交付: 每个模块的实现一定 ...
- 2018-2019-2 20165312《网络攻防技术》Exp1 PC平台逆向破解
2018-2019-2 20165312<网络攻防技术>Exp1 PC平台逆向破解 一.Exp1.1 直接修改程序机器指令,改变程序执行流程 知识要求:Call指令,EIP寄存器,指令跳转 ...
- 音乐出身的妹纸,零基础学习JAVA靠谱么
问:表示音乐出身的妹纸一枚 某一天突然觉得身边认识的是一群程序员 突然想 要不要也去试试... 众好友都觉得我该去做个老师,可是我怕我会误人子弟,祸害祖国下一代..... 要不要 要不要 学Ja ...
- C7.cpp
Arr[i]==*(ar+i) &arr[i]==ar+i 编译过程的最终产品是可执行程序------由一组机器语言指令组成 内联函数的优点是速度快,但是会占用很多内存,若是在 ...
- react-native shadow失效
做边框阴影,但是有时候会失效,内容产生阴影,而边框无效,今天发现了原因,没错,就是没有设置背景颜色导致的.如图
- Android 开发 倒计时功能 转载
原文地址:https://www.cnblogs.com/xch-yang/p/7920419.html Android为我们封装好了一个抽象类CountDownTimer,可以实现计时器功能: /* ...