blob: dbf1a9c103adb8f49a5f7afcdd8537587d256a42 [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 <elf.h>
#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/uio.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <syscall.h>
#include <time.h>
#include <unistd.h>
#include <atomic>
#include <latch>
#include <thread>
#include <linux/prctl.h>
#include <linux/sched.h>
#include "src/lib/fxl/strings/string_printf.h"
#if defined(__riscv)
#include <asm/ptrace.h>
#endif // __riscv
#include <gtest/gtest.h>
#include "src/lib/files/file.h"
#include "src/lib/files/path.h"
#include "src/starnix/tests/syscalls/cpp/syscall_matchers.h"
#include "src/starnix/tests/syscalls/cpp/test_helper.h"
constexpr int kOriginalSigno = SIGUSR1;
constexpr int kInjectedSigno = SIGUSR2;
constexpr int kInjectedErrno = EIO;
// user_regs_struct is not defined on __arm__
#if defined(__arm__)
struct user_regs_struct {
unsigned long regs[18];
};
#endif // defined(__arm__)
namespace {
TEST(PtraceTest, SetSigInfo) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
pid_t child_pid = helper.RunInForkedProcess([] {
struct sigaction sa = {};
sa.sa_sigaction = +[](int sig, siginfo_t *info, void *ucontext) {
if (sig != kInjectedSigno) {
_exit(1);
}
if (info->si_errno != kInjectedErrno) {
_exit(2);
}
_exit(0);
};
sa.sa_flags = SA_SIGINFO | SA_RESTART;
ASSERT_EQ(sigemptyset(&sa.sa_mask), 0);
sigaction(kInjectedSigno, &sa, nullptr);
sigaction(kOriginalSigno, &sa, nullptr);
ASSERT_EQ(ptrace(PTRACE_TRACEME, 0, 0, 0), 0);
raise(kOriginalSigno);
_exit(3);
});
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == kOriginalSigno) << " status " << status;
siginfo_t siginfo = {};
ASSERT_EQ(ptrace(PTRACE_GETSIGINFO, child_pid, 0, &siginfo), 0)
<< "ptrace failed with error " << strerror(errno);
ASSERT_EQ(kOriginalSigno, siginfo.si_signo);
ASSERT_EQ(SI_TKILL, siginfo.si_code);
// Replace the signal with kInjectedSigno, and check that the child exits
// with kInjectedSigno, indicating that signal injection was successful.
siginfo.si_signo = kInjectedSigno;
siginfo.si_errno = kInjectedErrno;
ASSERT_EQ(ptrace(PTRACE_SETSIGINFO, child_pid, 0, &siginfo), 0);
ASSERT_EQ(ptrace(PTRACE_DETACH, child_pid, 0, kInjectedSigno), 0);
}
#ifndef PTRACE_EVENT_STOP // Not defined in every libc
#define PTRACE_EVENT_STOP 128
#endif
TEST(PtraceTest, InterruptAfterListen) {
volatile int child_should_spin = 1;
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
pid_t child_pid = helper.RunInForkedProcess([&child_should_spin] {
const struct timespec req = {.tv_sec = 0, .tv_nsec = 1000};
while (child_should_spin) {
nanosleep(&req, nullptr);
}
_exit(0);
});
// In parent process.
ASSERT_NE(child_pid, 0);
ASSERT_EQ(ptrace(PTRACE_SEIZE, child_pid, 0, 0), 0);
int status;
ASSERT_EQ(waitpid(child_pid, &status, WNOHANG), 0);
// Stop the child with PTRACE_INTERRUPT.
ASSERT_EQ(ptrace(PTRACE_INTERRUPT, child_pid, 0, 0), 0);
ASSERT_EQ(waitpid(child_pid, &status, 0), child_pid);
ASSERT_EQ(SIGTRAP | (PTRACE_EVENT_STOP << 8), status >> 8);
ASSERT_EQ(ptrace(PTRACE_POKEDATA, child_pid, &child_should_spin, 0), 0) << strerror(errno);
// Send SIGSTOP to the child, then resume it, allowing it to proceed to
// signal-delivery-stop.
ASSERT_EQ(kill(child_pid, SIGSTOP), 0);
ASSERT_EQ(ptrace(PTRACE_CONT, child_pid, 0, 0), 0);
ASSERT_EQ(waitpid(child_pid, &status, 0), child_pid);
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << " status " << status;
// Move out of signal-delivery-stop and deliver the SIGSTOP.
ASSERT_EQ(ptrace(PTRACE_CONT, child_pid, 0, SIGSTOP), 0);
ASSERT_EQ(waitpid(child_pid, &status, 0), child_pid);
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
ASSERT_EQ(SIGSTOP | (PTRACE_EVENT_STOP << 8), status >> 8);
// Restart the child, but don't let it execute. Child continues to deliver
// notifications of when it gets stop / continue signals. This allows a
// normal SIGCONT signal to be sent to a child to restart it, rather than
// having the tracer restart it. The tracer can then detect the SIGCONT.
ASSERT_EQ(ptrace(PTRACE_LISTEN, child_pid, 0, 0), 0);
// "If the tracee was already stopped by a signal and PTRACE_LISTEN was sent
// to it, the tracee stops with PTRACE_EVENT_STOP and WSTOPSIG(status) returns
// the stop signal."
ASSERT_EQ(ptrace(PTRACE_INTERRUPT, child_pid, 0, 0), 0);
ASSERT_EQ(waitpid(child_pid, &status, 0), child_pid);
ASSERT_EQ(SIGSTOP | (PTRACE_EVENT_STOP << 8), status >> 8);
// Allow the tracer to proceed normally.
ASSERT_EQ(ptrace(PTRACE_CONT, child_pid, 0, 0), 0) << strerror(errno);
}
// None of this seems to be defined in our x64 and ARM sysroots.
#ifndef PTRACE_GET_SYSCALL_INFO
#define PTRACE_GET_SYSCALL_INFO 0x420e
#define PTRACE_SYSCALL_INFO_NONE 0
#define PTRACE_SYSCALL_INFO_ENTRY 1
#define PTRACE_SYSCALL_INFO_EXIT 2
#define PTRACE_SYSCALL_INFO_SECCOMP 3
struct ptrace_syscall_info {
uint8_t op;
uint8_t pad[3];
uint32_t arch;
uint64_t instruction_pointer;
uint64_t stack_pointer;
union {
struct {
uint64_t nr;
uint64_t args[6];
} entry;
struct {
int64_t rval;
uint8_t is_error;
} exit;
struct {
uint64_t nr;
uint64_t args[6];
uint32_t ret_data;
} seccomp;
};
};
#else
// In our RISC-V sysroot, this is called __ptrace_syscall_info
using ptrace_syscall_info = __ptrace_syscall_info;
#endif
TEST(PtraceTest, TraceSyscall) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
pid_t child_pid = helper.RunInForkedProcess([] {
ASSERT_EQ(ptrace(PTRACE_TRACEME, 0, 0, 0), 0);
raise(SIGSTOP);
struct timespec req = {.tv_sec = 0, .tv_nsec = 0};
nanosleep(&req, nullptr);
});
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << " status " << status;
ASSERT_EQ(0, ptrace(PTRACE_SETOPTIONS, child_pid, 0, PTRACE_O_TRACESYSGOOD))
<< "error " << strerror(errno);
ptrace_syscall_info info;
const int kExpectedNoneSize =
reinterpret_cast<uint8_t *>(&info.entry) - reinterpret_cast<uint8_t *>(&info);
const int kExpectedEntrySize =
reinterpret_cast<uint8_t *>(&info.entry.args[6]) - reinterpret_cast<uint8_t *>(&info);
const int kExpectedExitSize =
reinterpret_cast<uint8_t *>(&info.exit.is_error + 1) - reinterpret_cast<uint8_t *>(&info);
// We are not at a syscall entry
ASSERT_EQ(ptrace(static_cast<enum __ptrace_request>(PTRACE_GET_SYSCALL_INFO), child_pid,
sizeof(ptrace_syscall_info), &info),
kExpectedNoneSize);
ASSERT_EQ(info.op, PTRACE_SYSCALL_INFO_NONE);
bool found = false;
// We want to make sure we hit the "nanosleep" syscall. There can be various
// "hidden" syscalls in the tracee, depending on the implementation of "raise"
// and "nanosleep". So, we just keep trying until we hit nanosleep or exit.
for (int i = 0; i < 10; i++) {
ASSERT_EQ(ptrace(PTRACE_SYSCALL, child_pid, 0, 0), 0);
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
if (!WIFSTOPPED(status) || WSTOPSIG(status) != (SIGTRAP | 0x80)) {
break;
}
// We are now at a syscall entry
ASSERT_EQ(ptrace(static_cast<enum __ptrace_request>(PTRACE_GET_SYSCALL_INFO), child_pid,
sizeof(ptrace_syscall_info), &info),
kExpectedEntrySize);
ASSERT_EQ(info.op, PTRACE_SYSCALL_INFO_ENTRY);
switch (info.entry.nr) {
case __NR_clock_nanosleep:
case __NR_nanosleep:
found = true;
break;
case __NR_exit:
case __NR_exit_group:
goto exit_loop;
}
ASSERT_EQ(ptrace(PTRACE_SYSCALL, child_pid, 0, 0), 0);
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80))
<< "WIFSTOPPED(status) " << WIFSTOPPED(status) << " WSTOPSIG(status) " << WSTOPSIG(status);
// We are now at a syscall exit
ASSERT_EQ(ptrace(static_cast<enum __ptrace_request>(PTRACE_GET_SYSCALL_INFO), child_pid,
sizeof(ptrace_syscall_info), &info),
kExpectedExitSize);
ASSERT_EQ(info.op, PTRACE_SYSCALL_INFO_EXIT);
ASSERT_EQ(info.exit.rval, 0);
ASSERT_EQ(info.exit.is_error, 0);
}
exit_loop:
ASSERT_EQ(found, true) << "Never found nanosleep call";
ASSERT_EQ(ptrace(PTRACE_CONT, child_pid, 0, 0), 0);
}
#ifdef __x86_64__
static constexpr int kUnmaskedSignal = SIGUSR1;
// Linux has internal errnos that capture the circumstances when an interrupted
// syscall should restart rather than return. These are ordinarily invisible to
// the user - the syscall is either restarted, or the internal errno is replaced
// by EINTR. However, ptrace can detect them on ptrace-syscall-exit.
void TraceSyscallWithRestartWithCall(int call, long arg0, long arg1, long arg2, long arg3,
int expected_errno) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
helper.ExpectSignal(SIGKILL);
pid_t child_pid = helper.RunInForkedProcess([call, arg0, arg1, arg2, arg3] {
struct sigaction sa = {};
sa.sa_handler = [](int signo) {};
ASSERT_EQ(sigfillset(&sa.sa_mask), 0);
ASSERT_EQ(sigaction(kUnmaskedSignal, &sa, nullptr), 0);
ASSERT_EQ(sigprocmask(SIG_UNBLOCK, &sa.sa_mask, nullptr), 0);
ASSERT_EQ(ptrace(PTRACE_TRACEME, 0, 0, 0), 0);
raise(SIGSTOP);
// When the following syscalls are interrupted, errno should be some weird
// internal errno (expected_errno above). This means that the syscall will
// return -1 if it is interrupted by a signal that has a user handler.
ASSERT_EQ(-1, syscall(call, arg0, arg1, arg2, arg3));
ASSERT_EQ(EINTR, errno) << strerror(errno);
});
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
struct user_regs_struct regs = {};
int count = 0;
do {
// Suppress the SIGSTOP and wait for the child to enter syscall-enter-stop
// for the given syscall. Repeat this in case we're using a libc where
// raise() makes a syscall after sending the signal.
ASSERT_EQ(ptrace(PTRACE_SYSCALL, child_pid, 0, 0), 0);
ASSERT_EQ(waitpid(child_pid, &status, 0), child_pid);
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) << " status " << status;
ASSERT_EQ(ptrace(PTRACE_GETREGS, child_pid, 0, &regs), 0);
count += 1;
} while (static_cast<int>(regs.orig_rax) != call && count < 100);
ASSERT_EQ(call, static_cast<int>(regs.orig_rax));
ASSERT_EQ(-ENOSYS, static_cast<int>(regs.rax));
// Resume the child with PTRACE_SYSCALL and expect it to block in the syscall.
ASSERT_EQ(ptrace(PTRACE_SYSCALL, child_pid, 0, 0), 0);
test_helper::WaitUntilBlocked(child_pid, true);
ASSERT_EQ(waitpid(child_pid, &status, WNOHANG), 0);
// Send the child kUnmaskedSignal, causing it to return the given errno and enter
// syscall-exit-stop from the syscall.
ASSERT_EQ(kill(child_pid, kUnmaskedSignal), 0);
ASSERT_EQ(waitpid(child_pid, &status, 0), child_pid);
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) << " status " << status;
ASSERT_EQ(ptrace(PTRACE_GETREGS, child_pid, 0, &regs), 0);
ASSERT_EQ(call, static_cast<int>(regs.orig_rax));
ASSERT_EQ(-expected_errno, static_cast<int>(regs.rax));
kill(child_pid, SIGKILL);
ptrace(PTRACE_DETACH, child_pid, 0, 0);
}
static constexpr int ERESTARTNOHAND = 514;
static constexpr int ERESTART_RESTARTBLOCK = 516;
TEST(PtraceTest, TraceSyscallWithRestart_pause) {
ASSERT_NO_FATAL_FAILURE(TraceSyscallWithRestartWithCall(SYS_pause, 0, 0, 0, 0, ERESTARTNOHAND));
}
TEST(PtraceTest, TraceSyscallWithRestart_nanosleep) {
const struct timespec req = {.tv_sec = 10, .tv_nsec = 0};
ASSERT_NO_FATAL_FAILURE(TraceSyscallWithRestartWithCall(
SYS_nanosleep, reinterpret_cast<long>(&req), 0, 0, 0, ERESTART_RESTARTBLOCK));
}
TEST(PtraceTest, TraceSyscallWithRestart_rt_sigsuspend) {
sigset_t sigset;
ASSERT_EQ(0, sigfillset(&sigset));
ASSERT_EQ(0, sigdelset(&sigset, kUnmaskedSignal));
ASSERT_NO_FATAL_FAILURE(
TraceSyscallWithRestartWithCall(SYS_rt_sigsuspend, reinterpret_cast<long>(&sigset),
sizeof(unsigned long), 0, 0, ERESTARTNOHAND));
}
TEST(PtraceTest, TraceSyscallWithRestart_ppoll) {
struct timespec req = {.tv_sec = 10, .tv_nsec = 0};
ASSERT_NO_FATAL_FAILURE(TraceSyscallWithRestartWithCall(
SYS_ppoll, 0, 0, reinterpret_cast<long>(&req), 0, ERESTARTNOHAND));
}
TEST(PtraceTest, PokeUser) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
constexpr long kStartPattern = 0xabababab;
constexpr long kEndPattern = 0xcdcdcdcd;
pid_t child_pid = helper.RunInForkedProcess([kEndPattern] {
ASSERT_EQ(ptrace(PTRACE_TRACEME, 0, 0, 0), 0);
long output;
asm volatile("movq %0, %%rdi"
: // No output
: "r"(kStartPattern));
// Use kill explicitly because we check the syscall argument register below.
kill(getpid(), SIGSTOP);
asm volatile("movq %%rdi, %0" : "=r"(output));
ASSERT_EQ(output, kEndPattern);
});
ASSERT_NE(child_pid, 0);
// Wait for the child to send itself SIGSTOP and enter signal-delivery-stop.
int status;
ASSERT_EQ(waitpid(child_pid, &status, 0), child_pid);
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << " status " << status;
ASSERT_EQ(0,
ptrace(PTRACE_POKEUSER, child_pid, offsetof(struct user_regs_struct, rdi), kEndPattern))
<< strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_DETACH, child_pid, 0, SIGCONT));
}
#endif // __x86_64__
TEST(PtraceTest, GetGeneralRegs) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
pid_t child_pid = helper.RunInForkedProcess([] {
ASSERT_EQ(ptrace(PTRACE_TRACEME, 0, 0, 0), 0);
// Use kill explicitly because we check the syscall argument register below.
kill(getpid(), SIGSTOP);
_exit(0);
});
ASSERT_NE(child_pid, 0);
// Wait for the child to send itself SIGSTOP and enter signal-delivery-stop.
int status;
ASSERT_EQ(waitpid(child_pid, &status, 0), child_pid);
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << " status " << status;
#if defined(__x86_64__)
#define __REG rsi
#elif defined(__aarch64__) || defined(__arm__)
#define __REG regs[1]
#elif defined(__riscv)
#define __REG a1
#else
#error "Test does not support architecture for PTRACE_GETREGS";
#endif
// Get the general registers with PTRACE_GETREGSET. Make this too large so
// that ptrace can provide the correct value.
struct user_regs_struct regs_set[2];
struct iovec iov;
iov.iov_base = regs_set;
// Expect error on incorrect size.
iov.iov_len = sizeof(regs_set[0]) - 1;
ASSERT_EQ(ptrace(PTRACE_GETREGSET, child_pid, NT_PRSTATUS, &iov), -1)
<< "Error " << errno << " " << strerror(errno);
ASSERT_EQ(errno, EINVAL);
// Provide a too large value for iov_len to make sure that ptrace resets it
// correctly
iov.iov_len = sizeof(regs_set);
ASSERT_EQ(ptrace(PTRACE_GETREGSET, child_pid, NT_PRSTATUS, &iov), 0)
<< "Error " << errno << " " << strerror(errno);
// Make sure ptrace set the correct size for the user_regs_struct.
ASSERT_EQ(iov.iov_len, sizeof(struct user_regs_struct));
// Child called kill(2), with SIGSTOP as arg 2.
ASSERT_EQ(regs_set[0].__REG, static_cast<unsigned long>(SIGSTOP));
// The appropriate defines for this are not in the ptrace header for arm64.
#ifdef __x86_64__
// Get the general registers, with PTRACE_GETREGS
struct user_regs_struct regs_old;
ASSERT_EQ(ptrace(PTRACE_GETREGS, child_pid, nullptr, &regs_old), 0)
<< "Error " << errno << " " << strerror(errno);
ASSERT_EQ(regs_old.__REG, static_cast<unsigned long>(SIGSTOP));
#endif
// Get the appropriate general register with PTRACE_PEEKUSER
ASSERT_EQ(ptrace(PTRACE_PEEKUSER, child_pid, offsetof(struct user_regs_struct, __REG), nullptr),
SIGSTOP)
<< "Error " << errno << " " << strerror(errno);
// Suppress SIGSTOP and resume the child.
ASSERT_EQ(ptrace(PTRACE_DETACH, child_pid, 0, 0), 0);
}
namespace {
// As of this writing, our sysroot's syscall.h lacks the SYS_clone3 definition.
#ifndef SYS_clone3
#if defined(__aarch64__) || defined(__arm__) || defined(__x86_64__) || defined(__riscv)
constexpr int SYS_clone3 = 435;
#else
#error SYS_clone3 needs a definition for this architecture.
#endif
#endif
// Generate a child process that will spawn a grandchild process,both of which
// will be traced. We use SYS_clone3 directly here, as it removes libc
// discretion about whether this is fork/clone/vfork.
void ForkUsingClone3(bool is_seized, uint64_t addl_clone_args, pid_t *out) {
struct clone_args ca;
memset(&ca, 0, sizeof(ca));
ca.flags = addl_clone_args;
ca.exit_signal = SIGCHLD; // Needed in order to wait on the child.
pid_t child_pid = static_cast<pid_t>(syscall(SYS_clone3, &ca, sizeof(ca)));
if (child_pid == 0) {
if (!is_seized) {
ASSERT_EQ(ptrace(PTRACE_TRACEME, 0, 0, 0), 0);
}
raise(SIGSTOP);
pid_t grandchild_pid = static_cast<pid_t>(syscall(SYS_clone3, &ca, sizeof(ca)));
if (grandchild_pid == 0) {
// Automatically does a SIGSTOP if started traced
exit(0);
}
int status;
ASSERT_EQ(grandchild_pid, waitpid(grandchild_pid, &status, 0)) << strerror(errno);
ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0)
<< "Failure: WIFEXITED(status) =" << WIFEXITED(status)
<< " WEXITSTATUS(status) == " << WEXITSTATUS(status);
exit(0);
}
ASSERT_GT(child_pid, 0) << strerror(errno);
*out = child_pid;
}
template <typename T>
long get_event_msg(pid_t traced_pid, T *message) {
unsigned long value;
long return_code = ptrace(PTRACE_GETEVENTMSG, traced_pid, 0, &value);
*message = static_cast<T>(value);
return return_code;
}
void DetectForkAndContinue(pid_t child_pid, bool is_seized, bool child_stops_on_clone) {
int status;
pid_t grandchild_pid = 0;
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, 0));
if (child_stops_on_clone) {
// Continue until we hit a fork.
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && (status >> 8) == (SIGTRAP | (PTRACE_EVENT_FORK << 8)))
<< "status = " << status;
// Get the grandchild's pid as reported by ptrace
ASSERT_EQ(0, get_event_msg<pid_t>(child_pid, &grandchild_pid))
<< strerror(errno) << ": with child pid: " << child_pid;
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, 0))
<< strerror(errno) << " with child pid " << child_pid;
// A grandchild started with TRACEFORK will start with a SIGSTOP or a PTRACE_EVENT_STOP
// (depending on whether we used PTRACE_SEIZE to attach).
ASSERT_EQ(grandchild_pid, waitpid(grandchild_pid, &status, 0)) << strerror(errno);
} else {
grandchild_pid = waitpid(0, &status, 0);
ASSERT_NE(-1, grandchild_pid) << strerror(errno);
}
if (is_seized) {
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
int shifted_status = status >> 8;
ASSERT_TRUE(((PTRACE_EVENT_STOP << 8) | SIGTRAP) == shifted_status)
<< "shifted_status = " << shifted_status;
} else {
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP)
<< " status " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
}
ASSERT_EQ(0, ptrace(PTRACE_CONT, grandchild_pid, 0, SIGCONT));
// The grandchild should now exit.
ASSERT_EQ(grandchild_pid, waitpid(grandchild_pid, &status, 0)) << strerror(errno);
ASSERT_TRUE(WIFEXITED(status)) << "WIFEXITED(status) = " << WIFEXITED(status);
// When the grandchild exits, the child receives a SIGCHLD.
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGCHLD);
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, SIGCHLD));
// The child should now exit
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0)
<< "WIFEXITED(status) == " << WIFEXITED(status)
<< " WEXITSTATUS(status) == " << WEXITSTATUS(status);
}
} // namespace
// After a successful `PTRACE_ATTACH`, the traced process should receive SIGSTOP.
TEST(PtraceTest, PtraceAttachSendSigstop) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
pid_t pid;
ASSERT_TRUE((pid = fork()) >= 0);
if (pid == 0) {
ASSERT_THAT(raise(SIGSTOP), SyscallSucceeds());
} else {
int wstatus;
// Wait for the child to stop itself, then attach.
ASSERT_THAT(waitpid(pid, &wstatus, WUNTRACED), SyscallSucceeds());
ASSERT_TRUE(WIFSTOPPED(wstatus));
ASSERT_THAT(ptrace(PTRACE_ATTACH, pid, nullptr, nullptr), SyscallSucceeds());
// Expect that the child has received SIGSTOP since the last wait.
// In theory we can't expect the signal to be delivered by any specific deadline,
// but let's give up if it hasn't arrived within 5 seconds.
int wait_result = 0;
ASSERT_GE((wait_result = waitpid(pid, &wstatus, WNOHANG)), 0);
int retry_seconds = 0;
while (wait_result == 0 && retry_seconds < 5) {
sleep(1);
retry_seconds++;
ASSERT_GE((wait_result = waitpid(pid, &wstatus, WNOHANG)), 0);
}
EXPECT_EQ(wait_result, pid);
EXPECT_TRUE(WIFSTOPPED(wstatus));
EXPECT_EQ(WSTOPSIG(wstatus), SIGSTOP);
// Clean up: resume the child from the SIGSTOP sent by `ptrace`, then from
// the self-signaled SIGSTOP.
ASSERT_THAT(ptrace(PTRACE_CONT, pid, nullptr, 0), SyscallSucceeds());
ASSERT_THAT(waitpid(pid, &wstatus, 0), SyscallSucceedsWithValue(pid));
ASSERT_TRUE(WIFSTOPPED(wstatus));
ASSERT_EQ(WSTOPSIG(wstatus), SIGSTOP);
ASSERT_THAT(ptrace(PTRACE_CONT, pid, nullptr, 0), SyscallSucceeds());
ASSERT_THAT(waitpid(pid, &wstatus, 0), SyscallSucceeds());
ASSERT_TRUE(WIFEXITED(wstatus));
ASSERT_EQ(WEXITSTATUS(wstatus), 0);
}
});
EXPECT_TRUE(helper.WaitForChildren());
}
TEST(PtraceTest, PtraceEventStopWithFork) {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::IsStarnix()) {
GTEST_SKIP() << "This test does not work on Linux in CQ";
}
pid_t child_pid;
ForkUsingClone3(false, 0, &child_pid);
if (HasFatalFailure()) {
return;
}
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << " status " << status;
ASSERT_EQ(0, ptrace(PTRACE_SETOPTIONS, child_pid, 0, PTRACE_O_TRACEFORK))
<< "error " << strerror(errno);
DetectForkAndContinue(child_pid, false, true);
}
TEST(PtraceTest, PtraceEventStopWithForkAndSeize) {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::IsStarnix()) {
GTEST_SKIP() << "This test does not work on Linux in CQ";
}
pid_t child_pid;
ForkUsingClone3(true, 0, &child_pid);
if (HasFatalFailure()) {
return;
}
ASSERT_EQ(ptrace(PTRACE_SEIZE, child_pid, 0, PTRACE_O_TRACEFORK), 0) << strerror(errno);
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << " status " << status;
DetectForkAndContinue(child_pid, true, true);
}
TEST(PtraceTest, PtraceEventStopWithForkClonePtrace) {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::IsStarnix()) {
GTEST_SKIP() << "This test does not work on Linux in CQ";
}
pid_t child_pid;
ForkUsingClone3(false, CLONE_PTRACE, &child_pid);
if (HasFatalFailure()) {
return;
}
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << " status " << status;
DetectForkAndContinue(child_pid, false, false);
}
TEST(PtraceTest, PtraceEventStopWithVForkClonePtrace) {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::IsStarnix()) {
GTEST_SKIP() << "This test does not work on Linux in CQ";
}
pid_t child_pid = fork();
if (child_pid == 0) {
ASSERT_EQ(ptrace(PTRACE_TRACEME, 0, 0, 0), 0);
raise(SIGSTOP);
pid_t grandchild_pid = vfork();
if (grandchild_pid == 0) {
exit(99);
}
int status;
ASSERT_EQ(grandchild_pid, waitpid(grandchild_pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 99)
<< "Failure: WIFEXITED(status) =" << WIFEXITED(status)
<< " WEXITSTATUS(status) == " << WEXITSTATUS(status);
exit(0);
}
ASSERT_LT(0, child_pid);
pid_t grandchild_pid;
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << " status " << status;
ASSERT_EQ(0,
ptrace(PTRACE_SETOPTIONS, child_pid, 0, PTRACE_O_TRACEVFORK | PTRACE_O_TRACEVFORKDONE));
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, 0))
<< strerror(errno) << ": with child pid " << child_pid;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && (status >> 8) == (SIGTRAP | (PTRACE_EVENT_VFORK << 8)))
<< "status = " << status;
// Get the grandchild's pid as reported by ptrace
ASSERT_EQ(0, get_event_msg<pid_t>(child_pid, &grandchild_pid)) << strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, 0))
<< strerror(errno) << ": with child pid " << child_pid;
// Let the grandchild continue.
ASSERT_EQ(grandchild_pid, waitpid(grandchild_pid, &status, 0)) << strerror(errno);
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << " status " << status;
// Child should not have made progress..
ASSERT_EQ(0, waitpid(child_pid, &status, WNOHANG)) << strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_CONT, grandchild_pid, 0, 0)) << strerror(errno);
ASSERT_EQ(grandchild_pid, waitpid(grandchild_pid, &status, 0)) << strerror(errno);
ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 99)
<< "WIFEXITED(status) == " << WIFEXITED(status)
<< " WEXITSTATUS(status) == " << WEXITSTATUS(status);
// Grandchild is done, child should continue.
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)) << strerror(errno);
ASSERT_TRUE(WIFSTOPPED(status) && (status >> 8) == (SIGTRAP | (PTRACE_EVENT_VFORK_DONE << 8)))
<< "status = " << status;
ASSERT_EQ(0, ptrace(PTRACE_DETACH, child_pid, 0, 0)) << strerror(errno);
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)) << strerror(errno);
ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0)
<< "WIFEXITED(status) == " << WIFEXITED(status)
<< " WEXITSTATUS(status) == " << WEXITSTATUS(status);
}
constexpr int kBadExitStatus = 0xabababab;
void DoExec(pid_t *out) {
pid_t child_pid = fork();
if (child_pid == 0) {
ASSERT_EQ(ptrace(PTRACE_TRACEME, 0, 0, 0), 0) << strerror(errno);
raise(SIGSTOP);
std::string test_binary = "data/tests/deps/ptrace_test_exec_child";
if (!files::IsFile(test_binary)) {
// We're running on host
char self_path[PATH_MAX];
realpath("/proc/self/exe", self_path);
test_binary = files::JoinPath(files::GetDirectoryName(self_path), "ptrace_test_exec_child");
}
char *const argv[] = {const_cast<char *>(test_binary.c_str()), nullptr};
// execv happens without releasing futex, so futex's FUTEX_OWNER_DIED bit is set.
execve(test_binary.c_str(), argv, nullptr);
// Should not get here.
_exit(kBadExitStatus);
}
*out = child_pid;
}
// Ensure that the tracee sends a SIGTRAP when it encounters an exec and
// TRACEEXEC is not enabled.
TEST(PtraceTest, ExecveWithSigtrap) {
pid_t child_pid;
DoExec(&child_pid);
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, 0));
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
ASSERT_EQ(0, ptrace(PTRACE_DETACH, child_pid, 0, 0));
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0)
<< "WIFEXITED(status) == " << WIFEXITED(status)
<< " WEXITSTATUS(status) == " << WEXITSTATUS(status);
}
// Ensure that, if TRACEEXIT is enabled, and the tracee executes an exit, it
// then sends a SIGTRAP | (PTRACE_EVENT_EXIT << 8)
TEST(PtraceTest, PtraceEventStopWithExit) {
// TODO(https://fxbug.dev/322238868): This test does not work on the LTO
// builder in CQ.
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::IsStarnix()) {
GTEST_SKIP() << "This test does not work on Linux in CQ";
}
pid_t child_pid;
DoExec(&child_pid);
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
ASSERT_EQ(0, ptrace(PTRACE_SETOPTIONS, child_pid, 0, PTRACE_O_TRACEEXIT))
<< "error " << strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, 0));
// Wait for the exec
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, 0));
// Wait for the exit
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
ASSERT_EQ(SIGTRAP | (PTRACE_EVENT_EXIT << 8), status >> 8);
int exit_status = kBadExitStatus;
ASSERT_EQ(get_event_msg<int>(child_pid, &exit_status), 0);
// The actual exit status seems to change depending on how this test is run,
// so just make sure that something is returned.
ASSERT_TRUE(kBadExitStatus != exit_status)
<< "expected = " << kBadExitStatus << " actual: " << exit_status;
ASSERT_EQ(0, ptrace(PTRACE_DETACH, child_pid, 0, 0)) << " with child pid " << child_pid;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0)
<< "WIFEXITED(status) == " << WIFEXITED(status)
<< " WEXITSTATUS(status) == " << WEXITSTATUS(status);
}
// Ensure that, if TRACEEXEC is enabled, and the tracee executes an exec, it
// then sends a SIGTRAP | (PTRACE_EVENT_EXEC << 8).
TEST(PtraceTest, PtraceEventStopWithExecve) {
// TODO(https://fxbug.dev/322238868): This test does not work on the LTO
// builder in CQ.
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::IsStarnix()) {
GTEST_SKIP() << "This test does not work on Linux in CQ";
}
pid_t child_pid;
DoExec(&child_pid);
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
ASSERT_EQ(0, ptrace(PTRACE_SETOPTIONS, child_pid, 0, PTRACE_O_TRACEEXEC | PTRACE_O_TRACEEXIT))
<< "error " << strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, 0));
// Wait for the exec
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
ASSERT_EQ(SIGTRAP | (PTRACE_EVENT_EXEC << 8), status >> 8);
pid_t target_pid;
ASSERT_EQ(get_event_msg<pid_t>(child_pid, &target_pid), 0);
ASSERT_EQ(target_pid, child_pid);
ASSERT_EQ(0, ptrace(PTRACE_DETACH, child_pid, 0, 0))
<< strerror(errno) << ": with child pid " << child_pid;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0)
<< "WIFEXITED(status) == " << WIFEXITED(status)
<< " WEXITSTATUS(status) == " << WEXITSTATUS(status);
}
// Ensure that, if TRACEEXIT is enabled, and the tracee is killed with a
// SIGTERM, it sends a SIGTRAP | (PTRACE_EVENT_EXIT << 8)
TEST(PtraceTest, PtraceEventStopWithSignalExit) {
// TODO(https://fxbug.dev/322238868): This test does not work on the LTO
// builder in CQ.
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::IsStarnix()) {
GTEST_SKIP() << "This test does not work on Linux in CQ";
}
pid_t child_pid;
DoExec(&child_pid);
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
ASSERT_EQ(0, ptrace(PTRACE_SETOPTIONS, child_pid, 0, PTRACE_O_TRACEEXIT))
<< "error " << strerror(errno);
ASSERT_EQ(0, kill(child_pid, SIGTERM));
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, 0));
// Wait for the signal-delivery-stop
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTERM)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, SIGTERM));
// Wait for the exit
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
ASSERT_EQ(SIGTRAP | (PTRACE_EVENT_EXIT << 8), status >> 8);
int exit_status = 0xabababab;
ASSERT_EQ(get_event_msg<int>(child_pid, &exit_status), 0);
ASSERT_TRUE(SIGTERM == exit_status) << " exit_status " << exit_status;
ASSERT_EQ(0, ptrace(PTRACE_DETACH, child_pid, 0, 0))
<< strerror(errno) << " with child pid " << child_pid;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM)
<< "WIFSIGNALED(status) == " << WIFEXITED(status)
<< " WTERMSIG(status) == " << WTERMSIG(status);
}
namespace {
void GrandchildWithSigsuspendSigaction(int, siginfo_t *, void *) {
// NOP
}
} // namespace
// Test that traced child correctly resumes when signal needs to be delivered
// because of a temporary mask.
TEST(PtraceTest, GrandchildWithSigsuspend) {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::IsStarnix()) {
GTEST_SKIP() << "This test does not work on Linux in CQ";
}
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
pid_t child_pid = helper.RunInForkedProcess([] {
ASSERT_EQ(0, ptrace(PTRACE_TRACEME, 0, 0, 0));
ASSERT_EQ(0, raise(SIGSTOP));
sigset_t child_mask, old_mask;
ASSERT_EQ(0, sigemptyset(&child_mask));
ASSERT_EQ(0, sigaddset(&child_mask, SIGCHLD));
sigset_t empty_mask;
ASSERT_EQ(0, sigemptyset(&empty_mask));
struct sigaction sa, oldact;
sa.sa_sigaction = GrandchildWithSigsuspendSigaction;
sa.sa_mask = empty_mask;
ASSERT_EQ(0, sigaction(SIGCHLD, &sa, &oldact));
pid_t my_pid = getpid();
ASSERT_EQ(0, sigprocmask(SIG_BLOCK, &child_mask, &old_mask));
pid_t gc_pid = fork();
if (gc_pid == 0) {
test_helper::WaitUntilBlocked(my_pid, false);
exit(0);
}
ASSERT_EQ(-1, sigsuspend(&old_mask));
int status;
ASSERT_EQ(gc_pid, waitpid(gc_pid, &status, 0));
ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0)
<< "WIFEXITED(status) == " << WIFEXITED(status)
<< " WEXITSTATUS(status) == " << WEXITSTATUS(status);
});
int status;
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP)
<< WIFSTOPPED(status) << " " << WSTOPSIG(status);
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, 0));
ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGCHLD)
<< WIFSTOPPED(status) << " " << WSTOPSIG(status);
ASSERT_EQ(0, ptrace(PTRACE_CONT, child_pid, 0, SIGCHLD));
}
TEST(PtraceTest, ExitKill) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([]() {
// Test that the PtraceOExitKill works as expected.
// Set ourselves as the subreaper.
SAFE_SYSCALL(prctl(PR_SET_CHILD_SUBREAPER, 1));
pid_t tracer_pid = SAFE_SYSCALL(fork());
if (tracer_pid == 0) {
// We are the tracer. Spawn the tracee.
pid_t tracee_pid = SAFE_SYSCALL(fork());
if (tracee_pid == 0) {
ASSERT_THAT(ptrace(PTRACE_TRACEME, 0, nullptr, nullptr), SyscallSucceeds());
SAFE_SYSCALL(raise(SIGSTOP));
_exit(EXIT_FAILURE);
}
int status;
SAFE_SYSCALL(waitpid(tracee_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status));
EXPECT_THAT(ptrace(PTRACE_SETOPTIONS, tracee_pid, nullptr, PTRACE_O_EXITKILL),
SyscallSucceeds());
// With this exit, the kernel will send a sigkill to the tracee.
_exit(EXIT_SUCCESS);
}
int status;
pid_t pid = SAFE_SYSCALL(waitpid(tracer_pid, &status, 0));
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
pid = SAFE_SYSCALL(waitpid(-1, &status, 0));
EXPECT_NE(pid, tracer_pid);
EXPECT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL);
});
EXPECT_TRUE(helper.WaitForChildren());
}
TEST(PtraceTest, ExitKillFromThread) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([]() {
// Test that the PtraceOExitKill works as expected.
// Set ourselves as the subreaper.
SAFE_SYSCALL(prctl(PR_SET_CHILD_SUBREAPER, 1));
pid_t tgl_pid = SAFE_SYSCALL(fork());
if (tgl_pid == 0) {
// We are the thread-group leader. Create a thread that will be the ptracer.
std::atomic<pid_t> tracee_pid;
std::thread ptracer([&tracee_pid]() {
pid_t pid = SAFE_SYSCALL(fork());
if (pid == 0) {
ASSERT_THAT(ptrace(PTRACE_TRACEME, 0, nullptr, nullptr), SyscallSucceeds());
SAFE_SYSCALL(raise(SIGSTOP));
_exit(EXIT_FAILURE);
}
tracee_pid.store(pid);
int status;
SAFE_SYSCALL(waitpid(pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status));
EXPECT_THAT(ptrace(PTRACE_SETOPTIONS, pid, nullptr, PTRACE_O_EXITKILL), SyscallSucceeds());
});
ptracer.join();
// Tracee should exit once the thread that spawned it exited.
int status;
SAFE_SYSCALL(waitpid(tracee_pid.load(), &status, 0));
EXPECT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL);
_exit(EXIT_SUCCESS);
}
int status;
SAFE_SYSCALL(waitpid(tgl_pid, &status, 0));
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
});
EXPECT_TRUE(helper.WaitForChildren());
}
TEST(PtraceTest, PtraceAttachesToParentThread) {
test_helper::ForkHelper helper;
helper.RunInForkedProcess([]() {
SAFE_SYSCALL(prctl(PR_SET_CHILD_SUBREAPER, 1));
std::latch fork_done(1);
std::latch should_exit(1);
std::atomic<pid_t> tracee_pid;
std::thread ptracer([&tracee_pid, &fork_done, &should_exit]() {
pid_t pid = SAFE_SYSCALL(fork());
if (pid == 0) {
ASSERT_THAT(ptrace(PTRACE_TRACEME, 0, nullptr, nullptr), SyscallSucceeds());
// Can be controlled by the thread that spawned it.
SAFE_SYSCALL(raise(SIGSTOP));
// But no one else can make it continue.
SAFE_SYSCALL(raise(SIGSTOP));
}
int status;
SAFE_SYSCALL(waitpid(pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status));
EXPECT_THAT(ptrace(PTRACE_SETOPTIONS, pid, nullptr, PTRACE_O_EXITKILL), SyscallSucceeds());
EXPECT_THAT(ptrace(PTRACE_CONT, pid, nullptr, nullptr), SyscallSucceeds());
tracee_pid.store(pid);
fork_done.count_down();
should_exit.wait();
});
fork_done.wait();
std::thread another_thread([&tracee_pid]() {
int status;
SAFE_SYSCALL(waitpid(tracee_pid.load(), &status, 0));
ASSERT_TRUE(WIFSTOPPED(status));
EXPECT_THAT(ptrace(PTRACE_CONT, tracee_pid.load(), nullptr, nullptr),
SyscallFailsWithErrno(ESRCH));
});
another_thread.join();
int status;
// tracee is stopped, we know because of the waitpid in another_thread.
EXPECT_THAT(ptrace(PTRACE_CONT, tracee_pid.load(), nullptr, nullptr),
SyscallFailsWithErrno(ESRCH));
should_exit.count_down();
ptracer.join();
SAFE_SYSCALL(waitpid(tracee_pid.load(), &status, 0));
EXPECT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL);
});
EXPECT_TRUE(helper.WaitForChildren());
}
__attribute__((noinline)) void FunctionToBreak() {
// Placeholder instruction to be replaced with a breakpoint by tracer.
// Depending on the architecture, `nop` can be as small as 1 byte, so pad the function with
// multiple `nop` instructions to ensure that the breakpoint does not overwrite the next
// instruction.
asm volatile(
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n"
"nop\n");
}
// Sets a breakpoint in the child process using PTRACE_POKEDATA and expects that the child process
// triggers the breakpoint.
TEST(PtraceTest, PokeToSetBreakpoint) {
test_helper::ForkHelper helper;
helper.ExpectSignal(SIGTRAP);
pid_t child_pid = helper.RunInForkedProcess([] {
SAFE_SYSCALL(ptrace(PTRACE_TRACEME, 0, 0, 0));
raise(SIGSTOP);
FunctionToBreak();
});
ASSERT_NE(child_pid, 0);
// Reach child stop
int status;
ASSERT_EQ(SAFE_SYSCALL(waitpid(child_pid, &status, 0)), child_pid);
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
// Depending on the architecture and bitness, the breakpoint instruction could be smaller than
// word length. Read original word, and overwrite the breakpoint instruction to it but keep the
// rest as is.
const void *breakpoint_addr = reinterpret_cast<void *>(&FunctionToBreak);
errno = 0;
long original_data = ptrace(PTRACE_PEEKDATA, child_pid, breakpoint_addr, 0);
if (original_data == -1 && errno != 0) {
kill(child_pid, SIGKILL);
FAIL() << "PTRACE_PEEKDATA failed: " << strerror(errno) << "(" << errno << ")";
}
#if defined(__x86_64__)
const long break_insn = 0xCC;
long breakpoint_data = (original_data & ~0xFFL) | break_insn;
// On x86_64, PC is advanced after a breakpoint instruction, so the child should continue
// execution and terminate successfully.
helper.ExpectSignal(0);
#elif defined(__aarch64__)
const long break_insn = 0xD4200000;
long breakpoint_data = (original_data & ~0xFFFFFFFFL) | break_insn;
#elif defined(__arm__)
const long break_insn = 0xE1200070;
long breakpoint_data = (original_data & ~0xFFFFFFFFL) | break_insn;
#elif defined(__riscv)
const long break_insn = 0x00100073;
long breakpoint_data = (original_data & ~0xFFFFFFFFL) | break_insn;
#else
#error "Unsupported architecture"
#endif
SAFE_SYSCALL(ptrace(PTRACE_POKEDATA, child_pid, breakpoint_addr, breakpoint_data));
SAFE_SYSCALL(ptrace(PTRACE_CONT, child_pid, 0, 0));
// Child process should stop at the newly placed breakpoint.
ASSERT_EQ(SAFE_SYSCALL(waitpid(child_pid, &status, 0)), child_pid);
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP)
<< "status = " << status << " WIFSTOPPED = " << WIFSTOPPED(status)
<< " WSTOPSIG = " << WSTOPSIG(status);
SAFE_SYSCALL(ptrace(PTRACE_DETACH, child_pid, 0, 0));
ASSERT_TRUE(helper.WaitForChildren());
}
// Create 2 memory mappings next to each other, and poke to a memory address that spans both
// mappings. Use file-backed mapping so that they are guaranteed to be distinct VMOs.
//
// |--------- reserved memory region ---------|
// |----- mapping1 -----||----- mapping2 -----|
// [word]
TEST(PtraceTest, PokeAcrossMappings) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
void *memory_region = mmap(nullptr, 2 * page_size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
ASSERT_NE(memory_region, MAP_FAILED) << strerror(errno);
test_helper::ScopedTempFD tempfile1, tempfile2;
SAFE_SYSCALL(ftruncate(tempfile1.fd(), page_size));
SAFE_SYSCALL(ftruncate(tempfile2.fd(), page_size));
void *mapping1 =
mmap(memory_region, page_size, PROT_READ, MAP_PRIVATE | MAP_FIXED, tempfile1.fd(), 0);
ASSERT_NE(mapping1, MAP_FAILED) << strerror(errno);
uintptr_t mapping2_addr = reinterpret_cast<uintptr_t>(memory_region) + page_size;
void *mapping2 = mmap(reinterpret_cast<void *>(mapping2_addr), page_size, PROT_READ,
MAP_PRIVATE | MAP_FIXED, tempfile2.fd(), 0);
ASSERT_NE(mapping2, MAP_FAILED) << strerror(errno);
// Set the poke address 1 byte before the start of the second mapping.
void *poke_addr = reinterpret_cast<void *>(mapping2_addr - 1);
test_helper::ForkHelper fork_helper;
pid_t child_pid = fork_helper.RunInForkedProcess([&] {
SAFE_SYSCALL(ptrace(PTRACE_TRACEME, 0, 0, 0));
raise(SIGSTOP);
unsigned long value;
memcpy(&value, poke_addr, sizeof(unsigned long));
ASSERT_EQ(value, 0xBEEFUL);
_exit(0);
});
ASSERT_NE(child_pid, 0);
// Wait for child process to hit SIGSTOP.
int status = 0;
SAFE_SYSCALL(waitpid(child_pid, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << std::hex << status;
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid, poke_addr, 0xBEEFUL), SyscallSucceeds());
SAFE_SYSCALL(ptrace(PTRACE_DETACH, child_pid, 0, 0));
ASSERT_TRUE(fork_helper.WaitForChildren());
}
enum class BackingType {
ANONYMOUS,
MEMFD,
READ_ONLY_FILE,
WRITABLE_FILE,
};
std::string BackingTypeName(const testing::TestParamInfo<BackingType> &info) {
switch (info.param) {
case BackingType::ANONYMOUS:
return "Anonymous";
case BackingType::MEMFD:
return "Memfd";
case BackingType::READ_ONLY_FILE:
return "ReadOnlyFile";
case BackingType::WRITABLE_FILE:
return "WritableFile";
}
}
template <int map_flags, int prot_flags>
class PokeInMappingTest : public testing::TestWithParam<BackingType> {
public:
void SetUp() override {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
len_ = 2 * page_size;
int flags = map_flags;
int fd = -1;
switch (GetParam()) {
case BackingType::ANONYMOUS:
flags |= MAP_ANONYMOUS;
break;
case BackingType::MEMFD:
fd = memfd_create("ptrace_test", 0);
SAFE_SYSCALL(ftruncate(fd, len_));
ASSERT_NE(fd, -1) << strerror(errno);
break;
case BackingType::READ_ONLY_FILE:
fd = open("/proc/self/exe", O_RDONLY);
ASSERT_NE(fd, -1) << strerror(errno);
break;
case BackingType::WRITABLE_FILE:
fd = temp_file_.fd();
ASSERT_NE(fd, -1) << "ScopedTempFD is -1";
SAFE_SYSCALL(ftruncate(fd, len_));
break;
}
mapping_ = mmap(nullptr, len_, prot_flags, flags, fd, 0);
ASSERT_NE(mapping_, MAP_FAILED) << strerror(errno);
if (fd != -1) {
close(fd);
}
}
void TearDown() override {
SAFE_SYSCALL(ptrace(PTRACE_DETACH, child_pid_, 0, 0));
ASSERT_TRUE(helper_.WaitForChildren());
}
// If `assert_memory_after_stop` is true, checks that the first byte of memory is modified by
// ptrace_pokedata.
void CreateChildProcess(bool assert_memory_after_stop) {
helper_.OnlyWaitForForkedChildren();
child_pid_ = helper_.RunInForkedProcess([&] {
SAFE_SYSCALL(ptrace(PTRACE_TRACEME, 0, 0, 0));
raise(SIGSTOP);
if (assert_memory_after_stop) {
SAFE_SYSCALL(mprotect(mapping_, len_, PROT_READ));
ASSERT_EQ(static_cast<const volatile unsigned long *>(mapping_)[0], 0xBBUL);
}
_exit(0);
});
ASSERT_NE(child_pid_, 0);
}
// Wait for the child process to hit SIGSTOP.
// After this call, the child process is stopped and ptrace is attached.
void WaitForChildStop() {
int status = 0;
SAFE_SYSCALL(waitpid(child_pid_, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << std::hex << status;
}
protected:
test_helper::ForkHelper helper_;
test_helper::ScopedTempFD temp_file_;
pid_t child_pid_ = 0;
void *mapping_ = nullptr;
size_t len_ = 0;
};
// Poking in private memory should work, regardless of the backing and permissions.
using PokeInPrivateMappingTest = PokeInMappingTest<MAP_PRIVATE, PROT_NONE>;
TEST_P(PokeInPrivateMappingTest, Data) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping_, 0xBB), SyscallSucceeds());
}
TEST_P(PokeInPrivateMappingTest, Text) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKETEXT, child_pid_, mapping_, 0xBB), SyscallSucceeds());
}
INSTANTIATE_TEST_SUITE_P(PtracePokeMemory, PokeInPrivateMappingTest,
testing::Values(BackingType::ANONYMOUS, BackingType::MEMFD,
BackingType::READ_ONLY_FILE, BackingType::WRITABLE_FILE),
&BackingTypeName);
using PokeInPrivateROMappingTest = PokeInMappingTest<MAP_PRIVATE, PROT_READ>;
TEST_P(PokeInPrivateROMappingTest, Data) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping_, 0xBB), SyscallSucceeds());
}
TEST_P(PokeInPrivateROMappingTest, Text) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKETEXT, child_pid_, mapping_, 0xBB), SyscallSucceeds());
}
INSTANTIATE_TEST_SUITE_P(PtracePokeMemory, PokeInPrivateROMappingTest,
testing::Values(BackingType::ANONYMOUS, BackingType::MEMFD,
BackingType::READ_ONLY_FILE, BackingType::WRITABLE_FILE),
&BackingTypeName);
using PokeInPrivateRWMappingTest = PokeInMappingTest<MAP_PRIVATE, PROT_READ | PROT_WRITE>;
TEST_P(PokeInPrivateRWMappingTest, Data) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping_, 0xBB), SyscallSucceeds());
}
TEST_P(PokeInPrivateRWMappingTest, Text) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKETEXT, child_pid_, mapping_, 0xBB), SyscallSucceeds());
}
INSTANTIATE_TEST_SUITE_P(PtracePokeMemory, PokeInPrivateRWMappingTest,
testing::Values(BackingType::ANONYMOUS, BackingType::MEMFD,
BackingType::READ_ONLY_FILE, BackingType::WRITABLE_FILE),
&BackingTypeName);
using PokeInPrivateRXMappingTest = PokeInMappingTest<MAP_PRIVATE, PROT_READ | PROT_EXEC>;
TEST_P(PokeInPrivateRXMappingTest, Data) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping_, 0xBB), SyscallSucceeds());
}
TEST_P(PokeInPrivateRXMappingTest, Text) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKETEXT, child_pid_, mapping_, 0xBB), SyscallSucceeds());
}
INSTANTIATE_TEST_SUITE_P(PtracePokeMemory, PokeInPrivateRXMappingTest,
testing::Values(BackingType::ANONYMOUS, BackingType::MEMFD,
BackingType::READ_ONLY_FILE, BackingType::WRITABLE_FILE),
&BackingTypeName);
using PokeInPrivateRWXMappingTest =
PokeInMappingTest<MAP_PRIVATE, PROT_READ | PROT_WRITE | PROT_EXEC>;
TEST_P(PokeInPrivateRWXMappingTest, Data) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping_, 0xBB), SyscallSucceeds());
}
TEST_P(PokeInPrivateRWXMappingTest, Text) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKETEXT, child_pid_, mapping_, 0xBB), SyscallSucceeds());
}
INSTANTIATE_TEST_SUITE_P(PtracePokeMemory, PokeInPrivateRWXMappingTest,
testing::Values(BackingType::ANONYMOUS, BackingType::MEMFD,
BackingType::READ_ONLY_FILE, BackingType::WRITABLE_FILE),
&BackingTypeName);
// Poking in shared memory doesn't work, unless the process has writable permissions.
using PokeInSharedMappingTest = PokeInMappingTest<MAP_SHARED, PROT_NONE>;
TEST_P(PokeInSharedMappingTest, Data) {
CreateChildProcess(/*assert_memory_after_stop=*/false);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping_, 0xBB), SyscallFailsWithErrno(EIO));
}
TEST_P(PokeInSharedMappingTest, Text) {
CreateChildProcess(/*assert_memory_after_stop=*/false);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKETEXT, child_pid_, mapping_, 0xBB), SyscallFailsWithErrno(EIO));
}
INSTANTIATE_TEST_SUITE_P(PtracePokeMemory, PokeInSharedMappingTest,
testing::Values(BackingType::ANONYMOUS, BackingType::MEMFD,
BackingType::READ_ONLY_FILE, BackingType::WRITABLE_FILE),
&BackingTypeName);
using PokeInSharedROMappingTest = PokeInMappingTest<MAP_SHARED, PROT_READ>;
TEST_P(PokeInSharedROMappingTest, Data) {
CreateChildProcess(/*assert_memory_after_stop=*/false);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping_, 0xBB), SyscallFailsWithErrno(EIO));
}
TEST_P(PokeInSharedROMappingTest, Text) {
CreateChildProcess(/*assert_memory_after_stop=*/false);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKETEXT, child_pid_, mapping_, 0xBB), SyscallFailsWithErrno(EIO));
}
INSTANTIATE_TEST_SUITE_P(PtracePokeMemory, PokeInSharedROMappingTest,
testing::Values(BackingType::ANONYMOUS, BackingType::MEMFD,
BackingType::READ_ONLY_FILE, BackingType::WRITABLE_FILE),
&BackingTypeName);
using PokeInSharedRWMappingTest = PokeInMappingTest<MAP_SHARED, PROT_READ | PROT_WRITE>;
TEST_P(PokeInSharedRWMappingTest, Data) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping_, 0xBB), SyscallSucceeds());
EXPECT_EQ(static_cast<const volatile unsigned long *>(mapping_)[0], 0xBBUL);
}
TEST_P(PokeInSharedRWMappingTest, Text) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKETEXT, child_pid_, mapping_, 0xBB), SyscallSucceeds());
EXPECT_EQ(static_cast<const volatile unsigned long *>(mapping_)[0], 0xBBUL);
}
// Skip READ_ONLY_FILE because we cannot create writable memory from read-only file.
INSTANTIATE_TEST_SUITE_P(PtracePokeMemory, PokeInSharedRWMappingTest,
testing::Values(BackingType::ANONYMOUS, BackingType::MEMFD,
BackingType::WRITABLE_FILE),
&BackingTypeName);
using PokeInSharedRXMappingTest = PokeInMappingTest<MAP_SHARED, PROT_READ | PROT_EXEC>;
TEST_P(PokeInSharedRXMappingTest, Data) {
CreateChildProcess(/*assert_memory_after_stop=*/false);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping_, 0xBB), SyscallFailsWithErrno(EIO));
}
TEST_P(PokeInSharedRXMappingTest, Text) {
CreateChildProcess(/*assert_memory_after_stop=*/false);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKETEXT, child_pid_, mapping_, 0xBB), SyscallFailsWithErrno(EIO));
}
INSTANTIATE_TEST_SUITE_P(PtracePokeMemory, PokeInSharedRXMappingTest,
testing::Values(BackingType::ANONYMOUS, BackingType::MEMFD,
BackingType::READ_ONLY_FILE, BackingType::WRITABLE_FILE),
&BackingTypeName);
using PokeInSharedRWXMappingTest =
PokeInMappingTest<MAP_SHARED, PROT_READ | PROT_WRITE | PROT_EXEC>;
TEST_P(PokeInSharedRWXMappingTest, Data) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping_, 0xBB), SyscallSucceeds());
EXPECT_EQ(static_cast<const volatile unsigned long *>(mapping_)[0], 0xBBUL);
}
TEST_P(PokeInSharedRWXMappingTest, Text) {
CreateChildProcess(/*assert_memory_after_stop=*/true);
WaitForChildStop();
EXPECT_THAT(ptrace(PTRACE_POKETEXT, child_pid_, mapping_, 0xBB), SyscallSucceeds());
EXPECT_EQ(static_cast<const volatile unsigned long *>(mapping_)[0], 0xBBUL);
}
// Skip READ_ONLY_FILE because we cannot create writable memory from read-only file.
INSTANTIATE_TEST_SUITE_P(PtracePokeMemory, PokeInSharedRWXMappingTest,
testing::Values(BackingType::ANONYMOUS, BackingType::MEMFD,
BackingType::WRITABLE_FILE),
&BackingTypeName);
class PokeInKernelMappingTest : public testing::Test {
public:
void SetUp() override {
fork_helper_.OnlyWaitForForkedChildren();
fork_helper_.ExpectSignal(SIGKILL);
child_pid_ = fork_helper_.RunInForkedProcess([&] {
SAFE_SYSCALL(ptrace(PTRACE_TRACEME, 0, 0, 0));
raise(SIGSTOP);
_exit(0);
});
ASSERT_NE(child_pid_, 0);
int status = 0;
SAFE_SYSCALL(waitpid(child_pid_, &status, 0));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << std::hex << status;
}
void TearDown() override {
SAFE_SYSCALL(kill(child_pid_, SIGKILL));
EXPECT_TRUE(fork_helper_.WaitForChildren());
}
// Read child process'es mappings to find the region with given mapping_name.
std::optional<test_helper::MemoryMapping> GetChildMapping(const std::string &mapping_name) const {
std::string child_maps;
if (!files::ReadFileToString("/proc/" + std::to_string(child_pid_) + "/maps", &child_maps)) {
return std::nullopt;
}
return test_helper::find_memory_mapping(
[&](const test_helper::MemoryMapping &mapping) { return mapping.pathname == mapping_name; },
child_maps);
}
protected:
test_helper::ForkHelper fork_helper_;
pid_t child_pid_;
};
TEST_F(PokeInKernelMappingTest, VDSO) {
auto mapping = GetChildMapping("[vdso]");
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping->start, 0xBB), SyscallSucceeds());
}
TEST_F(PokeInKernelMappingTest, VVAR) {
auto mapping = GetChildMapping("[vvar]");
EXPECT_THAT(ptrace(PTRACE_POKEDATA, child_pid_, mapping->start, 0xBB),
SyscallFailsWithErrno(EIO));
}
} // namespace