platform_bionic/tests/sys_ptrace_test.cpp
Elliott Hughes 95646e6666 Add ASSERT_ERRNO and EXPECT_ERRNO (and use them).
We've talked about this many times in the past, but partners struggle to
understand "expected 38, got 22" in these contexts, and I always have to
go and check the header files just to be sure I'm sure.

I actually think the glibc geterrorname_np() function (which would
return "ENOSYS" rather than "Function not implemented") would be more
helpful, but I'll have to go and implement that first, and then come
back.

Being forced to go through all our errno assertions did also make me
want to use a more consistent style for our ENOSYS assertions in
particular --- there's a particularly readable idiom, and I'll also come
back and move more of those checks to the most readable idiom.

I've added a few missing `errno = 0`s before tests, and removed a few
stray `errno = 0`s from tests that don't actually make assertions about
errno, since I had to look at every single reference to errno anyway.

Test: treehugger
Change-Id: Iba7c56f2adc30288c3e00ade106635e515e88179
2023-09-21 14:15:59 -07:00

584 lines
18 KiB
C++

/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <sys/ptrace.h>
#include <elf.h>
#include <err.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/uio.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <unistd.h>
#include <chrono>
#include <thread>
#include <gtest/gtest.h>
#include <android-base/macros.h>
#include <android-base/unique_fd.h>
#include "utils.h"
using namespace std::chrono_literals;
using android::base::unique_fd;
// Host libc does not define this.
#ifndef TRAP_HWBKPT
#define TRAP_HWBKPT 4
#endif
class ChildGuard {
public:
explicit ChildGuard(pid_t pid) : pid(pid) {}
~ChildGuard() {
kill(pid, SIGKILL);
int status;
TEMP_FAILURE_RETRY(waitpid(pid, &status, 0));
}
private:
pid_t pid;
};
enum class HwFeature { Watchpoint, Breakpoint };
static void check_hw_feature_supported(pid_t child, HwFeature feature) {
#if defined(__arm__)
errno = 0;
long capabilities;
long result = ptrace(PTRACE_GETHBPREGS, child, 0, &capabilities);
if (result == -1) {
EXPECT_ERRNO(EIO);
GTEST_SKIP() << "Hardware debug support disabled at kernel configuration time";
}
uint8_t hb_count = capabilities & 0xff;
capabilities >>= 8;
uint8_t wp_count = capabilities & 0xff;
capabilities >>= 8;
uint8_t max_wp_size = capabilities & 0xff;
if (max_wp_size == 0) {
GTEST_SKIP() << "Kernel reports zero maximum watchpoint size";
} else if (feature == HwFeature::Watchpoint && wp_count == 0) {
GTEST_SKIP() << "Kernel reports zero hardware watchpoints";
} else if (feature == HwFeature::Breakpoint && hb_count == 0) {
GTEST_SKIP() << "Kernel reports zero hardware breakpoints";
}
#elif defined(__aarch64__)
user_hwdebug_state dreg_state;
iovec iov;
iov.iov_base = &dreg_state;
iov.iov_len = sizeof(dreg_state);
errno = 0;
long result = ptrace(PTRACE_GETREGSET, child,
feature == HwFeature::Watchpoint ? NT_ARM_HW_WATCH : NT_ARM_HW_BREAK, &iov);
if (result == -1) {
ASSERT_ERRNO(EINVAL);
GTEST_SKIP() << "Hardware support missing";
} else if ((dreg_state.dbg_info & 0xff) == 0) {
if (feature == HwFeature::Watchpoint) {
GTEST_SKIP() << "Kernel reports zero hardware watchpoints";
} else {
GTEST_SKIP() << "Kernel reports zero hardware breakpoints";
}
}
#else
// We assume watchpoints and breakpoints are always supported on x86.
UNUSED(child);
UNUSED(feature);
#endif
}
static void set_watchpoint(pid_t child, uintptr_t address, size_t size) {
ASSERT_EQ(0u, address & 0x7) << "address: " << address;
#if defined(__arm__) || defined(__aarch64__)
const unsigned byte_mask = (1 << size) - 1;
const unsigned type = 2; // Write.
const unsigned enable = 1;
const unsigned control = byte_mask << 5 | type << 3 | enable;
#ifdef __arm__
ASSERT_EQ(0, ptrace(PTRACE_SETHBPREGS, child, -1, &address)) << strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_SETHBPREGS, child, -2, &control)) << strerror(errno);
#else // aarch64
user_hwdebug_state dreg_state;
memset(&dreg_state, 0, sizeof dreg_state);
dreg_state.dbg_regs[0].addr = address;
dreg_state.dbg_regs[0].ctrl = control;
iovec iov;
iov.iov_base = &dreg_state;
iov.iov_len = offsetof(user_hwdebug_state, dbg_regs) + sizeof(dreg_state.dbg_regs[0]);
ASSERT_EQ(0, ptrace(PTRACE_SETREGSET, child, NT_ARM_HW_WATCH, &iov)) << strerror(errno);
#endif
#elif defined(__i386__) || defined(__x86_64__)
ASSERT_EQ(0, ptrace(PTRACE_POKEUSER, child, offsetof(user, u_debugreg[0]), address)) << strerror(errno);
errno = 0;
unsigned data = ptrace(PTRACE_PEEKUSER, child, offsetof(user, u_debugreg[7]), nullptr);
ASSERT_ERRNO(0);
const unsigned size_flag = (size == 8) ? 2 : size - 1;
const unsigned enable = 1;
const unsigned type = 1; // Write.
const unsigned mask = 3 << 18 | 3 << 16 | 1;
const unsigned value = size_flag << 18 | type << 16 | enable;
data &= mask;
data |= value;
ASSERT_EQ(0, ptrace(PTRACE_POKEUSER, child, offsetof(user, u_debugreg[7]), data)) << strerror(errno);
#else
UNUSED(child);
UNUSED(address);
UNUSED(size);
#endif
}
template <typename T>
static void run_watchpoint_test(std::function<void(T&)> child_func, size_t offset, size_t size) {
alignas(16) T data{};
pid_t child = fork();
ASSERT_NE(-1, child) << strerror(errno);
if (child == 0) {
// Extra precaution: make sure we go away if anything happens to our parent.
if (prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) == -1) {
perror("prctl(PR_SET_PDEATHSIG)");
_exit(1);
}
if (ptrace(PTRACE_TRACEME, 0, nullptr, nullptr) == -1) {
perror("ptrace(PTRACE_TRACEME)");
_exit(2);
}
child_func(data);
_exit(0);
}
ChildGuard guard(child);
int status;
ASSERT_EQ(child, TEMP_FAILURE_RETRY(waitpid(child, &status, __WALL))) << strerror(errno);
ASSERT_TRUE(WIFSTOPPED(status)) << "Status was: " << status;
ASSERT_EQ(SIGSTOP, WSTOPSIG(status)) << "Status was: " << status;
check_hw_feature_supported(child, HwFeature::Watchpoint);
if (::testing::Test::IsSkipped()) {
return;
}
set_watchpoint(child, uintptr_t(untag_address(&data)) + offset, size);
ASSERT_EQ(0, ptrace(PTRACE_CONT, child, nullptr, nullptr)) << strerror(errno);
ASSERT_EQ(child, TEMP_FAILURE_RETRY(waitpid(child, &status, __WALL))) << strerror(errno);
ASSERT_TRUE(WIFSTOPPED(status)) << "Status was: " << status;
ASSERT_EQ(SIGTRAP, WSTOPSIG(status)) << "Status was: " << status;
siginfo_t siginfo;
ASSERT_EQ(0, ptrace(PTRACE_GETSIGINFO, child, nullptr, &siginfo)) << strerror(errno);
ASSERT_EQ(TRAP_HWBKPT, siginfo.si_code);
#if defined(__arm__) || defined(__aarch64__)
ASSERT_LE(&data, siginfo.si_addr);
ASSERT_GT((&data) + 1, siginfo.si_addr);
#endif
}
template <typename T>
static void watchpoint_stress_child(unsigned cpu, T& data) {
cpu_set_t cpus;
CPU_ZERO(&cpus);
CPU_SET(cpu, &cpus);
if (sched_setaffinity(0, sizeof cpus, &cpus) == -1) {
perror("sched_setaffinity");
_exit(3);
}
raise(SIGSTOP); // Synchronize with the tracer, let it set the watchpoint.
data = 1; // Now trigger the watchpoint.
}
template <typename T>
static void run_watchpoint_stress(size_t cpu) {
run_watchpoint_test<T>(std::bind(watchpoint_stress_child<T>, cpu, std::placeholders::_1), 0,
sizeof(T));
}
// Test watchpoint API. The test is considered successful if our watchpoints get hit OR the
// system reports that watchpoint support is not present. We run the test for different
// watchpoint sizes, while pinning the process to each cpu in turn, for better coverage.
TEST(sys_ptrace, watchpoint_stress) {
cpu_set_t available_cpus;
ASSERT_EQ(0, sched_getaffinity(0, sizeof available_cpus, &available_cpus));
for (size_t cpu = 0; cpu < CPU_SETSIZE; ++cpu) {
if (!CPU_ISSET(cpu, &available_cpus)) continue;
run_watchpoint_stress<uint8_t>(cpu);
if (::testing::Test::IsSkipped()) {
// Only check first case, since all others would skip for same reason.
return;
}
run_watchpoint_stress<uint16_t>(cpu);
run_watchpoint_stress<uint32_t>(cpu);
#if defined(__LP64__)
run_watchpoint_stress<uint64_t>(cpu);
#endif
}
}
struct Uint128_t {
uint64_t data[2];
};
static void watchpoint_imprecise_child(Uint128_t& data) {
raise(SIGSTOP); // Synchronize with the tracer, let it set the watchpoint.
#if defined(__i386__) || defined(__x86_64__)
asm volatile("movdqa %%xmm0, %0" : : "m"(data));
#elif defined(__arm__)
asm volatile("stm %0, { r0, r1, r2, r3 }" : : "r"(&data));
#elif defined(__aarch64__)
asm volatile("stp x0, x1, %0" : : "m"(data));
#elif defined(__riscv)
UNUSED(data);
GTEST_LOG_(INFO) << "missing riscv64 instruction to store > 64 bits in one instruction";
#endif
}
// Test that the kernel is able to handle the case when the instruction writes
// to a larger block of memory than the one we are watching. If you see this
// test fail on arm64, you will likely need to cherry-pick fdfeff0f into your
// kernel.
TEST(sys_ptrace, watchpoint_imprecise) {
// This test relies on the infrastructure to timeout if the test hangs.
run_watchpoint_test<Uint128_t>(watchpoint_imprecise_child, 8, sizeof(void*));
}
static void __attribute__((noinline)) breakpoint_func() {
asm volatile("");
}
static void __attribute__((noreturn)) breakpoint_fork_child() {
// Extra precaution: make sure we go away if anything happens to our parent.
if (prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) == -1) {
perror("prctl(PR_SET_PDEATHSIG)");
_exit(1);
}
if (ptrace(PTRACE_TRACEME, 0, nullptr, nullptr) == -1) {
perror("ptrace(PTRACE_TRACEME)");
_exit(2);
}
raise(SIGSTOP); // Synchronize with the tracer, let it set the breakpoint.
breakpoint_func(); // Now trigger the breakpoint.
_exit(0);
}
static void set_breakpoint(pid_t child) {
uintptr_t address = uintptr_t(breakpoint_func);
#if defined(__arm__) || defined(__aarch64__)
address &= ~3;
const unsigned byte_mask = 0xf;
const unsigned enable = 1;
const unsigned control = byte_mask << 5 | enable;
#ifdef __arm__
ASSERT_EQ(0, ptrace(PTRACE_SETHBPREGS, child, 1, &address)) << strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_SETHBPREGS, child, 2, &control)) << strerror(errno);
#else // aarch64
user_hwdebug_state dreg_state;
memset(&dreg_state, 0, sizeof dreg_state);
dreg_state.dbg_regs[0].addr = reinterpret_cast<uintptr_t>(address);
dreg_state.dbg_regs[0].ctrl = control;
iovec iov;
iov.iov_base = &dreg_state;
iov.iov_len = offsetof(user_hwdebug_state, dbg_regs) + sizeof(dreg_state.dbg_regs[0]);
ASSERT_EQ(0, ptrace(PTRACE_SETREGSET, child, NT_ARM_HW_BREAK, &iov)) << strerror(errno);
#endif
#elif defined(__i386__) || defined(__x86_64__)
ASSERT_EQ(0, ptrace(PTRACE_POKEUSER, child, offsetof(user, u_debugreg[0]), address))
<< strerror(errno);
errno = 0;
unsigned data = ptrace(PTRACE_PEEKUSER, child, offsetof(user, u_debugreg[7]), nullptr);
ASSERT_ERRNO(0);
const unsigned size = 0;
const unsigned enable = 1;
const unsigned type = 0; // Execute
const unsigned mask = 3 << 18 | 3 << 16 | 1;
const unsigned value = size << 18 | type << 16 | enable;
data &= mask;
data |= value;
ASSERT_EQ(0, ptrace(PTRACE_POKEUSER, child, offsetof(user, u_debugreg[7]), data))
<< strerror(errno);
#else
UNUSED(child);
UNUSED(address);
#endif
}
// Test hardware breakpoint API. The test is considered successful if the breakpoints get hit OR the
// system reports that hardware breakpoint support is not present.
TEST(sys_ptrace, hardware_breakpoint) {
pid_t child = fork();
ASSERT_NE(-1, child) << strerror(errno);
if (child == 0) breakpoint_fork_child();
ChildGuard guard(child);
int status;
ASSERT_EQ(child, TEMP_FAILURE_RETRY(waitpid(child, &status, __WALL))) << strerror(errno);
ASSERT_TRUE(WIFSTOPPED(status)) << "Status was: " << status;
ASSERT_EQ(SIGSTOP, WSTOPSIG(status)) << "Status was: " << status;
check_hw_feature_supported(child, HwFeature::Breakpoint);
if (::testing::Test::IsSkipped()) {
return;
}
set_breakpoint(child);
ASSERT_EQ(0, ptrace(PTRACE_CONT, child, nullptr, nullptr)) << strerror(errno);
ASSERT_EQ(child, TEMP_FAILURE_RETRY(waitpid(child, &status, __WALL))) << strerror(errno);
ASSERT_TRUE(WIFSTOPPED(status)) << "Status was: " << status;
ASSERT_EQ(SIGTRAP, WSTOPSIG(status)) << "Status was: " << status;
siginfo_t siginfo;
ASSERT_EQ(0, ptrace(PTRACE_GETSIGINFO, child, nullptr, &siginfo)) << strerror(errno);
ASSERT_EQ(TRAP_HWBKPT, siginfo.si_code);
}
class PtraceResumptionTest : public ::testing::Test {
public:
unique_fd worker_pipe_write;
pid_t worker = -1;
pid_t tracer = -1;
PtraceResumptionTest() {
unique_fd worker_pipe_read;
if (!android::base::Pipe(&worker_pipe_read, &worker_pipe_write)) {
err(1, "failed to create pipe");
}
// Second pipe to synchronize the Yama ptracer setup.
unique_fd worker_pipe_setup_read, worker_pipe_setup_write;
if (!android::base::Pipe(&worker_pipe_setup_read, &worker_pipe_setup_write)) {
err(1, "failed to create pipe");
}
worker = fork();
if (worker == -1) {
err(1, "failed to fork worker");
} else if (worker == 0) {
char buf;
// Allow the tracer process, which is not a direct process ancestor, to
// be able to use ptrace(2) on this process when Yama LSM is active.
if (prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0) == -1) {
// if Yama is off prctl(PR_SET_PTRACER) returns EINVAL - don't log in this
// case since it's expected behaviour.
if (errno != EINVAL) {
err(1, "prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY) failed for pid %d", getpid());
}
}
worker_pipe_setup_write.reset();
worker_pipe_write.reset();
TEMP_FAILURE_RETRY(read(worker_pipe_read.get(), &buf, sizeof(buf)));
exit(0);
} else {
// Wait until the Yama ptracer is setup.
char buf;
worker_pipe_setup_write.reset();
TEMP_FAILURE_RETRY(read(worker_pipe_setup_read.get(), &buf, sizeof(buf)));
}
}
~PtraceResumptionTest() override {
}
void AssertDeath(int signo);
void StartTracer(std::function<void()> f) {
tracer = fork();
ASSERT_NE(-1, tracer);
if (tracer == 0) {
f();
if (HasFatalFailure()) {
exit(1);
}
exit(0);
}
}
bool WaitForTracer() {
if (tracer == -1) {
errx(1, "tracer not started");
}
int result;
pid_t rc = TEMP_FAILURE_RETRY(waitpid(tracer, &result, 0));
if (rc != tracer) {
printf("waitpid returned %d (%s)\n", rc, strerror(errno));
return false;
}
if (!WIFEXITED(result) && !WIFSIGNALED(result)) {
printf("!WIFEXITED && !WIFSIGNALED\n");
return false;
}
if (WIFEXITED(result)) {
if (WEXITSTATUS(result) != 0) {
printf("tracer failed\n");
return false;
}
}
return true;
}
bool WaitForWorker() {
if (worker == -1) {
errx(1, "worker not started");
}
int result;
pid_t rc = TEMP_FAILURE_RETRY(waitpid(worker, &result, WNOHANG));
if (rc != 0) {
printf("worker exited prematurely\n");
return false;
}
worker_pipe_write.reset();
rc = TEMP_FAILURE_RETRY(waitpid(worker, &result, 0));
if (rc != worker) {
printf("waitpid for worker returned %d (%s)\n", rc, strerror(errno));
return false;
}
if (!WIFEXITED(result)) {
printf("worker didn't exit\n");
return false;
}
if (WEXITSTATUS(result) != 0) {
printf("worker exited with status %d\n", WEXITSTATUS(result));
return false;
}
return true;
}
};
static void wait_for_ptrace_stop(pid_t pid) {
while (true) {
int status;
pid_t rc = TEMP_FAILURE_RETRY(waitpid(pid, &status, __WALL));
if (rc != pid) {
abort();
}
if (WIFSTOPPED(status)) {
return;
}
}
}
TEST_F(PtraceResumptionTest, smoke) {
// Make sure that the worker doesn't exit before the tracer stops tracing.
StartTracer([this]() {
ASSERT_EQ(0, ptrace(PTRACE_SEIZE, worker, 0, 0)) << strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_INTERRUPT, worker, 0, 0)) << strerror(errno);
wait_for_ptrace_stop(worker);
std::this_thread::sleep_for(500ms);
});
worker_pipe_write.reset();
std::this_thread::sleep_for(250ms);
int result;
ASSERT_EQ(0, TEMP_FAILURE_RETRY(waitpid(worker, &result, WNOHANG)));
ASSERT_TRUE(WaitForTracer());
ASSERT_EQ(worker, TEMP_FAILURE_RETRY(waitpid(worker, &result, 0)));
}
TEST_F(PtraceResumptionTest, seize) {
StartTracer([this]() { ASSERT_EQ(0, ptrace(PTRACE_SEIZE, worker, 0, 0)) << strerror(errno); });
ASSERT_TRUE(WaitForTracer());
ASSERT_TRUE(WaitForWorker());
}
TEST_F(PtraceResumptionTest, seize_interrupt) {
StartTracer([this]() {
ASSERT_EQ(0, ptrace(PTRACE_SEIZE, worker, 0, 0)) << strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_INTERRUPT, worker, 0, 0)) << strerror(errno);
wait_for_ptrace_stop(worker);
});
ASSERT_TRUE(WaitForTracer());
ASSERT_TRUE(WaitForWorker());
}
TEST_F(PtraceResumptionTest, seize_interrupt_cont) {
StartTracer([this]() {
ASSERT_EQ(0, ptrace(PTRACE_SEIZE, worker, 0, 0)) << strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_INTERRUPT, worker, 0, 0)) << strerror(errno);
wait_for_ptrace_stop(worker);
ASSERT_EQ(0, ptrace(PTRACE_CONT, worker, 0, 0)) << strerror(errno);
});
ASSERT_TRUE(WaitForTracer());
ASSERT_TRUE(WaitForWorker());
}
TEST_F(PtraceResumptionTest, zombie_seize) {
StartTracer([this]() { ASSERT_EQ(0, ptrace(PTRACE_SEIZE, worker, 0, 0)) << strerror(errno); });
ASSERT_TRUE(WaitForWorker());
ASSERT_TRUE(WaitForTracer());
}
TEST_F(PtraceResumptionTest, zombie_seize_interrupt) {
StartTracer([this]() {
ASSERT_EQ(0, ptrace(PTRACE_SEIZE, worker, 0, 0)) << strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_INTERRUPT, worker, 0, 0)) << strerror(errno);
wait_for_ptrace_stop(worker);
});
ASSERT_TRUE(WaitForWorker());
ASSERT_TRUE(WaitForTracer());
}
TEST_F(PtraceResumptionTest, zombie_seize_interrupt_cont) {
StartTracer([this]() {
ASSERT_EQ(0, ptrace(PTRACE_SEIZE, worker, 0, 0)) << strerror(errno);
ASSERT_EQ(0, ptrace(PTRACE_INTERRUPT, worker, 0, 0)) << strerror(errno);
wait_for_ptrace_stop(worker);
ASSERT_EQ(0, ptrace(PTRACE_CONT, worker, 0, 0)) << strerror(errno);
});
ASSERT_TRUE(WaitForWorker());
ASSERT_TRUE(WaitForTracer());
}