blob: 870cbe22de4370fc1c4a87442259a3b31dd8adc3 [file] [log] [blame] [edit]
// 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.
#ifndef SRC_STARNIX_TESTS_SYSCALLS_CPP_TEST_HELPER_H_
#define SRC_STARNIX_TESTS_SYSCALLS_CPP_TEST_HELPER_H_
#include <lib/fit/function.h>
#include <lib/fit/result.h>
#include <stdint.h>
#include <sys/eventfd.h>
#include <sys/mman.h>
#include <sys/uio.h>
#include <unistd.h>
#include <optional>
#include <string_view>
#include <vector>
#include <fbl/unique_fd.h>
#include <gtest/gtest.h>
#include <linux/genetlink.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/taskstats.h>
#define SAFE_SYSCALL(X) \
({ \
auto retval = (X); \
if (retval < 0) { \
ADD_FAILURE() << #X << " failed: " << strerror(errno) << "(" << errno << ")"; \
retval = {}; \
} \
retval; \
})
// TODO(https://fxbug.dev/317285180) don't skip on baseline
#define SAFE_SYSCALL_SKIP_ON_EPERM(X) \
({ \
auto retval = (X); \
if (retval < 0) { \
if (errno == EPERM) { \
GTEST_SKIP() << "Permission denied for " << #X << ", skipping tests."; \
} else { \
FAIL() << #X << " failed: " << strerror(errno) << "(" << errno << ")"; \
} \
} \
retval; \
})
#define ASSERT_RESULT_SUCCESS_AND_RETURN(S) \
({ \
auto retval = (S); \
ASSERT_TRUE(retval.is_ok()) << #S << " failed."; \
std::move(retval.value()); \
})
namespace test_helper {
struct ForkResult {
pid_t subprocess_id;
int subprocess_exit_status;
testing::AssertionResult determined_result;
};
// Helper class to handle test that needs to fork and do assertion on the child
// process.
class ForkHelper {
public:
ForkHelper();
~ForkHelper();
// Wait for all children of the current process, and return true if all exited
// with a 0 status or with the expected signal.
testing::AssertionResult WaitForChildren();
// Wait for a specific child of the current process, and return results on it.
ForkResult WaitForChild(pid_t child_pid);
// Wait for all children of the current process, and return results on each.
std::vector<ForkResult> WaitForChildrenWithResults();
// For the current process and execute the given |action| inside the child,
// then exit with a status equals to the number of failed expectation and
// assertion. Return immediately with the pid of the child.
pid_t RunInForkedProcess(fit::function<void()> action);
// If called, checks for process termination by the given signal, instead of expecting
// the forked process to terminate normally.
void ExpectSignal(int signum);
// If called, checks for process termination with the given exit value,
// instead of expecting the forked process to terminate normally.
void ExpectExitValue(int value);
// If called, only waits for the children explicitly forked by the ForkHelper, instead
// of waiting for all children.
void OnlyWaitForForkedChildren();
private:
std::vector<pid_t> child_pids_;
bool wait_for_all_children_;
int death_signum_;
int exit_value_;
std::vector<ForkResult> WaitForChildrenInternal(int exit_value, int death_signum);
ForkResult GenerateForkResult(pid_t pid, int wstatus, int exit_value, int death_signum);
};
// Helper class to handle tests that needs to clone processes.
class CloneHelper {
public:
CloneHelper();
~CloneHelper();
// Call clone with the specified childFunction and cloneFlags.
// Perform the necessary asserts to ensure the clone was performed with
// no errors and return the new process ID.
int runInClonedChild(unsigned int cloneFlags, int (*childFunction)(void *));
// Call clone and execute the given |action| inside the child,
// then exit with a status equal to the number of failed expectations and
// assertions. Return immediately with the pid of the child.
int runInClonedChild(unsigned int cloneFlags, std::function<void()> action);
// Handy trivial function for passing clone when we want the child to
// sleep for 1 second and return 0.
static int sleep_1sec(void *);
// Handy trivial function for passing clone when we want the child to
// do nothing and return 0.
static int doNothing(void *);
private:
uint8_t *_childStack;
uint8_t *_childStackBegin;
static constexpr size_t _childStackSize = 0x5000;
};
// Helper class to modify signal masks of processes
class SignalMaskHelper {
public:
// Blocks the specified signal and saves the original signal mask
// to _sigmaskCopy.
void blockSignal(int signal);
// Blocks the execution until the specified signal is received.
void waitForSignal(int signal);
// Blocks the execution until the specified signal is received or timed out.
int timedWaitForSignal(int signal, time_t msec);
// Sets the signal mask of the process with _sigmaskCopy.
void restoreSigmask();
private:
sigset_t _sigset;
sigset_t _sigmaskCopy;
};
class ScopedTempFD {
public:
ScopedTempFD();
~ScopedTempFD() { unlink(name_.c_str()); }
bool is_valid() const { return fd_.is_valid(); }
explicit operator bool() const { return is_valid(); }
const std::string &name() const { return name_; }
int fd() const { return fd_.get(); }
std::string name_;
fbl::unique_fd fd_;
};
class ScopedTempDir {
public:
ScopedTempDir();
~ScopedTempDir();
ScopedTempDir(const ScopedTempDir &) = delete;
ScopedTempDir &operator=(const ScopedTempDir &) = delete;
ScopedTempDir(ScopedTempDir &&) = default;
ScopedTempDir &operator=(ScopedTempDir &&) = default;
const std::string &path() const { return path_; }
private:
std::string path_;
};
class ScopedTempSymlink {
public:
explicit ScopedTempSymlink(const char *target_path);
~ScopedTempSymlink();
bool is_valid() const { return !path_.empty(); }
explicit operator bool() const { return is_valid(); }
const std::string &path() const { return path_; }
private:
std::string path_;
};
#define HANDLE_EINTR(x) \
({ \
decltype(x) eintr_wrapper_result; \
do { \
eintr_wrapper_result = (x); \
} while (eintr_wrapper_result == -1 && errno == EINTR); \
eintr_wrapper_result; \
})
void waitForChildSucceeds(unsigned int waitFlag, int cloneFlags, int (*childRunFunction)(void *),
int (*parentRunFunction)(void *));
void waitForChildFails(unsigned int waitFlag, int cloneFlags, int (*childRunFunction)(void *),
int (*parentRunFunction)(void *));
std::string get_tmp_path();
struct MemoryMapping {
uintptr_t start;
uintptr_t end;
std::string perms;
size_t offset;
std::string device;
size_t inode;
std::string pathname;
};
struct MemoryMappingExt : public MemoryMapping {
size_t rss;
std::vector<std::string> vm_flags;
public:
explicit MemoryMappingExt(const MemoryMapping &mapping) : MemoryMapping(mapping) {}
bool ContainsFlag(std::string_view flag) const {
return std::ranges::find(vm_flags, flag) != vm_flags.end();
}
friend std::ostream &operator<<(std::ostream &os, const MemoryMappingExt &mapping);
};
#define MY_NLMSG_OK(nlh, len) NLMSG_OK((nlh), static_cast<decltype((nlh)->nlmsg_len)>(len))
// Encoder for serializing netlink messages
class NetlinkEncoder {
public:
// Writes a value to the buffer at the current offset
template <typename T>
void Write(const T &value) {
Write(&value, sizeof(T));
}
void WriteSpan(const std::span<const uint8_t> &data) { Write(data.data(), data.size_bytes()); }
void WriteString(const std::string_view &data) { Write(data.data(), data.size()); }
// Writes a value to the buffer at a specified offset
template <typename T>
void Write(T &value, size_t offset) {
Write(&value, offset, sizeof(T));
}
// Reads a value from the buffer at a specified offset
template <typename T>
void Read(T &out, size_t offset) {
Read(&out, offset, sizeof(T));
}
NetlinkEncoder() = default;
NetlinkEncoder(__u16 type, __u16 flags) { StartMessage(type, flags); }
// Starts a new netlink message
void StartMessage(__u16 type, __u16 flags) {
Clear();
nlmsghdr header = {};
header.nlmsg_type = type;
// Length is encoded when the message is finalized
header.nlmsg_len = sizeof(nlmsghdr);
header.nlmsg_flags = flags;
header.nlmsg_pid = getpid();
header.nlmsg_seq = sequence_;
netlink_header = offset_;
Write(header);
}
// Begins a genetlink message
void BeginGenetlinkHeader(__u8 cmd) {
genlmsghdr hdr = {};
hdr.cmd = cmd;
hdr.version = 1;
genetlink_header = offset_;
Write(hdr);
}
// Starts encoding an NLA
void BeginNla(__u16 type) {
nlattr attr = {};
attr.nla_type = type;
// Updated when the NLA is finalized
attr.nla_len = 0;
nla_start = offset_;
Write(attr);
}
// Finishes encoding an NLA
void EndNla() {
nlattr attr;
Read(attr, nla_start);
attr.nla_len += static_cast<__u16>(offset_ - nla_start);
Write(attr, nla_start);
}
template <typename T>
void AddRtAttr(uint16_t type, T &attr) {
Write(rtattr{
.rta_len = static_cast<uint16_t>(RTA_LENGTH(sizeof(T))),
.rta_type = type,
});
Write(attr);
}
// Finalizes the message, allowing it to be sent using sendmsg.
void Finalize(iovec &out) {
nlmsghdr hdr;
Read(hdr, netlink_header);
out.iov_base = data_.data() + netlink_header;
hdr.nlmsg_len = static_cast<uint32_t>(offset_);
out.iov_len = hdr.nlmsg_len;
sequence_++;
Write(hdr, netlink_header);
}
// Clears the buffer, invalidating any iovecs that were
// obtained from this encoder.
void Clear() { offset_ = 0; }
uint32_t sequence() const { return sequence_; }
private:
void Write(const void *data, size_t len) {
size_t min_size = offset_ + len;
if (min_size > data_.size()) {
data_.resize(min_size);
}
memcpy(data_.data() + offset_, data, len);
offset_ += len;
}
void Read(void *data, size_t offset, size_t len) { memcpy(data, data_.data() + offset, len); }
void Write(const void *data, size_t offset, size_t len) {
memcpy(data_.data() + offset, data, len);
}
__u32 sequence_ = 0;
size_t offset_ = 0;
size_t nla_start;
size_t netlink_header;
size_t genetlink_header;
std::vector<uint8_t> data_;
};
// A RRAI classes than handles a memory mapping. The container will ensure the
// mapping is destroyed when the object is deleted.
class ScopedMMap {
public:
static fit::result<int, ScopedMMap> MMap(void *addr, size_t length, int prot, int flags, int fd,
off_t offset) {
void *mapping = mmap(addr, length, prot, flags, fd, offset);
if (mapping == MAP_FAILED) {
int error = errno;
return fit::error(error);
}
return fit::ok(ScopedMMap(mapping, length));
}
ScopedMMap(const ScopedMMap &) = delete;
ScopedMMap &operator=(const ScopedMMap &) = delete;
ScopedMMap(ScopedMMap &&other) noexcept : mapping_(MAP_FAILED), length_(0) {
*this = std::move(other);
}
~ScopedMMap() { Unmap(); }
ScopedMMap &operator=(ScopedMMap &&other) noexcept {
Unmap();
mapping_ = other.mapping_;
length_ = other.length_;
other.mapping_ = MAP_FAILED;
return *this;
}
void Unmap() {
if (is_valid()) {
munmap(mapping_, length_);
mapping_ = MAP_FAILED;
}
}
bool is_valid() const { return mapping_ != MAP_FAILED; }
explicit operator bool() const { return is_valid(); }
void *mapping() const { return mapping_; }
private:
explicit ScopedMMap(void *mapping, size_t length) : mapping_(mapping), length_(length) {}
void *mapping_;
size_t length_;
};
// A RRAI classes than handles a mount. The container will ensure the
// unmount when the object is deleted.
class ScopedMount {
public:
static fit::result<int, ScopedMount> Mount(const std::string &source, const std::string &target,
const std::string &filesystemtype,
unsigned long mountflags, const void *data);
ScopedMount(const ScopedMount &) = delete;
ScopedMount &operator=(const ScopedMount &) = delete;
ScopedMount &operator=(ScopedMount &&other) = delete;
ScopedMount(ScopedMount &&other) noexcept;
~ScopedMount();
void Unmount();
private:
explicit ScopedMount(std::string target_path);
std::string target_path_;
bool is_mounted_ = false;
};
std::optional<size_t> parse_field_in_kb(std::string_view value);
// A semaphore implemented with EventFd. Works across Threads and Forks.
class EventFdSem {
public:
explicit EventFdSem(int initial_value) {
fd_ = fbl::unique_fd(eventfd(initial_value, EFD_SEMAPHORE));
}
~EventFdSem() = default;
EventFdSem(EventFdSem &&other) noexcept = default;
EventFdSem &operator=(EventFdSem &&other) noexcept = default;
EventFdSem(const EventFdSem &) = delete;
EventFdSem &operator=(const EventFdSem &) = delete;
int Wait() {
eventfd_t val;
return static_cast<int>(TEMP_FAILURE_RETRY(eventfd_read(fd_.get(), &val)));
}
int Notify(int value) { return eventfd_write(fd_.get(), value); }
private:
fbl::unique_fd fd_;
};
// Returns the first memory mapping that matches the given predicate.
std::optional<MemoryMapping> find_memory_mapping(fit::function<bool(const MemoryMapping &)> match,
std::string_view maps);
std::optional<MemoryMapping> find_memory_mapping(uintptr_t addr, std::string_view maps);
// Same as above, but with info from smaps
std::optional<MemoryMappingExt> find_memory_mapping_ext(
fit::function<bool(const MemoryMappingExt &)> match, std::string_view maps);
std::optional<MemoryMappingExt> find_memory_mapping_ext(uintptr_t addr, std::string_view maps);
// Returns a random hex string of the given length.
std::string RandomHexString(size_t length);
// Returns true if running with sysadmin capabilities.
bool HasSysAdmin();
// Returns true if running with the given capability.
bool HasCapability(uint32_t cap);
// Returns true if running on Starnix. This is likely only necessary when there are known bugs.
bool IsStarnix();
// Returns true if reported kernel version is equal or greater than a given one.
bool IsKernelVersionAtLeast(int min_major, int min_minor);
/// Unmount anything mounted at or under `path` and remove it.
void RecursiveUnmountAndRemove(const std::string &path);
// Attempts to read a byte from the given memory address.
// Returns whether the read succeeded or not.
bool TryRead(uintptr_t addr);
// Attempts to write a zero byte to the given memory address.
// Returns whether the write succeeded or not.
bool TryWrite(uintptr_t addr);
// Wrapper for the memfd_create system call.
int MemFdCreate(const char *name, unsigned int flags);
// Wrapper for the pidfd_open system call.
int PidFdOpen(pid_t pid, unsigned int flags);
void WaitUntilBlocked(pid_t target, bool ignore_tracer);
enum AccessType { Read, Write };
// Checks whether the provided access segfaults.
testing::AssertionResult TestThatAccessSegfaults(void *test_address, AccessType type);
// A RAII container for a pair of file descriptors representing a pipe.
class ScopedPipe {
public:
ScopedPipe();
ScopedPipe(ScopedPipe &&o);
ScopedPipe &operator=(ScopedPipe &&o);
ScopedPipe(const ScopedPipe &) = delete;
ScopedPipe &operator=(const ScopedPipe &) = delete;
const fbl::unique_fd &ReadSide() const { return read_side_; }
fbl::unique_fd &ReadSide() { return read_side_; }
const fbl::unique_fd &WriteSide() const { return write_side_; }
fbl::unique_fd &WriteSide() { return write_side_; }
private:
fbl::unique_fd read_side_;
fbl::unique_fd write_side_;
};
// A means of unblocking some other thread or process that is waiting (or may in the
// future wait) on a corresponding `Holder`.
class Poker {
public:
Poker() = default;
explicit Poker(fbl::unique_fd pipe_write_side);
Poker(Poker &&o);
Poker &operator=(Poker &&o);
Poker(const Poker &) = delete;
Poker &operator=(const Poker &) = delete;
bool is_valid() const { return pipe_write_side_.is_valid(); }
explicit operator bool() const { return is_valid(); }
void poke();
private:
fbl::unique_fd pipe_write_side_;
};
// A means of blocking until some other thread or process uses a corresponding `Poker`
// to indicate that some desired state has been reached and further blocking is no longer
// necessary.
class Holder {
public:
Holder() = default;
explicit Holder(fbl::unique_fd pipe_read_side);
Holder(Holder &&o);
Holder &operator=(Holder &&o);
Holder(const Holder &) = delete;
Holder &operator=(const Holder &) = delete;
bool is_valid() const { return pipe_read_side_.is_valid(); }
explicit operator bool() const { return is_valid(); }
void hold();
private:
fbl::unique_fd pipe_read_side_;
};
// A simple IPC synchronization mechanism for use in multithread and multiprocess
// tests.
struct Rendezvous {
Poker poker;
Holder holder;
};
// Constructs a new pipe-backed `Poker`-`Holder` pair for synchronization of two
// threads or processes.
Rendezvous MakeRendezvous();
// Constructs a `Poker`-`Holder` pair from an existing pipe.
Rendezvous MakeRendezvous(ScopedPipe pipe);
} // namespace test_helper
#endif // SRC_STARNIX_TESTS_SYSCALLS_CPP_TEST_HELPER_H_