1 /*
2 * Copyright (c) 2025 Apple Computer, 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 #include <darwintest.h>
30 #include <stdlib.h>
31 #include <unistd.h>
32 #include <mach/exception_types.h>
33 #include <sys/wait.h>
34 #include <sys/sysctl.h>
35 #include <sys/code_signing.h>
36 #include <signal.h>
37 #include <sys/ptrace.h>
38 #include <sys/mman.h>
39 #include <fcntl.h>
40 #include <os/crashlog_private.h>
41
42 #include "exc_helpers.h"
43 #include "test_utils.h"
44
45 T_GLOBAL_META(
46 T_META_NAMESPACE("xnu"),
47 T_META_RADAR_COMPONENT_NAME("xnu"),
48 T_META_RADAR_COMPONENT_VERSION("arm"),
49 T_META_OWNER("p_tennen")
50 );
51
52 static size_t
exception_handler_expect_not_called(mach_port_t task __unused,mach_port_t thread __unused,exception_type_t type __unused,mach_exception_data_t codes __unused)53 exception_handler_expect_not_called(mach_port_t task __unused, mach_port_t thread __unused,
54 exception_type_t type __unused, mach_exception_data_t codes __unused)
55 {
56 T_ASSERT_FAIL("kernel ran exception handler instead of terminating process");
57 return 0;
58 }
59
60 static void
signal_handler_expect_not_called(int sig,siginfo_t * sip __unused,void * ucontext __unused)61 signal_handler_expect_not_called(int sig, siginfo_t *sip __unused, void *ucontext __unused)
62 {
63 T_FAIL("kernel dispatched signal handler instead of terminating process");
64 }
65
66 T_DECL(uncatchable_fatal_trap_developer_mode_disabled,
67 "Ensure a maybe-unrecoverable trap label is uncatchable with !developer_mode",
68 T_META_REQUIRES_SYSCTL_EQ("security.mac.amfi.developer_mode_status", 0),
69 T_META_ENABLED(TARGET_CPU_ARM64)
70 )
71 {
72 /* Given a child process that sets up some mechanisms to catch an exception/signal */
73 /* And developer mode is disabled and we're not being debugged */
74 pid_t pid = fork();
75 T_QUIET; T_ASSERT_POSIX_SUCCESS(pid, "fork");
76
77 if (pid == 0) {
78 /*
79 * Try to catch the exception in two ways:
80 * - via setting up a Mach exception handler, and
81 * - via sigaction
82 */
83 mach_port_t exc_port = create_exception_port(EXC_MASK_ALL);
84 run_exception_handler(exc_port, (exc_handler_callback_t)exception_handler_expect_not_called);
85
86 struct sigaction sa = {
87 .sa_sigaction = signal_handler_expect_not_called,
88 .sa_flags = SA_SIGINFO
89 };
90 sigfillset(&sa.sa_mask);
91
92 T_ASSERT_POSIX_ZERO(sigaction(SIGILL, &sa, NULL), NULL);
93
94 /* When the child issues a maybe-fatal trap label */
95 /* 0xB000 is the start of the 'runtimes-owned traps' range in xnu */
96 os_fatal_trap(0xB000);
97 /* The brk above should have been treated as unrecoverable by the kernel */
98 T_FAIL("child ran past unrecoverable brk");
99 } else {
100 int status;
101 int err = waitpid(pid, &status, 0);
102 T_QUIET; T_ASSERT_POSIX_SUCCESS(err, "waitpid");
103
104 /* Then the child does not have an opportunity to run its exception handlers, and is immediately killed */
105 T_EXPECT_TRUE(WIFSIGNALED(status), "child terminated due to signal");
106 T_EXPECT_EQ(SIGKILL, WTERMSIG(status), "child terminated due to SIGKILL");
107 }
108 }
109
110 T_DECL(uncatchable_fatal_trap_developer_mode_enabled,
111 "Ensure an maybe-unrecoverable trap label is uncatchable with developer_mode",
112 T_META_REQUIRES_SYSCTL_EQ("security.mac.amfi.developer_mode_status", 1),
113 T_META_ENABLED(TARGET_CPU_ARM64)
114 )
115 {
116 /* Given a child process that sets up some mechanisms to catch an exception/signal */
117 /* And developer mode is enabled, but we're not being debugged */
118 pid_t pid = fork();
119 T_QUIET; T_ASSERT_POSIX_SUCCESS(pid, "fork");
120
121 if (pid == 0) {
122 /*
123 * Try to catch the exception in two ways:
124 * - via setting up a Mach exception handler, and
125 * - via sigaction
126 */
127 mach_port_t exc_port = create_exception_port(EXC_MASK_ALL);
128 run_exception_handler(exc_port, (exc_handler_callback_t)exception_handler_expect_not_called);
129
130 struct sigaction sa = {
131 .sa_sigaction = signal_handler_expect_not_called,
132 .sa_flags = SA_SIGINFO
133 };
134 sigfillset(&sa.sa_mask);
135
136 T_ASSERT_POSIX_ZERO(sigaction(SIGILL, &sa, NULL), NULL);
137
138 /* When the child issues a maybe-fatal trap label */
139 /* 0xB000 is the start of the 'runtimes-owned traps' range in xnu */
140 os_fatal_trap(0xB000);
141 /* The brk above should have been treated as unrecoverable by the kernel */
142 T_FAIL("child ran past brk");
143 } else {
144 int status;
145 int err = waitpid(pid, &status, 0);
146 T_QUIET; T_ASSERT_POSIX_SUCCESS(err, "waitpid");
147
148 /* Then the child does not have an opportunity to run its exception handlers, and is immediately killed */
149 T_EXPECT_TRUE(WIFSIGNALED(status), "child terminated due to signal");
150 T_EXPECT_EQ(SIGKILL, WTERMSIG(status), "child terminated due to SIGKILL");
151 }
152 }
153
154 static bool* shared_was_mach_exception_handler_called = NULL;
155 static bool* shared_was_posix_signal_handler_called = NULL;
156
157 static size_t
exception_handler_expect_called(mach_port_t task __unused,mach_port_t thread __unused,exception_type_t type __unused,mach_exception_data_t codes __unused)158 exception_handler_expect_called(mach_port_t task __unused, mach_port_t thread __unused,
159 exception_type_t type __unused, mach_exception_data_t codes __unused)
160 {
161 T_PASS("Our Mach exception handler ran");
162 *shared_was_mach_exception_handler_called = true;
163 exit(0);
164 return 0;
165 }
166
167 static void
signal_handler_expect_called(int sig,siginfo_t * sip __unused,void * ucontext __unused)168 signal_handler_expect_called(int sig, siginfo_t *sip __unused, void *ucontext __unused)
169 {
170 T_PASS("Our BSD signal handler ran");
171 *shared_was_posix_signal_handler_called = true;
172 exit(0);
173 }
174
175
176 T_DECL(uncatchable_fatal_trap_debugged,
177 "Ensure an maybe-unrecoverable trap label is catchable under a debugger",
178 T_META_REQUIRES_SYSCTL_EQ("security.mac.amfi.developer_mode_status", 1),
179 /* It's not straightforward to ptrace on platforms other than macOS, so don't bother */
180 // T_META_ENABLED(TARGET_CPU_ARM64 && TARGET_OS_OSX)
181 T_META_ENABLED(false) /* rdar://153223014 */
182 )
183 {
184 /* Given a child process that sets up some mechanisms to catch an exception/signal */
185 /* And developer mode is enabled, and the child is being debugged */
186 int ret;
187
188 const char* memory_path = "uncatchable_fatal_trap_debugged";
189 shm_unlink(memory_path);
190 int shm_fd = shm_open(memory_path, O_RDWR | O_CREAT);
191 T_ASSERT_POSIX_SUCCESS(shm_fd, "Created shared memory");
192 ret = ftruncate(shm_fd, sizeof(bool) * 2);
193 T_ASSERT_POSIX_SUCCESS(ret, "ftruncate");
194
195 shared_was_mach_exception_handler_called = (bool*)mmap(NULL, sizeof(bool), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
196 shared_was_posix_signal_handler_called = (bool*)mmap(NULL, sizeof(bool), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
197 bool* has_parent_connected = (bool*)mmap(NULL, sizeof(bool), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
198 *has_parent_connected = false;
199
200 pid_t pid = fork();
201 T_QUIET; T_ASSERT_POSIX_SUCCESS(pid, "fork");
202
203 if (pid == 0) {
204 /* Allow the parent to attach */
205 while (!*has_parent_connected) {
206 sleep(1);
207 }
208
209 /*
210 * Try to catch the exception in two ways:
211 * - via setting up a Mach exception handler, and
212 * - via sigaction
213 */
214 mach_port_t exc_port = create_exception_port(EXC_MASK_ALL);
215 run_exception_handler(exc_port, (exc_handler_callback_t)exception_handler_expect_called);
216
217 struct sigaction sa = {
218 .sa_sigaction = signal_handler_expect_called,
219 .sa_flags = SA_SIGINFO
220 };
221 sigfillset(&sa.sa_mask);
222
223 T_ASSERT_POSIX_ZERO(sigaction(SIGILL, &sa, NULL), NULL);
224
225 /* When the child issues a maybe-fatal trap label */
226 /* 0xB000 is the start of the 'runtimes-owned traps' range in xnu */
227 os_fatal_trap(0xB000);
228 /* The brk above should have terminated this thread */
229 T_FAIL("child ran past brk");
230 } else {
231 /* Attach to the child so it's marked as being debugged */
232 ret = ptrace(PT_ATTACHEXC, pid, 0, 0);
233 T_EXPECT_POSIX_SUCCESS(ret, "ptrace PT_ATTACHEXC");
234 ret = ptrace(PT_CONTINUE, pid, (caddr_t)1, 0);
235 T_EXPECT_POSIX_SUCCESS(ret, "ptrace PT_CONTINUE");
236 /* And let the child know that it can carry on */
237 *has_parent_connected = true;
238
239 int status;
240 int err = waitpid(pid, &status, 0);
241 T_QUIET; T_ASSERT_POSIX_SUCCESS(err, "waitpid");
242
243 /*
244 * Then the child is given an opportunity to run its exception handlers,
245 * which we witness by its setting of a shared boolean and clean exit(0).
246 */
247 T_EXPECT_TRUE(WIFEXITED(status), "child exited");
248 T_EXPECT_TRUE(*shared_was_mach_exception_handler_called
249 || *shared_was_posix_signal_handler_called,
250 "Expected one of our handlers to be dispatched");
251
252 T_ASSERT_POSIX_SUCCESS(close(shm_fd), "Closed shm fd");
253 T_ASSERT_POSIX_SUCCESS(shm_unlink(memory_path), "Unlinked");
254 }
255 }
256