【原创】访问Linux进程文件表导致系统异常复位的排查记录

前提知识:

   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等变量的呀。

关于这个问题,还是要从业务的源代码分析。业务中做文件压缩的模拟代码如下:

 1         pid_t pid;
 2         if ( (pid = vfork())<0 )
 3         {
 4            debug(("fork first process  error.") );
 5         }
 6
 7         if (pid == 0)
 8         {
 9             if ( (pid = vfork())<0 )
10             {
11                 debug(("fork second process error.") );
12             }
13
14             if (pid == 0)
15             {
16                 if(execlp("/XXX/mygzip.sh", "-f", ttemp.c_str(), t.c_str(), (char *) 0) <0 )
17                 {
18                     debug(("execlp gzip error.") );
19                 }
20             }
21             _exit(0);
22         }
23         else
24         {
25             if ( waitpid(pid, NULL, 0) <0 )
26             {
27                 debug( ("wait error.") );
28             }
29         }

  业务代码里通过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标记,会等待子进程返回。

1 int sys_vfork(struct pt_regs *regs)
2 {
3     return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, 0,  NULL, NULL);
4 }
 1 long do_fork(unsigned long clone_flags,
 2           unsigned long stack_start,
 3           struct pt_regs *regs,
 4           unsigned long stack_size,
 5           int __user *parent_tidptr,
 6           int __user *child_tidptr)
 7 {
 8     ......
 9     /* copy files,mm,fs,namespace等信息 */
10     p = copy_process(clone_flags, stack_start, regs, stack_size,
11              child_tidptr, NULL, trace);
12     /*
13      * Do this prior waking up the new thread - the thread pointer
14      * might get invalid after that point, if the thread exits quickly.
15      */
16     if (!IS_ERR(p)) {
17         struct completion vfork;
18
19         ......
20
21         wake_up_new_task(p);
22
23         tracehook_report_clone_complete(trace, regs,
24                         clone_flags, nr, p);
25
26         if (clone_flags & CLONE_VFORK) {
27             freezer_do_not_count();
28             wait_for_completion(&vfork);  /* 等待子进程返回 */
29             freezer_count();
30             tracehook_report_vfork_done(p, nr);
31         }
32     } else {
33         nr = PTR_ERR(p);
34     }
35     return nr;
36 }

   

     以上说明,正常情况,子进程执行完成后,父进程才继续执行,其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函数族返回。

   业务代码调用了execlp函数,子进程启动mygzip.sh脚本后,立即返回了,父进程等到了子进程退出,也调用了_exit(0)函数。

   接着分析exit函数实现,会发现do_exit函数会释放父进程的mm,files等数据:

 1 NORET_TYPE void do_exit(long code)
 2 {
 3     struct task_struct *tsk = current;
 4         ......
 5     exit_signals(tsk);  /* sets PF_EXITING */
 6     ......
 7     exit_mm(tsk);   /* 释放mm数据 */
 8         ......
 9     exit_sem(tsk);
10     exit_files(tsk);  /* 释放打开的文件表 */
11     exit_fs(tsk);
12     check_stack_usage();
13     exit_thread();
14
15     ......
16 }

  

  exit_files函数实现:

 1 void exit_files(struct task_struct *tsk)
 2 {
 3     struct files_struct * files = tsk->files;
 4
 5     if (files) {
 6         task_lock(tsk);
 7         tsk->files = NULL;  /* 进程files赋值为NULL */
 8         task_unlock(tsk);
 9         put_files_struct(files); /* 会调用close_files函数,接着看下面的代码 */
10     }
11 }

  put_files_struct函数:

 1 void put_files_struct(struct files_struct *files)
 2 {
 3     struct fdtable *fdt;
 4
 5     if (atomic_dec_and_test(&files->count)) {
 6         close_files(files);  /* 会调用 cond_resched(); */
 7         ......19     }
20 }

  

closes_files函数:

 1 static void close_files(struct files_struct * files)
 2 {
 3 ......
 4     rcu_read_lock();
 5     fdt = files_fdtable(files);
 6     rcu_read_unlock();
 7     for (;;) {
 8         unsigned long set;
 9         i = j * __NFDBITS;
10         if (i >= fdt->max_fds)
11             break;
12         set = fdt->open_fds->fds_bits[j++];
13         while (set) {
14             if (set & 1) {
15                 struct file * file = xchg(&fdt->fd[i], NULL);
16                 if (file) {
17                     filp_close(file, files);
18                     cond_resched();  /* 正式这一句代码,让gzip进程有了执行的机会,父进程此时还未完全退出,但是其files已经是NULL */
19                 }
20             }
21             i++;
22             set >>= 1;
23         }
24     }
25 }

  cond_resched();

正式这一句代码,让gzip进程有了执行的机会,父进程此时还未完全退出,但是其files已经是NULL。当gzip访问父进程的files变量时,

就会出现NULL访问异常,系统异常复位。

  经过以上的分析,可以得出如下结论:

1.由于子进程访问了父进程的空files,导致了系统异常;

2.由于vfork和execlp函数的特性,共同决定了父进程files值为NULL的可能;

   3.子进程通过parent访问父进程的成员变量是不安全的。

 

最后一个问题:如果才能安全访问进程的parent及其成员变量呢?这又是一个课题了,有待后续分析。

 PS:您的支持是对博主最大的鼓励??,感谢您的认真阅读。
    
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文地址:https://www.cnblogs.com/smith9527/p/10246847.html

