blob: c92aa6bd9433b5910e2272a828e3ae5d93ceb18a [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ld-startup-spawn-process-tests-posix.h"
#include <fcntl.h>
#include <lib/elfldltl/testing/get-test-data.h>
#include <signal.h>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <cstring>
#include <fbl/unique_fd.h>
#include <gtest/gtest.h>
namespace ld::testing {
namespace {
// Pack up a nullptr-terminated array of the argument pointers.
std::vector<char*> ArgvPtrs(const std::vector<std::string>& argv) {
std::vector<char*> argv_ptrs;
argv_ptrs.reserve(argv.size() + 1);
for (const std::string& arg : argv) {
argv_ptrs.push_back(const_cast<char*>(arg.c_str()));
}
argv_ptrs.push_back(nullptr);
return argv_ptrs;
}
// The nicer thing to use is posix_spawn, and that's a closer parallel with
// fdio_spawn that is used on Fuchsia. However, the POSIX standard API doesn't
// have the features like addfchdir_np and it's nicer to only change that state
// in the child rather than also in the parent. Newer glibc (Linux) versions
// support extensions that are sufficient for the needs:
// * 2.29 added posix_spawn_file_actions_addfchdir_np.
// * 2.34 added posix_spawn_file_actions_addclosefrom_np
#if defined(__GLIBC_PREREQ) && __GLIBC_PREREQ(2, 34)
// The SpawnPlan object collects actions to be applied by posix_spawn.
// All the fd's passed to it must remain live until posix_spawn is called.
class SpawnPlan {
public:
SpawnPlan() {
int error = posix_spawn_file_actions_init(&actions_);
EXPECT_EQ(error, 0) << strerror(error);
}
void Fchdir(int fd) {
int error = posix_spawn_file_actions_addfchdir_np(&actions_, fd);
EXPECT_EQ(error, 0) << strerror(error);
}
void Dup2(int from, int to) {
int error = posix_spawn_file_actions_adddup2(&actions_, from, to);
EXPECT_EQ(error, 0) << strerror(error);
}
void Closefrom(int from) {
int error = posix_spawn_file_actions_addclosefrom_np(&actions_, from);
EXPECT_EQ(error, 0) << strerror(error);
}
~SpawnPlan() {
int error = posix_spawn_file_actions_destroy(&actions_);
EXPECT_EQ(error, 0) << strerror(error);
}
pid_t Launch(const std::string& executable, const std::vector<std::string>& argv) {
pid_t pid = -1;
int error =
posix_spawn(&pid, executable.c_str(), &actions_, nullptr, ArgvPtrs(argv).data(), nullptr);
EXPECT_EQ(error, 0) << strerror(error);
return pid;
}
private:
posix_spawn_file_actions_t actions_;
};
#else
// Support the same abstracted version of the posix_spawn_file_actions_t API
// but implemented directly using fork and execve.
class SpawnPlan {
public:
void Fchdir(int fd) { fchdir_ = fd; }
void Dup2(int from, int to) { dup2_.emplace_back(from, to); }
void Closefrom(int from) {
// TODO(mcgrathr): Newer glibc versions have the closefrom() call, but not
// the build's current sysroot version. Rather than do the potentially
// costly technique of looping up to getdtablesize(), just allow for some
// descriptors to leak, and only close the ones we know about. This is
// really just extra surety anyway, since ideally all the extra fds that
// were opened had FD_CLOEXEC set.
for (auto [dup_from, dup_to] : dup2_) {
EXPECT_GE(dup_from, from);
auto is_dup_from = [dup_from = dup_from](const auto& dup) -> bool {
return dup.first == dup_from;
};
if (std::none_of(dup2_.begin(), dup2_.end(), is_dup_from)) {
EXPECT_EQ(close(dup_from), 0) << "close(" << dup_from << "): " << strerror(errno);
}
}
}
pid_t Launch(const std::string& executable, const std::vector<std::string>& argv,
const std::vector<std::string>& envp) {
fflush(nullptr); // Flush buffers before fork duplicates them.
pid_t pid = fork();
if (pid == 0) {
Child(executable, argv, envp);
}
EXPECT_GT(pid, 0) << "fork: " << strerror(errno);
return pid;
}
private:
[[noreturn]] void Child(const std::string& executable, const std::vector<std::string>& argv,
const std::vector<std::string>& envp) {
// Child side. Install state before exec.
if (fchdir_ >= 0) {
EXPECT_EQ(fchdir(fchdir_), 0) << "fchdir: " << strerror(errno);
}
for (auto [from, to] : dup2_) {
if (from != to) {
EXPECT_EQ(dup2(from, to), to) << "dup2(" << from << ", " << to << "): " << strerror(errno);
}
};
if (!::testing::Test::HasFailure()) {
EXPECT_EQ(execve(executable.c_str(), ArgvPtrs(argv).data(), ArgvPtrs(envp).data()), 0)
<< "execve: " << executable << ": " << strerror(errno);
}
fflush(nullptr);
_exit(127);
}
int fchdir_ = -1;
std::vector<std::pair<int, int>> dup2_;
};
#endif
} // namespace
void LdStartupSpawnProcessTests::Init(std::initializer_list<std::string_view> args,
std::initializer_list<std::string_view> env) {
argv_ = std::vector<std::string>{args.begin(), args.end()};
envp_ = std::vector<std::string>{env.begin(), env.end()};
}
void LdStartupSpawnProcessTests::Load(std::string_view executable_name) {
ASSERT_NO_FATAL_FAILURE(LoadTestDir(executable_name));
executable_ = executable_name;
ASSERT_NO_FATAL_FAILURE(CheckNeededLibs());
}
int64_t LdStartupSpawnProcessTests::Run() {
SpawnPlan spawn;
// Change into the directory where all the test ELF files can be found.
spawn.Fchdir(test_dir());
// Put /dev/null on stdin and stdout. They should not be used.
fbl::unique_fd null_fd{open("/dev/null", O_RDWR, O_CLOEXEC)};
EXPECT_TRUE(null_fd) << "/dev/null: " << strerror(errno);
fbl::unique_fd log_fd;
spawn.Dup2(null_fd.get(), STDIN_FILENO);
spawn.Dup2(null_fd.get(), STDOUT_FILENO);
// Put the log pipe on stderr to collect any diagnostics.
InitLog(log_fd);
spawn.Dup2(log_fd.get(), STDERR_FILENO);
// Close all other fd's in case any were opened without CLOEXEC.
spawn.Closefrom(STDERR_FILENO + 1);
if (HasFailure()) {
return -1;
}
// Launch the child.
pid_ = spawn.Launch(executable_, argv_, envp_);
if (HasFailure()) {
return -1;
}
// Wait for it to die.
int status;
pid_t waited = waitpid(pid_, &status, 0);
EXPECT_EQ(waited, pid_) << strerror(errno);
if (waited != pid_) {
return -1;
}
// Return an exit code or termination signal (+ 128, in the style of sh).
EXPECT_FALSE(WIFSTOPPED(status)) << strsignal(WSTOPSIG(status));
if (WIFSIGNALED(status)) {
return 128 + WTERMSIG(status);
}
EXPECT_TRUE(WIFEXITED(status));
return WEXITSTATUS(status);
}
LdStartupSpawnProcessTests::~LdStartupSpawnProcessTests() {
if (pid_ > 0) {
if (kill(pid_, SIGKILL) < 0) {
EXPECT_EQ(errno, ESRCH) << strerror(errno);
}
}
}
} // namespace ld::testing