nixpkgs/pkgs/build-support/libredirect/libredirect.c
aszlig 8e6c0c14a4
libredirect: Fix segfault handling null paths
While using libredirect in conjunction with geckodriver, I stumbled on
odd segfaults that happened when running the wrapped statx() call from
libredirect:

  0x00007ffff7ddd541 in __strncmp_avx2 () from .../lib/libc.so.6
  0x00007ffff7f6fe57 in statx () from .../lib/libredirect.so
  0x00005555558d35bd in std::sys::unix::fs::try_statx::h2045d39b0c66d4e8 ()
  0x00005555558d2230 in std::sys::unix::fs::stat::ha063998dfb361520 ()
  0x0000555555714019 in mozversion::firefox_version::hdc3b57eb04947426 ()
  0x00005555556a603c in geckodriver::capabilities::FirefoxCapabilities::version::h58e289917bd3c721 ()
  0x00005555556a77f5 in <geckodriver::capabilities::FirefoxCapabilities as webdriver::capabilities::BrowserCapabilities>::validate_custom::h62d23cf9fd63b719 ()
  0x000055555562a7c8 in webdriver::capabilities::SpecNewSessionParameters::validate::h60da250d33f0989f ()
  0x00005555556d7a13 in <core::iter::adapters::map::Map<I,F> as core::iter::traits::iterator::Iterator>::try_fold::h9427a360a3d0bf8f ()
  0x0000555555669d85 in <alloc::vec::Vec<T> as alloc::vec::spec_from_iter::SpecFromIter<T,I>>::from_iter::hd274d536ea29bb33 ()
  0x00005555555c05ef in core::iter::adapters::try_process::hdf96a01ec1f9b8bd ()
  0x000055555561768d in <webdriver::capabilities::SpecNewSessionParameters as webdriver::capabilities::CapabilitiesMatching>::match_browser::hfbd8c38f6db17e9f ()
  0x00005555555ca6ef in <geckodriver::marionette::MarionetteHandler as webdriver::server::WebDriverHandler<geckodriver::command::GeckoExtensionRoute>>::handle_command::h13b98b9cb87a69d6 ()
  0x00005555555e859e in webdriver::server::Dispatcher<T,U>::run::h746a8bf2f0bc24fd ()
  0x000055555569ff0f in std::sys_common::backtrace::__rust_begin_short_backtrace::h3b920773bd467d2a ()
  0x00005555555dbc99 in core::ops::function::FnOnce::call_once{{vtable.shim}}::h81ba7228877515f7 ()
  0x00005555558d31a3 in std::sys::unix:🧵:Thread:🆕:thread_start::h4514580219a899c5 ()
  0x00007ffff7d0ce24 in start_thread () from .../lib/libc.so.6
  0x00007ffff7d8e9b0 in clone3 () from .../lib/libc.so.6

The reason why I found this odd was because it happens in the following
piece of code (shortened a bit):

   1 static const char * rewrite(const char * path, char * buf)
   2 {
   3   if (path == NULL) return path;
   4   for (int n = 0; n < nrRedirects; ++n) {
   5     int len = strlen(from[n]);
   6     if (strncmp(path, from[n], len) != 0) continue;
   7     if (snprintf(buf, PATH_MAX, "%s%s", to[n], path + len) >= PATH_MAX)
   8       abort();
   9     return buf;
  10   }
  11   return path;
  12 }

When inspecting the assembly, I found that the check for the null
pointer in line 3 was completely missing and the code was directly
entering the loop and then eventually segfault when running strncmp()
with a null pointer as its first argument.

I confirmed that indeed that check was missing by compiling libredirect
with "-O0" and comparing the generated assembly with the optimized one.
The one compiled with "-O0" had that check while the optimized one did
not and indeed when running geckodriver with the unoptimized version it
worked fine.

Digging in the Git history, I found 5677ce2008,
which actually introduced the null pointer check. Going back to that
commit however, the check actually was still in the generated assembly.

So I bisected between that commit and the most recent one and ended up
with commit ca8aa5dc87, which moved
everything to use GCC 7.

I haven't found out why *exactly* GCC was optimizing the check away, but
playing around on Godbolt with various other compilers seems that other
compilers such as Clang are doing it as well. Additionally, given that
passing NULL to stat() is UB, my guess is that compilers tend to assume
that such an argument can't be NULL. My assumption is based on the fact
that GCC warns with "argument 1 null where non-null expected" when
passing NULL to eg. stat().

To address this for now, I marked the path argument of the rewrite()
volatile and also added a test that should cause a segfault in case this
would regress again as it already did.

