| // 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 |