Search code examples
cbashglibcsyslogld-preload

LD_PRELOAD-ed open() + __xstat() + syslog() result into EBADF


I am on a Fedora 30 box with GLIBC 2.29 and kernel 5.2.18-200.fc30.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);
}

It works in all cases I could think of, but not this one:

$ 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

However, if I comment out syslog() in favour of fprintf(), it works:

$ 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

So, my dear friends, how do I debug why using syslog() results into a EBADF?

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

Updates:

  1. Unable to reproduce on Fedora 32-beta
  2. The following command also reproduces the same problem:
    $ LD_PRELOAD=$PWD/liboverride.so bash -c "echo blah | xargs -I{} sh -c 'echo {} | cat'"
    
  3. Interestingly, if I replace cat with /usr/bin/cat the problem goes away.

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

Update: Based on Carlos's answer, I ran a git bisect on findutils (xargs) and found that my scenario was (unintentionally?) fixed with a feature addition:

commit 40cd25147b4461979c0d992299f2c101f9034f7a
Author: Bernhard Voelker <[email protected]>
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

Solution

  • Your overriden open and __xstat must not have any side-effects that can be seen by the running process.

    No process expects open or __xstat to close and reopen the lowest numbered file descriptor, nor that it should be opened O_CLOEXEC, but this is indeed what syslog does if it finds that the logging socket has failed.

    The solution is that you must call closelog after calling syslog to avoid any side-effects becoming visible to the process.

    The failure scenario looks like this:

    • xargs closes stdin.
    • xargs calls open or stat.
    • liboverride.so's logging calls syslog which opens a socket, and gets fd 0 as the socket fd.
    • xargs calls fork.
    • xargs calls dup2 to dup the right piped fd to stdin, and so overwrites fd 0 with the new stdin (the expectation is that nothing else could have opened fd 0)
    • xargs is about to call execve but...
    • xargs calls stat just before execve
    • liboverride.so's logging calls syslog and the implementation detects the sendto has failed, closes fd 0, and reopens fd 0 as the socket fd with O_CLOEXEC and logs a message.
    • xargs calls execve to run rev and the O_CLOEXEC socket fd, fd 0, is closed.
    • rev expects fd 0 to be stdin, but it is closed and so fails to read from it and writes an error message to that effect on stdout (which is still valid).

    When you write wrappers you must take care to avoid such side-effects. In this case it's relatively easy to use closelog, but that may not always be the case.

    Depending on your version of xargs there may be more or less work done between the fork and exec and so it may work if liboverride.os's logging function is not called before the exec.