1 /*
2 * Copyright (c) 2024 Apple Inc. All rights reserved.
3 *
4 * @APPLE_OSREFERENCE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. The rights granted to you under the License
10 * may not be used to create, or enable the creation or redistribution of,
11 * unlawful or unlicensed copies of an Apple operating system, or to
12 * circumvent, violate, or enable the circumvention or violation of, any
13 * terms of an Apple operating system software license agreement.
14 *
15 * Please obtain a copy of the License at
16 * http://www.opensource.apple.com/apsl/ and read it before using this file.
17 *
18 * The Original Code and all software distributed under the License are
19 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
20 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
21 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
22 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
23 * Please see the License for the specific language governing rights and
24 * limitations under the License.
25 *
26 * @APPLE_OSREFERENCE_LICENSE_HEADER_END@
27 */
28
29 /*
30 * try_read_write.c
31 *
32 * Helper functions for userspace tests to read or write memory and
33 * verify that EXC_BAD_ACCESS is or is not generated by that operation.
34 */
35
36 #include <assert.h>
37 #include <stdbool.h>
38 #include <stdatomic.h>
39 #include <ptrauth.h>
40 #include <darwintest.h>
41 #include <dispatch/dispatch.h>
42
43 #include "exc_helpers.h"
44 #include "try_read_write.h"
45
46 /*
47 * -- Implementation overview --
48 *
49 * try_read_byte() and try_write_byte() operate by performing
50 * a read or write instruction with a Mach exception handler
51 * in place.
52 *
53 * The exception handler catches EXC_BAD_ACCESS. If the bad access
54 * came from our designated read or write instructions then it
55 * records the exception that occurred to thread-local storage
56 * and moves that thread's program counter to resume execution
57 * and recover from the exception.
58 *
59 * Unrecognized exceptions, and EXC_BAD_ACCESS exceptions from
60 * unrecognized instructions, either go uncaught or are caught and
61 * re-raised. In either case they lead to an ordinary crash. This
62 * means we don't get false positives where the test expects one
63 * crash but incorrectly passes after crashing in some unrelated way.
64 * We can be precise about what the fault was and where it came from.
65 *
66 * We use Mach exceptions instead of signals because
67 * on watchOS signal handlers do not receive the thread
68 * state so they cannot recover from the signal.
69 *
70 * try_read_write_exception_handler()
71 * our exception handler, installed using tests/exc_helpers.c
72 *
73 * read_byte() and write_byte()
74 * our designated read and write instructions, recognized by
75 * the exception handler and specially structured to allow
76 * recovery by changing the PC
77 *
78 * try_read_write_thread_t
79 * thread-local storage to record the caught exception
80 */
81
82 static dispatch_once_t try_read_write_initializer;
83 static mach_port_t try_read_write_exc_port;
84
85 /*
86 * Bespoke thread-local storage for threads inside try_read_write.
87 * We can't use pthread local storage because the Mach exception
88 * handler needs to access it and that exception handler runs on
89 * a different thread.
90 *
91 * Access by the Mach exception thread is safe because the real thread
92 * is suspended at that point. (This scheme would be unsound if the
93 * real thread raised an exception while manipulating the thread-local
94 * data, but we don't try to cover that case.)
95 */
96 typedef struct {
97 mach_port_t thread;
98 kern_return_t exception_kr; /* EXC_BAD_ADDRESS sub-code */
99 uint64_t exception_pc; /* PC of faulting instruction */
100 uint64_t exception_memory; /* Memory address of faulting access */
101 } try_read_write_thread_t;
102
103 #define TRY_READ_WRITE_MAX_THREADS 128
104 static pthread_mutex_t try_read_write_thread_list_mutex = PTHREAD_MUTEX_INITIALIZER;
105 static unsigned try_read_write_thread_count = 0;
106 static try_read_write_thread_t try_read_write_thread_list[TRY_READ_WRITE_MAX_THREADS];
107 static __thread try_read_write_thread_t *try_read_write_thread_self;
108
109 /*
110 * Look up the try_read_write_thread_t for a Mach thread.
111 * If create == true and no info was found, add it to the list.
112 * Returns NULL if no info was found and create == false.
113 */
114 static __attribute__((overloadable))
115 try_read_write_thread_t *
thread_info_for_mach_thread(mach_port_t thread_port,bool create)116 thread_info_for_mach_thread(mach_port_t thread_port, bool create)
117 {
118 /* first look for a cached value in real thread-local storage */
119 if (mach_thread_self() == thread_port) {
120 try_read_write_thread_t *info = try_read_write_thread_self;
121 if (info) {
122 return info;
123 }
124 }
125
126 int err = pthread_mutex_lock(&try_read_write_thread_list_mutex);
127 assert(err == 0);
128
129 /* search the list */
130 for (unsigned i = 0; i < try_read_write_thread_count; i++) {
131 try_read_write_thread_t *info = &try_read_write_thread_list[i];
132 if (info->thread == thread_port) {
133 pthread_mutex_unlock(&try_read_write_thread_list_mutex);
134 if (mach_thread_self() == thread_port) {
135 try_read_write_thread_self = info;
136 }
137 return info;
138 }
139 }
140
141 /* not in list - create if requested */
142 if (create) {
143 assert(try_read_write_thread_count < TRY_READ_WRITE_MAX_THREADS);
144 try_read_write_thread_t *info = &try_read_write_thread_list[try_read_write_thread_count++];
145 info->thread = thread_port;
146 info->exception_kr = 0;
147 pthread_mutex_unlock(&try_read_write_thread_list_mutex);
148 if (mach_thread_self() == thread_port) {
149 try_read_write_thread_self = info;
150 }
151 return info;
152 }
153
154 pthread_mutex_unlock(&try_read_write_thread_list_mutex);
155 return NULL;
156 }
157
158 static __attribute__((overloadable))
159 try_read_write_thread_t *
thread_info_for_mach_thread(mach_port_t thread_port)160 thread_info_for_mach_thread(mach_port_t thread_port)
161 {
162 return thread_info_for_mach_thread(thread_port, false /* create */);
163 }
164
165
166 /*
167 * read_byte() and write_byte() are functions that
168 * read or write memory as their first instruction.
169 * Used to test memory access that may provoke an exception.
170 *
171 * try_read_write_exception_handler() below checks if the exception PC
172 * is equal to one of these functions. The first instruction must be
173 * the memory access instruction.
174 *
175 * try_read_write_exception_handler() below increments the PC by four bytes.
176 * The memory access instruction must be padded to exactly four bytes.
177 */
178
179 static uint64_t __attribute__((naked))
read_byte(mach_vm_address_t addr)180 read_byte(mach_vm_address_t addr)
181 {
182 #if __arm64__
183 asm("\n ldrb w0, [x0]"
184 "\n ret");
185 #elif __x86_64__
186 asm("\n movb (%rdi), %al"
187 "\n nop" /* pad load to four bytes */
188 "\n nop"
189 "\n ret");
190 #else
191 # error unknown architecture
192 #endif
193 }
194
195 static void __attribute__((naked))
write_byte(mach_vm_address_t addr,uint8_t value)196 write_byte(mach_vm_address_t addr, uint8_t value)
197 {
198 #if __arm64__
199 asm("\n strb w1, [x0]"
200 "\n ret");
201 #elif __x86_64__
202 asm("\n movb %sil, (%rdi)"
203 "\n nop" /* pad store to four bytes */
204 "\n ret");
205 #else
206 # error unknown architecture
207 #endif
208 }
209
210
211 /*
212 * Mach exception handler for EXC_BAD_ACCESS called by exc_helpers.
213 * Returns the number of bytes to advance the PC to resolve the exception.
214 */
215 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)216 try_read_write_exception_handler(
217 __unused mach_port_t task,
218 mach_port_t thread,
219 exception_type_t exception,
220 mach_exception_data_t codes,
221 uint64_t exception_pc)
222 {
223 assert(exception == EXC_BAD_ACCESS);
224 try_read_write_thread_t *info = thread_info_for_mach_thread(thread);
225 assert(info); /* we do not expect exceptions from other threads */
226
227 uint64_t read_byte_pc = (uint64_t)ptrauth_strip(&read_byte, ptrauth_key_function_pointer);
228 uint64_t write_byte_pc = (uint64_t)ptrauth_strip(&write_byte, ptrauth_key_function_pointer);
229
230 if (exception_pc != read_byte_pc && exception_pc != write_byte_pc) {
231 /* this exception isn't one of ours - re-raise it */
232 if (verbose_exc_helper) {
233 T_LOG("not a try_read_write exception");
234 }
235 return EXC_HELPER_HALT;
236 }
237
238 assert(info->exception_kr == 0); /* no nested exceptions allowed */
239
240 info->exception_pc = exception_pc;
241 info->exception_kr = codes[0];
242 info->exception_memory = codes[1];
243 if (verbose_exc_helper) {
244 T_LOG("try_read_write exception: pc 0x%llx kr %d mem 0x%llx",
245 info->exception_pc, info->exception_kr, info->exception_memory);
246 }
247
248 /* advance pc by 4 bytes to recover */
249 return 4;
250 }
251
252 /*
253 * Create an exc_helpers exception handler port and thread,
254 * and install the exception handler port on this thread.
255 */
256 static void
initialize_exception_handlers(void)257 initialize_exception_handlers(void)
258 {
259 try_read_write_exc_port = create_exception_port(EXC_MASK_BAD_ACCESS);
260 repeat_exception_handler(try_read_write_exc_port, try_read_write_exception_handler);
261 }
262
263 /*
264 * Begin try_read_write exception handling on this thread.
265 */
266 static void
begin_expected_exceptions(void)267 begin_expected_exceptions(void)
268 {
269 dispatch_once(&try_read_write_initializer, ^{
270 initialize_exception_handlers();
271 });
272
273 try_read_write_thread_t *info = try_read_write_thread_self;
274 if (!info) {
275 set_thread_exception_port(try_read_write_exc_port, EXC_MASK_BAD_ACCESS);
276 info = thread_info_for_mach_thread(mach_thread_self(), true /* create */);
277 }
278
279 info->exception_kr = 0;
280 info->exception_pc = 0;
281 info->exception_memory = 0;
282 }
283
284 /*
285 * End try_read_write exception handling on this thread.
286 * Returns the caught exception data, if any.
287 */
288 static void
end_expected_exceptions(kern_return_t * const out_kr,uint64_t * const out_pc,uint64_t * const out_memory)289 end_expected_exceptions(
290 kern_return_t * const out_kr,
291 uint64_t * const out_pc,
292 uint64_t * const out_memory)
293 {
294 try_read_write_thread_t *info = try_read_write_thread_self;
295 assert(info);
296 *out_kr = info->exception_kr;
297 *out_pc = info->exception_pc;
298 *out_memory = info->exception_memory;
299 }
300
301
302 extern bool
try_read_byte(mach_vm_address_t addr,uint8_t * const out_byte,kern_return_t * const out_error)303 try_read_byte(
304 mach_vm_address_t addr,
305 uint8_t * const out_byte,
306 kern_return_t * const out_error)
307 {
308 kern_return_t exception_kr;
309 uint64_t exception_pc;
310 uint64_t exception_memory;
311
312 begin_expected_exceptions();
313 *out_byte = read_byte(addr);
314 end_expected_exceptions(&exception_kr, &exception_pc, &exception_memory);
315
316 /*
317 * pc was verified inside the exception handler.
318 * kr will be verified by the caller.
319 * Verify address here.
320 */
321
322 if (exception_kr != KERN_SUCCESS) {
323 assert(exception_memory == addr);
324 }
325
326 *out_error = exception_kr;
327 return exception_kr == 0;
328 }
329
330 extern bool
try_write_byte(mach_vm_address_t addr,uint8_t byte,kern_return_t * const out_error)331 try_write_byte(
332 mach_vm_address_t addr,
333 uint8_t byte,
334 kern_return_t * const out_error)
335 {
336 kern_return_t exception_kr;
337 uint64_t exception_pc;
338 uint64_t exception_memory;
339
340 begin_expected_exceptions();
341 write_byte(addr, byte);
342 end_expected_exceptions(&exception_kr, &exception_pc, &exception_memory);
343
344 /*
345 * pc was verified inside the exception handler.
346 * kr will be verified by the caller.
347 * Verify address here.
348 */
349
350 if (exception_kr != KERN_SUCCESS) {
351 assert(exception_memory == addr);
352 }
353
354 *out_error = exception_kr;
355 return exception_kr == 0;
356 }
357