LD_PRELOAD-ed open() + __xstat() + syslog() 结果进入 EBADF
LD_PRELOAD-ed open() + __xstat() + syslog() result into EBADF
我在装有 GLIBC 2.29 和内核 5.2.18-200.fc30 的 Fedora 30 机器上。x86_64
$ rpm -qf /usr/lib64/libc.so.6
glibc-2.29-28.fc30.x86_64
override.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
所以,我亲爱的朋友们,我该如何调试为什么使用 syslog()
结果变成 EBADF
?
============================================= ============================
更新:
- 无法在 Fedora 32-beta 上重现
- 以下命令也重现了同样的问题:
$ LD_PRELOAD=$PWD/liboverride.so bash -c "echo blah | xargs -I{} sh -c 'echo {} | cat'"
- 有趣的是,如果我将
cat
替换为 /usr/bin/cat
,问题就会消失。
============================================= ============================
更新:根据 Carlos 的回答,我 运行 对 findutils (xargs) 进行了 git 平分,发现我的场景(无意中?)通过添加功能修复:
commit 40cd25147b4461979c0d992299f2c101f9034f7a
Author: Bernhard Voelker <mail@bernhard-voelker.de>
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
不能有任何可以被 运行ning 进程看到的副作用。
没有进程期望 open
或 __xstat
关闭并重新打开编号最小的文件描述符,也不应该打开它 O_CLOEXEC,但这确实是 syslog
如果它发现日志记录套接字失败了。
解决方案是您必须在调用 syslog
之后调用 closelog
以避免任何副作用对进程可见。
失败场景如下所示:
- xargs 关闭标准输入。
- xargs 调用
open
或 stat
.
- 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,并重新打开 fd 0 作为带有 O_CLOEXEC 的套接字 fd并记录一条消息。
- xargs 调用
execve
到 运行 rev 并且 O_CLOEXEC 套接字 fd,fd 0 被关闭。
- rev 期望 fd 0 为 stdin,但它已关闭,因此无法从中读取并在 stdout 上写入一条错误消息(仍然有效)。
当您编写包装器时,您必须注意避免此类副作用。在这种情况下,使用起来相对容易 closelog
,但情况可能并非总是如此。
根据您的 xargs 版本,fork 和 exec 之间可能会完成或多或少的工作,因此如果 liboverride.os 的日志记录功能未在 exec
之前调用,它可能会起作用。
我在装有 GLIBC 2.29 和内核 5.2.18-200.fc30 的 Fedora 30 机器上。x86_64
$ rpm -qf /usr/lib64/libc.so.6
glibc-2.29-28.fc30.x86_64
override.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
所以,我亲爱的朋友们,我该如何调试为什么使用 syslog()
结果变成 EBADF
?
============================================= ============================
更新:
- 无法在 Fedora 32-beta 上重现
- 以下命令也重现了同样的问题:
$ LD_PRELOAD=$PWD/liboverride.so bash -c "echo blah | xargs -I{} sh -c 'echo {} | cat'"
- 有趣的是,如果我将
cat
替换为/usr/bin/cat
,问题就会消失。
============================================= ============================
更新:根据 Carlos 的回答,我 运行 对 findutils (xargs) 进行了 git 平分,发现我的场景(无意中?)通过添加功能修复:
commit 40cd25147b4461979c0d992299f2c101f9034f7a
Author: Bernhard Voelker <mail@bernhard-voelker.de>
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
不能有任何可以被 运行ning 进程看到的副作用。
没有进程期望 open
或 __xstat
关闭并重新打开编号最小的文件描述符,也不应该打开它 O_CLOEXEC,但这确实是 syslog
如果它发现日志记录套接字失败了。
解决方案是您必须在调用 syslog
之后调用 closelog
以避免任何副作用对进程可见。
失败场景如下所示:
- xargs 关闭标准输入。
- xargs 调用
open
或stat
. - liboverride.so 的日志记录调用
syslog
打开套接字,并获取 fd 0 作为套接字 fd。 - xargs 调用
fork
. - xargs 调用
dup2
将正确的管道 fd 复制到 stdin,因此用新的 stdin 覆盖 fd 0(预期没有其他任何东西可以打开 fd 0) - xargs 即将调用
execve
但是... - xargs 在
execve
之前调用 - liboverride.so 的日志记录调用
syslog
并且实现检测到sendto
失败,关闭 fd 0,并重新打开 fd 0 作为带有 O_CLOEXEC 的套接字 fd并记录一条消息。 - xargs 调用
execve
到 运行 rev 并且 O_CLOEXEC 套接字 fd,fd 0 被关闭。 - rev 期望 fd 0 为 stdin,但它已关闭,因此无法从中读取并在 stdout 上写入一条错误消息(仍然有效)。
stat
当您编写包装器时,您必须注意避免此类副作用。在这种情况下,使用起来相对容易 closelog
,但情况可能并非总是如此。
根据您的 xargs 版本,fork 和 exec 之间可能会完成或多或少的工作,因此如果 liboverride.os 的日志记录功能未在 exec
之前调用,它可能会起作用。