/*
* Copyright (c) 2024 Apple Inc. All rights reserved.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. The rights granted to you under the License
* may not be used to create, or enable the creation or redistribution of,
* unlawful or unlicensed copies of an Apple operating system, or to
* circumvent, violate, or enable the circumvention or violation of, any
* terms of an Apple operating system software license agreement.
*
* Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_END@
*/
/*
* try_read_write.c
*
* Helper functions for userspace tests to read or write memory and
* verify that EXC_BAD_ACCESS is or is not generated by that operation.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <ptrauth.h>
#include <darwintest.h>
#include <dispatch/dispatch.h>
#include "exc_helpers.h"
#include "try_read_write.h"
/*
* -- Implementation overview --
*
* try_read_byte() and try_write_byte() operate by performing
* a read or write instruction with a Mach exception handler
* in place.
*
* The exception handler catches EXC_BAD_ACCESS. If the bad access
* came from our designated read or write instructions then it
* records the exception that occurred to thread-local storage
* and moves that thread's program counter to resume execution
* and recover from the exception.
*
* Unrecognized exceptions, and EXC_BAD_ACCESS exceptions from
* unrecognized instructions, either go uncaught or are caught and
* re-raised. In either case they lead to an ordinary crash. This
* means we don't get false positives where the test expects one
* crash but incorrectly passes after crashing in some unrelated way.
* We can be precise about what the fault was and where it came from.
*
* We use Mach exceptions instead of signals because
* on watchOS signal handlers do not receive the thread
* state so they cannot recover from the signal.
*
* try_read_write_exception_handler()
* our exception handler, installed using tests/exc_helpers.c
*
* read_byte() and write_byte()
* our designated read and write instructions, recognized by
* the exception handler and specially structured to allow
* recovery by changing the PC
*
* try_read_write_thread_t
* thread-local storage to record the caught exception
*/
static dispatch_once_t try_read_write_initializer;
static mach_port_t try_read_write_exc_port;
/*
* Bespoke thread-local storage for threads inside try_read_write.
* We can't use pthread local storage because the Mach exception
* handler needs to access it and that exception handler runs on
* a different thread.
*
* Access by the Mach exception thread is safe because the real thread
* is suspended at that point. (This scheme would be unsound if the
* real thread raised an exception while manipulating the thread-local
* data, but we don't try to cover that case.)
*/
typedef struct {
mach_port_t thread;
kern_return_t exception_kr; /* EXC_BAD_ADDRESS sub-code */
uint64_t exception_pc; /* PC of faulting instruction */
uint64_t exception_memory; /* Memory address of faulting access */
} try_read_write_thread_t;
#define TRY_READ_WRITE_MAX_THREADS 128
static pthread_mutex_t try_read_write_thread_list_mutex = PTHREAD_MUTEX_INITIALIZER;
static unsigned try_read_write_thread_count = 0;
static try_read_write_thread_t try_read_write_thread_list[TRY_READ_WRITE_MAX_THREADS];
static __thread try_read_write_thread_t *try_read_write_thread_self;
/*
* Look up the try_read_write_thread_t for a Mach thread.
* If create == true and no info was found, add it to the list.
* Returns NULL if no info was found and create == false.
*/
static __attribute__((overloadable))
try_read_write_thread_t *
thread_info_for_mach_thread(mach_port_t thread_port, bool create)
{
/* first look for a cached value in real thread-local storage */
if (mach_thread_self() == thread_port) {
try_read_write_thread_t *info = try_read_write_thread_self;
if (info) {
return info;
}
}
int err = pthread_mutex_lock(&try_read_write_thread_list_mutex);
assert(err == 0);
/* search the list */
for (unsigned i = 0; i < try_read_write_thread_count; i++) {
try_read_write_thread_t *info = &try_read_write_thread_list[i];
if (info->thread == thread_port) {
pthread_mutex_unlock(&try_read_write_thread_list_mutex);
if (mach_thread_self() == thread_port) {
try_read_write_thread_self = info;
}
return info;
}
}
/* not in list - create if requested */
if (create) {
assert(try_read_write_thread_count < TRY_READ_WRITE_MAX_THREADS);
try_read_write_thread_t *info = &try_read_write_thread_list[try_read_write_thread_count++];
info->thread = thread_port;
info->exception_kr = 0;
pthread_mutex_unlock(&try_read_write_thread_list_mutex);
if (mach_thread_self() == thread_port) {
try_read_write_thread_self = info;
}
return info;
}
pthread_mutex_unlock(&try_read_write_thread_list_mutex);
return NULL;
}
static __attribute__((overloadable))
try_read_write_thread_t *
thread_info_for_mach_thread(mach_port_t thread_port)
{
return thread_info_for_mach_thread(thread_port, false /* create */);
}
/*
* read_byte() and write_byte() are functions that
* read or write memory as their first instruction.
* Used to test memory access that may provoke an exception.
*
* try_read_write_exception_handler() below checks if the exception PC
* is equal to one of these functions. The first instruction must be
* the memory access instruction.
*
* try_read_write_exception_handler() below increments the PC by four bytes.
* The memory access instruction must be padded to exactly four bytes.
*/
static uint64_t __attribute__((naked))
read_byte(mach_vm_address_t addr)
{
#if __arm64__
asm("\n ldrb w0, [x0]"
"\n ret");
#elif __x86_64__
asm("\n movb (%rdi), %al"
"\n nop" /* pad load to four bytes */
"\n nop"
"\n ret");
#else
# error unknown architecture
#endif
}
static void __attribute__((naked))
write_byte(mach_vm_address_t addr, uint8_t value)
{
#if __arm64__
asm("\n strb w1, [x0]"
"\n ret");
#elif __x86_64__
asm("\n movb %sil, (%rdi)"
"\n nop" /* pad store to four bytes */
"\n ret");
#else
# error unknown architecture
#endif
}
/*
* Mach exception handler for EXC_BAD_ACCESS called by exc_helpers.
* Returns the number of bytes to advance the PC to resolve the exception.
*/
static size_t
try_read_write_exception_handler(
__unused mach_port_t task,
mach_port_t thread,
exception_type_t exception,
mach_exception_data_t codes,
uint64_t exception_pc)
{
assert(exception == EXC_BAD_ACCESS);
try_read_write_thread_t *info = thread_info_for_mach_thread(thread);
assert(info); /* we do not expect exceptions from other threads */
uint64_t read_byte_pc = (uint64_t)ptrauth_strip(&read_byte, ptrauth_key_function_pointer);
uint64_t write_byte_pc = (uint64_t)ptrauth_strip(&write_byte, ptrauth_key_function_pointer);
if (exception_pc != read_byte_pc && exception_pc != write_byte_pc) {
/* this exception isn't one of ours - re-raise it */
if (verbose_exc_helper) {
T_LOG("not a try_read_write exception");
}
return EXC_HELPER_HALT;
}
assert(info->exception_kr == 0); /* no nested exceptions allowed */
info->exception_pc = exception_pc;
info->exception_kr = codes[0];
info->exception_memory = codes[1];
if (verbose_exc_helper) {
T_LOG("try_read_write exception: pc 0x%llx kr %d mem 0x%llx",
info->exception_pc, info->exception_kr, info->exception_memory);
}
/* advance pc by 4 bytes to recover */
return 4;
}
/*
* Create an exc_helpers exception handler port and thread,
* and install the exception handler port on this thread.
*/
static void
initialize_exception_handlers(void)
{
try_read_write_exc_port = create_exception_port(EXC_MASK_BAD_ACCESS);
repeat_exception_handler(try_read_write_exc_port, try_read_write_exception_handler);
}
/*
* Begin try_read_write exception handling on this thread.
*/
static void
begin_expected_exceptions(void)
{
dispatch_once(&try_read_write_initializer, ^{
initialize_exception_handlers();
});
try_read_write_thread_t *info = try_read_write_thread_self;
if (!info) {
set_thread_exception_port(try_read_write_exc_port, EXC_MASK_BAD_ACCESS);
info = thread_info_for_mach_thread(mach_thread_self(), true /* create */);
}
info->exception_kr = 0;
info->exception_pc = 0;
info->exception_memory = 0;
}
/*
* End try_read_write exception handling on this thread.
* Returns the caught exception data, if any.
*/
static void
end_expected_exceptions(
kern_return_t * const out_kr,
uint64_t * const out_pc,
uint64_t * const out_memory)
{
try_read_write_thread_t *info = try_read_write_thread_self;
assert(info);
*out_kr = info->exception_kr;
*out_pc = info->exception_pc;
*out_memory = info->exception_memory;
}
extern bool
try_read_byte(
mach_vm_address_t addr,
uint8_t * const out_byte,
kern_return_t * const out_error)
{
kern_return_t exception_kr;
uint64_t exception_pc;
uint64_t exception_memory;
begin_expected_exceptions();
*out_byte = read_byte(addr);
end_expected_exceptions(&exception_kr, &exception_pc, &exception_memory);
/*
* pc was verified inside the exception handler.
* kr will be verified by the caller.
* Verify address here.
*/
if (exception_kr != KERN_SUCCESS) {
assert(exception_memory == addr);
}
*out_error = exception_kr;
return exception_kr == 0;
}
extern bool
try_write_byte(
mach_vm_address_t addr,
uint8_t byte,
kern_return_t * const out_error)
{
kern_return_t exception_kr;
uint64_t exception_pc;
uint64_t exception_memory;
begin_expected_exceptions();
write_byte(addr, byte);
end_expected_exceptions(&exception_kr, &exception_pc, &exception_memory);
/*
* pc was verified inside the exception handler.
* kr will be verified by the caller.
* Verify address here.
*/
if (exception_kr != KERN_SUCCESS) {
assert(exception_memory == addr);
}
*out_error = exception_kr;
return exception_kr == 0;
}