xref: /xnu-12377.81.4/tests/try_read_write.c (revision 043036a2b3718f7f0be807e2870f8f47d3fa0796)
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