// BUILD: $CC target.c -o $BUILD_DIR/target.exe -DRUN_DIR="$RUN_DIR" // BUILD: $CC foo.c -o $BUILD_DIR/libfoo.dylib -dynamiclib // BUILD: $CXX main.mm -o $BUILD_DIR/dyld_process_info_notify.exe -DRUN_DIR="$RUN_DIR" // BUILD: $TASK_FOR_PID_ENABLE $BUILD_DIR/dyld_process_info_notify.exe // RUN: $SUDO ./dyld_process_info_notify.exe #include <stdio.h> #include <stdlib.h> #include <dlfcn.h> #include <unistd.h> #include <signal.h> #include <spawn.h> #include <errno.h> #include <libgen.h> #include <sys/proc.h> #include <mach/mach.h> #include <sys/param.h> #include <mach/machine.h> #include <mach-o/dyld_images.h> #include <mach-o/dyld_process_info.h> #include <dispatch/dispatch.h> #include <Availability.h> #include "test_support.h" //FIXME: We need to add some concurrent access tests //FIXME: Add cross architecture tests back now that arm64e macOS exists extern char** environ; // This is a one shot semaphore implementation that is QoS aware with integreated logging struct OneShotSemaphore { OneShotSemaphore(const char* N) :_name(strdup(N)), _block(dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{})) {} bool wait() { LOG("Waiting for semaphore %s", _name); dispatch_time_t tenSecondFromNow = dispatch_time(DISPATCH_WALLTIME_NOW, 10 * NSEC_PER_SEC); if (dispatch_block_wait(_block, tenSecondFromNow) != 0) { LOG("Timeout for semaphore %s", _name); return false; } return true; } void signal() { LOG("Signalling semaphore %s", _name); _block(); } private: const char* _name; dispatch_block_t _block; }; void launchTest(bool launchSuspended, bool disconnectEarly) { LOG("launchTest (%s)", launchSuspended ? "suspended" : "unsuspened"); LOG("launchTest (%s)", disconnectEarly ? "disconnect early" : "normal disconnnect"); dispatch_queue_t queue = dispatch_queue_create("com.apple.dyld.test.dyld_process_info", NULL); dispatch_queue_t signalQueue = dispatch_queue_create("com.apple.dyld.test.dyld_process_info.signals", NULL); // We use these blocks as semaphores. We do it this way so have ownership for QOS and so we get logging __block OneShotSemaphore childReady("childReady"); __block OneShotSemaphore childExit("childExit"); __block OneShotSemaphore childDone("childDone"); __block OneShotSemaphore childExitNotification("childExitNotification"); // We control our interactions with the sub ordinate process via signals, but if we send signals before its signal handlers // are installed it will terminate. We wait for it to SIGUSR1 us to indicate it is ready, so we need to setup a signal handler for // that. signal(SIGUSR1, SIG_IGN); dispatch_source_t usr1SignalSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGUSR1, 0, signalQueue); dispatch_source_set_event_handler(usr1SignalSource, ^{ LOG("Got SIGUSR1"); childReady.signal(); }); dispatch_resume(usr1SignalSource); signal(SIGUSR2, SIG_IGN); dispatch_source_t usr2SignalSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGUSR2, 0, signalQueue); dispatch_source_set_event_handler(usr2SignalSource, ^{ LOG("Got SIGUSR2"); childDone.signal(); }); dispatch_resume(usr2SignalSource); pid_t pid; task_t task; __block bool sawMainExecutable = false; __block bool sawlibSystem = false; __block bool gotMainNotice = false; __block bool gotMainNoticeBeforeAllInitialDylibs = false; __block bool gotFooNoticeBeforeMain = false; __block int libFooLoadCount = 0; __block int libFooUnloadCount = 0; __block dyld_process_info_notify handle; _process process; process.set_executable_path(RUN_DIR "/target.exe"); const char* env[] = { "TEST_OUTPUT=None", NULL}; process.set_env(env); process.set_launch_suspended(launchSuspended); process.set_exit_handler(^(pid_t pid) { // This is almost all logging code, the only functional element of it // is calling the childExit() semaphore int status = 0; int dispStatus = 0; (void)waitpid(pid, &status, 0); const char* exitType = "UNKNOWN"; if (WIFEXITED(status)) { exitType = "exit()"; dispStatus = WEXITSTATUS(status); } if (WIFSIGNALED(status)) { exitType = "signal"; dispStatus = WTERMSIG(status); } LOG("DIED via %s (pid: %d, status: %d)", exitType, pid, dispStatus); childExit.signal(); }); // Launch process pid = process.launch(); LOG("launchTest pid (%u)", pid); if ( task_read_for_pid(mach_task_self(), pid, &task) != KERN_SUCCESS ) { FAIL("task_read_for_pid()"); } // Attach notifier kern_return_t kr; unsigned count = 0; do { handle = _dyld_process_info_notify( task, queue, ^(bool unload, uint64_t timestamp, uint64_t machHeader, const uuid_t uuid, const char* path) { LOG("Handler called"); if ( strstr(path, "/target.exe") != NULL ) sawMainExecutable = true; if ( strstr(path, "/libSystem") != NULL ) sawlibSystem = true; if ( strstr(path, "/libfoo.dylib") != NULL ) { if ( !gotMainNotice ) { gotFooNoticeBeforeMain = true; } if ( unload ) { ++libFooUnloadCount; } else { ++libFooLoadCount; if (disconnectEarly) { _dyld_process_info_notify_release(handle); } } } }, ^{ LOG("TERMINATED (pid: %d)", pid); childExitNotification.signal(); }, &kr); ++count; if ( handle == NULL ) LOG("_dyld_process_info_notify() returned NULL, result=%d, count=%d", kr, count); } while ( (handle == NULL) && (count < 5) ); LOG("launchTest handler registered"); if ( handle == NULL ) { FAIL("Did not not get handle"); } // if suspended attach main notifier and unsuspend if (launchSuspended) { // If the process starts suspended register for main(), // otherwise skip since this test is a race between // process setup and notification registration _dyld_process_info_notify_main(handle, ^{ LOG("target entering main()"); gotMainNotice = true; if ( !sawMainExecutable || !sawlibSystem ) gotMainNoticeBeforeAllInitialDylibs = true; }); kill(pid, SIGCONT); LOG("Sent SIGCONT"); } if (!childReady.wait()) { FAIL("Timed out waiting for child to signal it is ready"); } kill(pid, SIGUSR1); LOG("Sent SIGUSR1"); if (!childDone.wait()) { FAIL("Timed out waiting for child to finish dlopen()/dlclose() operations"); } if (launchSuspended) { if ( !sawMainExecutable ) { FAIL("Did not get load notification of main executable"); } if ( !gotMainNotice ) { FAIL("Did not get notification of main()"); } if ( gotMainNoticeBeforeAllInitialDylibs ) { FAIL("Notification of main() arrived before all initial dylibs"); } if ( gotFooNoticeBeforeMain ) { FAIL("Notification of main() arrived after libfoo load notice"); } if ( !sawlibSystem ) { FAIL("Did not get load notification of libSystem"); } } kill(pid, SIGTERM); LOG("Sent SIGTERM"); if (!childExitNotification.wait()) { FAIL("Timed out waiting for child exit notification via _dyld_process_info_notify"); } if ( disconnectEarly ) { if ( libFooLoadCount != 1 ) { FAIL("Got %d load notifications about libFoo instead of 1", libFooLoadCount); } if ( libFooUnloadCount != 0 ) { FAIL("Got %d unload notifications about libFoo instead of 1", libFooUnloadCount); } } else { if ( libFooLoadCount != 3 ) { FAIL("Got %d load notifications about libFoo instead of 3", libFooLoadCount); } if ( libFooUnloadCount != 3 ) { FAIL("Got %d unload notifications about libFoo instead of 3", libFooUnloadCount); } } if (!childExit.wait()) { FAIL("Timed out waiting for child cleanup"); } // Tear down dispatch_source_cancel(usr1SignalSource); dispatch_source_cancel(usr2SignalSource); if (!disconnectEarly) { _dyld_process_info_notify_release(handle); } } static void testSelfAttach(void) { __block OneShotSemaphore teardownSempahore("self test teardownSempahore"); __block bool dylibLoadNotified = false; kern_return_t kr = KERN_SUCCESS; dispatch_queue_t queue = dispatch_queue_create("com.apple.dyld.test.dyld_process_info.self-attach", NULL); dyld_process_info_notify handle = _dyld_process_info_notify(mach_task_self(), queue, ^(bool unload, uint64_t timestamp, uint64_t machHeader, const uuid_t uuid, const char* path) { if ( strstr(path, "/libfoo.dylib") != NULL ) { dylibLoadNotified = true; } }, ^{ teardownSempahore.signal(); }, &kr); if ( handle == NULL ) { LOG("_dyld_process_info_notify() returned NULL, result=%d", kr); } void* h = dlopen(RUN_DIR "/libfoo.dylib", 0); dlclose(h); if (!dylibLoadNotified) { FAIL("testSelfAttach"); } _dyld_process_info_notify_release(handle); teardownSempahore.wait(); // Get the all image info task_dyld_info_data_t taskDyldInfo; mach_msg_type_number_t taskDyldInfoCount = TASK_DYLD_INFO_COUNT; if (task_info(mach_task_self(), TASK_DYLD_INFO, (task_info_t)&taskDyldInfo, &taskDyldInfoCount) != KERN_SUCCESS) { FAIL("Could not find all image info"); } dyld_all_image_infos* infos = (dyld_all_image_infos*)taskDyldInfo.all_image_info_addr; // Find a slot for the right uint8_t notifySlot; for (uint8_t notifySlot = 0; notifySlot < DYLD_MAX_PROCESS_INFO_NOTIFY_COUNT; ++notifySlot) { if (infos->notifyPorts[notifySlot] != 0) { FAIL("Port array entry %u not cleaned up, expected 0, got %u", notifySlot, infos->notifyPorts[notifySlot]); } } } int main(int argc, const char* argv[], const char* envp[], const char* apple[]) { // test 1) attempt to monitor the monitoring process testSelfAttach(); // test 2) launch test program suspended and wait for it to run to completion launchTest(true, false); // test 3) launch test program in unsuspended and wait for it to run to completion launchTest(false, false); // test 4) launch test program suspended and disconnect from it after the first dlopen() in target.exe launchTest(true, true); // test 5) launch test program unsuspended and disconnect from it after the first dlopen() in target.exe launchTest(false, true); PASS("Success"); }