• linux 覆盖可执行文件的问题


    测试环境是3.10.0 内核。

    有一次操作中,发现cp -f A B执行的时候,行为不一样:

    当B没被打开,则正常覆盖B。

    当B是被打开,但没有被执行,则能覆盖,

    当B被打开,且被执行,则不能直接覆盖,而是创建一个同名文件,然后写这个文件,同时B的inode在os中用lsof看的话,是delete。

    问题是:为什么被执行的文件不能覆盖,它通过什么机制保护的?

    通过strace,发现cp -f的时候,如果目标文件正在被执行,那么返回的是 ETXTBSY,为什么会返回busy,以及这个busy是怎么设置的呢?

    stap 一下:

    probe kernel.function("do_dentry_open").return
    {
       if($return == -26)---------这个就是busy的错误码
      {
        print_backtrace();
         exit();
       }
    }

    通过搜索并stap内核代码,发现流程如下:

    Returning from:  0xffffffff812062d0 : do_dentry_open+0x0/0x2e0 [kernel]
    Returning to  :  0xffffffff8120664a : vfs_open+0x5a/0xb0 [kernel]
     0xffffffff812179ad : do_last+0x1ed/0x12c0 [kernel]
     0xffffffff816be459 : kretprobe_trampoline+0x0/0x57 [kernel]
     0xffff88557427ffd8
     0xffffffff8121b0eb : do_filp_open+0x4b/0xb0 [kernel] (inexact)
     0xffffffff81125a87 : __audit_getname+0x97/0xb0 [kernel] (inexact)
     0xffffffff8122840a : __alloc_fd+0x8a/0x130 [kernel] (inexact)
     0xffffffff81207a13 : do_sys_open+0xf3/0x1f0 [kernel] (inexact)
     0xffffffff81207b2e : sys_open+0x1e/0x20 [kernel] (inexact)
     0xffffffff816c5991 : tracesys+0x9d/0xc3 [kernel] (inexact)
    static int do_dentry_open(struct file *f,
                  struct inode *inode,
                  int (*open)(struct inode *, struct file *),
                  const struct cred *cred)
    {
        static const struct file_operations empty_fops = {};
        int error;
    
        f->f_mode = OPEN_FMODE(f->f_flags) | FMODE_LSEEK |
                    FMODE_PREAD | FMODE_PWRITE;
    
        if (unlikely(f->f_flags & O_PATH))
            f->f_mode = FMODE_PATH;
    
        path_get(&f->f_path);
        f->f_inode = inode;
        if (f->f_mode & FMODE_WRITE) {
            error = __get_file_write_access(inode, f->f_path.mnt);
    static inline int __get_file_write_access(struct inode *inode,
                          struct vfsmount *mnt)
    {
        int error;
        error = get_write_access(inode);

    报错的最终函数就是get_write_access:

    static inline int get_write_access(struct inode *inode)
    {
        return atomic_inc_unless_negative(&inode->i_writecount) ? 0 : -ETXTBSY;
    }

    看来,文件在被执行的时候,会将 inode->i_writecount 值会被设置为负值?查看代码,调用链是:

    stub_execve -->sys_execve-->do_execve_common-->do_open_exec-->deny_write_access

    我照样stap一下:

    一个gdb程序用来打开一个文件,然后gdb断住:

    (gdb) n
    6         FILE *pFile=NULL;
    (gdb)
    7         char * Mode="a+";
    (gdb)
    8         int ret=0;
    (gdb)
    9         pFile = fopen("main.o", Mode);
    (gdb)
    10        if(!pFile)
    (gdb)
    15          ret=fputs("abcd", pFile);

    另外一边,使用stap进行跟踪:

    probe kernel.function("do_open_exec").return
    {
       if($return == -26)
        {
         print_backtrace();
         exit();
        }
    
    }

    然后第三个窗口执行./main.o:

    strace ./main.o
    execve("./main.o", ["./main.o"], [/* 38 vars */]) = -1 ETXTBSY (Text file busy)
    write(2, "strace: exec: Text file busy
    ", 29strace: exec: Text file busy
    ) = 29
    exit_group(1)                           = ?
    +++ exited with 1 +++

    发现确实报错的是busy,也就是 deny_write_access 返回busy。

    stap的结果是:

    [root@localhost code]# stap cp_fail.stp
    System Call Monitoring Started (10 seconds)...
    Returning from:  0xffffffff8120ffe0 : do_open_exec+0x0/0x100 [kernel]
    Returning to  :  0xffffffff81210f73 : do_execve_common.isra.24+0x203/0x6c0 [kernel]
     0xffffffff812116c9 : sys_execve+0x29/0x30 [kernel]
     0xffffffff816c5c98 : stub_execve+0x48/0x80 [kernel]

    当然,如果执行在前,而open再写入在后,则报busy的是后面的open。

    阶段性总结一下:

    对于文件的写入,内核需要判断当前文件是否正在被执行,如果在的话,则拒绝写入,当一个可执行文件已经为write而open时,此时的可执行文件是不允许被执行的。反过来,一个文件正在执行时,它也是不允许同时被write模式而open的。相关函数如下。

    /*
     * get_write_access() gets write permission for a file.
     * put_write_access() releases this write permission.
     * This is used for regular files.
     * We cannot support write (and maybe mmap read-write shared) accesses and
     * MAP_DENYWRITE mmappings simultaneously. The i_writecount field of an inode
     * can have the following values:
     * 0: no writers, no VM_DENYWRITE mappings
     * < 0: (-i_writecount) vm_area_structs with VM_DENYWRITE set exist
     * > 0: (i_writecount) users are writing to the file.--------------------------可以有多个write
     *
     * Normally we operate on that counter with atomic_{inc,dec} and it's safe
     * except for the cases where we don't hold i_writecount yet. Then we need to
     * use {get,deny}_write_access() - these functions check the sign and refuse
     * to do the change if sign is wrong.
     */
    static inline int get_write_access(struct inode *inode)
    {
        return atomic_inc_unless_negative(&inode->i_writecount) ? 0 : -ETXTBSY;
    }
    static inline int deny_write_access(struct file *file)
    {
        struct inode *inode = file_inode(file);
        return atomic_dec_unless_positive(&inode->i_writecount) ? 0 : -ETXTBSY;
    }
    static inline void put_write_access(struct inode * inode)
    {
        atomic_dec(&inode->i_writecount);
    }
    static inline void allow_write_access(struct file *file)
    {
        if (file)
            atomic_inc(&file_inode(file)->i_writecount);
    }
    static inline bool inode_is_open_for_write(const struct inode *inode)
    {
        return atomic_read(&inode->i_writecount) > 0;
    }

    接下来,描述一下故障现场,当时的情况是,nginx程序正在运行,系统升级,有测试人员将so库使用cp -f 覆盖,由于此时某些so已经被该程序所使用,现在把这些so文件覆盖了,导致了该程序崩溃。结果正在运行的nginx出现了段错误,

    [6908922.939768] nginx[10922]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908922.947471] nginx[10923]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908922.947851] nginx[10927]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908922.949222] nginx[10916]: segfault at 73c6 ip 00000000000073c6 sp 00007fff70ef6508 error 14 in nginx (deleted)[400000+46c000]
    [6908922.960546] nginx[10932]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908922.962161] nginx[10919]: segfault at 73c6 ip 00000000000073c6 sp 00007fff70ef6508 error 14 in nginx (deleted)[400000+46c000]
    [6908927.932537] show_signal_msg: 1228 callbacks suppressed
    [6908927.932543] nginx[12283]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908927.934137] nginx[12284]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908927.936796] nginx[12286]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908927.938551] nginx[12285]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908927.941886] nginx[12288]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908927.943306] nginx[12287]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908927.945299] nginx[12289]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908927.949962] nginx[12291]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908927.950502] nginx[12290]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]
    [6908927.953350] nginx[12293]: segfault at 73c6 ip 00000000000073c6 sp 00007fffeb5e7868 error 14 in nginx[400000+422000]

    按照之前的分析,正在运行的代码,是不能覆盖的,那为什么动态库被覆盖了呢?下面来分析一下动态链接库的调用。

    [root@localhost code]# cat dll.c
    #include <stdio.h>
    void dll_function(const char* szString)
    {
            printf("%s
    ", szString);
    }
    cat dll_main.c
    void dll_function(const char* szString);
    int main()
    {
           dll_function("call dll now!!!!!!");
           return 0;
    }
    [root@localhost code]# gcc -c -fPIC dll.c
    [root@localhost code]# gcc -shared -fPIC -o libdllfun.so dll.o
    [root@localhost code]# cp libdllfun.so /usr/lib
    [root@localhost code]# gcc -o dll_test dll_main.c -L. -ldllfun
    strace ./dll_test
    execve("./dll_test", ["./dll_test"], [/* 38 vars */]) = 0
    brk(NULL)                               = 0x2254000
    mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f024eb8f000
    access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
    open("/usr/lib/tls/x86_64/libdllfun.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
    stat("/usr/lib/tls/x86_64", 0x7ffe6791bd40) = -1 ENOENT (No such file or directory)
    open("/usr/lib/tls/libdllfun.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
    stat("/usr/lib/tls", 0x7ffe6791bd40)    = -1 ENOENT (No such file or directory)
    open("/usr/lib/x86_64/libdllfun.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
    stat("/usr/lib/x86_64", 0x7ffe6791bd40) = -1 ENOENT (No such file or directory)
    open("/usr/lib/libdllfun.so", O_RDONLY|O_CLOEXEC) = 3-----------------------找到并打开对应的dll文件
    read(3, "177ELF2113>13405"..., 832) = 832
    fstat(3, {st_mode=S_IFREG|0750, st_size=7912, ...}) = 0
    mmap(NULL, 2101304, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f024e76d000-----------map对应的代码

      mprotect(0x7f024e76e000, 2093056, PROT_NONE) = 0
      mmap(0x7f024e96d000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0x7f024e96d000
      close(3)-------------------文件关闭

     

    由测试代码可知,linux加载动态库是先读取elf头,然后使用的mmap方式加载代码,虽然也有open,但open的时候是O_RDONLY|O_CLOEXEC

    而不是我们前面例子里面的wirte方式打开,既然如此,那么文件中的内容在被异常更改之后,就可能执行代码出错。为此,我也测试了一下,确实如此,有时候出现段错误,有时候

    出现sigbus,有时候一点问题没有,完全取决于你修改的文件的位置。

    12           dll_function32("call 32!!!!!!");
    (gdb)
    
    Program received signal SIGSEGV, Segmentation fault.
    
    或者:
    12           dll_function32("call 32!!!!!!");
    (gdb)
    
    Program received signal SIGBUS, Bus error.

    然而-----------------------------------------

    测试人员同意了我的判断,然后他做了一个变态的事情,就是把所有的lib库备份到一个路径,然后等nginx运行之后,将原路径的lib删除,再这个lib文件拷贝回去,结果发现,还是段错误了。

    我看了一下前后的i节点,都不一样了,不段错误才怪。

    然后测试人员还是不死心,使用mv的方式来修改,结果发现一切正常,因为inode没变,文件的内容页没变,所以没有问题。

    另外,有必要提一下,如果使用cp -rf来拷贝,当源文件是一个实体文件,而目的文件是一个软连接的时候,其实最终会将源实体文件拷贝到目的文件所指向的实体文件。

    还有个细节,cp -rf 的时候,cp的顺序,取决于ls -u看到的顺序,而ls -u看到的顺序,取决于文件系统实现。

     
    水平有限,如果有错误,请帮忙提醒我。如果您觉得本文对您有帮助,可以点击下面的 推荐 支持一下我。版权所有,需要转发请带上本文源地址,博客一直在更新,欢迎 关注 。
  • 相关阅读:
    Liunx用户运行模式
    指令——ps -ef
    文本编辑器vim/vi——末行模式
    文本编辑器vim/vi——模式切换及输入模式
    pandas中Dataframe的查询方法([], loc, iloc, at, iat, ix)
    Python的numpy库中rand(),randn(),randint(),random_integers()的使用
    python常见面试题
    读取gzmt.csv文件,计算均值及概率
    NumPy数组对象
    写一个带文本菜单的程序,菜单项如下 (1) 取五个数的和 (2) 取五个数的平均值 (X) 退出。
  • 原文地址:https://www.cnblogs.com/10087622blog/p/9732768.html
Copyright © 2020-2023  润新知