blob: 118d57c0c55391bb333f59a3c2ec6bb23a12e359 [file] [log] [blame] [edit]
// 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 "src/starnix/tests/syscalls/cpp/task_test.h"
#include <fcntl.h>
#include <limits.h>
#include <sched.h>
#include <strings.h>
#include <sys/fsuid.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/select.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <cstdint>
#include <filesystem>
#include <thread>
#include <fbl/algorithm.h>
#include <gtest/gtest.h>
#include <linux/sched.h>
#include "src/lib/files/directory.h"
#include "src/lib/files/file.h"
#include "src/lib/fxl/strings/split_string.h"
#include "src/lib/fxl/strings/string_number_conversions.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "src/starnix/tests/syscalls/cpp/syscall_matchers.h"
#include "src/starnix/tests/syscalls/cpp/test_helper.h"
namespace {
#define MAX_PAGE_ALIGNMENT (1 << 21)
// Unless the split between the data and bss sections happens to be page-aligned, initial part
// of the bss section will be in the same page as the last part of the data section.
// By aligning g_global_variable_bss to a value bigger than the page size, we prevent it from
// ending up in this shared page
alignas(MAX_PAGE_ALIGNMENT) volatile int g_global_variable_bss = 0;
volatile size_t g_global_variable_data = 15;
volatile size_t g_fork_doesnt_drop_writes = 0;
// As of this writing, our sysroot's syscall.h lacks the SYS_clone3 definition.
#ifndef SYS_clone3
#if defined(__aarch64__) || defined(__x86_64__) || defined(__riscv)
#define SYS_clone3 435
#else
#error SYS_clone3 needs a definition for this architecture.
#endif
#endif
constexpr int kChildExpectedExitCode = 21;
constexpr int kChildErrorExitCode = kChildExpectedExitCode + 1;
constexpr uid_t kUser1Uid = 65533;
constexpr gid_t kUser1Gid = 65534;
bool change_ids(uid_t user, gid_t group) {
return (setresgid(group, group, group) == 0) && (setresuid(user, user, user) == 0);
}
void WaitForReadFd(int fd) {
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
SAFE_SYSCALL(TEMP_FAILURE_RETRY(select(fd + 1, &read_fds, nullptr, nullptr, nullptr)));
EXPECT_TRUE(FD_ISSET(fd, &read_fds));
}
pid_t ForkUsingClone3(const clone_args* cl_args, size_t size) {
return static_cast<pid_t>(syscall(SYS_clone3, cl_args, size));
}
// calls clone3 and executes a function, calling exit with its return value.
pid_t DoClone3(const clone_args* cl_args, size_t size, int (*func)(void*), void* param) {
pid_t pid;
// clone3 lets you specify a new stack, but not which function to run.
// This means that after the clone3 syscall, the child will be running on a
// new stack, not being able to access any local variables from before the
// clone.
//
// We have to manually call into the new function in assembly, being careful
// to not refer to any variables from the stack.
#if defined(__aarch64__)
__asm__ volatile(
"mov x0, %[cl_args]\n"
"mov x1, %[size]\n"
"mov w8, %[clone3]\n"
"svc #0\n"
"cbnz x0, 1f\n"
"mov x0, %[param]\n"
"blr %[func]\n"
"mov w8, %[exit]\n"
"svc #0\n"
"brk #1\n"
"1:\n"
"mov %w[pid], w0\n"
: [pid] "=r"(pid)
: [cl_args] "r"(cl_args), "m"(*cl_args), [size] "r"(size), [func] "r"(func),
[param] "r"(param), [clone3] "i"(SYS_clone3), [exit] "i"(SYS_exit)
: "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9", "x10", "x11", "x12", "x13",
"x14", "x15", "x16", "x17", "cc", "memory");
#elif defined(__arm__)
__asm__ volatile(
"mov r0, %[cl_args]\n"
"mov r1, %[size]\n"
"mov r7, %[clone3]\n"
"svc #0\n"
"cmp r0, #0\n"
"bne 1f\n"
"mov r0, %[param]\n"
"blx %[func]\n"
"mov r7, %[exit]\n"
"svc #0\n"
"bkpt\n"
"1:\n"
"mov %[pid], r0\n"
: [pid] "=r"(pid)
: [cl_args] "r"(cl_args), "m"(*cl_args), [size] "r"(size), [func] "r"(func),
[param] "r"(param), [clone3] "i"(SYS_clone3), [exit] "i"(SYS_exit)
: "r0", "r1", "r2", "r3", "r7", "r12", "lr", "cc", "memory");
#elif defined(__x86_64__)
__asm__ volatile(
"syscall\n"
"test %%rax, %%rax\n"
"jnz 1f\n"
"movq %[param], %%rdi\n"
"callq *%[func]\n"
"movl %%eax, %%edi\n"
"movl %[exit], %%eax\n"
"syscall\n"
"ud2\n"
"1:\n"
"movl %%eax, %[pid]\n"
: [pid] "=g"(pid)
: "D"(cl_args), "m"(*cl_args), "S"(size),
"a"(SYS_clone3), [func] "r"(func), [param] "r"(param), [exit] "i"(SYS_exit)
: "rcx", "rdx", "r8", "r9", "r10", "r11", "cc", "memory");
#elif defined(__riscv)
__asm__ volatile(
"mv a0, %[cl_args]\n"
"mv a1, %[size]\n"
"li a7, %[sys_clone3]\n"
"ecall\n"
"bnez a0, 1f\n"
"mv a0, %[param]\n"
"jalr %[func]\n"
"li a7, %[sys_exit]\n"
"ecall\n"
"ebreak\n"
"1:\n"
"mv %[pid], a0\n"
: [pid] "=r"(pid)
: [cl_args] "r"(cl_args), "m"(*cl_args), [size] "r"(size), [func] "r"(func),
[param] "r"(param), [sys_clone3] "i"(SYS_clone3), [sys_exit] "i"(SYS_exit)
: "ra", "t0", "t1", "t2", "t3", "t4", "t5", "t6", "a0", "a1", "a2", "a3", "a4", "a5", "a6",
"a7", "s1", "memory");
#else
#error clone3 needs a manual asm wrapper.
#endif
if (pid < 0) {
errno = -pid;
pid = -1;
}
return pid;
}
int stack_test_func(void* a) {
// Force a stack write by creating an asm block
// that has an input that needs to come from memory.
int pid = *reinterpret_cast<int*>(a);
__asm__("" ::"m"(pid));
if (getpid() != pid)
return kChildErrorExitCode;
return kChildExpectedExitCode;
}
int empty_func(void*) { return 0; }
void ReadStackStart(pid_t pid, uintptr_t* result) {
std::string contents, path = fxl::StringPrintf("/proc/%ld/stat", static_cast<long>(pid));
ASSERT_TRUE(files::ReadFileToString(path, &contents));
auto parts = fxl::SplitString(contents, " ", fxl::kTrimWhitespace, fxl::kSplitWantAll);
ASSERT_GE(parts.size(), 28U) << contents;
ASSERT_TRUE(fxl::StringToNumberWithError(parts[27], result)) << parts[27];
}
void ReadStackSize(pid_t pid, uintptr_t* result) {
std::string contents, path = fxl::StringPrintf("/proc/%ld/status", static_cast<long>(pid));
ASSERT_TRUE(files::ReadFileToString(path, &contents));
std::vector<std::string_view> lines =
fxl::SplitString(contents, "\n", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
bool vm_stk_found = false;
for (auto line : lines) {
auto key_and_value =
fxl::SplitString(line, ": ", fxl::kTrimWhitespace, fxl::kSplitWantNonEmpty);
if (key_and_value.size() >= 2 && key_and_value[0] == "VmStk") {
ASSERT_TRUE(fxl::StringToNumberWithError(key_and_value[1], result)) << line;
vm_stk_found = true;
break;
}
}
ASSERT_TRUE(vm_stk_found) << contents;
}
void ReadProcPidFile(pid_t pid, const char* name, std::vector<uint8_t>* result) {
std::string path = fxl::StringPrintf("/proc/%ld/%s", static_cast<long>(pid), name);
ASSERT_TRUE(files::ReadFileToVector(path, result));
}
} // namespace
// Creates a child process using the "clone3()" syscall and waits on it.
// The child uses a different stack than the parent.
TEST(Task, Clone3_ChangeStack) {
struct clone_args ca;
bzero(&ca, sizeof(ca));
ca.flags = CLONE_PARENT_SETTID | CLONE_CHILD_SETTID;
ca.exit_signal = SIGCHLD; // Needed in order to wait on the child.
// Ask for the child PID to be reported to both the parent and the child for validation.
uint64_t child_pid_from_clone = 0;
ca.parent_tid = reinterpret_cast<uint64_t>(&child_pid_from_clone);
ca.child_tid = reinterpret_cast<uint64_t>(&child_pid_from_clone);
constexpr size_t kStackSize = 0x5000;
void* stack_addr = mmap(nullptr, kStackSize, PROT_WRITE | PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
ASSERT_NE(MAP_FAILED, stack_addr);
ca.stack = reinterpret_cast<uint64_t>(stack_addr);
ca.stack_size = kStackSize;
auto child_pid = DoClone3(&ca, sizeof(ca), &stack_test_func, &child_pid_from_clone);
ASSERT_NE(child_pid, -1);
EXPECT_EQ(static_cast<pid_t>(child_pid_from_clone), child_pid);
// Wait for the child to terminate and validate the exit code. Note that it returns a different
// exit code above to indicate its state wasn't as expected.
int wait_status = 0;
pid_t wait_result = waitpid(child_pid, &wait_status, 0);
EXPECT_EQ(wait_result, child_pid);
EXPECT_TRUE(WIFEXITED(wait_status));
auto exit_status = WEXITSTATUS(wait_status);
EXPECT_NE(exit_status, kChildErrorExitCode) << "Child process reported state was unexpected.";
EXPECT_EQ(exit_status, kChildExpectedExitCode) << "Wrong exit code from child process.";
ASSERT_EQ(0, munmap(stack_addr, kStackSize));
}
// Forks a child process using the "clone3()" syscall and waits on it, validating some parameters.
TEST(Task, Clone3_Fork) {
struct clone_args ca;
bzero(&ca, sizeof(ca));
ca.flags = CLONE_PARENT_SETTID | CLONE_CHILD_SETTID;
ca.exit_signal = SIGCHLD; // Needed in order to wait on the child.
// Ask for the child PID to be reported to both the parent and the child for validation.
uint64_t child_pid_from_clone = 0;
ca.parent_tid = reinterpret_cast<uint64_t>(&child_pid_from_clone);
ca.child_tid = reinterpret_cast<uint64_t>(&child_pid_from_clone);
auto child_pid = ForkUsingClone3(&ca, sizeof(ca));
ASSERT_NE(child_pid, -1);
if (child_pid == 0) {
// In child process. We'd like to EXPECT_EQ the pid but this is a child process and the gtest
// failure won't get caught. Instead, return a different result code and the parent will notice
// and issue an error about the state being unexpected.
if (getpid() != static_cast<pid_t>(child_pid_from_clone))
exit(kChildErrorExitCode);
exit(kChildExpectedExitCode);
} else {
EXPECT_EQ(static_cast<pid_t>(child_pid_from_clone), child_pid);
// Wait for the child to terminate and validate the exit code. Note that it returns a different
// exit code above to indicate its state wasn't as expected.
int wait_status = 0;
pid_t wait_result = waitpid(child_pid, &wait_status, 0);
EXPECT_EQ(wait_result, child_pid);
EXPECT_TRUE(WIFEXITED(wait_status));
auto exit_status = WEXITSTATUS(wait_status);
EXPECT_NE(exit_status, kChildErrorExitCode) << "Child process reported state was unexpected.";
EXPECT_EQ(exit_status, kChildExpectedExitCode) << "Wrong exit code from child process.";
}
}
// Forks a child process using the "clone3()" syscall and requests a PID-FD for it.
TEST(Task, Clone3_PidFd) {
struct clone_args ca;
bzero(&ca, sizeof(ca));
ca.flags = CLONE_PIDFD;
ca.exit_signal = SIGCHLD; // Needed in order to wait on the child.
// Ask for a PID FD through which the child process can be observed.
fbl::unique_fd pid_fd;
ca.pidfd = reinterpret_cast<uint64_t>(pid_fd.reset_and_get_address());
auto child_pid = ForkUsingClone3(&ca, sizeof(ca));
ASSERT_NE(child_pid, -1) << strerror(errno);
if (child_pid == 0) {
exit(kChildExpectedExitCode);
} else {
EXPECT_TRUE(pid_fd.is_valid());
// Wait for the child to terminate.
int wait_status = 0;
pid_t wait_result = waitpid(child_pid, &wait_status, 0);
EXPECT_EQ(wait_result, child_pid) << strerror(errno);
}
}
static int ClonePidFdFunctionExit(void*) { exit(kChildExpectedExitCode); }
// Forks a child process using the "clone()" syscall and requests a PID-FD for it.
TEST(Task, Clone_PidFd) {
constexpr size_t kStackSize = 1024 * 16;
void* stack_low = mmap(nullptr, kStackSize, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
ASSERT_NE(stack_low, MAP_FAILED);
void* stack_high = static_cast<char*>(stack_low) + kStackSize; // Pass in the top of the stack.
fbl::unique_fd pid_fd;
auto child_pid = clone(&ClonePidFdFunctionExit, stack_high, SIGCHLD | CLONE_PIDFD,
/*arg=*/nullptr, pid_fd.reset_and_get_address());
ASSERT_NE(child_pid, -1) << strerror(errno);
EXPECT_TRUE(pid_fd.is_valid());
// Wait for the child to terminate.
int wait_status = 0;
pid_t wait_result = waitpid(child_pid, &wait_status, 0);
EXPECT_EQ(wait_result, child_pid) << strerror(errno);
}
// Forks a child process using the "clone()" syscall and requests both PID-FD and parent TID.
TEST(Task, Clone_PidFdAndParentTid) {
constexpr size_t kStackSize = 1024 * 16;
void* stack_low = mmap(nullptr, kStackSize, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
ASSERT_NE(stack_low, MAP_FAILED);
void* stack_high = static_cast<char*>(stack_low) + kStackSize; // Pass in the top of the stack.
uintptr_t out_arg{};
auto child_pid =
clone(&ClonePidFdFunctionExit, stack_high, SIGCHLD | CLONE_PIDFD | CLONE_PARENT_SETTID,
/*arg=*/nullptr, &out_arg);
EXPECT_EQ(child_pid, -1);
}
TEST(Task, Clone3_InvalidSize) {
struct clone_args ca;
bzero(&ca, sizeof(ca));
// Pass a structure size smaller than the first supported version, it should report EINVAL.
EXPECT_EQ(-1, DoClone3(&ca, CLONE_ARGS_SIZE_VER0 - 8, &empty_func, nullptr));
EXPECT_EQ(EINVAL, errno);
}
static int CloneVForkFunctionSleepExit(void* param) {
struct timespec wait{.tv_sec = 0, .tv_nsec = kCloneVforkSleepUS * 1000};
nanosleep(&wait, nullptr);
// Note: exit() is a stdlib function that exits the whole process which we don't want.
// _exit just exits the current thread which is what matches clone().
_exit(1);
return 0;
}
// Tests a CLONE_VFORK and the cloned thread exits before calling execve. The clone() call should
// block until the thread exits.
TEST(Task, CloneVfork_exit) {
constexpr size_t kStackSize = 1024 * 16;
void* stack_low = mmap(nullptr, kStackSize, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
ASSERT_NE(stack_low, MAP_FAILED);
void* stack_high = static_cast<char*>(stack_low) + kStackSize; // Pass in the top of the stack.
struct timeval begin;
struct timezone tz;
gettimeofday(&begin, &tz);
// This uses the glibc "clone()" wrapper function which takes a function pointer.
int result = clone(&CloneVForkFunctionSleepExit, stack_high, CLONE_VFORK, 0);
ASSERT_NE(result, -1);
struct timeval end;
gettimeofday(&end, &tz);
// The clone function should have been blocked for at least as long as the sleep was for.
uint64_t elapsed_us = ((int64_t)end.tv_sec - (int64_t)begin.tv_sec) * 1000000ll +
((int64_t)end.tv_usec - (int64_t)begin.tv_usec);
EXPECT_GT(elapsed_us, kCloneVforkSleepUS);
}
// Invokes `brk()` syscall directly. The syscall returns the requested `new_break` on success,
// or the old break on failure. Setting `new_break` to null always fails, so can be used to
// retrieve the current break.
// Some tests invoke this directly so as to bypass the `brk()` wrapper provided by some libc
// implementations, that has different behaviour, matching the POSIX specification.
uintptr_t brk_syscall(uintptr_t new_break) { return syscall(SYS_brk, new_break); }
TEST(Task, BrkReturnsCurrentBreakOnFailure) {
// Tests that the brk system call doesn't return an error, instead it returns
// the current value of the program break.
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
// This should always fail, returning the program_break
uintptr_t program_break = brk_syscall(UINTPTR_MAX);
// Try to reserve something beyond the program break, aligned to page size.
uintptr_t map_addr = (program_break & ~(page_size - 1)) + page_size;
uintptr_t res =
reinterpret_cast<uintptr_t>(mmap(reinterpret_cast<void*>(map_addr), page_size, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0));
ASSERT_EQ(res, map_addr);
uintptr_t new_break = brk_syscall(map_addr + page_size);
// brk should fail.
EXPECT_EQ(new_break, program_break);
_exit(testing::Test::HasFailure());
});
EXPECT_TRUE(helper.WaitForChildren());
}
TEST(Task, BrkShrinkAfterFork) {
// Tests that a program can shrink their break after forking.
const void* kSbrkError = reinterpret_cast<void*>(-1);
test_helper::ForkHelper helper;
constexpr int brk_increment = 0x4000;
ASSERT_NE(kSbrkError, sbrk(brk_increment));
void* old_brk = sbrk(0);
ASSERT_NE(kSbrkError, old_brk);
helper.RunInForkedProcess([&] {
ASSERT_EQ(old_brk, sbrk(0));
ASSERT_NE(kSbrkError, sbrk(-brk_increment));
});
// Make sure fork didn't change our current break.
ASSERT_EQ(old_brk, sbrk(0));
// Restore the old brk.
ASSERT_NE(kSbrkError, sbrk(-brk_increment));
}
// Returns true if the `len` bytes at `ptr` all have value `value`.
static bool mem_contains(volatile char* ptr, size_t len, char value) {
for (const auto* end = ptr + len; ptr != end; ++ptr) {
if (*ptr != value)
return false;
}
return true;
}
// Moves the program break to a page aligned address, and returns the old break address.
static uintptr_t set_brk_page_aligned() {
size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
uintptr_t original_break = brk_syscall(0);
uintptr_t aligned_break = (original_break + page_size) & ~(page_size - 1);
if (brk_syscall(aligned_break) != aligned_break) {
return 0;
}
return original_break;
}
// Verify that if the program break is reduced down to a page-aligned address, and then
// regrown, then all the pages, including the one pointed to by the page-aligned address,
// are zeroed.
TEST(Task, BrkIsZeroedAfterShrinkAndRegrow) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
const void* kSbrkError = reinterpret_cast<void*>(-1);
ASSERT_NE(set_brk_page_aligned(), 0u);
// Allocate space via `sbrk()`, and fill it with non-zero bytes.
constexpr intptr_t kBreakIncrement = 0x4142;
void* base_break = sbrk(kBreakIncrement);
ASSERT_NE(base_break, kSbrkError) << "sbrk failed: " << std::strerror(errno);
char* memory = static_cast<char*>(base_break);
ASSERT_TRUE(mem_contains(memory, static_cast<size_t>(kBreakIncrement), 0));
memset(memory, 'a', kBreakIncrement);
ASSERT_FALSE(mem_contains(memory, static_cast<size_t>(kBreakIncrement), 0));
// Shrink the `sbrk()` to free the pages, then re-allocate them.
ASSERT_NE(sbrk(-kBreakIncrement), kSbrkError) << "sbrk failed: " << std::strerror(errno);
ASSERT_NE(sbrk(kBreakIncrement), kSbrkError) << "sbrk failed: " << std::strerror(errno);
// The re-allocated range should now be zeroed.
EXPECT_TRUE(mem_contains(memory, static_cast<size_t>(kBreakIncrement), 0));
_exit(testing::Test::HasFailure());
});
}
// Verifies that overwriting the program break with private or shared mappings does not
// prevent the program break from being shrunk, and regrown.
TEST(Task, BrkShrinkUnmapsAnonMmap) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
ASSERT_NE(set_brk_page_aligned(), 0u);
size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
// Allocate eight pages from the program break.
uintptr_t program_break = brk_syscall(0);
uintptr_t new_break = brk_syscall(program_break + (8 * page_size));
ASSERT_EQ(new_break, program_break + (8 * page_size));
// Anonymously map over second and third pages, with RW and read-only private pages.
uintptr_t addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + page_size), page_size, PROT_WRITE | PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + page_size);
addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + (2 * page_size)), page_size, PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + (2 * page_size));
// Anonymously map over fifth and sixth pages, with RW and read-only shared pages.
addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + (4 * page_size)), page_size,
PROT_WRITE | PROT_READ, MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + (4 * page_size));
addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + (5 * page_size)), page_size, PROT_READ,
MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + (5 * page_size));
// Shrink the program break back down.
uintptr_t final_break = brk_syscall(program_break);
EXPECT_EQ(final_break, program_break);
// Validate that all eight pages are no longer available.
for (size_t i = 0; i < 8; i++) {
EXPECT_FALSE(test_helper::TryRead(program_break + (i * page_size)))
<< "page " << i + 1 << " is readable";
}
// Regrow and validate that all eight pages are then readable.
new_break = brk_syscall(program_break + (8 * page_size));
EXPECT_EQ(new_break, program_break + (8 * page_size));
for (size_t i = 0; i < 8; i++) {
EXPECT_TRUE(test_helper::TryRead(program_break + (i * page_size)))
<< "page " << i + 1 << " not readable";
}
_exit(testing::Test::HasFailure());
});
}
// Verifies that the program break can be extended after `mmap()` has been used to overwrite
// preceding parts of the program break area, and that doing so does not alter those
// mappings.
TEST(Task, BrkGrowsAfterAnonMmap) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
ASSERT_NE(set_brk_page_aligned(), 0u);
size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
// Allocate eight pages from the program break, and set them non-zero.
uintptr_t program_break = brk_syscall(0);
uintptr_t new_break = brk_syscall(program_break + (8 * page_size));
ASSERT_EQ(new_break, program_break + (8 * page_size));
char* break_memory = reinterpret_cast<char*>(program_break);
memset(break_memory, 'a', 8 * page_size);
ASSERT_TRUE(mem_contains(break_memory, 8 * page_size, 'a'));
// Anonymously map over second and third pages, with RW and read-only private pages.
uintptr_t addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + page_size), page_size, PROT_WRITE | PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + page_size);
addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + (2 * page_size)), page_size, PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + (2 * page_size));
// Anonymously map over fifth and sixth pages, with RW and read-only shared pages.
addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + (4 * page_size)), page_size,
PROT_WRITE | PROT_READ, MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + (4 * page_size));
addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + (5 * page_size)), page_size, PROT_READ,
MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + (5 * page_size));
// Fill the writable `mmap()`ed pages with identifiable content.
memset(break_memory + page_size, 'b', page_size);
memset(break_memory + 4 * page_size, 'c', page_size);
// Extend the program break by another eight pages.
new_break = brk_syscall(program_break + (16 * page_size));
ASSERT_EQ(new_break, program_break + (16 * page_size));
// Everything preceding the old break should be preserved.
EXPECT_TRUE(mem_contains(break_memory, page_size, 'a')); // first of the old break pages
EXPECT_TRUE(mem_contains(break_memory + page_size, page_size, 'b'));
EXPECT_TRUE(mem_contains(break_memory + 4 * page_size, page_size, 'c'));
EXPECT_TRUE(mem_contains(break_memory + 7 * page_size, page_size,
'a')); // last of the old break pages.
// The new break area will be zeroed.
EXPECT_TRUE(mem_contains(break_memory + 8 * page_size, 8 * page_size, 0));
_exit(testing::Test::HasFailure());
});
}
// Verify that the program break will not grow over existing private mappings.
TEST(Task, BrkWillNotGrowOverPrivateAnonMmap) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
ASSERT_NE(set_brk_page_aligned(), 0u);
size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
// Create anonymous mapped pages after the break.
uintptr_t program_break = brk_syscall(0);
// Anonymously map over second and third pages, with RW and read-only private pages.
uintptr_t addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + page_size), page_size, PROT_WRITE | PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + page_size);
addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + (2 * page_size)), page_size, PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + (2 * page_size));
// Attempt to grow the program break over the private mappings.
// This should fail, causing the old program break position to remain current.
uintptr_t new_break = brk_syscall(program_break + (8 * page_size));
EXPECT_EQ(new_break, program_break);
_exit(testing::Test::HasFailure());
});
}
// Verify that the program break will not grow over existing private mappings.
TEST(Task, BrkWillNotGrowOverSharedAnonMmap) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
ASSERT_NE(set_brk_page_aligned(), 0u);
size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
// Create shared mapped pages after the break.
uintptr_t program_break = brk_syscall(0);
// Anonymously map over second and fourth pages, with RW and read-only shared pages.
uintptr_t addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + (page_size)), page_size,
PROT_WRITE | PROT_READ, MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + page_size);
addr = reinterpret_cast<uintptr_t>(
mmap(reinterpret_cast<void*>(program_break + (3 * page_size)), page_size, PROT_READ,
MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED, -1, 0));
ASSERT_EQ(addr, program_break + (3 * page_size));
// Attempt to grow the program break over the shared mappings.
// This should fail, causing the old program break position to remain current.
uintptr_t new_break = brk_syscall(program_break + (8 * page_size));
EXPECT_EQ(new_break, program_break);
_exit(testing::Test::HasFailure());
});
}
// Verify that the `brk()` syscall continues to function after pages within the
// program break have been explicitly unmapped by the caller.
TEST(Task, BrkCanBeUnmapped) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
ASSERT_NE(set_brk_page_aligned(), 0u);
size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
// Allocate a page from the program break.
uintptr_t program_break = brk_syscall(0);
uintptr_t new_break = brk_syscall(program_break + page_size);
// Unmap the last page we just added.
ASSERT_TRUE(test_helper::TryRead(program_break));
SAFE_SYSCALL(munmap(reinterpret_cast<void*>(program_break), page_size));
EXPECT_FALSE(test_helper::TryRead(program_break));
// The program break hasn't changed.
uintptr_t break_after_unmap = brk_syscall(0);
EXPECT_EQ(break_after_unmap, new_break);
// The program break can still be extended.
uintptr_t final_break = brk_syscall(new_break + page_size);
EXPECT_EQ(final_break, new_break + page_size);
// The final page of the break is readable, and the unmapped page is still not.
EXPECT_TRUE(test_helper::TryRead(new_break));
EXPECT_FALSE(test_helper::TryRead(program_break));
// The break can be rewound over the unmapped region.
EXPECT_EQ(brk_syscall(program_break), program_break);
_exit(testing::Test::HasFailure());
});
EXPECT_TRUE(helper.WaitForChildren());
}
// Verify that the current program break address is treated as outside the break.
TEST(Task, BrkIsOutOfRange) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([] {
// Set the program break to be page-aligned.
ASSERT_NE(set_brk_page_aligned(), 0u);
// The break address should refer to the first byte of the page after the break,
// and therefore not be mapped.
uintptr_t aligned_break = brk_syscall(0);
EXPECT_FALSE(test_helper::TryRead(aligned_break));
// Increasing the break by one byte effectively "allocates" the byte at the
// aligned break address, causing that page to now be mapped.
uintptr_t increased_break = aligned_break + 1;
EXPECT_EQ(brk_syscall(increased_break), increased_break);
EXPECT_TRUE(test_helper::TryRead(aligned_break));
// Reducing the break by one byte "deallocates" the byte at the aligned
// address, causing the page to be unmapped.
EXPECT_EQ(brk_syscall(aligned_break), aligned_break);
EXPECT_FALSE(test_helper::TryRead(aligned_break));
_exit(testing::Test::HasFailure());
});
EXPECT_TRUE(helper.WaitForChildren());
}
TEST(Task, ChildCantModifyParent) {
ASSERT_GT(MAX_PAGE_ALIGNMENT, getpagesize());
test_helper::ForkHelper helper;
g_global_variable_data = 1;
g_global_variable_bss = 10;
volatile int local_variable = 100;
volatile int* heap_variable = new volatile int();
*heap_variable = 1000;
ASSERT_EQ(g_global_variable_data, 1u);
ASSERT_EQ(g_global_variable_bss, 10);
ASSERT_EQ(local_variable, 100);
ASSERT_EQ(*heap_variable, 1000);
helper.RunInForkedProcess([&] {
g_global_variable_data = 2;
g_global_variable_bss = 20;
local_variable = 200;
*heap_variable = 2000;
});
ASSERT_TRUE(helper.WaitForChildren());
EXPECT_EQ(g_global_variable_data, 1u);
EXPECT_EQ(g_global_variable_bss, 10);
EXPECT_EQ(local_variable, 100);
EXPECT_EQ(*heap_variable, 1000);
delete heap_variable;
}
TEST(Task, ForkDoesntDropWrites) {
// This tests creates a thread that keeps reading
// and writing to a pager-backed memory region (the data section of this
// binary).
//
// This is to test that during fork, writes to pager-backed vmos are not
// dropped if we have concurrent processes writing to memory while the kernel
// changes those mappings.
std::atomic<bool> stop = false;
std::atomic<bool> success = false;
std::vector<pid_t> pids;
std::thread writer([&stop, &success, &data = g_fork_doesnt_drop_writes]() {
data = 0;
size_t i = 0;
success = false;
while (!stop) {
i++;
data += 1;
if (data != i) {
return;
}
}
success = true;
});
for (size_t i = 0; i < 1000; i++) {
pid_t pid = fork();
if (pid == 0) {
_exit(0);
}
pids.push_back(pid);
}
stop = true;
writer.join();
EXPECT_TRUE(success.load());
for (auto pid : pids) {
int status;
EXPECT_EQ(waitpid(pid, &status, 0), pid);
EXPECT_TRUE(WIFEXITED(status));
EXPECT_EQ(WEXITSTATUS(status), 0);
}
}
TEST(Task, ParentCantModifyChild) {
ASSERT_GT(MAX_PAGE_ALIGNMENT, getpagesize());
test_helper::ForkHelper helper;
g_global_variable_data = 1;
g_global_variable_bss = 10;
volatile int local_variable = 100;
volatile int* heap_variable = new volatile int();
*heap_variable = 1000;
ASSERT_EQ(g_global_variable_data, 1u);
ASSERT_EQ(g_global_variable_bss, 10);
ASSERT_EQ(local_variable, 100);
ASSERT_EQ(*heap_variable, 1000);
test_helper::SignalMaskHelper signal_helper = test_helper::SignalMaskHelper();
signal_helper.blockSignal(SIGUSR1);
pid_t child_pid = helper.RunInForkedProcess([&] {
signal_helper.waitForSignal(SIGUSR1);
EXPECT_EQ(g_global_variable_data, 1u);
EXPECT_EQ(g_global_variable_bss, 10);
EXPECT_EQ(local_variable, 100);
EXPECT_EQ(*heap_variable, 1000);
});
g_global_variable_data = 2;
g_global_variable_bss = 20;
local_variable = 200;
*heap_variable = 2000;
ASSERT_EQ(kill(child_pid, SIGUSR1), 0);
ASSERT_TRUE(helper.WaitForChildren());
signal_helper.restoreSigmask();
delete heap_variable;
}
constexpr size_t kVecSize = 100;
constexpr size_t kPageLimit = 32;
TEST(Task, ExecveArgumentExceedsMaxArgStrlen) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([] {
size_t arg_size = kPageLimit * sysconf(_SC_PAGESIZE);
std::vector<char> arg(arg_size + 1, 'a');
arg[arg_size] = '\0';
char* argv[] = {arg.data(), nullptr};
char* envp[] = {nullptr};
EXPECT_NE(execve("/proc/self/exe", argv, envp), 0);
EXPECT_EQ(errno, E2BIG);
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(Task, ExecveArgvExceedsLimit) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([] {
size_t arg_size = kPageLimit * sysconf(_SC_PAGESIZE);
std::vector<char> arg(arg_size, 'a');
arg[arg_size - 1] = '\0';
char* argv[kVecSize];
std::fill_n(argv, kVecSize - 1, arg.data());
argv[kVecSize - 1] = nullptr;
char* envp[] = {nullptr};
EXPECT_NE(execve("/proc/self/exe", argv, envp), 0);
EXPECT_EQ(errno, E2BIG);
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(Task, ExecveArgvEnvExceedLimit) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([] {
size_t arg_size = kPageLimit * sysconf(_SC_PAGESIZE);
std::vector<char> string(arg_size, 'a');
string[arg_size - 1] = '\0';
char* argv[] = {string.data(), nullptr};
char* envp[kVecSize];
std::fill_n(envp, kVecSize - 1, string.data());
envp[kVecSize - 1] = nullptr;
EXPECT_NE(execve("/proc/self/exe", argv, envp), 0);
EXPECT_EQ(errno, E2BIG);
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(Task, ExecvePathnameTooLong) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([] {
constexpr size_t path_size = PATH_MAX + 1;
// We use '/' here because ////// (...) is a valid path:
// More than two leading / should be considered as one,
// So this path resolves to "/".
//
// Each path component is limited to NAME_MAX, so if we
// use a different character we would need to add a delimiter
// every NAME_MAX characters.
std::vector<char> pathname(path_size, '/');
pathname[path_size - 1] = '\0';
char* argv[] = {nullptr};
char* envp[] = {nullptr};
EXPECT_NE(execve(pathname.data(), argv, envp), 0);
EXPECT_EQ(errno, ENAMETOOLONG);
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(Task, ForkPreservesStackStart) {
pid_t parent_pid = getpid();
test_helper::ForkHelper helper;
helper.RunInForkedProcess([parent_pid] {
uintptr_t parent_stack_start, child_stack_start;
ASSERT_NO_FATAL_FAILURE(ReadStackStart(parent_pid, &parent_stack_start));
ASSERT_NO_FATAL_FAILURE(ReadStackStart(getpid(), &child_stack_start));
EXPECT_EQ(parent_stack_start, child_stack_start);
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(Task, ForkPreservesStackSize) {
pid_t parent_pid = getpid();
test_helper::ForkHelper helper;
helper.RunInForkedProcess([parent_pid] {
uintptr_t parent_stack_size, child_stack_size;
ASSERT_NO_FATAL_FAILURE(ReadStackSize(parent_pid, &parent_stack_size));
ASSERT_NO_FATAL_FAILURE(ReadStackSize(getpid(), &child_stack_size));
EXPECT_EQ(parent_stack_size, child_stack_size);
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(Task, ForkPreservesAux) {
pid_t parent_pid = getpid();
test_helper::ForkHelper helper;
helper.RunInForkedProcess([parent_pid] {
std::vector<uint8_t> parent_auxv, child_auxv;
ASSERT_NO_FATAL_FAILURE(ReadProcPidFile(parent_pid, "auxv", &parent_auxv));
ASSERT_NO_FATAL_FAILURE(ReadProcPidFile(getpid(), "auxv", &child_auxv));
EXPECT_EQ(parent_auxv, child_auxv);
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(Task, ForkPreservesCmdline) {
pid_t parent_pid = getpid();
test_helper::ForkHelper helper;
helper.RunInForkedProcess([parent_pid] {
std::vector<uint8_t> parent_cmdline, child_cmdline;
ASSERT_NO_FATAL_FAILURE(ReadProcPidFile(parent_pid, "cmdline", &parent_cmdline));
ASSERT_NO_FATAL_FAILURE(ReadProcPidFile(getpid(), "cmdline", &child_cmdline));
EXPECT_EQ(parent_cmdline, child_cmdline);
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(Task, ForkPreservesEnviron) {
pid_t parent_pid = getpid();
test_helper::ForkHelper helper;
helper.RunInForkedProcess([parent_pid] {
std::vector<uint8_t> parent_environ, child_environ;
ASSERT_NO_FATAL_FAILURE(ReadProcPidFile(parent_pid, "environ", &parent_environ));
ASSERT_NO_FATAL_FAILURE(ReadProcPidFile(getpid(), "environ", &child_environ));
EXPECT_EQ(parent_environ, child_environ);
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(Task, KillESRCH) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([]() {
pid_t pid = getpid();
// A pid after fork is guaranteed to not collide with process group ids.
EXPECT_THAT(kill(-pid, SIGCHLD), SyscallFailsWithErrno(ESRCH));
});
}
TEST(Task, KillChildEPERM) {
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "test requires root privileges";
}
test_helper::ForkHelper helper;
helper.RunInForkedProcess([]() {
test_helper::EventFdSem fd(0);
pid_t child_pid = SAFE_SYSCALL(fork());
if (child_pid == 0) {
fd.Wait();
_exit(EXIT_SUCCESS);
}
SAFE_SYSCALL(change_ids(kUser1Uid, kUser1Gid));
EXPECT_THAT(kill(child_pid, SIGCHLD), SyscallFailsWithErrno(EPERM));
fd.Notify(1);
SAFE_SYSCALL(waitpid(child_pid, nullptr, 0));
});
}
TEST(Task, KillZombie) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([]() {
pid_t child_pid = SAFE_SYSCALL(fork());
if (child_pid == 0) {
_exit(EXIT_SUCCESS);
}
int pidfd = test_helper::PidFdOpen(child_pid, 0);
WaitForReadFd(pidfd);
close(pidfd);
EXPECT_THAT(kill(child_pid, SIGCHLD), SyscallSucceeds());
SAFE_SYSCALL(waitpid(child_pid, nullptr, 0));
});
}
TEST(Task, KillZombieEPERM) {
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "test requires root privileges";
}
test_helper::ForkHelper helper;
helper.RunInForkedProcess([]() {
pid_t child_pid = SAFE_SYSCALL(fork());
if (child_pid == 0) {
_exit(EXIT_SUCCESS);
}
int pidfd = test_helper::PidFdOpen(child_pid, 0);
WaitForReadFd(pidfd);
close(pidfd);
SAFE_SYSCALL(change_ids(kUser1Uid, kUser1Gid));
EXPECT_THAT(kill(child_pid, SIGCHLD), SyscallFailsWithErrno(EPERM));
SAFE_SYSCALL(waitpid(child_pid, nullptr, 0));
});
}
TEST(Task, KillParentEPERM) {
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "test requires root privileges";
}
test_helper::ForkHelper helper;
helper.RunInForkedProcess([]() {
test_helper::EventFdSem fd(0);
pid_t child_pid = SAFE_SYSCALL(fork());
if (child_pid == 0) {
pid_t parent_pid = getppid();
SAFE_SYSCALL(change_ids(kUser1Uid, kUser1Gid));
EXPECT_THAT(kill(parent_pid, SIGCHLD), SyscallFailsWithErrno(EPERM));
SAFE_SYSCALL(fd.Notify(1));
_exit(EXIT_SUCCESS);
}
fd.Wait();
SAFE_SYSCALL(waitpid(child_pid, nullptr, 0));
});
}
TEST(Task, CantReadLowAddresses) {
if (!test_helper::IsStarnix()) {
GTEST_SKIP();
}
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
constexpr uintptr_t kLowMemoryLimit = 16 * 1024 * 1024;
for (uintptr_t addr = 0x0; addr < kLowMemoryLimit; addr += page_size) {
EXPECT_FALSE(test_helper::TryRead(addr));
}
}
TEST(Task, CantWriteLowAddresses) {
if (!test_helper::IsStarnix()) {
GTEST_SKIP();
}
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
constexpr uintptr_t kLowMemoryLimit = 16 * 1024 * 1024;
for (uintptr_t addr = 0x0; addr < kLowMemoryLimit; addr += page_size) {
EXPECT_FALSE(test_helper::TryWrite(addr));
}
}
class CloneAndExecTest : public ::testing::Test {
protected:
void SetUp() { clone_exec_helper_ = GetTestResourcePath(kCloneExecHelperBinary); }
void CloneAndExec(int clone_flags, const std::vector<std::string>& arguments) {
// Clone the process, with the caller-supplied flags.
int child_pid = static_cast<int>(SAFE_SYSCALL(
syscall(SYS_clone, clone_flags | SIGCHLD, nullptr, nullptr, nullptr, nullptr)));
if (child_pid == 0) {
// This is the child process, so exec() the specified command.
std::vector<char*> args;
args.push_back(clone_exec_helper_.data());
for (const std::string& argument : arguments) {
args.push_back(const_cast<char*>(argument.data()));
}
args.push_back(nullptr);
char* const envp[] = {nullptr};
SAFE_SYSCALL(execve(clone_exec_helper_.c_str(), args.data(), envp));
_exit(EXIT_FAILURE);
}
// This is the parent process. Wait for the child process to complete, then return to the caller
// to perform test validation.
int wstatus = 0;
SAFE_SYSCALL(waitpid(child_pid, &wstatus, 0));
if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) {
ADD_FAILURE() << "wait_status: WIFEXITED(wstatus) = " << WIFEXITED(wstatus)
<< ", WEXITSTATUS(wstatus) = " << WEXITSTATUS(wstatus)
<< ", WTERMSIG(wstatus) = " << WTERMSIG(wstatus);
}
}
static std::string GetCurrentWorkingDirectory() {
std::string cwd = get_current_dir_name();
if (cwd.empty()) {
_exit(EXIT_FAILURE);
}
return cwd;
}
static std::string GetTestResourcePath(const std::string& resource) {
std::filesystem::path test_file = std::filesystem::path("data/tests/deps") / resource;
std::error_code ec;
bool file_exists = std::filesystem::exists(test_file, ec);
EXPECT_FALSE(ec) << "failed to check if file exists: " << ec;
if (!file_exists) {
char self_path[PATH_MAX];
realpath("/proc/self/exe", self_path);
std::filesystem::path directory = std::filesystem::path(self_path).parent_path();
return directory / resource;
}
return test_file;
}
static constexpr char kCloneExecHelperBinary[] = "clone_exec_helper";
std::string clone_exec_helper_;
};
TEST_F(CloneAndExecTest, ExecUnsharesCloneFiles) {
// Fork into a subprocess from which to `clone(CLONE_FILES)`, to avoid breaking the main test
// process if things go awry.
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
// Open a temporary file to use as the "canary" for the FD table remaining shared.
auto fd = test_helper::ScopedTempFD();
ASSERT_THAT(fcntl(fd.fd(), F_GETFD), SyscallSucceeds());
// The child process will run the helper with a command to close `fd`.
std::vector<std::string> args{"close_fd", std::to_string(fd.fd())};
CloneAndExec(CLONE_FILES, args);
// Verify that the FD is still open in our FD table.
EXPECT_THAT(fcntl(fd.fd(), F_GETFD), SyscallSucceeds());
_exit(testing::Test::HasFailure());
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST_F(CloneAndExecTest, ExecDoesNotUnshareCloneFs) {
// Fork into a subprocess from which to `clone(CLONE_FS)`, to avoid breaking the main test
// process if things go awry.
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
// The child process will run the helper with a command-line to modify the current working
// directory.
std::string original_cwd = GetCurrentWorkingDirectory();
// Create a temporary directory to use as the new CWD.
test_helper::ScopedTempDir new_cwd;
std::vector<std::string> args{"set_cwd", new_cwd.path()};
CloneAndExec(CLONE_FS, args);
// Verify that this process' current working directory has changed.
EXPECT_EQ(new_cwd.path(), GetCurrentWorkingDirectory());
_exit(testing::Test::HasFailure());
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST_F(CloneAndExecTest, ChildWithCloneFsCanChangeCwd) {
// Fork into a subprocess from which to `clone(CLONE_FS)`, to avoid breaking the main test
// process.
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
// The child process will change its current working directory from the original value.
std::string original_cwd = GetCurrentWorkingDirectory();
// Create a temporary directory to use as the new CWD.
test_helper::ScopedTempDir new_cwd;
// Fork the child with CLONE_FS.
int child_pid = static_cast<int>(
SAFE_SYSCALL(syscall(SYS_clone, CLONE_FS | SIGCHLD, nullptr, nullptr, nullptr, nullptr)));
if (child_pid == 0) {
// This is the child process, so change the current working directory.
if (chdir(new_cwd.path().c_str()) != 0) {
fprintf(stderr, "Failed to chdir to %s: %d (%s)\n", new_cwd.path().c_str(), errno,
strerror(errno));
exit(EXIT_FAILURE);
}
_exit(0);
}
// This is the parent process. Wait for the child process to complete, then check whether the
// current working directory has changed.
int wstatus = 0;
SAFE_SYSCALL(waitpid(child_pid, &wstatus, 0));
if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) {
ADD_FAILURE() << "wait_status: WIFEXITED(wstatus) = " << WIFEXITED(wstatus)
<< ", WEXITSTATUS(wstatus) = " << WEXITSTATUS(wstatus)
<< ", WTERMSIG(wstatus) = " << WTERMSIG(wstatus);
}
// Verify that this process' current working directory has changed.
EXPECT_NE(original_cwd, GetCurrentWorkingDirectory());
_exit(testing::Test::HasFailure());
});
ASSERT_TRUE(helper.WaitForChildren());
}
class DumpableTest : public ::testing::Test {
protected:
void SetUp() {
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "test requires root privileges";
}
FILE* fp = fopen("/proc/sys/fs/suid_dumpable", "r");
EXPECT_NE(fp, nullptr);
// TODO(https://fxbug.dev/322874210): EXPECT that the read succeeds, do not
// hard-code a value.
if (fscanf(fp, "%d", &dumpable_default_) != 1) {
dumpable_default_ = 0;
}
fclose(fp);
}
int dumpable_default_ = -1;
};
TEST_F(DumpableTest, DumpablePersistsAcrossFork) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
SAFE_SYSCALL(prctl(PR_SET_DUMPABLE, 1));
pid_t child_pid = SAFE_SYSCALL(fork());
if (child_pid == 0) {
EXPECT_EQ(prctl(PR_GET_DUMPABLE), 1);
exit(testing::Test::HasFailure());
}
int status;
SAFE_SYSCALL(waitpid(child_pid, &status, 0));
EXPECT_TRUE(WIFEXITED(status));
EXPECT_EQ(WEXITSTATUS(status), 0);
});
EXPECT_TRUE(helper.WaitForChildren());
}
TEST_F(DumpableTest, DumpableDropsOnUserChange) {
for (size_t combination = 0b00000000; combination <= 0b11111111; combination++) {
uid_t ruid, euid, suid, fuid;
gid_t rgid, egid, sgid, fgid;
ruid = (combination & 0b00000001) ? kUser1Uid : 0;
euid = (combination & 0b00000010) ? kUser1Uid : 0;
suid = (combination & 0b00000100) ? kUser1Uid : 0;
fuid = (combination & 0b00001000) ? kUser1Uid : 0;
rgid = (combination & 0b00010000) ? kUser1Gid : 0;
egid = (combination & 0b00100000) ? kUser1Gid : 0;
sgid = (combination & 0b01000000) ? kUser1Gid : 0;
fgid = (combination & 0b10000000) ? kUser1Gid : 0;
bool expected_change =
(euid == kUser1Uid || egid == kUser1Gid || fuid == kUser1Uid || fgid == kUser1Gid);
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
SCOPED_TRACE(fxl::StringPrintf("resfsuid: (%d, %d, %d, %d) resfsgid: (%d, %d, %d, %d)", ruid,
euid, suid, fuid, rgid, egid, sgid, fgid));
SAFE_SYSCALL(prctl(PR_SET_DUMPABLE, 1));
SAFE_SYSCALL(setresgid(rgid, egid, sgid));
SAFE_SYSCALL(setfsgid(fgid));
SAFE_SYSCALL(setfsuid(fuid));
SAFE_SYSCALL(setresuid(ruid, euid, suid));
int dumpable = prctl(PR_GET_DUMPABLE);
if (expected_change) {
EXPECT_EQ(dumpable, dumpable_default_);
} else {
EXPECT_EQ(dumpable, 1);
}
});
EXPECT_TRUE(helper.WaitForChildren());
}
}