blob: 51dd927ed94e47221d119e87d97c4cb062df713a [file] [log] [blame]
// Copyright 2022 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 "src/starnix/tests/syscalls/cpp/test_helper.h"
#include <dirent.h>
#include <fcntl.h>
#include <lib/fit/function.h>
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <algorithm>
#include <fstream>
#include <optional>
#include <string_view>
#include <gtest/gtest.h>
#include <linux/capability.h>
#include "src/lib/fxl/strings/split_string.h"
#include "src/lib/fxl/strings/string_number_conversions.h"
#include "src/starnix/tests/syscalls/cpp/capabilities_helper.h"
#include "src/starnix/tests/syscalls/cpp/syscall_matchers.h"
namespace test_helper {
namespace {
int lambda_wrapper(void *func_ptr) {
auto func = static_cast<std::function<void()> *>(func_ptr);
(*func)();
_exit(testing::Test::HasFailure());
return 0;
}
} // namespace
ForkResult ForkHelper::GenerateForkResult(pid_t pid, int wstatus, int exit_value,
int death_signum) {
// If we had a normal death signal wired up, only fail if the exit status wasn't expected.
if (death_signum == 0 && (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != exit_value)) {
auto unexpected_exit = ::testing::AssertionFailure()
<< "wait_status: WIFEXITED(wstatus) = " << WIFEXITED(wstatus)
<< ", WEXITSTATUS(wstatus) = " << WEXITSTATUS(wstatus)
<< ", WTERMSIG(wstatus) = " << WTERMSIG(wstatus);
return {.subprocess_id = pid,
.subprocess_exit_status = WEXITSTATUS(wstatus),
.determined_result = unexpected_exit};
}
// If the death signal was set differently, fail if the termination signal didn't match.
else if (death_signum != 0 && (!WIFSIGNALED(wstatus) || WTERMSIG(wstatus) != death_signum)) {
auto unexpected_death = ::testing::AssertionFailure()
<< "wait_status: WIFSIGNALED(wstatus) = " << WIFSIGNALED(wstatus)
<< ", WEXITSTATUS(wstatus) = " << WEXITSTATUS(wstatus)
<< ", WTERMSIG(wstatus) = " << WTERMSIG(wstatus);
return {.subprocess_id = pid,
.subprocess_exit_status = WEXITSTATUS(wstatus),
.determined_result = unexpected_death};
}
// Otherwise, we can mark this forked process as successful.
else {
return {.subprocess_id = pid,
.subprocess_exit_status = WEXITSTATUS(wstatus),
.determined_result = testing::AssertionSuccess()};
}
}
std::vector<ForkResult> ForkHelper::WaitForChildrenInternal(int exit_value, int death_signum) {
std::vector<ForkResult> results;
while (wait_for_all_children_ || !child_pids_.empty()) {
int wstatus;
pid_t pid = wait(&wstatus);
if (pid == -1) {
if (errno == EINTR) {
continue;
}
if (errno == ECHILD) {
// No more children, reaping is done.
return results;
}
// Another error is unexpected, so we'll push that to the results stack.
auto unexpected_failure = testing::AssertionFailure()
<< "wait error: " << strerror(errno) << "(" << errno << ")";
results.push_back({.subprocess_id = pid,
.subprocess_exit_status = errno,
.determined_result = unexpected_failure});
}
bool check_result = wait_for_all_children_;
if (!check_result) {
auto it = std::ranges::find(child_pids_, pid);
if (it != child_pids_.end()) {
child_pids_.erase(it);
check_result = true;
}
}
if (check_result) {
results.push_back(GenerateForkResult(pid, wstatus, exit_value, death_signum));
}
}
return results;
}
ForkHelper::ForkHelper() : wait_for_all_children_(true), death_signum_(0), exit_value_(0) {
// Ensure that all children will ends up being parented to the process that
// created the helper.
prctl(PR_SET_CHILD_SUBREAPER, 1);
}
ForkHelper::~ForkHelper() {
// Wait for all remaining children, and ensure none failed.
auto results = WaitForChildrenInternal(exit_value_, death_signum_);
for (const auto &result : results) {
// EXPECT_TRUE on the result directly will implicitly print failure details.!
EXPECT_TRUE(result.determined_result);
}
}
void ForkHelper::OnlyWaitForForkedChildren() { wait_for_all_children_ = false; }
void ForkHelper::ExpectSignal(int signum) { death_signum_ = signum; }
void ForkHelper::ExpectExitValue(int value) { exit_value_ = value; }
testing::AssertionResult ForkHelper::WaitForChildren() {
auto results = WaitForChildrenInternal(exit_value_, death_signum_);
for (const auto &result : results) {
if (result.determined_result != testing::AssertionSuccess()) {
return result.determined_result;
}
}
return testing::AssertionSuccess();
}
ForkResult ForkHelper::WaitForChild(pid_t child_pid) {
int wstatus;
int pid;
// Continually loop waitpid to handle EINTR signals.
while ((pid = waitpid(child_pid, &wstatus, 0)) == -1 && errno == EINTR) {
}
if (pid == -1) {
auto unexpected_failure = testing::AssertionFailure()
<< "wait error: " << strerror(errno) << "(" << errno << ")";
return {.subprocess_id = pid,
.subprocess_exit_status = errno,
.determined_result = unexpected_failure};
}
auto it = std::ranges::find(child_pids_, pid);
EXPECT_TRUE(it != child_pids_.end());
child_pids_.erase(it);
return GenerateForkResult(pid, wstatus, exit_value_, death_signum_);
}
std::vector<ForkResult> ForkHelper::WaitForChildrenWithResults() {
return WaitForChildrenInternal(exit_value_, death_signum_);
}
pid_t ForkHelper::RunInForkedProcess(fit::function<void()> action) {
pid_t pid = SAFE_SYSCALL(fork());
if (pid != 0) {
child_pids_.push_back(pid);
return pid;
}
action();
_exit(testing::Test::HasFailure());
}
CloneHelper::CloneHelper() {
// Stack setup
this->_childStack =
static_cast<uint8_t *>(mmap(nullptr, CloneHelper::_childStackSize, PROT_WRITE | PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
if (this->_childStack == MAP_FAILED) {
std::cerr << "CloneHelper mmap failed, errno was set to '" << strerror(errno) << "' (" << errno
<< ").\n";
assert(false);
}
this->_childStackBegin = this->_childStack + CloneHelper::_childStackSize;
}
CloneHelper::~CloneHelper() { munmap(this->_childStack, CloneHelper::_childStackSize); }
int CloneHelper::runInClonedChild(unsigned int cloneFlags, int (*childFunction)(void *)) {
int childPid = clone(childFunction, this->_childStackBegin, cloneFlags, nullptr);
assert(childPid != -1);
return childPid;
}
int CloneHelper::runInClonedChild(unsigned int cloneFlags, std::function<void()> action) {
return SAFE_SYSCALL(clone(lambda_wrapper, this->_childStackBegin, cloneFlags, &action));
}
int CloneHelper::sleep_1sec(void *) {
struct timespec res;
res.tv_sec = 1;
res.tv_nsec = 0;
clock_nanosleep(CLOCK_MONOTONIC, 0, &res, &res);
return 0;
}
int CloneHelper::doNothing(void *) { return 0; }
void SignalMaskHelper::blockSignal(int signal) {
sigemptyset(&this->_sigset);
sigaddset(&this->_sigset, signal);
sigprocmask(SIG_BLOCK, &this->_sigset, &this->_sigmaskCopy);
}
void SignalMaskHelper::waitForSignal(int signal) {
int sig;
int result = static_cast<int>(TEMP_FAILURE_RETRY(sigwait(&this->_sigset, &sig)));
ASSERT_EQ(result, 0);
ASSERT_EQ(sig, signal);
}
int SignalMaskHelper::timedWaitForSignal(int signal, time_t msec) {
siginfo_t siginfo;
struct timespec ts;
ts.tv_sec = 0;
ts.tv_nsec = msec * 1000000;
return static_cast<int>(TEMP_FAILURE_RETRY(sigtimedwait(&this->_sigset, &siginfo, &ts)));
}
void SignalMaskHelper::restoreSigmask() { sigprocmask(SIG_SETMASK, &this->_sigmaskCopy, nullptr); }
ScopedTempFD::ScopedTempFD() : name_("/tmp/proc_test_file_XXXXXX") {
char *mut_name = const_cast<char *>(name_.c_str());
fd_ = fbl::unique_fd(mkstemp(mut_name));
}
ScopedTempDir::ScopedTempDir() {
path_ = get_tmp_path() + "/testdirXXXXXX";
if (!mkdtemp(path_.data())) {
path_.clear();
}
}
ScopedTempDir::~ScopedTempDir() {
if (!path_.empty()) {
RecursiveUnmountAndRemove(path_);
}
}
ScopedTempSymlink::ScopedTempSymlink(const char *target_path) {
std::string prefix = "/tmp/syscall_test_symlink_";
size_t retries = 100;
while (retries--) {
std::string path = prefix + RandomHexString(6);
if (symlink(target_path, path.c_str()) == 0) {
path_ = path;
return;
}
}
path_.clear();
}
ScopedTempSymlink::~ScopedTempSymlink() {
if (!path_.empty()) {
unlink(path_.c_str());
}
}
void waitForChildSucceeds(unsigned int waitFlag, int cloneFlags, int (*childRunFunction)(void *),
int (*parentRunFunction)(void *)) {
CloneHelper cloneHelper;
int expectedWaitPid = cloneHelper.runInClonedChild(cloneFlags, childRunFunction);
parentRunFunction(nullptr);
int expectedWaitStatus = 0;
int expectedErrno = 0;
int actualWaitStatus;
int actualWaitPid = waitpid(expectedWaitPid, &actualWaitStatus, waitFlag);
EXPECT_EQ(actualWaitPid, expectedWaitPid);
EXPECT_EQ(actualWaitStatus, expectedWaitStatus);
EXPECT_EQ(errno, expectedErrno);
}
void waitForChildFails(unsigned int waitFlag, int cloneFlags, int (*childRunFunction)(void *),
int (*parentRunFunction)(void *)) {
CloneHelper cloneHelper;
int pid = cloneHelper.runInClonedChild(cloneFlags, childRunFunction);
parentRunFunction(nullptr);
int expectedWaitPid = -1;
int actualWaitPid = waitpid(pid, nullptr, waitFlag);
EXPECT_EQ(actualWaitPid, expectedWaitPid);
EXPECT_EQ(errno, ECHILD);
errno = 0;
}
std::string get_tmp_path() {
static std::string tmp_path = [] {
const char *tmp = getenv("TEST_TMPDIR");
if (tmp == nullptr)
tmp = "/tmp";
return tmp;
}();
return tmp_path;
}
namespace {
std::optional<MemoryMapping> parse_mapping_entry(std::string_view line) {
// format:
// start-end perms offset device inode path
std::vector<std::string_view> parts =
fxl::SplitString(line, " ", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
if (parts.size() < 5) {
return std::nullopt;
}
std::vector<std::string_view> addrs =
fxl::SplitString(parts[0], "-", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
if (addrs.size() != 2) {
return std::nullopt;
}
uintptr_t start;
uintptr_t end;
if (!fxl::StringToNumberWithError(addrs[0], &start, fxl::Base::k16) ||
!fxl::StringToNumberWithError(addrs[1], &end, fxl::Base::k16)) {
return std::nullopt;
}
size_t offset;
size_t inode;
if (!fxl::StringToNumberWithError(parts[2], &offset, fxl::Base::k16) ||
!fxl::StringToNumberWithError(parts[4], &inode, fxl::Base::k10)) {
return std::nullopt;
}
std::string pathname;
if (parts.size() > 5) {
// The pathname always starts at pos 73.
pathname = line.substr(73);
}
return MemoryMapping{
.start = start,
.end = end,
.perms = std::string(parts[1]),
.offset = offset,
.device = std::string(parts[3]),
.inode = inode,
.pathname = pathname,
};
}
} // namespace
std::optional<size_t> parse_field_in_kb(std::string_view value) {
const std::string_view suffix = " kB";
if (!value.ends_with(suffix)) {
return std::nullopt;
}
value.remove_suffix(suffix.length());
size_t result;
if (!fxl::StringToNumberWithError(value, &result, fxl::Base::k10)) {
return std::nullopt;
}
return result;
}
std::optional<MemoryMapping> find_memory_mapping(fit::function<bool(const MemoryMapping &)> match,
std::string_view maps) {
std::vector<std::string_view> lines =
fxl::SplitString(maps, "\n", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
for (auto line : lines) {
std::optional<MemoryMapping> mapping = parse_mapping_entry(line);
if (!mapping) {
return std::nullopt;
}
if (match(*mapping)) {
return mapping;
}
}
return std::nullopt;
}
std::optional<MemoryMapping> find_memory_mapping(uintptr_t addr, std::string_view maps) {
return find_memory_mapping(
[addr](const MemoryMapping &mapping) { return mapping.start <= addr && addr < mapping.end; },
maps);
}
std::optional<MemoryMappingExt> find_memory_mapping_ext(
fit::function<bool(const MemoryMappingExt &)> match, std::string_view maps) {
std::vector<std::string_view> lines =
fxl::SplitString(maps, "\n", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
std::optional<MemoryMappingExt> current_mapping;
for (auto line : lines) {
std::optional<MemoryMapping> maybe_new_mapping = parse_mapping_entry(line);
if (maybe_new_mapping) {
if (current_mapping && match(*current_mapping)) {
return current_mapping;
}
current_mapping = MemoryMappingExt(*maybe_new_mapping);
continue;
}
std::vector<std::string_view> fields =
fxl::SplitString(line, ":", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
if (fields.size() != 2) {
return std::nullopt;
}
if (fields[0] == "Rss") {
if (std::optional<size_t> rss = parse_field_in_kb(fields[1])) {
current_mapping->rss = *rss;
} else {
return std::nullopt;
}
}
if (fields[0] == "VmFlags") {
current_mapping->vm_flags =
fxl::SplitStringCopy(fields[1], " ", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
}
}
if (current_mapping && match(*current_mapping)) {
return current_mapping;
}
return std::nullopt;
}
std::optional<MemoryMappingExt> find_memory_mapping_ext(uintptr_t addr, std::string_view maps) {
return find_memory_mapping_ext(
[addr](const MemoryMappingExt &mapping) {
return mapping.start <= addr && addr < mapping.end;
},
maps);
}
std::ostream &operator<<(std::ostream &os, const MemoryMappingExt &mapping) {
os << "\tstart:\t0x" << std::hex << mapping.start << "\n";
os << "\tend:\t0x" << std::hex << mapping.end << "\n";
os << "\tperms:\t" << mapping.perms << "\n";
os << "\toffset:\t0x" << mapping.offset << "\n";
os << "\tdevice:\t" << mapping.device << "\n";
os << "\tinode:\t" << mapping.inode << "\n";
os << "\tpath:\t" << mapping.pathname << "\n";
os << "\trss:\t" << mapping.rss << "\n";
os << "\tflags:\t";
for (auto &vm_flag : mapping.vm_flags) {
os << vm_flag << " ";
}
return os;
}
std::string RandomHexString(size_t length) {
constexpr char kHexCharacters[] = "0123456789ABCDEF";
constexpr size_t kRadix = sizeof(kHexCharacters) - 1;
std::string value(length, '\0');
for (size_t i = 0; i < length; ++i) {
value[i] = kHexCharacters[random() % kRadix];
}
return value;
}
bool HasSysAdmin() { return HasCapability(CAP_SYS_ADMIN); }
bool IsStarnix() {
struct utsname buf;
return uname(&buf) == 0 && strstr(buf.release, "starnix") != nullptr;
}
bool IsKernelVersionAtLeast(int min_major, int min_minor) {
struct utsname buf;
int major, minor;
if (uname(&buf) != 0) {
return false;
}
if (sscanf(buf.release, "%d.%d:", &major, &minor) != 2) {
return false;
}
return major > min_major || (major == min_major && minor >= min_minor);
}
void RecursiveUnmountAndRemove(const std::string &path) {
if (HasSysAdmin()) {
// Repeatedly call umount to handle shadowed mounts properly.
do {
errno = 0;
ASSERT_THAT(umount2(path.c_str(), MNT_DETACH),
AnyOf(SyscallSucceeds(), SyscallFailsWithErrno(EINVAL)))
<< path;
} while (errno != EINVAL);
}
int dir_fd = open(path.c_str(), O_DIRECTORY | O_NOFOLLOW);
if (dir_fd >= 0) {
DIR *dir = fdopendir(dir_fd);
EXPECT_NE(dir, nullptr) << "fdopendir: " << std::strerror(errno);
while (struct dirent *entry = readdir(dir)) {
std::string name(entry->d_name);
if (name == "." || name == "..")
continue;
std::string subpath = std::string(path) + "/" + name;
if (entry->d_type == DT_DIR) {
RecursiveUnmountAndRemove(subpath);
} else {
EXPECT_THAT(unlink(subpath.c_str()), SyscallSucceeds()) << subpath;
}
}
EXPECT_EQ(closedir(dir), 0) << "closedir: " << std::strerror(errno);
}
EXPECT_THAT(rmdir(path.c_str()), SyscallSucceeds());
}
int MemFdCreate(const char *name, unsigned int flags) {
return static_cast<int>(syscall(SYS_memfd_create, name, flags));
}
int PidFdOpen(pid_t pid, unsigned int flags) {
return static_cast<int>(syscall(SYS_pidfd_open, pid, flags));
}
// Attempts to read a byte from the given memory address.
// Returns whether the read succeeded or not.
bool TryRead(uintptr_t addr) {
fbl::unique_fd mem_fd(MemFdCreate("try_read", O_WRONLY));
EXPECT_TRUE(mem_fd.is_valid());
return write(mem_fd.get(), reinterpret_cast<void *>(addr), 1) == 1;
}
// Attempts to write a zero byte to the given memory address.
// Returns whether the write succeeded or not.
bool TryWrite(uintptr_t addr) {
fbl::unique_fd zero_fd(open("/dev/zero", O_RDONLY));
EXPECT_TRUE(zero_fd.is_valid());
return read(zero_fd.get(), reinterpret_cast<void *>(addr), 1) == 1;
}
// Loop until the target process indicates a sleeping state in /proc/pid/stat.
void WaitUntilBlocked(pid_t target, bool ignore_tracer) {
for (int i = 0; i < 100000; i++) {
// Loop until the target task is paused.
std::string fname = "/proc/" + std::to_string(target) + "/stat";
std::ifstream t(fname);
if (!t.is_open()) {
FAIL() << "File " << fname << " not found";
}
std::stringstream buffer;
buffer << t.rdbuf();
if (buffer.str().find("S") != std::string::npos ||
(!ignore_tracer && buffer.str().find("t") != std::string::npos)) {
break;
}
// Give up if we don't seem to be getting to sleep.
if (i == 99999)
FAIL() << "Failed to wait for pid " << target
<< " to block. resulting status: " << buffer.str();
}
}
// This variable is accessed from within a signal handler and thus must be declared volatile.
static volatile void *expected_fault_address;
testing::AssertionResult TestThatAccessSegfaults(void *test_address, AccessType type) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([test_address, type] {
struct sigaction action;
action.sa_sigaction = [](int signo, siginfo_t *info, void *ucontext) {
if (signo == SIGSEGV && info->si_addr == expected_fault_address) {
_exit(EXIT_SUCCESS);
} else {
_exit(EXIT_FAILURE);
}
};
action.sa_flags = SA_SIGINFO;
SAFE_SYSCALL(sigaction(SIGSEGV, &action, nullptr));
expected_fault_address = test_address;
if (type == AccessType::Read) {
*static_cast<volatile std::byte *>(test_address);
} else {
*static_cast<volatile std::byte *>(test_address) = std::byte{};
}
FAIL() << "Must have observed segfault after access.";
});
return helper.WaitForChildren();
}
ScopedMount::ScopedMount(std::string target_path)
: target_path_(std::move(target_path)), is_mounted_(true) {}
fit::result<int, ScopedMount> ScopedMount::Mount(const std::string &source,
const std::string &target,
const std::string &filesystemtype,
unsigned long mountflags, const void *data) {
if (mount(source.c_str(), target.c_str(), filesystemtype.c_str(), mountflags, data) != 0) {
int error = errno;
return fit::error(error);
}
return fit::ok(ScopedMount(target));
}
ScopedMount::ScopedMount(ScopedMount &&other) noexcept {
Unmount();
is_mounted_ = other.is_mounted_;
target_path_ = other.target_path_;
other.is_mounted_ = false;
}
ScopedMount::~ScopedMount() { Unmount(); }
void ScopedMount::Unmount() {
if (is_mounted_) {
umount(target_path_.c_str());
is_mounted_ = false;
}
}
Poker::Poker(fbl::unique_fd pipe_write_side) : pipe_write_side_(std::move(pipe_write_side)) {}
Poker::Poker(Poker &&o) : pipe_write_side_(std::move(o.pipe_write_side_)) {}
Poker &Poker::operator=(Poker &&o) {
pipe_write_side_ = std::move(o.pipe_write_side_);
return *this;
}
void Poker::poke() {
char clog[] = "clog"; // This data will enter but never leave the pipe.
SAFE_SYSCALL(write(pipe_write_side_.get(), clog, sizeof(clog)));
SAFE_SYSCALL(pipe_write_side_.reset());
}
Holder::Holder(fbl::unique_fd pipe_read_side) : pipe_read_side_(std::move(pipe_read_side)) {}
Holder::Holder(Holder &&o) : pipe_read_side_(std::move(o.pipe_read_side_)) {}
Holder &Holder::operator=(Holder &&o) {
pipe_read_side_ = std::move(o.pipe_read_side_);
return *this;
}
void Holder::hold() {
int pipe_read_side_fd = pipe_read_side_.get();
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(pipe_read_side_fd, &read_fds);
SAFE_SYSCALL(select(pipe_read_side_fd + 1, &read_fds, nullptr, nullptr, nullptr));
}
Rendezvous MakeRendezvous() {
static_assert(sizeof(int) == sizeof(fbl::unique_fd));
fbl::unique_fd unique_fds[2];
SAFE_SYSCALL(pipe(reinterpret_cast<int *>(&unique_fds)));
return {
.poker = Poker(std::move(unique_fds[1])),
.holder = Holder(std::move(unique_fds[0])),
};
}
} // namespace test_helper