时间: 2025-01-18 07:59:47

【原创】访问Linux进程文件表导致系统异常复位的排查记录的相关文章

修改注册表导致系统进不去,安全模式也进不去

一天一不小把注册表下面的SAM和security的两个项删除了,重启后系统进不去进不了桌面了,安全模式也进不去, 于是系统之家尝试了以下解决办法: 1.用win7 系统U盘来修复,结果仍然不行 2.使用注册表修复工具,结果提示找不到相应的dll文件 3.使用系统还原,可是没建立系统还原点 最后,使用系统本身备份的注册表文件替换掉现有的,这样才解决了. 具体的做法是,用win7PE系统U盘启动,进入系统后,把原来系统下面的 C:\windows\system32\config\RegBack目录,

【原创】linux压缩文件夹时排除特定文件夹

linux下压缩解压文件这是非常常用的命令,我们需要很经常的来打包文件做转移或者其他的什么操作. 有时公司的程序员需要把正在运行网站的程序下载下来,需要打包,但是比如备份文件夹有十几G,上传图片的文件夹也很大,总不能把其他文件夹写上一遍吧? 通过查找,我也找到两种方法,在这里和大家分享一下 1. $(ls | grep -v xx) tar -zcf tmp.tgz $(ls | grep -v xx) grep -v :反检索,只显示不匹配的行.这样就排除了特定的文件夹.这个方法也适用与zip

linux重要文件丢失导致系统故障,修复方法,(以 libc.so.6库损坏,rpm软件包故障为例)

第一步:找出损坏的文件及它的安装包,安装修复.修复之前先明确你损坏的文件是属于那一个软件包,这个可以在一台能正常运行的同版本的服务器上查看. 好了知道什么文件损坏,接下来只要安装修复就好了. 第二步:如果是实体服务器出现上述故障就直接按重启按钮,如果是虚拟机那就直接按重置. 第三步:从光驱引导,并进入救援模式. 在BIOS下面设置从光驱引导,不同的主板引导的方式不一样,常用的快捷按键有 F12ESC 一般开机的时候会有提示. 第四步,安装对应的软件. 在安装过程中,可能会于RPM库损坏导致没法直

linux服务器硬件报错,系统异常重启检测-MCElog

mcelog 是 x86 的 Linux 系统上用来检查硬件错误,特别是内存和CPU错误的工具.比如服务器隔一段时间莫名的重启一次,而message和syslog又检测不到有价值的信息. 通常发生MCE报错的原因有如下:1.内存报错或者ECC问题2.处理器过热3.系统总线错误4.CPU或者硬件缓存错误 一般来说当有错误提示时,需要优先注意内存问题,但由于现在内存控制器是集成在cpu里,所以有个别情况是由CPU问题引起的 一.如果是联网的情况下,yum源配置可用则yum install mcelo

Linux - 进程 (二) 进程创建

详细见:https://github.com/ZhangzheBJUT/linux 一 进程概述 一个进程都由另一个称之为父进程的进程启动,被父进程启动的进程叫做子进程.Linux系统启动时候,它将运行一个名为init的进程,该进程是系统运行的第一个进程,它的进程号为1,它负责管理其它进程,可以把它看做是操作系统进程管理器,它是其它所有进程的祖先进程.系统中的进程要么是由init进程启动,要么是由init进程启动的其他进程启动. 使用ps命令输出中的PPID栏给出的是父进程的进程ID,它是启动这

Linux各个文件及其含义

树状目录结构: 以下是对这些目录的解释: /bin:bin是Binary的缩写, 这个目录存放着最经常使用的命令. /boot:这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件以及镜像文件. /dev :dev是Device(设备)的缩写, 该目录下存放的是Linux的外部设备,在Linux中访问设备的方式和访问文件的方式是相同的. /etc:这个目录用来存放所有的系统管理所需要的配置文件和子目录. /home:用户的主目录,在Linux中,每个用户都有一个自己的目录,一般该目录

LINUX删除文件,但空间不释放

1.问题描述: rm  /tmp/access_log 通过rm删除大文件之后,查看磁盘结果显示磁盘占用依然是100%,空间并没有被释放. 2.解决思路 一般说来不会出现删除文件后空间不释放的情况,但是也存在例外,比如文件被进程锁定,或者有进程一直在向这个文件写数据等等,要理解这个问题,就需要知道Linux下文件的存储机制和存储结构. 一个文件在文件系统中的存放分为两个部分:数据部分和指针部分,指针位于文件系统的meta-data中,数据被删除后,这个指针就从meta-data中清除了,而数据部

linux内核文件IO的系统调用实现分析(open)

http://blog.chinaunix.net/uid-23969156-id-3086824.html 1.          引言      从事Linux环境工作2年有余,一直懵懵懂懂,1年前拜读了<莱昂氏UNIX源代码分析>一书,感觉自己的学习道路漫漫且修远.最近受chinaunix的精华文帖启发,拟将近来的部分内核调用分析笔记拿出来与各前辈先进共同探讨学习,以壮个人学习之路.      本部分主要讲述的是文件I/O操作的2.6.11内核版本实现,包括了主要的数据结构.宏定义和函数

linux初学者-文件权限

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