nixpkgs/pkgs/build-support/libredirect/test.c

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

177 lines
4.5 KiB
C
Raw Normal View History

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <spawn.h>
#include <stdio.h>
2021-05-21 17:59:30 +00:00
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#define TESTDIR "/bar/baz"
#define TESTPATH "/foo/bar/test"
2021-09-03 20:03:38 +00:00
#define SUBTEST "./test sub"
extern char **environ;
void test_spawn(void) {
pid_t pid;
int ret;
posix_spawn_file_actions_t file_actions;
char *argv[] = {"true", NULL};
assert(posix_spawn_file_actions_init(&file_actions) == 0);
ret = posix_spawn(&pid, TESTPATH, &file_actions, NULL, argv, environ);
assert(ret == 0);
assert(waitpid(pid, NULL, 0) != -1);
}
void test_execv(void) {
char *argv[] = {"true", NULL};
assert(execv(TESTPATH, argv) == 0);
}
2021-05-21 17:59:30 +00:00
void test_system(void) {
assert(system(TESTPATH) == 0);
}
2021-09-03 20:03:38 +00:00
void test_subprocess(void) {
assert(system(SUBTEST) == 0);
}
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::new::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 5677ce200831a83a7b3d37f2cfc4ec00c10, 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 ca8aa5dc877344082864cf75512e59c7a1d0ad8c, 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-07-09 19:37:35 +00:00
void test_stat_with_null_path(void) {
// This checks whether the compiler optimizes away the null pointer check
// on the path passed to stat(). If that's the case, the following code
// should segfault.
struct stat buf;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"
stat(NULL, &buf);
#pragma GCC diagnostic pop
}
void assert_mktemp_path(
const char * orig_prefix,
const char * orig_suffix,
const char * updated
) {
// prefix unchanged
assert(strncmp(updated, orig_prefix, strlen(orig_prefix)) == 0);
// wildcards replaced
assert(strcmp(updated + strlen(orig_prefix), "XXXXXX") != 0);
// suffix unchanged
assert(strcmp(updated + strlen(orig_prefix) + 6, orig_suffix) == 0);
}
2021-09-03 20:03:38 +00:00
int main(int argc, char *argv[])
{
FILE *testfp;
int testfd;
struct stat testsb;
#ifdef __GLIBC__
struct stat64 testsb64;
#endif
#if defined(__linux__) && defined(STATX_TYPE)
struct statx testsbx;
#endif
char buf[PATH_MAX];
testfp = fopen(TESTPATH, "r");
assert(testfp != NULL);
fclose(testfp);
testfd = open(TESTPATH, O_RDONLY);
assert(testfd != -1);
close(testfd);
assert(access(TESTPATH, X_OK) == 0);
assert(stat(TESTPATH, &testsb) != -1);
#ifdef __GLIBC__
assert(stat64(TESTPATH, &testsb64) != -1);
#endif
assert(fstatat(123, TESTPATH, &testsb, 0) != -1);
#ifdef __GLIBC__
assert(fstatat64(123, TESTPATH, &testsb64, 0) != -1);
#endif
#if defined(__linux__) && defined(STATX_TYPE)
assert(statx(123, TESTPATH, 0, STATX_ALL, &testsbx) != -1);
#endif
assert(getcwd(buf, PATH_MAX) != NULL);
assert(chdir(TESTDIR) == 0);
assert(chdir(buf) == 0);
assert(mkdir(TESTDIR "/dir-mkdir", 0777) == 0);
assert(unlink(TESTDIR "/dir-mkdir") == -1); // it's a directory!
#ifndef __APPLE__
assert(errno == EISDIR);
#endif
assert(rmdir(TESTDIR "/dir-mkdir") == 0);
assert(unlink(TESTDIR "/dir-mkdir") == -1);
assert(errno == ENOENT);
assert(mkdirat(123, TESTDIR "/dir-mkdirat", 0777) == 0);
assert(unlinkat(123, TESTDIR "/dir-mkdirat", 0) == -1); // it's a directory!
#ifndef __APPLE__
assert(errno == EISDIR);
#endif
assert(unlinkat(123, TESTDIR "/dir-mkdirat", AT_REMOVEDIR) == 0);
strncpy(buf, TESTDIR "/tempXXXXXX", PATH_MAX);
testfd = mkstemp(buf);
assert(testfd > 0);
assert_mktemp_path(TESTDIR "/temp", "", buf);
close(testfd);
strncpy(buf, TESTDIR "/tempXXXXXX", PATH_MAX);
testfd = mkostemp(buf, 0);
assert(testfd > 0);
assert_mktemp_path(TESTDIR "/temp", "", buf);
close(testfd);
strncpy(buf, TESTDIR "/tempXXXXXX.test", PATH_MAX);
testfd = mkstemps(buf, strlen(".test"));
assert(testfd > 0);
assert_mktemp_path(TESTDIR "/temp", ".test", buf);
close(testfd);
strncpy(buf, TESTDIR "/tempXXXXXX.test", PATH_MAX);
testfd = mkostemps(buf, strlen(".test"), 0);
assert(testfd > 0);
assert_mktemp_path(TESTDIR "/temp", ".test", buf);
close(testfd);
2022-01-16 22:37:37 +00:00
strncpy(buf, TESTDIR "/tempXXXXXX", PATH_MAX);
assert(mkdtemp(buf) == buf);
assert_mktemp_path(TESTDIR "/temp", "", buf);
2022-01-16 23:09:14 +00:00
strncpy(buf, TESTDIR "/tempXXXXXX", PATH_MAX);
assert(mktemp(buf) == buf);
assert_mktemp_path(TESTDIR "/temp", "", buf);
test_spawn();
2021-05-21 17:59:30 +00:00
test_system();
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::new::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 5677ce200831a83a7b3d37f2cfc4ec00c10, 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 ca8aa5dc877344082864cf75512e59c7a1d0ad8c, 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-07-09 19:37:35 +00:00
test_stat_with_null_path();
2021-09-03 20:03:38 +00:00
// Only run subprocess if no arguments are given
// as the subprocess will be called without argument
// otherwise we will have infinite recursion
if (argc == 1) {
test_subprocess();
}
test_execv();
/* If all goes well, this is never reached because test_execv() replaces
* the current process.
*/
return 0;
}