| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <lib/arch/arm64/system.h> |
| #include <lib/elf-psabi/sp.h> |
| #include <lib/fit/defer.h> |
| #include <lib/zircon-internal/default_stack_size.h> |
| #include <lib/zx/exception.h> |
| #include <lib/zx/process.h> |
| #include <lib/zx/thread.h> |
| #include <zircon/features.h> |
| #include <zircon/hw/debug/arm64.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/threads.h> |
| |
| #include <test-utils/test-utils.h> |
| #include <zxtest/zxtest.h> |
| |
| namespace { |
| |
| #if defined(__aarch64__) |
| |
| constexpr size_t kTagShift = 56; |
| constexpr uint8_t kTestTag = 0xAB; |
| |
| constexpr uint64_t AddTag(uintptr_t ptr, uint8_t tag) { |
| constexpr uint64_t kTagMask = UINT64_C(0xff) << kTagShift; |
| ZX_ASSERT((kTagMask & ptr) == 0 && "Expected an untagged pointer."); |
| return (static_cast<uint64_t>(tag) << kTagShift) | static_cast<uint64_t>(ptr); |
| } |
| |
| template <typename T> |
| T* AddTag(T* ptr, uint8_t tag) { |
| return reinterpret_cast<T*>(AddTag(reinterpret_cast<uintptr_t>(ptr), tag)); |
| } |
| |
| // Disable sanitizers for this because any sanitizer that involves doing a |
| // right shift to get a shadow memory location could cause a tag to leak into |
| // bit 55, leading to an incorrect shadow being referenced. This will affect |
| // ASan and eventually HWASan. |
| #ifdef __clang__ |
| [[clang::no_sanitize("all")]] |
| #endif |
| void DerefTaggedPtr(int* ptr) { |
| *ptr = 1; |
| } |
| |
| TEST(TopByteIgnoreTests, AddressTaggingGetSystemFeaturesAArch64) { |
| uint32_t features = 0; |
| ASSERT_OK(zx_system_get_features(ZX_FEATURE_KIND_ADDRESS_TAGGING, &features)); |
| ASSERT_EQ(features, ZX_ARM64_FEATURE_ADDRESS_TAGGING_TBI); |
| |
| // Since TBI is supported, we can access tagged pointers. |
| int val = 0; |
| DerefTaggedPtr(AddTag(&val, kTestTag)); |
| ASSERT_EQ(val, 1); |
| } |
| |
| using crash_function_t = void (*)(uintptr_t arg1, uintptr_t arg2); |
| |
| // To test the crashing cases, we'll spawn a raw Zircon thread with no C |
| // library assistance so there are no hidden data structures to clean up after |
| // the thread is killed. |
| void CatchCrash(crash_function_t crash_function, uintptr_t arg1, |
| fit::function<void(zx::thread&)> before_start, zx_exception_report_t* report) { |
| zx::thread crash_thread; |
| constexpr std::string_view kThreadName = "Address tagging test thread"; |
| ASSERT_OK(zx::thread::create(*zx::process::self(), kThreadName.data(), kThreadName.size(), 0, |
| &crash_thread)); |
| |
| zx::suspend_token suspend; |
| if (before_start) { |
| // This ensures the thread will be suspended after starting. This is needed |
| // for writing the thread state after it's running, but before we run anything |
| // in the entry point. |
| ASSERT_OK(crash_thread.suspend(&suspend)); |
| ASSERT_TRUE(suspend); |
| } |
| |
| // Set up to receive thread exceptions for the new thread. |
| zx::channel exception_channel; |
| ASSERT_OK(crash_thread.create_exception_channel(0, &exception_channel)); |
| |
| // Start it running with a stack and PC at the crash function's entry point. |
| constexpr size_t kThreadStackSize = ZIRCON_DEFAULT_STACK_SIZE; |
| std::unique_ptr<std::byte[]> crash_thread_stack = std::make_unique<std::byte[]>(kThreadStackSize); |
| const uintptr_t pc = reinterpret_cast<uintptr_t>(crash_function); |
| const uintptr_t sp = compute_initial_stack_pointer( |
| reinterpret_cast<uintptr_t>(crash_thread_stack.get()), kThreadStackSize); |
| ASSERT_OK(crash_thread.start(pc, sp, arg1, 0)); |
| |
| if (before_start) { |
| // The thread is now running, but it should be immediately suspended. |
| zx_signals_t observed; |
| ASSERT_OK(crash_thread.wait_one(ZX_THREAD_SUSPENDED, zx::time::infinite(), &observed)); |
| ASSERT_NE(observed & ZX_THREAD_SUSPENDED, 0); |
| |
| // Run any setup while the thread is suspended but before we dive into |
| // the function. |
| before_start(crash_thread); |
| |
| // Resume the thread. |
| suspend.reset(); |
| ASSERT_EQ(crash_thread.wait_one(ZX_THREAD_RUNNING, zx::time::infinite(), &observed), ZX_OK); |
| } |
| |
| // Wait for the exception channel to be readable. This will happen when |
| // thread crashes and triggers the exception. |
| tu_channel_wait_readable(exception_channel.get()); |
| |
| // Get the FAR from the exception report. |
| ASSERT_OK(crash_thread.get_info(ZX_INFO_THREAD_EXCEPTION_REPORT, report, sizeof(*report), nullptr, |
| nullptr)); |
| |
| // Read the exception message. |
| zx::exception exc; |
| zx_exception_info_t exc_info; |
| uint32_t nbytes, nhandles; |
| ASSERT_OK(exception_channel.read(0, &exc_info, exc.reset_and_get_address(), sizeof(exc_info), 1, |
| &nbytes, &nhandles)); |
| ASSERT_EQ(sizeof(exc_info), nbytes); |
| ASSERT_EQ(1, nhandles); |
| |
| // We can also retrieve the FAR from the thread debug regs. Let's just make |
| // sure it's the same as what's in the exception report. |
| zx_thread_state_debug_regs_t regs = {}; |
| ASSERT_OK(crash_thread.read_state(ZX_THREAD_STATE_DEBUG_REGS, ®s, sizeof(regs))); |
| ASSERT_EQ(report->context.arch.u.arm_64.far, regs.far); |
| |
| // When the exception handle is closed (by the zx::exception destructor at |
| // the end of the function), the thread will resume from the exception. Set |
| // it up to "resume" by doing an immediate thread exit. This should make it |
| // safe to assume its stack will never be used again from here on out. (The |
| // stack will also be freed by a destructor at the end of the function.) |
| constexpr uint32_t kExceptionState = ZX_EXCEPTION_STATE_THREAD_EXIT; |
| ASSERT_OK(exc.set_property(ZX_PROP_EXCEPTION_STATE, &kExceptionState, sizeof(kExceptionState))); |
| } |
| |
| #ifdef __clang__ |
| [[clang::no_sanitize("all")]] |
| #endif |
| [[noreturn]] void |
| DerefTaggedPtrCrash(uintptr_t arg1, uintptr_t /*arg2*/) { |
| *(reinterpret_cast<int*>(arg1)) = 1; |
| __builtin_trap(); |
| } |
| |
| TEST(TopByteIgnoreTests, TaggedFARSegfault) { |
| // This is effectively a nullptr dereference. |
| uintptr_t tagged_ptr = AddTag(0, kTestTag); |
| zx_exception_report_t report = {}; |
| ASSERT_NO_FATAL_FAILURE( |
| CatchCrash(DerefTaggedPtrCrash, tagged_ptr, /*before_start=*/nullptr, &report)); |
| ASSERT_EQ(report.context.arch.u.arm_64.far, tagged_ptr); |
| } |
| |
| int gVariableToChange = 0; |
| |
| void SetupWatchpoint(zx::thread& crash_thread) { |
| zx_thread_state_debug_regs_t debug_regs = {}; |
| |
| // Turn on this HW watchpoint. |
| ARM64_DBGWCR_E_SET(&debug_regs.hw_wps[0].dbgwcr, 1); |
| |
| // The BAS bits form an 8-bit mask that filters out matches on the aligned |
| // 8-byte address range indicated by the DBGWVR value based on the byte(s) |
| // accessed. So setting this to 0xff ensures that any kind of access to any |
| // of the 8 bytes will be trapped. |
| ARM64_DBGWCR_BAS_SET(&debug_regs.hw_wps[0].dbgwcr, 0xff); |
| |
| // Only watch stores. |
| ARM64_DBGWCR_LSC_SET(&debug_regs.hw_wps[0].dbgwcr, 0b10); |
| |
| // Use the untagged address. We should be able to compare against up to bit |
| // 55 when doing watchpoint address comparisons. The ARM spec also requires |
| // that bits 63:49 be a sign extension of bit 48 (that is, it cannot be tagged) |
| // (D13.3.12). |
| debug_regs.hw_wps[0].dbgwvr = reinterpret_cast<uintptr_t>(&gVariableToChange); |
| |
| ASSERT_OK(crash_thread.write_state(ZX_THREAD_STATE_DEBUG_REGS, &debug_regs, sizeof(debug_regs))); |
| } |
| |
| TEST(TopByteIgnoreTests, TaggedFARWatchpoint) { |
| uint64_t watched_addr = reinterpret_cast<uint64_t>(&gVariableToChange); |
| |
| uintptr_t tagged_ptr = AddTag(watched_addr, kTestTag); |
| zx_exception_report_t report = {}; |
| ASSERT_NO_FATAL_FAILURE(CatchCrash(DerefTaggedPtrCrash, tagged_ptr, SetupWatchpoint, &report)); |
| EXPECT_EQ(report.header.type, ZX_EXCP_HW_BREAKPOINT); |
| EXPECT_EQ(report.context.arch.u.arm_64.far, tagged_ptr); |
| } |
| |
| static zx_koid_t get_object_koid(zx_handle_t handle) { |
| zx_info_handle_basic_t info; |
| if (zx_object_get_info(handle, ZX_INFO_HANDLE_BASIC, &info, sizeof(info), NULL, NULL) != ZX_OK) { |
| return ZX_KOID_INVALID; |
| } |
| return info.koid; |
| } |
| |
| void TestFutexWaitWake(uint8_t wait_tag, uint8_t wake_tag, uint8_t get_owner_tag) { |
| constexpr uint32_t kThreadWakeAllCount = std::numeric_limits<uint32_t>::max(); |
| constexpr zx_futex_t kFutexVal = 1; |
| zx_futex_t futex = kFutexVal; |
| thrd_t thread; |
| struct ThreadArgs { |
| zx_futex_t* futex; |
| std::atomic<bool> about_to_wait; |
| zx_handle_t new_owner; |
| }; |
| ThreadArgs thread_args{ |
| AddTag(&futex, wait_tag), |
| false, |
| thrd_get_zx_handle(thrd_current()), |
| }; |
| |
| // Start a new thread that will wait until the current thread wakes the futex. |
| ASSERT_EQ(thrd_create( |
| &thread, |
| [](void* arg) -> int { |
| auto* thread_args = reinterpret_cast<ThreadArgs*>(arg); |
| thread_args->about_to_wait.store(true); |
| // Note that we pass in the futex value separately rather than derefing the futex |
| // pointer because, under ASan, a tagged futex pointer will cause some tag bits to |
| // spill into the rest of the pointer when calculating shadow memory if we do the |
| // dereference. This avoids us needing to do that. |
| zx_status_t status = |
| zx_futex_wait(thread_args->futex, kFutexVal, thread_args->new_owner, |
| zx_deadline_after(ZX_TIME_INFINITE)); |
| return static_cast<int>(status); |
| }, |
| &thread_args), |
| 0); |
| |
| // If something goes wrong and we bail out early, do our best to shut down as cleanly |
| auto cleanup = fit::defer([&]() { |
| EXPECT_OK(zx_futex_wake(&futex, kThreadWakeAllCount)); |
| int result; |
| EXPECT_EQ(thrd_join(thread, &result), 0); |
| EXPECT_EQ(result, 0); |
| }); |
| |
| // Ensure that we're waiting on the futex before we wake it. |
| zx_info_thread_t info = {}; |
| while (info.state != ZX_THREAD_STATE_BLOCKED_FUTEX) { |
| ASSERT_OK(zx_object_get_info(thrd_get_zx_handle(thread), ZX_INFO_THREAD, &info, sizeof(info), |
| nullptr, nullptr)); |
| } |
| |
| // Check the owner. |
| zx_koid_t owner; |
| EXPECT_OK(zx_futex_get_owner(AddTag(&futex, get_owner_tag), &owner)); |
| EXPECT_EQ(owner, get_object_koid(thrd_get_zx_handle(thrd_current()))); |
| } |
| |
| TEST(TopByteIgnoreTests, FutexWaitWake) { |
| TestFutexWaitWake(0, 0, 0); // Wait and wake same futex on the same tag. |
| TestFutexWaitWake(kTestTag, kTestTag, kTestTag); // Wait and wake same futex on the same tag. |
| TestFutexWaitWake(kTestTag, kTestTag + 1, |
| kTestTag + 2); // Wait and wake same futex on different tags. |
| } |
| |
| #ifdef __clang__ |
| [[clang::no_sanitize("all")]] |
| #endif |
| uint8_t |
| UnsanitizedLoad(volatile uint8_t* ptr) { |
| return *ptr; |
| } |
| |
| TEST(TopByteIgnoreTests, VmmPageFaultHandlerDataAbort) { |
| zx_handle_t root_vmar = zx_vmar_root_self(); |
| |
| // Create a new vmar to manage that we will eventually decommit. |
| zx_handle_t decommit_vmar; |
| uintptr_t addr; |
| ASSERT_OK(zx_vmar_allocate(root_vmar, |
| ZX_VM_CAN_MAP_SPECIFIC | ZX_VM_CAN_MAP_READ | ZX_VM_CAN_MAP_WRITE, 0, |
| zx_system_get_page_size() * 8, &decommit_vmar, &addr)); |
| |
| // Create a vmo we can write to. |
| zx_handle_t vmo; |
| ASSERT_OK(zx_vmo_create(zx_system_get_page_size(), 0, &vmo)); |
| |
| uint64_t mapping_addr; |
| ASSERT_OK(zx_vmar_map(decommit_vmar, ZX_VM_SPECIFIC | ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, |
| zx_system_get_page_size(), vmo, 0, zx_system_get_page_size(), |
| &mapping_addr)); |
| |
| // We should be able to write normally. |
| *reinterpret_cast<volatile uint8_t*>(mapping_addr) = 42; |
| |
| // After decommitting, the page is zero-filled. It will still be accessible, but not mapped to |
| // anything. This will result in a permission fault that will be handled successfully by the |
| // kernel's page fault handler. What we want to test for is that even if this pointer is tagged, |
| // then the kernel will still be able to handle this page fault successfully. |
| EXPECT_OK(zx_vmar_op_range(decommit_vmar, ZX_VMAR_OP_DECOMMIT, mapping_addr, |
| zx_system_get_page_size(), nullptr, 0)); |
| mapping_addr = AddTag(mapping_addr, kTestTag); |
| |
| // Do not do a regular dereference because ASan will right-shift the tag into the address bits |
| // then complain that this address doesn't have a corresponding shadow. |
| EXPECT_EQ(UnsanitizedLoad(reinterpret_cast<volatile uint8_t*>(mapping_addr)), 0); |
| } |
| |
| arch::ArmExceptionSyndromeRegister::ExceptionClass GetEC(uint64_t esr) { |
| return arch::ArmExceptionSyndromeRegister::Get().FromValue(esr).ec(); |
| } |
| |
| // Making it global static ensures this is in rodata. |
| static constexpr uint32_t kUdf0 = 0; |
| |
| TEST(TopByteIgnoreTests, InstructionAbortNoTag) { |
| // Unlike a data abort, instruction aborts on AArch64 will not include the tag in the FAR, so a |
| // tag will never reach the VM layer via an instruction abort. This test verifies the FAR does not |
| // include the tag in this case. |
| uintptr_t pc = AddTag(reinterpret_cast<uintptr_t>(&kUdf0), kTestTag); |
| zx_exception_report_t report = {}; |
| |
| ASSERT_NO_FATAL_FAILURE(CatchCrash(reinterpret_cast<crash_function_t>(pc), /*arg1=*/0, |
| /*before_start=*/nullptr, &report)); |
| EXPECT_EQ(report.header.type, ZX_EXCP_FATAL_PAGE_FAULT); |
| ASSERT_EQ(GetEC(report.context.arch.u.arm_64.esr), |
| arch::ArmExceptionSyndromeRegister::ExceptionClass::kInstructionAbortLowerEl); |
| EXPECT_EQ(report.context.arch.u.arm_64.far, reinterpret_cast<uintptr_t>(&kUdf0)); |
| } |
| |
| #elif defined(__x86_64__) |
| |
| TEST(TopByteIgnoreTests, AddressTaggingGetSystemFeaturesX86_64) { |
| uint32_t features = 0; |
| ASSERT_OK(zx_system_get_features(ZX_FEATURE_KIND_ADDRESS_TAGGING, &features)); |
| ASSERT_EQ(features, 0); |
| } |
| |
| #endif |
| |
| } // namespace |