前提知识:

   Linux内核、Linux 进程和文件数据结构、vmcore解析、汇编语言

问题背景:

   这个问题出自项目的一个安全模块,主要功能是确定某进程是否有权限访问其正在访问的文件。

实现功能时,需要在内核里通过扫描该进程打开的文件表,获取文件的路径,和安全模块里配置的可访问文件的进程白名单进行匹配;

模块会一直到搜索到进程pid为1的进程,也就是init进程。在访问中间某个父进程的文件表时,出现struct task_struct的files指针为空的情况,

导致系统异常复位。

  下面就是这次异常的分析和定位过程,希望对大家有所帮助,有什么想法我们可以交流讨论。

接到现场保障时,没想到又是这个模块导致的,因为这个模块刚升级版本,首先是确认系统有无crash vmcore文件生成,还好这次有

vmcore文件,先登录上去看异常堆栈吧。

  确实是安全内核模块出了异常,为了保密,我省去了很多信息。

在dmesg.txt文件里,还有一句话:

   <1>[259267.001561] BUG: unable to handle kernel NULL pointer dereference at 0000000000000008

  从堆栈信息可以产出,异常进程comm名称是gzip,pid为10877,task指针为ffff88207f7f2380,异常原因是访问了NULL指针。

  通过nm和addr2line命令,我们定位具体出异常的代码行:

  include/linux/fdtable.h: 87

  

  紧接着查看files_fdtable代码实现,是对files->fdt的访问:

  #define files_fdtable(files) \

        (rcu_dereference_check_fdtable((files), (files)->fdt))
 

  分析到这里,初步判断是访问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进程文件表导致系统异常复位的排查记录的更多相关文章

  1. Linux中生成Core Dump系统异常信息记录文件的教程

    Linux中生成Core Dump系统异常信息记录文件的教程 http://www.jb51.net/LINUXjishu/473351.html

  2. 【进程/作业管理】篇章一:Linux进程及其管理(系统监控类工具)----glances、dstat

    glances   dstat   glances命令详解 相对于htop工具的使用,这里介绍一下glances工具的使用,我个人是比较喜欢这款工具的,主要就是由于glances这款工具可以将系统状态 ...

  3. Linux 进程必知必会

    上一篇文章只是简单的描述了一下 Linux 基本概念,通过几个例子来说明 Linux 基本应用程序,然后以 Linux 基本内核构造来结尾.那么本篇文章我们就深入理解一下 Linux 内核来理解 Li ...

  4. Linux大文件已删除,但df查看已使用的空间并未减少解决

    在我的生活当中遇到磁盘快满了,这时候准备去删除一些大文件 于是我使用ncdu 查看了一下当前系统占用资源比较多的是那些文件,结果一看是elasticsearch的日志文件,好吧,竟然找到源头了,那就把 ...

  5. Linux各个文件及其含义

    树状目录结构: 以下是对这些目录的解释: /bin:bin是Binary的缩写, 这个目录存放着最经常使用的命令. /boot:这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像 ...

  6. linux初学者-文件权限

    linux初学者-文件权限 lunix系统都是以文件的形式存在,自然而然的就会要求不同的用户拥有不同的权限,这也是系统能够运行的根本保证,下文将对文件的权限管理进行简要的介绍. 1.文件属性的查看 - ...

  7. 【Linux 进程】孤儿进程、僵尸进程和守护进程

    1.孤儿进程: 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程.孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作.孤儿进程是 ...

  8. 代码中会话同步(同步redis)导致的异常问题

    背景: 第一天拷贝了一份tomcat(配置了redis会话同步),部署了erp-rocketmq应用(用作给顾客发送消息).            第二天早晨,整个erp系统出现异常情况: 1> ...

  9. MySQL实例多库某张表数据文件损坏导致xxx库无法访问故障恢复

    一.问题发现 命令行进入数据库实例手动给某张表进行alter操作,发现如下报错. mysql> use xx_xxx; No connection. Trying to reconnect... ...

随机推荐

  1. hiero.ui获取实例名的方法

    在hiero.ui中经常会通过hiero.ui.windowManager().windows()来获取当前QMainWindow中的QWidget子窗口,而这些子窗口是以实例对象的方式返回的,如果想 ...

  2. pecl 更换对应php版本

    转自  https://www.jianshu.com/p/fee58d93e8b1 版本问题 如果我们是通过apt-get或者yum安装的php,现在一般默认的都是php7.0版本. pecl安装过 ...

  3. RN 时间戳

    let curTime = Date.now(); 获取到当前时间 curTime: 1555120690696 是指从1970.1.1到现在的毫秒(ms)数 cxk() { //之前时间 let p ...

  4. 关于Oracle字符集在dmp文件导入导出中的乱码影响

    Oracle 在进行dmp备份和还原的时候,服务器端字符集和客户端字符集会对这个过程有较大影响,特别是数据表中存储了中文.存储过程中使用了中文编码(注释)的时候,如果没有处理好字符集的问题,在进行还原 ...

  5. [蓝桥杯]PREV-22.历届试题_国王的烦恼

    问题描述 C国由n个小岛组成,为了方便小岛之间联络,C国在小岛间建立了m座大桥,每座大桥连接两座小岛.两个小岛间可能存在多座桥连接.然而,由于海水冲刷,有一些大桥面临着不能使用的危险. 如果两个小岛间 ...

  6. HTML5 新的 Input 类型

    Input 类型: color(拾色器) color 类型用在input字段主要用于选取颜色,如下所示: 支持浏览器 实例 从拾色器中选择一个颜色: 选择你喜欢的颜色: <input type= ...

  7. 前后台数据交换,printwriter、jsonobject、jsonarray、ajax请求,数据交换

    后台代码: public void findByIDEquipment() { getResponse().setCharacterEncoding("UTF-8"); getRe ...

  8. CSS的块级元素和内联元素的概念

    三生有幸,偶然之下知道了<CSS世界>这本书,让我产生了探究 CSS 的想法. 这里对 CSS 中的块级元素和内联元素的概念做一个简单的整理. 可能对于我们前端开发人员来讲,一般接触到的元 ...

  9. Docker端口映射与容器互联

    Docker提供了两个功能来满足访问的基本需求:一是允许映射容器内应用的服务端口到本地宿主主机:另一个是互联机制实现多个容器间通过容器名来快速实现访问. 一.端口映射实现访问容器 当容器中运行一些网络 ...

  10. [UE4]虚幻引擎的C++环境安装

    一.一般使用VS2017开发 二.需要勾选“使用C++的游戏开发”