Signed-off-by: aszlig <aszlig@nix.build>
2023-08-19 00:58:43 +02:00

531 lines
15 KiB
C

#define _GNU_SOURCE
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#include <string.h>
#include <spawn.h>
#include <dirent.h>
#define MAX_REDIRECTS 128
#ifdef __APPLE__
struct dyld_interpose {
const void * replacement;
const void * replacee;
};
#define WRAPPER(ret, name) static ret _libredirect_wrapper_##name
#define LOOKUP_REAL(name) &name
#define WRAPPER_DEF(name) \
__attribute__((used)) static struct dyld_interpose _libredirect_interpose_##name \
__attribute__((section("__DATA,__interpose"))) = { &_libredirect_wrapper_##name, &name };
#else
#define WRAPPER(ret, name) ret name
#define LOOKUP_REAL(name) dlsym(RTLD_NEXT, #name)
#define WRAPPER_DEF(name)
#endif
static int nrRedirects = 0;
static char * from[MAX_REDIRECTS];
static char * to[MAX_REDIRECTS];
static int isInitialized = 0;
// FIXME: might run too late.
static void init() __attribute__((constructor));
static void init()
{
if (isInitialized) return;
char * spec = getenv("NIX_REDIRECTS");
if (!spec) return;
// Ensure we only run this code once.
// We do not do `unsetenv("NIX_REDIRECTS")` to ensure that redirects
// also get initialized for subprocesses.
isInitialized = 1;
char * spec2 = malloc(strlen(spec) + 1);
strcpy(spec2, spec);
char * pos = spec2, * eq;
while ((eq = strchr(pos, '='))) {
*eq = 0;
from[nrRedirects] = pos;
pos = eq + 1;
to[nrRedirects] = pos;
nrRedirects++;
if (nrRedirects == MAX_REDIRECTS) break;
char * end = strchr(pos, ':');
if (!end) break;
*end = 0;
pos = end + 1;
}
}
static const char * rewrite(const char * volatile path, char * buf)
{
// Marking the path volatile is needed so the the following check isn't
// optimized away by the compiler.
if (path == NULL) return path;
for (int n = 0; n < nrRedirects; ++n) {
int len = strlen(from[n]);
if (strncmp(path, from[n], len) != 0) continue;
if (snprintf(buf, PATH_MAX, "%s%s", to[n], path + len) >= PATH_MAX)
abort();
return buf;
}
return path;
}
static char * rewrite_non_const(char * path, char * buf)
{
// as long as the argument `path` is non-const, we can consider discarding
// the const qualifier of the return value to be safe.
return (char *)rewrite(path, buf);
}
static int open_needs_mode(int flags)
{
#ifdef O_TMPFILE
return (flags & O_CREAT) || (flags & O_TMPFILE) == O_TMPFILE;
#else
return flags & O_CREAT;
#endif
}
/* The following set of Glibc library functions is very incomplete -
it contains only what we needed for programs in Nixpkgs. Just add
more functions as needed. */
WRAPPER(int, open)(const char * path, int flags, ...)
{
int (*open_real) (const char *, int, ...) = LOOKUP_REAL(open);
mode_t mode = 0;
if (open_needs_mode(flags)) {
va_list ap;
va_start(ap, flags);
mode = va_arg(ap, mode_t);
va_end(ap);
}
char buf[PATH_MAX];
return open_real(rewrite(path, buf), flags, mode);
}
WRAPPER_DEF(open)
// In musl libc, open64 is simply a macro for open
#if !defined(__APPLE__) && !defined(open64)
WRAPPER(int, open64)(const char * path, int flags, ...)
{
int (*open64_real) (const char *, int, mode_t) = LOOKUP_REAL(open64);
mode_t mode = 0;
if (open_needs_mode(flags)) {
va_list ap;
va_start(ap, flags);
mode = va_arg(ap, mode_t);
va_end(ap);
}
char buf[PATH_MAX];
return open64_real(rewrite(path, buf), flags, mode);
}
WRAPPER_DEF(open64)
#endif
WRAPPER(int, openat)(int dirfd, const char * path, int flags, ...)
{
int (*openat_real) (int, const char *, int, ...) = LOOKUP_REAL(openat);
mode_t mode = 0;
if (open_needs_mode(flags)) {
va_list ap;
va_start(ap, flags);
mode = va_arg(ap, mode_t);
va_end(ap);
}
char buf[PATH_MAX];
return openat_real(dirfd, rewrite(path, buf), flags, mode);
}
WRAPPER_DEF(openat)
WRAPPER(FILE *, fopen)(const char * path, const char * mode)
{
FILE * (*fopen_real) (const char *, const char *) = LOOKUP_REAL(fopen);
char buf[PATH_MAX];
return fopen_real(rewrite(path, buf), mode);
}
WRAPPER_DEF(fopen)
#ifdef __GLIBC__
WRAPPER(FILE *, __nss_files_fopen)(const char * path)
{
FILE * (*__nss_files_fopen_real) (const char *) = LOOKUP_REAL(__nss_files_fopen);
char buf[PATH_MAX];
return __nss_files_fopen_real(rewrite(path, buf));
}
WRAPPER_DEF(__nss_files_fopen)
#endif
// In musl libc, fopen64 is simply a macro for fopen
#if !defined(__APPLE__) && !defined(fopen64)
WRAPPER(FILE *, fopen64)(const char * path, const char * mode)
{
FILE * (*fopen64_real) (const char *, const char *) = LOOKUP_REAL(fopen64);
char buf[PATH_MAX];
return fopen64_real(rewrite(path, buf), mode);
}
WRAPPER_DEF(fopen64)
#endif
#ifdef __linux__
WRAPPER(int, __xstat)(int ver, const char * path, struct stat * st)
{
int (*__xstat_real) (int ver, const char *, struct stat *) = LOOKUP_REAL(__xstat);
char buf[PATH_MAX];
return __xstat_real(ver, rewrite(path, buf), st);
}
WRAPPER_DEF(__xstat)
#endif
#ifdef __linux__
WRAPPER(int, __xstat64)(int ver, const char * path, struct stat64 * st)
{
int (*__xstat64_real) (int ver, const char *, struct stat64 *) = LOOKUP_REAL(__xstat64);
char buf[PATH_MAX];
return __xstat64_real(ver, rewrite(path, buf), st);
}
WRAPPER_DEF(__xstat64)
#endif
#if defined(__linux__) && defined(STATX_TYPE)
WRAPPER(int, statx)(int dirfd, const char * restrict pathname, int flags,
unsigned int mask, struct statx * restrict statxbuf)
{
int (*statx_real) (int, const char * restrict, int,
unsigned int, struct statx * restrict) = LOOKUP_REAL(statx);
char buf[PATH_MAX];
return statx_real(dirfd, rewrite(pathname, buf), flags, mask, statxbuf);
}
WRAPPER_DEF(statx)
#endif
WRAPPER(int, fstatat)(int dirfd, const char * pathname, struct stat * statbuf, int flags)
{
int (*fstatat_real) (int, const char *, struct stat *, int) = LOOKUP_REAL(fstatat);
char buf[PATH_MAX];
return fstatat_real(dirfd, rewrite(pathname, buf), statbuf, flags);
}
WRAPPER_DEF(fstatat);
// In musl libc, fstatat64 is simply a macro for fstatat
#if !defined(__APPLE__) && !defined(fstatat64)
WRAPPER(int, fstatat64)(int dirfd, const char * pathname, struct stat64 * statbuf, int flags)
{
int (*fstatat64_real) (int, const char *, struct stat64 *, int) = LOOKUP_REAL(fstatat64);
char buf[PATH_MAX];
return fstatat64_real(dirfd, rewrite(pathname, buf), statbuf, flags);
}
WRAPPER_DEF(fstatat64);
#endif
WRAPPER(int, stat)(const char * path, struct stat * st)
{
int (*__stat_real) (const char *, struct stat *) = LOOKUP_REAL(stat);
char buf[PATH_MAX];
return __stat_real(rewrite(path, buf), st);
}
WRAPPER_DEF(stat)
// In musl libc, stat64 is simply a macro for stat
#if !defined(__APPLE__) && !defined(stat64)
WRAPPER(int, stat64)(const char * path, struct stat64 * st)
{
int (*stat64_real) (const char *, struct stat64 *) = LOOKUP_REAL(stat64);
char buf[PATH_MAX];
return stat64_real(rewrite(path, buf), st);
}
WRAPPER_DEF(stat64)
#endif
WRAPPER(int, access)(const char * path, int mode)
{
int (*access_real) (const char *, int mode) = LOOKUP_REAL(access);
char buf[PATH_MAX];
return access_real(rewrite(path, buf), mode);
}
WRAPPER_DEF(access)
WRAPPER(int, posix_spawn)(pid_t * pid, const char * path,
const posix_spawn_file_actions_t * file_actions,
const posix_spawnattr_t * attrp,
char * const argv[], char * const envp[])
{
int (*posix_spawn_real) (pid_t *, const char *,
const posix_spawn_file_actions_t *,
const posix_spawnattr_t *,
char * const argv[], char * const envp[]) = LOOKUP_REAL(posix_spawn);
char buf[PATH_MAX];
return posix_spawn_real(pid, rewrite(path, buf), file_actions, attrp, argv, envp);
}
WRAPPER_DEF(posix_spawn)
WRAPPER(int, posix_spawnp)(pid_t * pid, const char * file,
const posix_spawn_file_actions_t * file_actions,
const posix_spawnattr_t * attrp,
char * const argv[], char * const envp[])
{
int (*posix_spawnp_real) (pid_t *, const char *,
const posix_spawn_file_actions_t *,
const posix_spawnattr_t *,
char * const argv[], char * const envp[]) = LOOKUP_REAL(posix_spawnp);
char buf[PATH_MAX];
return posix_spawnp_real(pid, rewrite(file, buf), file_actions, attrp, argv, envp);
}
WRAPPER_DEF(posix_spawnp)
WRAPPER(int, execv)(const char * path, char * const argv[])
{
int (*execv_real) (const char * path, char * const argv[]) = LOOKUP_REAL(execv);
char buf[PATH_MAX];
return execv_real(rewrite(path, buf), argv);
}
WRAPPER_DEF(execv)
WRAPPER(int, execvp)(const char * path, char * const argv[])
{
int (*_execvp) (const char *, char * const argv[]) = LOOKUP_REAL(execvp);
char buf[PATH_MAX];
return _execvp(rewrite(path, buf), argv);
}
WRAPPER_DEF(execvp)
WRAPPER(int, execve)(const char * path, char * const argv[], char * const envp[])
{
int (*_execve) (const char *, char * const argv[], char * const envp[]) = LOOKUP_REAL(execve);
char buf[PATH_MAX];
return _execve(rewrite(path, buf), argv, envp);
}
WRAPPER_DEF(execve)
WRAPPER(DIR *, opendir)(const char * path)
{
char buf[PATH_MAX];
DIR * (*_opendir) (const char*) = LOOKUP_REAL(opendir);
return _opendir(rewrite(path, buf));
}
WRAPPER_DEF(opendir)
#define SYSTEM_CMD_MAX 512
static char * replace_substring(char * source, char * buf, char * replace_string, char * start_ptr, char * suffix_ptr) {
char head[SYSTEM_CMD_MAX] = {0};
strncpy(head, source, start_ptr - source);
char tail[SYSTEM_CMD_MAX] = {0};
if(suffix_ptr < source + strlen(source)) {
strcpy(tail, suffix_ptr);
}
sprintf(buf, "%s%s%s", head, replace_string, tail);
return buf;
}
static char * replace_string(char * buf, char * from, char * to) {
int num_matches = 0;
char * matches[SYSTEM_CMD_MAX];
int from_len = strlen(from);
for(int i=0; i<strlen(buf); i++){
char *cmp_start = buf + i;
if(strncmp(from, cmp_start, from_len) == 0){
matches[num_matches] = cmp_start;
num_matches++;
}
}
int len_diff = strlen(to) - strlen(from);
for(int n = 0; n < num_matches; n++) {
char replaced[SYSTEM_CMD_MAX];
replace_substring(buf, replaced, to, matches[n], matches[n]+from_len);
strcpy(buf, replaced);
for(int nn = n+1; nn < num_matches; nn++) {
matches[nn] += len_diff;
}
}
return buf;
}
static void rewriteSystemCall(const char * command, char * buf) {
char * p = buf;
#ifdef __APPLE__
// The dyld environment variable is not inherited by the subprocess spawned
// by system(), so this hack redefines it.
Dl_info info;
dladdr(&rewriteSystemCall, &info);
p = stpcpy(p, "export DYLD_INSERT_LIBRARIES=");
p = stpcpy(p, info.dli_fname);
p = stpcpy(p, ";");
#endif
stpcpy(p, command);
for (int n = 0; n < nrRedirects; ++n) {
replace_string(buf, from[n], to[n]);
}
}
WRAPPER(int, system)(const char *command)
{
int (*_system) (const char*) = LOOKUP_REAL(system);
char newCommand[SYSTEM_CMD_MAX];
rewriteSystemCall(command, newCommand);
return _system(newCommand);
}
WRAPPER_DEF(system)
WRAPPER(int, chdir)(const char *path)
{
int (*chdir_real) (const char *) = LOOKUP_REAL(chdir);
char buf[PATH_MAX];
return chdir_real(rewrite(path, buf));
}
WRAPPER_DEF(chdir);
WRAPPER(int, mkdir)(const char *path, mode_t mode)
{
int (*mkdir_real) (const char *path, mode_t mode) = LOOKUP_REAL(mkdir);
char buf[PATH_MAX];
return mkdir_real(rewrite(path, buf), mode);
}
WRAPPER_DEF(mkdir)
WRAPPER(int, mkdirat)(int dirfd, const char *path, mode_t mode)
{
int (*mkdirat_real) (int dirfd, const char *path, mode_t mode) = LOOKUP_REAL(mkdirat);
char buf[PATH_MAX];
return mkdirat_real(dirfd, rewrite(path, buf), mode);
}
WRAPPER_DEF(mkdirat)
WRAPPER(int, unlink)(const char *path)
{
int (*unlink_real) (const char *path) = LOOKUP_REAL(unlink);
char buf[PATH_MAX];
return unlink_real(rewrite(path, buf));
}
WRAPPER_DEF(unlink)
WRAPPER(int, unlinkat)(int dirfd, const char *path, int flags)
{
int (*unlinkat_real) (int dirfd, const char *path, int flags) = LOOKUP_REAL(unlinkat);
char buf[PATH_MAX];
return unlinkat_real(dirfd, rewrite(path, buf), flags);
}
WRAPPER_DEF(unlinkat)
WRAPPER(int, rmdir)(const char *path)
{
int (*rmdir_real) (const char *path) = LOOKUP_REAL(rmdir);
char buf[PATH_MAX];
return rmdir_real(rewrite(path, buf));
}
WRAPPER_DEF(rmdir)
static void copy_temp_wildcard(char * dest, char * src, int suffixlen) {
int dest_len = strnlen(dest, PATH_MAX);
int src_len = strnlen(src, PATH_MAX);
memcpy(dest + dest_len - (6 + suffixlen), src + src_len - (6 + suffixlen), 6);
}
WRAPPER(int, mkstemp)(char *template)
{
int (*mkstemp_real) (char *template) = LOOKUP_REAL(mkstemp);
char buf[PATH_MAX];
char * rewritten = rewrite_non_const(template, buf);
int retval = mkstemp_real(rewritten);
if (retval >= 0 && rewritten != template) {
copy_temp_wildcard(template, rewritten, 0);
}
return retval;
}
WRAPPER_DEF(mkstemp)
WRAPPER(int, mkostemp)(char *template, int flags)
{
int (*mkostemp_real) (char *template, int flags) = LOOKUP_REAL(mkostemp);
char buf[PATH_MAX];
char * rewritten = rewrite_non_const(template, buf);
int retval = mkostemp_real(rewritten, flags);
if (retval >= 0 && rewritten != template) {
copy_temp_wildcard(template, rewritten, 0);
}
return retval;
}
WRAPPER_DEF(mkostemp)
WRAPPER(int, mkstemps)(char *template, int suffixlen)
{
int (*mkstemps_real) (char *template, int suffixlen) = LOOKUP_REAL(mkstemps);
char buf[PATH_MAX];
char * rewritten = rewrite_non_const(template, buf);
int retval = mkstemps_real(rewritten, suffixlen);
if (retval >= 0 && rewritten != template) {
copy_temp_wildcard(template, rewritten, suffixlen);
}
return retval;
}
WRAPPER_DEF(mkstemps)
WRAPPER(int, mkostemps)(char *template, int suffixlen, int flags)
{
int (*mkostemps_real) (char *template, int suffixlen, int flags) = LOOKUP_REAL(mkostemps);
char buf[PATH_MAX];
char * rewritten = rewrite_non_const(template, buf);
int retval = mkostemps_real(rewritten, suffixlen, flags);
if (retval >= 0 && rewritten != template) {
copy_temp_wildcard(template, rewritten, suffixlen);
}
return retval;
}
WRAPPER_DEF(mkostemps)
WRAPPER(char *, mkdtemp)(char *template)
{
char * (*mkdtemp_real) (char *template) = LOOKUP_REAL(mkdtemp);
char buf[PATH_MAX];
char * rewritten = rewrite_non_const(template, buf);
char * retval = mkdtemp_real(rewritten);
if (retval == NULL) {
return retval;
};
if (rewritten != template) {
copy_temp_wildcard(template, rewritten, 0);
}
return template;
}
WRAPPER_DEF(mkdtemp)
WRAPPER(char *, mktemp)(char *template)
{
char * (*mktemp_real) (char *template) = LOOKUP_REAL(mktemp);
char buf[PATH_MAX];
char * rewritten = rewrite_non_const(template, buf);
char * retval = mktemp_real(rewritten);
if (retval == NULL) {
return retval;
};
if (rewritten != template) {
copy_temp_wildcard(template, rewritten, 0);
}
return template;
}
WRAPPER_DEF(mktemp)