c - LD_PRELOAD-ed open() + __xstat() + syslog() 结果存入 EBADF

标签 c bash glibc syslog ld-preload

我使用的是带有 GLIBC 2.29 和内核 5.2.18-200.fc30.x86_64 的 Fedora 30 机器

$ rpm -qf /usr/lib64/libc.so.6
glibc-2.29-28.fc30.x86_64

覆盖.c:

#define open Oopen
#define __xstat __Xxstat

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <sys/stat.h>
#include <syslog.h>
#include <errno.h>

#undef open
#undef __xstat

#ifndef DEBUG
#define DEBUG 1
#endif

#define LOG(fmt, ...)                                                          \
  do {                                                                         \
    if (DEBUG) {                                                               \
      int errno_ = errno;                                                      \
      /* fprintf(stderr, "override|%s: " fmt, __func__, __VA_ARGS__); */       \
      syslog(LOG_INFO | LOG_USER, "override|%s: " fmt, __func__, __VA_ARGS__); \
      errno = errno_;                                                          \
    }                                                                          \
  } while (0)

/* Function pointers to hold the value of the glibc functions */
static int (*real_open)(const char *str, int flags, mode_t mode);
static int (*real___xstat)(int ver, const char *str, struct stat *buf);

int open(const char *str, int flags, mode_t mode) {
  LOG("%s\n", str);
  real_open = dlsym(RTLD_NEXT, __func__);
  return real_open(str, flags, mode);
}

int __xstat(int ver, const char *str, struct stat *buf) {
  LOG("%s\n", str);
  real___xstat = dlsym(RTLD_NEXT, __func__);
  return real___xstat(ver, str, buf);
}

它适用于我能想到的所有情况,但不适用于此:

$ gcc -DDEBUG=1 -fPIC -shared -o liboverride.so override.c -ldl -Wall -Wextra -Werror
$ LD_PRELOAD=$PWD/liboverride.so bash -c "echo blah | xargs -I{} sh -c 'echo {} | rev'"
rev: stdin: Bad file descriptor

但是,如果我注释掉 syslog() 以支持 fprintf(),它会起作用:

$ gcc -DDEBUG=1 -fPIC -shared -o liboverride.so override.c -ldl -Wall -Wextra -Werror
$ LD_PRELOAD=$PWD/liboverride.so bash -c "echo blah | xargs -I{} sh -c 'echo {} | rev'"
override|open: /dev/tty
override|__xstat: /tmp/nwani_1587079071
override|__xstat: .
...
... yada ...
... yada ...
... yada ...
...
halb <----------------------------- !
...
... yada ...
... yada ...
... yada ...
override|__xstat: /usr/share/terminfo

那么,我亲爱的 friend 们,我该如何调试为什么使用syslog()结果会变成EBADF

================================================== =========================

更新:

  1. 无法在 Fedora 32-beta 上重现
  2. 以下命令也会重现相同的问题:
    $ LD_PRELOAD=$PWD/liboverride.so bash -c "echo blah | xargs -I{} sh -c 'echo {} | cat'"
    
  3. 有趣的是,如果我将 cat 替换为 /usr/bin/cat,问题就会消失。

================================================== =========================

更新:根据 Carlos 的回答,我在 findutils (xargs) 上运行了 git bisect,发现我的场景(无意中?)通过添加功能修复了:

commit 40cd25147b4461979c0d992299f2c101f9034f7a
Author: Bernhard Voelker <<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="6904080005290b0c1b0701081b0d441f060c05020c1b470d0c" rel="noreferrer noopener nofollow">[email protected]</a>>
Date:   Tue Jun 6 08:19:29 2017 +0200

    xargs: add -o, --open-tty option

    This option is available in the xargs implementation of FreeBSD, NetBSD,
    OpenBSD and in the Apple variant.  Add it for compatibility.

    * xargs/xargs.c (open_tty): Add static flag for the new option.
    (longopts): Add member.
    (main): Handle the 'o' case in the getopt_long() loop.
    (prep_child_for_exec): Redirect stdin of the child to /dev/tty when
    the -o option is given.  Furthermore, move the just-opened file
    descriptor to STDIN_FILENO.
    (usage): Document the new option.
    * bootstrap.conf (gnulib_modules): Add dup2.
    * xargs/xargs.1 (SYNOPSIS): Replace the too-long list of options by
    "[options]" - they are listed later anyway.
    (OPTIONS): Document the new option.
    (STANDARDS CONFORMANCE): Mention that the -o option is an extension.
    * doc/find.texi (xargs options): Document the new option.
    (Invoking the shell from xargs): Amend the explanation of the
    redirection example with a note about the -o option.
    (Viewing And Editing): Likewise.
    (Error Messages From xargs): Add the message when dup2() fails.
    (NEWS): Mention the new option.

    Fixes http://savannah.gnu.org/bugs/?51151

最佳答案

您的重写的 open__xstat 不得有任何运行进程可以看到的副作用。

没有进程期望 open__xstat 关闭并重新打开编号最低的文件描述符,也不希望它应该打开 O_CLOEXEC,但这确实是 syslog 如果发现日志记录套接字发生故障,则会执行此操作。

解决方案是您必须在调用 syslog 后调用 closelog 以避免任何副作用对进程可见。

失败场景如下所示:

  • xargs 关闭标准输入。
  • xargs 调用 openstat
  • liboverride.so 的日志记录调用 syslog 打开一个套接字,并获取 fd 0 作为套接字 fd。
  • xargs 调用 fork
  • xargs 调用 dup2 将正确的管道 fd 复制到 stdin,因此用新的 stdin 覆盖 fd 0(期望没有其他东西可以打开 fd 0)
  • xargs 即将调用 execve 但是...
  • xargs 在 execve 之前调用 stat
  • liboverride.so 的日志记录调用 syslog,并且实现检测到 sendto 失败,关闭 fd 0,然后使用 O_CLOEXEC 重新打开 fd 0 作为套接字 fd,并记录一条消息.
  • xargs 调用 execve 来运行 rev,并且关闭 O_CLOEXEC 套接字 fd(fd 0)。
  • rev 期望 fd 0 成为标准输入,但它已关闭,因此无法从中读取数据,并在标准输出上写入一条错误消息(该消息仍然有效)。

当您编写包装器时,必须小心避免此类副作用。在这种情况下,使用 closelog 相对容易,但情况可能并非总是如此。

根据您的 xargs 版本,fork 和 exec 之间可能会完成或多或少的工作,因此如果在 exec 之前未调用 liboverride.os 的日志记录函数,它可能会起作用。

关于c - LD_PRELOAD-ed open() + __xstat() + syslog() 结果存入 EBADF,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61261969/

相关文章:

c - 在 C 中使用函数作为参数

c - 函数中使用的内存何时会空闲??(C 编程)

linux - 运行 bash 脚本时,echo 输出到哪里?

linux - 如何检查 glibc 中的 fastbin 大小

c - 设置新模型时是否需要释放GtkListStore?

c - pthreads:等到 read() 返回值 > 0

bash - 为什么我的 ERR 陷阱会针对 TERM 信号执行?

regex - 重命名文件或移动文件(如果在 Linux bash 中以逗号分隔)

gcc - 为什么 GCC 需要 "-lpthread"来链接 pthread 函数,但不需要参数来链接其他函数?

linux - Glibc - 使用 Qt Creator 静态构建或强制程序使用正确的版本