blob: 9886e0b4e7a75c31a1fe8afce47d42825421932c [file]
// Copyright 2026 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 <lib/captive-thread/captive-thread.h>
#include <lib/captive-thread/registers.h>
#include <lib/captive-thread/testing/matchers.h>
#include <lib/elfldltl/machine.h>
#include <zircon/syscalls.h>
#include <cstdint>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace {
using ::testing::_;
using ::testing::AllOf;
using ::testing::Eq;
using ::testing::Field;
using ::testing::IsSupersetOf;
using ::testing::Ne;
using ::testing::Not;
using ::testing::Pair;
std::string_view TestName() {
return ::testing::UnitTest::GetInstance()->current_test_info()->name();
}
// Do a bit cast and also hide the value from the compiler so it cannot be
// constant-folded in callers.
template <typename T>
requires(sizeof(T) <= sizeof(uintptr_t))
T LaunderAs(auto value)
requires(sizeof(value) <= sizeof(uintptr_t))
{
T result;
__asm__("" : "=r"(result) : "0"(value));
return result;
}
// This always crashes and never returns, but the compiler doesn't know that.
// In fact, it can return if the PC is advanced past the trap instruction.
void Crash() {
// This is the same instruction that __builtin_trap() emits. But the
// compiler knows that __builtin_trap() cannot return and so it may decide to
// move its instruction out of the straight-ahead path even if it's hidden
// that the trap path is always taken.
__asm__ volatile(
#ifdef __aarch64__
"brk #1"
#elifdef __riscv
"unimp"
#elifdef __x86_64__
"ud2"
#endif
);
}
// This is called just to demonstrate actual call and return instructions,
// since it cannot be inlined.
[[gnu::noinline]] void CallAndReturn() {}
template <uintptr_t Address = 0>
[[clang::no_sanitize("all")]] void PageFault() {
*LaunderAs<volatile int*>(Address) = 0;
}
TEST(CaptiveThreadTests, Destroy) {
{
// Destroyed immediately after construction.
captive_thread::CaptiveThread thread([] {});
}
}
TEST(CaptiveThreadTests, ExplicitJoin) {
captive_thread::CaptiveThread thread([] {});
EXPECT_FALSE(thread.Joined());
thread.ForceJoin();
EXPECT_TRUE(thread.Joined());
}
TEST(CaptiveThreadTests, BlockUntilSuccess) {
bool ran = false;
{
captive_thread::CaptiveThread thread([&ran] { ran = true; });
thread.BlockUntilSuccess();
EXPECT_TRUE(thread.Joined());
}
EXPECT_TRUE(ran);
}
TEST(CaptiveThreadTests, CrashAndJoin) {
captive_thread::CaptiveThread thread{Crash};
thread.ForceJoin();
}
TEST(CaptiveThreadTests, WaitForException) {
bool started = false, finished = false;
{
captive_thread::CaptiveThread thread{[&started, &finished] {
started = true;
Crash();
finished = true; // Should not be reached, but the compiler won't know.
}};
zx::result result = thread.WaitForException();
EXPECT_TRUE(result.is_ok()) << result.status_value();
EXPECT_FALSE(thread.Joined());
EXPECT_TRUE(thread.InException());
EXPECT_TRUE(thread.IsStopped());
}
EXPECT_TRUE(started);
EXPECT_FALSE(finished);
}
TEST(CaptiveThreadTests, ExceptionReport) {
captive_thread::CaptiveThread thread{Crash};
zx::result result = thread.WaitForException();
EXPECT_TRUE(result.is_ok()) << result.status_value();
EXPECT_THAT(thread.ExceptionReport(),
Optional(Field(&zx_exception_report_t::header,
Field(&zx_exception_header_t::type, captive_thread::kTrapException))));
}
TEST(CaptiveThreadTests, Suspend) {
captive_thread::CaptiveThread thread{[] {
while (true) {
zx::nanosleep(zx::time::infinite());
}
}};
zx::result suspend_result = thread.Suspend();
ASSERT_TRUE(suspend_result.is_ok()) << suspend_result.status_value();
EXPECT_TRUE(thread.InSuspend());
EXPECT_TRUE(thread.IsStopped());
EXPECT_FALSE(thread.InException());
zx::result wait_result = thread.WaitForStop();
EXPECT_TRUE(wait_result.is_ok()) << wait_result.status_value();
}
TEST(CaptiveThreadTests, SuspendVsException) {
captive_thread::CaptiveThread thread{Crash};
// It might already have an exception pending, but we haven't checked yet, so
// Suspend() will always start a suspension.
zx::result suspend_result = thread.Suspend();
ASSERT_TRUE(suspend_result.is_ok()) << suspend_result.status_value();
EXPECT_TRUE(thread.InSuspend());
EXPECT_TRUE(thread.IsStopped());
// This might get either the exception or the suspension first.
zx::result wait_result = thread.WaitForStop();
EXPECT_TRUE(wait_result.is_ok()) << wait_result.status_value();
// Either way, the suspension is still in place.
EXPECT_TRUE(thread.IsStopped());
EXPECT_TRUE(thread.InSuspend());
}
TEST(CaptiveThreadTests, Matchers) {
using captive_thread::testing::IsPageFault;
{
captive_thread::CaptiveThread thread{Crash};
EXPECT_THAT(thread.WaitForException(), GotException(captive_thread::testing::IsTrap()));
EXPECT_THAT(thread.WaitForException(), captive_thread::testing::IsTrap());
}
{
captive_thread::CaptiveThread thread{PageFault};
EXPECT_THAT(thread.WaitForException(), GotException(IsPageFault()));
EXPECT_THAT(thread.WaitForException(), IsPageFault());
EXPECT_THAT(thread.WaitForException(), GotException(IsPageFault(0)));
}
{
captive_thread::CaptiveThread thread{PageFault<0x123>};
EXPECT_THAT(thread.WaitForException(), IsPageFault(Ne(0)));
EXPECT_THAT(thread.WaitForException(), IsPageFault(Eq(0x123)));
}
{
// Basic death test usage.
using captive_thread::CaptiveThread;
using captive_thread::testing::GotException;
EXPECT_THAT(CaptiveThread(Crash).WaitForException(), GotException());
EXPECT_THAT(CaptiveThread([] {}).WaitForException(), Not(GotException()));
}
}
TEST(CaptiveThreadTests, Registers) {
captive_thread::CaptiveThread thread{Crash};
EXPECT_THAT(thread.WaitForException(), captive_thread::testing::HasRegisters());
EXPECT_THAT(thread.WaitForException(), HasRegisters(captive_thread::testing::WithPc(Ne(0))));
EXPECT_THAT(thread.Registers(), HasRegisters(captive_thread::testing::WithTp(Ne(0))));
auto regs = thread.Registers();
EXPECT_TRUE(regs.is_ok()) << regs.status_string();
if (regs.is_ok()) {
EXPECT_THAT(*regs, captive_thread::testing::WithSp(Ne(0)));
// The names differ, but there are pairs for each special register.
auto special = captive_thread::SpecialRegisters(*regs);
std::vector matchers{
Pair(_, special.pc()),
Pair(_, special.sp()),
Pair(_, special.tp()),
};
if (std::optional<uint64_t> ra = special.ra()) {
matchers.push_back(Pair(_, *ra));
}
if (std::optional<uint64_t> scsp = special.scsp()) {
matchers.push_back(Pair(_, *scsp));
}
auto special_regs = captive_thread::testing::AsContainer(IsSupersetOf(matchers));
EXPECT_THAT(thread.Registers(), HasRegisters(special_regs));
}
}
TEST(CaptiveThreadTests, ResolveException) {
bool started = true, finished = false;
captive_thread::CaptiveThread thread{[&started, &finished] {
started = true;
Crash();
finished = true;
}};
ASSERT_THAT(thread.WaitForException(),
AllOf(captive_thread::testing::IsTrap(), //
captive_thread::testing::HasRegisters()));
EXPECT_TRUE(started);
EXPECT_FALSE(finished);
auto regs = *thread.Registers();
captive_thread::SpecialRegisters(regs).pc() += captive_thread::kTrapInstructionSize;
zx::result result = thread.SetRegisters(regs);
ASSERT_TRUE(result.is_ok()) << result.status_string();
thread.ResolveException();
EXPECT_FALSE(thread.IsStopped());
ASSERT_THAT(thread.WaitForException(), Not(captive_thread::testing::GotException()))
<< ::testing::PrintToString(captive_thread::testing::RegistersAsContainer(thread));
ASSERT_FALSE(thread.IsStopped());
thread.BlockUntilSuccess();
EXPECT_TRUE(finished);
}
TEST(CaptiveThreadTests, SingleStep) {
using captive_thread::testing::GotSingleStep;
bool started = true, finished = false;
captive_thread::CaptiveThread thread{[&started, &finished] {
started = true;
Crash();
// Do some stuff that's interesting to single-step through.
if (LaunderAs<bool>(true)) { // Conditional branch, taken.
CallAndReturn();
}
if (LaunderAs<bool>(false)) { // Conditional branch, not taken.
Crash();
}
finished = true;
}};
ASSERT_THAT(thread.WaitForException(),
AllOf(captive_thread::testing::IsTrap(), //
captive_thread::testing::HasRegisters()));
EXPECT_TRUE(started);
EXPECT_FALSE(finished);
auto regs = *thread.Registers();
captive_thread::SpecialRegisters(regs).pc() += captive_thread::kTrapInstructionSize;
zx::result result = thread.SetRegisters(regs);
ASSERT_TRUE(result.is_ok()) << result.status_string();
zx::result step = thread.ResolveExceptionSingleStep();
ASSERT_TRUE(step.is_ok()) << step.status_string();
EXPECT_THAT(thread.WaitForException(), GotSingleStep());
EXPECT_FALSE(finished); // It needs more than one instruction to get there.
// Single-step until it's done the store.
int steps = 1;
do {
zx::result step = thread.ResolveExceptionSingleStep();
ASSERT_TRUE(step.is_ok()) << step.status_value() << " at step " << steps;
++steps;
} while (::testing::Value(thread.WaitForException(), GotSingleStep()) && !finished);
// If the loop broke for another reason, the assert will report why.
EXPECT_TRUE(finished) << "after step " << steps;
ASSERT_THAT(thread.WaitForException(), GotSingleStep()) << "after step " << steps;
// Now let it run to completion without getting another single-step trap.
thread.ResolveException();
thread.BlockUntilSuccess();
}
TEST(CaptiveThreadTests, CreateRaw) {
using captive_thread::ArgumentRegisters;
using captive_thread::testing::GotException;
using captive_thread::testing::HasRegisters;
using captive_thread::testing::IsPageFault;
using captive_thread::testing::WithPc;
using captive_thread::testing::WithSp;
using ::testing::AllOf;
using ::testing::ElementsAre;
constexpr captive_thread::CaptiveThread::Raw kRegs{
.pc = 0x1230, // Canonical but guaranteed to fault.
.sp = 0x4560, // Canonical and ABI-aligned but guaranteed to fault.
// Arbitrary values.
.arg1 = 0xdeadbeef12345678,
.arg2 = 0xdeadbeef87654321,
};
zx::result result = captive_thread::CaptiveThread::CreateRaw(TestName(), kRegs);
ASSERT_TRUE(result.is_ok()) << result.status_string();
std::unique_ptr thread = *std::move(result);
ASSERT_TRUE(thread);
ASSERT_THAT(thread->WaitForException(),
AllOf(GotException(IsPageFault(0x1230)),
HasRegisters(AllOf(WithPc(kRegs.pc), WithSp(kRegs.sp)))));
const std::vector<uint64_t> args{
std::from_range,
std::views::take(ArgumentRegisters(*thread->Registers()), 2),
};
EXPECT_THAT(args, ElementsAre(kRegs.arg1, kRegs.arg2));
}
constexpr size_t kRawStackSize = 1024; // Plenty to call into the vDSO.
class alignas(elfldltl::AbiTraits<>::kStackAlignment<size_t>) RawStack
: public std::array<std::byte, kRawStackSize> {
public:
constexpr RawStack() = default;
uintptr_t sp() const {
return elfldltl::AbiTraits<>::InitialStackPointer(reinterpret_cast<uintptr_t>(data()), size());
}
// Registers to start running on this stack in the vDSO.
template <auto* Call = &zx_thread_exit>
captive_thread::CaptiveThread::Raw call_regs(uint64_t arg1 = 0, uint64_t arg2 = 0) const {
return {
.pc = reinterpret_cast<uintptr_t>(Call),
.sp = sp(),
.arg1 = arg1,
.arg2 = arg2,
};
}
};
TEST(CaptiveThreadTests, RawExit) {
using captive_thread::testing::GotException;
using ::testing::Not;
// Start running directly in the vDSO with enough stack for zx_thread_exit().
std::unique_ptr stack = std::make_unique<RawStack>();
zx::result result = captive_thread::CaptiveThread::CreateRaw(TestName(), stack->call_regs());
ASSERT_TRUE(result.is_ok()) << result.status_string();
std::unique_ptr thread = *std::move(result);
ASSERT_TRUE(thread);
ASSERT_THAT(thread->WaitForException(), Not(GotException()));
thread->BlockUntilSuccess();
}
TEST(CaptiveThreadTests, CreateRawSuspended) {
using captive_thread::ArgumentRegisters;
using captive_thread::testing::GotException;
using captive_thread::testing::HasRegisters;
using captive_thread::testing::WithPc;
using captive_thread::testing::WithSp;
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::Not;
// Start running directly in the vDSO with enough stack for zx_thread_exit().
std::unique_ptr stack = std::make_unique<RawStack>();
const auto regs = stack->call_regs(0xdeadbeef1234, 0xdeadbeef5678);
zx::result result = captive_thread::CaptiveThread::CreateRaw(TestName(), regs, true);
ASSERT_TRUE(result.is_ok()) << result.status_string();
std::unique_ptr thread = *std::move(result);
ASSERT_TRUE(thread);
ASSERT_TRUE(thread->InSuspend());
ASSERT_THAT(thread->WaitForStop(), Not(GotException()));
ASSERT_TRUE(thread->IsStopped());
ASSERT_FALSE(thread->InException());
ASSERT_FALSE(thread->ExceptionReport());
ASSERT_THAT(thread->Registers(), HasRegisters(AllOf(WithPc(regs.pc), WithSp(regs.sp))));
const std::vector<uint64_t> args{
std::from_range,
std::views::take(ArgumentRegisters(*thread->Registers()), 2),
};
EXPECT_THAT(args, ElementsAre(regs.arg1, regs.arg2));
thread->Resume();
thread->BlockUntilSuccess();
}
TEST(CaptiveThreadTests, StartRaw) {
using captive_thread::ArgumentRegisters;
using captive_thread::testing::GotException;
using captive_thread::testing::IsPageFault;
using ::testing::AllOf;
std::string_view name = TestName();
zx::thread thread_handle;
zx::result create = zx::make_result(zx::thread::create(
*zx::process::self(), name.data(), static_cast<uint32_t>(name.size()), 0, &thread_handle));
ASSERT_TRUE(create.is_ok()) << create.status_string();
constexpr uint64_t kBadPc = 0x1230;
zx::result result =
captive_thread::CaptiveThread::StartRaw(std::move(thread_handle), {.pc = kBadPc});
ASSERT_TRUE(result.is_ok()) << result.status_string();
std::unique_ptr thread = *std::move(result);
ASSERT_TRUE(thread);
EXPECT_THAT(thread->WaitForException(), GotException(IsPageFault(kBadPc)));
}
TEST(CaptiveThreadTests, StartRawSuspended) {
using captive_thread::ArgumentRegisters;
using captive_thread::testing::GotException;
using captive_thread::testing::HasRegisters;
using captive_thread::testing::IsPageFault;
using captive_thread::testing::WithPc;
using captive_thread::testing::WithSp;
using ::testing::AllOf;
std::string_view name = TestName();
zx::thread thread_handle;
zx::result create = zx::make_result(zx::thread::create(
*zx::process::self(), name.data(), static_cast<uint32_t>(name.size()), 0, &thread_handle));
ASSERT_TRUE(create.is_ok()) << create.status_string();
zx::suspend_token token;
zx::result suspend = zx::make_result(thread_handle.suspend(&token));
ASSERT_TRUE(suspend.is_ok()) << suspend.status_string();
constexpr uint64_t kBadPc = 0x1230;
zx::result result = captive_thread::CaptiveThread::StartRaw( //
std::move(thread_handle), {.pc = kBadPc}, std::move(token));
ASSERT_TRUE(result.is_ok()) << result.status_string();
std::unique_ptr thread = *std::move(result);
ASSERT_TRUE(thread);
ASSERT_TRUE(thread->InSuspend());
EXPECT_THAT(thread->WaitForStop(), Not(GotException()));
EXPECT_THAT(thread->Registers(), HasRegisters(AllOf(WithPc(kBadPc), WithSp(0))));
thread->Resume();
EXPECT_THAT(thread->WaitForException(), GotException(IsPageFault(kBadPc)));
}
} // namespace