Loading...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 | // Copyright (c) 2025 Apple Inc. All rights reserved. #include <signal.h> #include <unistd.h> #include <errno.h> #include <sys/wait.h> #include <sys/time.h> #include <pthread.h> #include <stdlib.h> #include <stdio.h> #include <spawn.h> #include <mach/mach.h> #include <mach/semaphore.h> #include <darwintest.h> T_GLOBAL_META(T_META_NAMESPACE("xnu.exec"), T_META_RADAR_COMPONENT_NAME("xnu"), T_META_RADAR_COMPONENT_VERSION("spawn")); // Test parameters #define KILL_ZERO_SPIN_ITERATIONS 5000 #define KILL_ZERO_SPIN_INTERVAL_USEC 50 // 50 microseconds between kill(pid, 0) calls #define EXEC_PROGRAMS_COUNT 3 static volatile int stop_kill_spinning = 0; static volatile int kill_zero_eperm_count = 0; static volatile int kill_zero_total_count = 0; static volatile int kill_zero_esrch_count = 0; // Different exec programs to test various exec scenarios static char *exec_programs[][3] = { {"/bin/sleep", "3", NULL}, // Simple sleep {"/usr/bin/true", NULL, NULL}, // Quick exit {"/bin/echo", "exec_test", NULL}, // Quick with output }; // Thread that continuously spins on kill(pid, 0) to check PID validity static void * kill_zero_spin_thread(void *arg) { pid_t *child_pid_ptr = (pid_t *)arg; pid_t child_pid; int result; T_LOG("Starting kill(pid, 0) spin thread, waiting for pid to be set"); // Wait for child_pid to become non-zero (mimics launchd behavior) while (!stop_kill_spinning && *child_pid_ptr == 0) { usleep(1); // Brief pause while waiting for spawn } child_pid = *child_pid_ptr; if (child_pid == 0) { T_LOG("Thread exiting - no pid was set"); return NULL; } T_LOG("Got pid %d, starting kill(pid, 0) spinning", child_pid); while (!stop_kill_spinning && kill_zero_total_count < KILL_ZERO_SPIN_ITERATIONS) { result = kill(child_pid, 0); kill_zero_total_count++; if (result != 0) { if (errno == ESRCH) { // Process has exited - this is expected eventually kill_zero_esrch_count++; T_LOG("Process %d no longer exists after %d kill(0) calls (ESRCH)", child_pid, kill_zero_total_count); break; } else if (errno == EPERM) { // This should NEVER happen according to UNIX semantics kill_zero_eperm_count++; T_LOG("ERROR: EPERM on kill(pid, 0) call %d for pid %d - this violates UNIX semantics!", kill_zero_total_count, child_pid); } else { // Other unexpected errors - these should cause test failure T_FAIL("Unexpected error %d (%s) on kill(pid, 0) call %d for pid %d", errno, strerror(errno), kill_zero_total_count, child_pid); } } // Very short delay between checks to maximize race window if (KILL_ZERO_SPIN_INTERVAL_USEC > 0) { usleep(KILL_ZERO_SPIN_INTERVAL_USEC); } } T_LOG("kill(pid, 0) spin thread completed: %d total calls, %d EPERM errors, %d ESRCH", kill_zero_total_count, kill_zero_eperm_count, kill_zero_esrch_count); return NULL; } // Thread function for rapid-fire kill(pid, 0) spinning with no delay static void * rapid_fire_kill_zero_spin_thread(void *arg) { struct { pid_t pid; semaphore_t ready_sem; } *thread_args = arg; pid_t pid = thread_args->pid; int rapid_fire_calls = 0; // Signal that the thread is ready to start spinning kern_return_t kr = semaphore_signal(thread_args->ready_sem); T_ASSERT_MACH_SUCCESS(kr, "semaphore_signal"); while (!stop_kill_spinning && rapid_fire_calls < 10000) { int result = kill(pid, 0); rapid_fire_calls++; if (result != 0) { if (errno == ESRCH) { T_LOG("Process %d exited after %d rapid kill(0) calls", pid, rapid_fire_calls); break; } else if (errno == EPERM) { kill_zero_eperm_count++; T_LOG("CRITICAL ERROR: EPERM on rapid kill(pid, 0) call %d", rapid_fire_calls); } else { // Other unexpected errors - these should cause test failure T_FAIL("Unexpected error %d (%s) on rapid kill(pid, 0) call %d for pid %d", errno, strerror(errno), rapid_fire_calls, pid); } } // NO delay - maximum race condition detection } kill_zero_total_count = rapid_fire_calls; T_LOG("Rapid fire spinning completed: %d calls, %d EPERM errors", rapid_fire_calls, kill_zero_eperm_count); return NULL; } static void test_kill_zero_during_spawn(char **exec_args, const char *test_desc) { T_LOG("Testing kill(pid, 0) during posix_spawn of %s", test_desc); // Reset counters for this test iteration stop_kill_spinning = 0; kill_zero_eperm_count = 0; kill_zero_total_count = 0; kill_zero_esrch_count = 0; pid_t child_pid = 0; // Initialize to 0 so thread can wait for it // Start the kill(pid, 0) spinning thread BEFORE spawning to catch even more races // This mimics launchd's internal behavior of spinning on the pid memory location pthread_t spin_thread; int ret = pthread_create(&spin_thread, NULL, kill_zero_spin_thread, &child_pid); T_ASSERT_POSIX_SUCCESS(ret, "pthread_create"); T_LOG("Started kill(pid, 0) thread, now calling posix_spawn"); // Use posix_spawn to create the process - this avoids fork() issues with libdarwintest int spawn_result = posix_spawn(&child_pid, exec_args[0], NULL, NULL, exec_args, NULL); T_ASSERT_POSIX_SUCCESS(spawn_result, "posix_spawn"); T_LOG("Spawned process %d, thread should now detect it and start spinning", child_pid); // Let the spinning continue for a time to catch any spawn/initialization races usleep(100000); // 100ms should be enough for process startup // Stop the kill spinning stop_kill_spinning = 1; // Wait for spin thread to complete ret = pthread_join(spin_thread, NULL); T_ASSERT_POSIX_SUCCESS(ret, "pthread_join"); // Critical assertion: kill(pid, 0) should NEVER return EPERM during spawn T_ASSERT_EQ(kill_zero_eperm_count, 0, "kill(pid, 0) should never return EPERM during spawn - found %d EPERM errors in %d calls", kill_zero_eperm_count, kill_zero_total_count); // Verify we actually performed multiple kill(pid, 0) calls T_ASSERT_GT(kill_zero_total_count, 10, "Should have performed multiple kill(pid, 0) calls during spawn (got %d)", kill_zero_total_count); // Clean up: wait for child to exit int status; pid_t waited_pid = waitpid(child_pid, &status, 0); T_ASSERT_EQ(waited_pid, child_pid, "waitpid"); // Log the status and whether the process exited normally or was killed if (WIFEXITED(status)) { T_LOG("Process %d exited normally with status %d", child_pid, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { T_LOG("Process %d was killed by signal %d", child_pid, WTERMSIG(status)); } else { T_LOG("Process %d terminated with unknown status 0x%x", child_pid, status); } T_LOG("Test completed: %s - %d kill(pid, 0) calls, %d EPERM (should be 0), %d ESRCH", test_desc, kill_zero_total_count, kill_zero_eperm_count, kill_zero_esrch_count); } T_DECL(exec_kill_zero_no_eperm, "Verify that kill(pid, 0) never returns EPERM during exec", T_META_TAG_VM_PREFERRED) { T_LOG("Testing that kill(pid, 0) never fails with EPERM during exec()"); T_LOG("UNIX semantics guarantee that kill(pid, 0) can be used to check PID validity"); // Test with different exec scenarios to maximize race window coverage for (int i = 0; i < EXEC_PROGRAMS_COUNT; i++) { char **exec_args = exec_programs[i]; char test_desc[256]; snprintf(test_desc, sizeof(test_desc), "%s", exec_args[0]); test_kill_zero_during_spawn(exec_args, test_desc); // Brief pause between test iterations usleep(10000); } T_PASS("All exec scenarios completed without EPERM on kill(pid, 0)"); } T_DECL(exec_kill_zero_no_eperm_stress, "Stress test kill(pid, 0) across many rapid exec operations", T_META_TAG_VM_PREFERRED) { const int stress_iterations = 20; int total_kill_calls = 0; int total_eperm_errors = 0; T_LOG("Running stress test with %d rapid exec iterations", stress_iterations); for (int iteration = 0; iteration < stress_iterations; iteration++) { // Alternate between different exec programs for variety int prog_idx = iteration % EXEC_PROGRAMS_COUNT; char **exec_args = exec_programs[prog_idx]; char test_desc[256]; snprintf(test_desc, sizeof(test_desc), "iteration_%d_%s", iteration + 1, exec_args[0]); T_LOG("Stress iteration %d/%d: %s", iteration + 1, stress_iterations, test_desc); test_kill_zero_during_spawn(exec_args, test_desc); // Accumulate statistics total_kill_calls += kill_zero_total_count; total_eperm_errors += kill_zero_eperm_count; // Very brief pause to avoid overwhelming the system usleep(5000); } T_LOG("Stress test completed: %d total kill(pid, 0) calls across %d iterations", total_kill_calls, stress_iterations); // Critical verification: NO EPERM errors across all iterations T_ASSERT_EQ(total_eperm_errors, 0, "kill(pid, 0) should never return EPERM - found %d EPERM errors across %d calls", total_eperm_errors, total_kill_calls); // Verify we actually did significant testing T_ASSERT_GT(total_kill_calls, stress_iterations * 10, "Should have performed substantial kill(pid, 0) testing"); T_PASS("Stress test passed: %d kill(pid, 0) calls with 0 EPERM errors", total_kill_calls); } T_DECL(exec_kill_zero_very_rapid_spin, "Test kill(pid, 0) with very rapid spinning during exec", T_META_TAG_VM_PREFERRED) { T_LOG("Testing kill(pid, 0) with minimal delay between calls during spawn"); // Use /bin/sleep with longer duration to ensure spawn window char *long_sleep_args[] = {"/bin/sleep", "5", NULL}; // Reset counters stop_kill_spinning = 0; kill_zero_eperm_count = 0; kill_zero_total_count = 0; kill_zero_esrch_count = 0; // Use posix_spawn instead of fork() + exec() pid_t child_pid; int spawn_result = posix_spawn(&child_pid, long_sleep_args[0], NULL, NULL, long_sleep_args, NULL); T_ASSERT_POSIX_SUCCESS(spawn_result, "posix_spawn"); T_LOG("Starting rapid fire kill(pid, 0) spinning for pid %d", child_pid); // Create semaphore for thread synchronization semaphore_t thread_ready_sem; kern_return_t kr = semaphore_create(mach_task_self(), &thread_ready_sem, SYNC_POLICY_FIFO, 0); T_ASSERT_MACH_SUCCESS(kr, "semaphore_create"); // Prepare thread arguments struct { pid_t pid; semaphore_t ready_sem; } thread_args = { .pid = child_pid, .ready_sem = thread_ready_sem }; // Create spinning thread with no delay pthread_t spin_thread; int ret = pthread_create(&spin_thread, NULL, rapid_fire_kill_zero_spin_thread, &thread_args); T_ASSERT_POSIX_SUCCESS(ret, "pthread_create"); // Wait for thread to be ready instead of using usleep kr = semaphore_wait(thread_ready_sem); if (kr != KERN_SUCCESS) { // Ensure thread is joined even on error to prevent access to invalid stack memory stop_kill_spinning = 1; pthread_join(spin_thread, NULL); T_ASSERT_MACH_SUCCESS(kr, "semaphore_wait"); } // Let rapid spinning continue during spawn/exec usleep(200000); // 200ms // Stop spinning stop_kill_spinning = 1; // Wait for spinner to complete - must always happen since thread references stack memory ret = pthread_join(spin_thread, NULL); T_ASSERT_POSIX_SUCCESS(ret, "pthread_join"); // THE CRITICAL TEST: No EPERM should ever occur T_ASSERT_EQ(kill_zero_eperm_count, 0, "Rapid fire kill(pid, 0) should never return EPERM during exec - got %d errors in %d calls", kill_zero_eperm_count, kill_zero_total_count); T_ASSERT_GT(kill_zero_total_count, 100, "Should have performed many rapid fire kill(pid, 0) calls (got %d)", kill_zero_total_count); // Clean up child int status; pid_t waited_pid = waitpid(child_pid, &status, 0); T_ASSERT_EQ(waited_pid, child_pid, "waitpid"); // Log the status and whether the process exited normally or was killed if (WIFEXITED(status)) { T_LOG("Process %d exited normally with status %d", child_pid, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { T_LOG("Process %d was killed by signal %d", child_pid, WTERMSIG(status)); } else { T_LOG("Process %d terminated with unknown status 0x%x", child_pid, status); } // Cleanup semaphore semaphore_destroy(mach_task_self(), thread_ready_sem); T_LOG("Rapid fire test completed: %d kill(pid, 0) calls with %d EPERM errors (should be 0)", kill_zero_total_count, kill_zero_eperm_count); } |