// // GTMServiceManagement.c // // Copyright 2010 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. // // As per http://openp2p.com/pub/a/oreilly/ask_tim/2001/codepolicy.html // The following functions // open_devnull // spc_sanitize_files // spc_drop_privileges // are derived from Chapter 1 of "Secure Programming Cookbook for C and C++" by // John Viega and Matt Messier. Copyright 2003 O'Reilly & Associates. // ISBN 0-596-00394-3 // Note: launch_data_t have different ownership semantics than CFType/NSObjects. // In general if you create one, you are responsible for releasing it. // However, if you add it to a collection (LAUNCH_DATA_DICTIONARY, // LAUNCH_DATA_ARRAY), you no longer own it, and are no longer // responsible for releasing it (you may be responsible for the array // or dictionary of course). A corrollary of this is that a // launch_data_t can only be in one collection at any given time. #include "GTMServiceManagement.h" #if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_4 #include #include #include #include #include #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10 && \ MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8 #include #include #endif #pragma clang diagnostic push // Ignore all of the deprecation warnings for GTMServiceManagement #pragma clang diagnostic ignored "-Wdeprecated-declarations" typedef struct { CFMutableDictionaryRef dict; bool convert_non_standard_objects; CFErrorRef *error; } GTMLToCFDictContext; typedef struct { launch_data_t dict; CFErrorRef *error; } GTMCFToLDictContext; static bool IsOsYosemiteOrGreater() { #if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10 // In 10.10, [[NSProcessInfo processInfo] operatingSystemVersion] exists, // but if we can assume 10.10 we already know the answer. return true; #elif MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_8 // Gestalt() is deprected in 10.8, and the recommended replacement is sysctl. // https://developer.apple.com/library/mac/releasenotes/General/CarbonCoreDeprecations/index.html#//apple_ref/doc/uid/TP40012224-CH1-SW16 const size_t N = 128; char buffer[N]; size_t buffer_size = N; int ctl_name[] = {CTL_KERN, KERN_OSRELEASE}; if (sysctl(ctl_name, 2, buffer, &buffer_size, NULL, 0) != 0) { return false; } // The buffer now contains a string of the form XX.YY.ZZ, where // XX is the major kernel version component. char* period_pos = strchr(buffer, '.'); if (!period_pos) { return false; } *period_pos = '\0'; long kernel_version_major = strtol(buffer, NULL, 10); // Kernel version 14 corresponds to OS X 10.10 Yosemite. return kernel_version_major >= 14; #else SInt32 version_major; SInt32 version_minor; __Require_noErr(Gestalt(gestaltSystemVersionMajor, &version_major), failedGestalt); __Require_noErr(Gestalt(gestaltSystemVersionMinor, &version_minor), failedGestalt); return version_major > 10 || (version_major == 10 && version_minor >= 10); failedGestalt: return false; #endif } static CFErrorRef GTMCFLaunchCreateUnlocalizedError(CFIndex code, CFStringRef format, ...) CF_FORMAT_FUNCTION(2, 3); static CFErrorRef GTMCFLaunchCreateUnlocalizedError(CFIndex code, CFStringRef format, ...) { CFDictionaryRef user_info = NULL; if (format) { va_list args; va_start(args, format); CFStringRef string = CFStringCreateWithFormatAndArguments(kCFAllocatorDefault, NULL, format, args); user_info = CFDictionaryCreate(kCFAllocatorDefault, (const void **)&kCFErrorDescriptionKey, (const void **)&string, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFRelease(string); va_end(args); } CFErrorRef error = CFErrorCreate(kCFAllocatorDefault, kCFErrorDomainPOSIX, code, user_info); if (user_info) { CFRelease(user_info); } return error; } static void GTMConvertCFDictEntryToLaunchDataDictEntry(const void *key, const void *value, void *context) { GTMCFToLDictContext *local_context = (GTMCFToLDictContext *)context; if (*(local_context->error)) return; launch_data_t launch_value = GTMLaunchDataCreateFromCFType(value, local_context->error); if (launch_value) { launch_data_t launch_key = GTMLaunchDataCreateFromCFType(key, local_context->error); if (launch_key) { bool goodInsert = launch_data_dict_insert(local_context->dict, launch_value, launch_data_get_string(launch_key)); if (!goodInsert) { *(local_context->error) = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("launch_data_dict_insert " "failed key: %@ value: %@"), key, value); launch_data_free(launch_value); } launch_data_free(launch_key); } } } static void GTMConvertLaunchDataDictEntryToCFDictEntry(const launch_data_t value, const char *key, void *context) { GTMLToCFDictContext *local_context = (GTMLToCFDictContext *)context; if (*(local_context->error)) return; CFTypeRef cf_value = GTMCFTypeCreateFromLaunchData(value, local_context->convert_non_standard_objects, local_context->error); if (cf_value) { CFStringRef cf_key = CFStringCreateWithCString(kCFAllocatorDefault, key, kCFStringEncodingUTF8); if (cf_key) { CFDictionarySetValue(local_context->dict, cf_key, cf_value); CFRelease(cf_key); } else { *(local_context->error) = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("Unable to create key %s"), key); } CFRelease(cf_value); } } static launch_data_t GTMPerformOnLabel(const char *verb, CFStringRef jobLabel, CFErrorRef *error) { launch_data_t resp = NULL; launch_data_t label = GTMLaunchDataCreateFromCFType(jobLabel, error); if (*error == NULL) { launch_data_t msg = launch_data_alloc(LAUNCH_DATA_DICTIONARY); launch_data_dict_insert(msg, label, verb); resp = launch_msg(msg); launch_data_free(msg); if (!resp) { *error = GTMCFLaunchCreateUnlocalizedError(errno, CFSTR("")); } } return resp; } launch_data_t GTMLaunchDataCreateFromCFType(CFTypeRef cf_type_ref, CFErrorRef *error) { launch_data_t result = NULL; CFErrorRef local_error = NULL; if (cf_type_ref == NULL) { local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("NULL CFType")); goto exit; } CFTypeID cf_type = CFGetTypeID(cf_type_ref); if (cf_type == CFStringGetTypeID()) { CFIndex length = CFStringGetLength(cf_type_ref); CFIndex max_length = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; char *buffer = calloc(max_length, sizeof(char)); size_t buffer_size = max_length * sizeof(char); if (buffer) { if (CFStringGetCString(cf_type_ref, buffer, buffer_size, kCFStringEncodingUTF8)) { result = launch_data_alloc(LAUNCH_DATA_STRING); launch_data_set_string(result, buffer); } else { local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("CFStringGetCString failed %@"), cf_type_ref); } free(buffer); } else { local_error = GTMCFLaunchCreateUnlocalizedError(ENOMEM, CFSTR("calloc of %lu failed"), (unsigned long)buffer_size); } } else if (cf_type == CFBooleanGetTypeID()) { result = launch_data_alloc(LAUNCH_DATA_BOOL); launch_data_set_bool(result, CFBooleanGetValue(cf_type_ref)); } else if (cf_type == CFArrayGetTypeID()) { CFIndex count = CFArrayGetCount(cf_type_ref); result = launch_data_alloc(LAUNCH_DATA_ARRAY); for (CFIndex i = 0; i < count; i++) { CFTypeRef array_value = CFArrayGetValueAtIndex(cf_type_ref, i); if (array_value) { launch_data_t launch_value = GTMLaunchDataCreateFromCFType(array_value, &local_error); if (local_error) break; launch_data_array_set_index(result, launch_value, i); } } } else if (cf_type == CFDictionaryGetTypeID()) { result = launch_data_alloc(LAUNCH_DATA_DICTIONARY); GTMCFToLDictContext context = { result, &local_error }; CFDictionaryApplyFunction(cf_type_ref, GTMConvertCFDictEntryToLaunchDataDictEntry, &context); } else if (cf_type == CFDataGetTypeID()) { result = launch_data_alloc(LAUNCH_DATA_OPAQUE); launch_data_set_opaque(result, CFDataGetBytePtr(cf_type_ref), CFDataGetLength(cf_type_ref)); } else if (cf_type == CFNumberGetTypeID()) { CFNumberType cf_number_type = CFNumberGetType(cf_type_ref); switch (cf_number_type) { case kCFNumberSInt8Type: case kCFNumberSInt16Type: case kCFNumberSInt32Type: case kCFNumberSInt64Type: case kCFNumberCharType: case kCFNumberShortType: case kCFNumberIntType: case kCFNumberLongType: case kCFNumberLongLongType: case kCFNumberCFIndexType: case kCFNumberNSIntegerType:{ long long value; if (CFNumberGetValue(cf_type_ref, kCFNumberLongLongType, &value)) { result = launch_data_alloc(LAUNCH_DATA_INTEGER); launch_data_set_integer(result, value); } else { local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("Unknown to convert: %@"), cf_type_ref); } break; } case kCFNumberFloat32Type: case kCFNumberFloat64Type: case kCFNumberFloatType: case kCFNumberDoubleType: case kCFNumberCGFloatType: { double value; if (CFNumberGetValue(cf_type_ref, kCFNumberDoubleType, &value)) { result = launch_data_alloc(LAUNCH_DATA_REAL); launch_data_set_real(result, value); } else { local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("Unknown to convert: %@"), cf_type_ref); } break; } default: local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("Unknown CFNumberType %lld"), (long long)cf_number_type); break; } } else { local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("Unknown CFTypeID %lu"), (unsigned long)cf_type); } exit: if (error) { *error = local_error; } else if (local_error) { #ifdef DEBUG CFShow(local_error); #endif // DEBUG CFRelease(local_error); } return result; } CFTypeRef GTMCFTypeCreateFromLaunchData(launch_data_t ldata, bool convert_non_standard_objects, CFErrorRef *error) { CFTypeRef cf_type_ref = NULL; CFErrorRef local_error = NULL; if (ldata == NULL) { local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("NULL ldata")); goto exit; } launch_data_type_t ldata_type = launch_data_get_type(ldata); switch (ldata_type) { case LAUNCH_DATA_STRING: cf_type_ref = CFStringCreateWithCString(kCFAllocatorDefault, launch_data_get_string(ldata), kCFStringEncodingUTF8); break; case LAUNCH_DATA_INTEGER: { long long value = launch_data_get_integer(ldata); cf_type_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberLongLongType, &value); break; } case LAUNCH_DATA_REAL: { double value = launch_data_get_real(ldata); cf_type_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &value); break; } case LAUNCH_DATA_BOOL: { bool value = launch_data_get_bool(ldata); cf_type_ref = value ? kCFBooleanTrue : kCFBooleanFalse; CFRetain(cf_type_ref); break; } case LAUNCH_DATA_OPAQUE: { // Must get the data before we get the size. // Otherwise the size will come back faulty on macOS 10.11.6. // Radar: 28509492 launch_data_get_opaque_size gives wrong size void *data = launch_data_get_opaque(ldata); size_t size = launch_data_get_opaque_size(ldata); cf_type_ref = CFDataCreate(kCFAllocatorDefault, data, size); break; } case LAUNCH_DATA_ARRAY: { size_t count = launch_data_array_get_count(ldata); cf_type_ref = CFArrayCreateMutable(kCFAllocatorDefault, count, &kCFTypeArrayCallBacks); if (cf_type_ref) { for (size_t i = 0; !local_error && i < count; i++) { launch_data_t l_sub_data = launch_data_array_get_index(ldata, i); CFTypeRef cf_sub_type = GTMCFTypeCreateFromLaunchData(l_sub_data, convert_non_standard_objects, &local_error); if (cf_sub_type) { CFArrayAppendValue((CFMutableArrayRef)cf_type_ref, cf_sub_type); CFRelease(cf_sub_type); } } } break; } case LAUNCH_DATA_DICTIONARY: cf_type_ref = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); if (cf_type_ref) { GTMLToCFDictContext context = { (CFMutableDictionaryRef)cf_type_ref, convert_non_standard_objects, &local_error }; launch_data_dict_iterate(ldata, GTMConvertLaunchDataDictEntryToCFDictEntry, &context); } break; case LAUNCH_DATA_FD: if (convert_non_standard_objects) { int file_descriptor = launch_data_get_fd(ldata); cf_type_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &file_descriptor); } break; case LAUNCH_DATA_MACHPORT: if (convert_non_standard_objects) { mach_port_t port = launch_data_get_machport(ldata); cf_type_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &port); } break; default: local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("Unknown launchd type %d"), ldata_type); break; } exit: if (error) { *error = local_error; } else if (local_error) { #ifdef DEBUG CFShow(local_error); #endif // DEBUG CFRelease(local_error); } return cf_type_ref; } // open the standard file descs to devnull. static int open_devnull(int fd) { FILE *f = NULL; if (fd == STDIN_FILENO) { f = freopen(_PATH_DEVNULL, "rb", stdin); } else if (fd == STDOUT_FILENO) { f = freopen(_PATH_DEVNULL, "wb", stdout); } else if (fd == STDERR_FILENO) { f = freopen(_PATH_DEVNULL, "wb", stderr); } return (f && fileno(f) == fd); } void spc_sanitize_files(void) { int standard_fds[] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO }; int standard_fds_count = (int)(sizeof(standard_fds) / sizeof(standard_fds[0])); // Make sure all open descriptors other than the standard ones are closed int fds = getdtablesize(); for (int i = standard_fds_count; i < fds; ++i) close(i); // Verify that the standard descriptors are open. If they're not, attempt to // open them using /dev/null. If any are unsuccessful, abort. for (int i = 0; i < standard_fds_count; ++i) { struct stat st; int fd = standard_fds[i]; if (fstat(fd, &st) == -1 && (errno != EBADF || !open_devnull(fd))) { abort(); } } } void spc_drop_privileges(void) { gid_t newgid = getgid(), oldgid = getegid(); uid_t newuid = getuid(), olduid = geteuid(); // If root privileges are to be dropped, be sure to pare down the ancillary // groups for the process before doing anything else because the setgroups() // system call requires root privileges. Drop ancillary groups regardless of // whether privileges are being dropped temporarily or permanently. if (!olduid) setgroups(1, &newgid); if (newgid != oldgid) { if (setregid(-1, newgid) == -1) { abort(); } } if (newuid != olduid) { if (setregid(-1, newuid) == -1) { abort(); } } // verify that the changes were successful if (newgid != oldgid && (setegid(oldgid) != -1 || getegid() != newgid)) { abort(); } if (newuid != olduid && (seteuid(olduid) != -1 || geteuid() != newuid)) { abort(); } } Boolean GTMSMJobSubmit(CFDictionaryRef cf_job, CFErrorRef *error) { // We launch our jobs using launchctl instead of doing it by hand // because launchctl does a whole pile of parsing of the job internally // to handle the sockets cases that we don't want to duplicate here. int fd = -1; CFDataRef xmlData = NULL; CFErrorRef local_error = NULL; if (!cf_job) { local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("NULL Job."), NULL); goto exit; } CFStringRef jobLabel = CFDictionaryGetValue(cf_job, CFSTR(LAUNCH_JOBKEY_LABEL)); if (!jobLabel) { local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("Job missing label."), NULL); goto exit; } CFDictionaryRef jobDict = GTMSMJobCopyDictionary(jobLabel); if (jobDict) { CFRelease(jobDict); local_error = GTMCFLaunchCreateUnlocalizedError(EEXIST, CFSTR("Job already exists %@."), jobLabel); goto exit; } xmlData = CFPropertyListCreateXMLData(NULL, cf_job); if (!xmlData) { local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("Invalid Job %@."), jobLabel); goto exit; } char fileName[] = _PATH_TMP "GTMServiceManagement.XXXXXX.plist"; fd = mkstemps(fileName, 6); if (fd == -1) { local_error = GTMCFLaunchCreateUnlocalizedError(errno, CFSTR("Unable to create %s."), fileName); goto exit; } write(fd, CFDataGetBytePtr(xmlData), CFDataGetLength(xmlData)); close(fd); pid_t childpid = fork(); if (childpid == -1) { local_error = GTMCFLaunchCreateUnlocalizedError(errno, CFSTR("Unable to fork."), NULL); goto exit; } if (childpid != 0) { // Parent process int status = 0; pid_t pid = -1; do { pid = waitpid(childpid, &status, 0); } while (pid == -1 && errno == EINTR); if (pid == -1) { local_error = GTMCFLaunchCreateUnlocalizedError(errno, CFSTR("waitpid failed.")); goto exit; } else if (WEXITSTATUS(status)) { local_error = GTMCFLaunchCreateUnlocalizedError(ECHILD, CFSTR("Child exit status: %d " "pid: %d"), WEXITSTATUS(status), childpid); goto exit; } } else { // Child Process spc_sanitize_files(); spc_drop_privileges(); const char *args[] = { "launchctl", "load", fileName, NULL }; execve("/bin/launchctl", (char* const*)args, NULL); abort(); } exit: if (xmlData) { CFRelease(xmlData); } if (fd != -1) { unlink(fileName); } if (error) { *error = local_error; } else if (local_error) { #ifdef DEBUG CFShow(local_error); #endif // DEBUG CFRelease(local_error); } return local_error == NULL; } CFDictionaryRef GTMSMCopyJobCheckInDictionary(CFErrorRef *error) { CFErrorRef local_error = NULL; CFDictionaryRef check_in_dict = NULL; launch_data_t msg = launch_data_new_string(LAUNCH_KEY_CHECKIN); launch_data_t resp = launch_msg(msg); launch_data_free(msg); if (resp) { launch_data_type_t resp_type = launch_data_get_type(resp); switch (resp_type) { case LAUNCH_DATA_DICTIONARY: check_in_dict = GTMCFTypeCreateFromLaunchData(resp, true, &local_error); break; case LAUNCH_DATA_ERRNO: { int e = launch_data_get_errno(resp); if (e) { local_error = GTMCFLaunchCreateUnlocalizedError(e, CFSTR("")); } break; } default: local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("unknown response from launchd %d"), resp_type); break; } launch_data_free(resp); } else { local_error = GTMCFLaunchCreateUnlocalizedError(errno, CFSTR("")); } if (error) { *error = local_error; } else if (local_error) { #ifdef DEBUG CFShow(local_error); #endif // DEBUG CFRelease(local_error); } return check_in_dict; } Boolean GTMSMJobRemove(CFStringRef jobLabel, CFErrorRef *error) { CFErrorRef local_error = NULL; launch_data_t resp = GTMPerformOnLabel(LAUNCH_KEY_REMOVEJOB, jobLabel, &local_error); if (resp) { launch_data_type_t resp_type = launch_data_get_type(resp); switch (resp_type) { case LAUNCH_DATA_ERRNO: { int e = launch_data_get_errno(resp); // In OSX 10.10+, launch_msg(LAUNCH_KEY_REMOVEJOB, ...) returns the // error EINPROGRESS if the job was running at the time it was removed. // This should be considered a success as it was on earlier OS versions. if (e == EINPROGRESS && IsOsYosemiteOrGreater()) { break; } if (e) { local_error = GTMCFLaunchCreateUnlocalizedError(e, CFSTR("")); } break; } default: local_error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("unknown response from launchd %d"), resp_type); break; } launch_data_free(resp); } if (error) { *error = local_error; } else if (local_error) { #ifdef DEBUG CFShow(local_error); #endif // DEBUG CFRelease(local_error); } return local_error == NULL; } CFDictionaryRef GTMSMJobCopyDictionary(CFStringRef jobLabel) { CFDictionaryRef dict = NULL; CFErrorRef error = NULL; launch_data_t resp = GTMPerformOnLabel(LAUNCH_KEY_GETJOB, jobLabel, &error); if (resp) { launch_data_type_t ldata_Type = launch_data_get_type(resp); if (ldata_Type == LAUNCH_DATA_DICTIONARY) { dict = GTMCFTypeCreateFromLaunchData(resp, true, &error); } else { error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("Unknown launchd type %d"), ldata_Type); } launch_data_free(resp); } if (error) { #ifdef DEBUG CFShow(error); #endif // DEBUG CFRelease(error); } return dict; } CFDictionaryRef GTMSMCopyAllJobDictionaries(void) { CFDictionaryRef dict = NULL; launch_data_t msg = launch_data_new_string(LAUNCH_KEY_GETJOBS); launch_data_t resp = launch_msg(msg); launch_data_free(msg); CFErrorRef error = NULL; if (resp) { launch_data_type_t ldata_Type = launch_data_get_type(resp); if (ldata_Type == LAUNCH_DATA_DICTIONARY) { dict = GTMCFTypeCreateFromLaunchData(resp, true, &error); } else { error = GTMCFLaunchCreateUnlocalizedError(EINVAL, CFSTR("Unknown launchd type %d"), ldata_Type); } launch_data_free(resp); } else { error = GTMCFLaunchCreateUnlocalizedError(errno, CFSTR("")); } if (error) { #ifdef DEBUG CFShow(error); #endif // DEBUG CFRelease(error); } return dict; } #pragma clang diagnostic pop #endif // if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_4