diff options
author | gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2010-11-04 22:00:47 +0000 |
---|---|---|
committer | gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2010-11-04 22:00:47 +0000 |
commit | 2e0d716e8fe85d33f1b68e2830d2f33c79dd8289 (patch) | |
tree | 1cbf70170d2a4c66bc708d02edb47e0bf5f4f95e /Foundation | |
parent | 5dcfbf8df7de8cbdc2dbd699cdb980b4d18fb1c7 (diff) |
[Author: dmaclach]
Clean up GTMServiceManagement so that it works with sockets.
Add better tests.
R=thomasvl
DELTA=478 (303 added, 109 deleted, 66 changed)
Diffstat (limited to 'Foundation')
-rw-r--r-- | Foundation/GTMServiceManagement.c | 295 | ||||
-rw-r--r-- | Foundation/GTMServiceManagement.h | 4 | ||||
-rw-r--r-- | Foundation/GTMServiceManagementTest.m | 82 | ||||
-rw-r--r-- | Foundation/GTMServiceManagementTestingHarness.c | 31 |
4 files changed, 331 insertions, 81 deletions
diff --git a/Foundation/GTMServiceManagement.c b/Foundation/GTMServiceManagement.c index d6b009d..20fb6cc 100644 --- a/Foundation/GTMServiceManagement.c +++ b/Foundation/GTMServiceManagement.c @@ -16,6 +16,15 @@ // 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, @@ -25,6 +34,10 @@ // launch_data_t can only be in one collection at any given time. #include "GTMServiceManagement.h" +#include <paths.h> +#include <unistd.h> +#include <sys/stat.h> +#include <vproc.h> #if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_4 @@ -271,7 +284,9 @@ exit: if (error) { *error = local_error; } else if (local_error) { +#ifdef DEBUG CFShow(local_error); +#endif // DEBUG CFRelease(local_error); } return result; @@ -394,92 +409,182 @@ 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 = sizeof(standard_fds) / sizeof(standard_fds[0]); + + // Make sure all open descriptors other than the standard ones are closed + int fds = getdtablesize(); + for (int fd = standard_fds_count; fd < fds; fd++) close(fd); + + // 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 fd = 0; fd < standard_fds_count; fd++) { + struct stat st; + 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; + char fileName[] = _PATH_TMP "GTMServiceManagement.XXXXXX"; CFErrorRef local_error = NULL; - launch_data_t launch_job = GTMLaunchDataCreateFromCFType(cf_job, - &local_error); - if (!local_error) { - launch_data_t jobs = launch_data_alloc(LAUNCH_DATA_ARRAY); - launch_data_array_set_index(jobs, launch_job, 0); - launch_data_t msg = launch_data_alloc(LAUNCH_DATA_DICTIONARY); - launch_data_dict_insert(msg, jobs, LAUNCH_KEY_SUBMITJOB); - launch_data_t resp = launch_msg(msg); - if (resp) { - launch_data_type_t resp_type = launch_data_get_type(resp); - switch (resp_type) { - case LAUNCH_DATA_ARRAY: - for (size_t i = 0; i < launch_data_array_get_count(jobs); i++) { - launch_data_t job_response = launch_data_array_get_index(resp, i); - launch_data_t job = launch_data_array_get_index(jobs, i); - launch_data_t job_label - = launch_data_dict_lookup(job, LAUNCH_JOBKEY_LABEL); - const char *job_string - = job_label ? launch_data_get_string(job_label) : "Unlabeled job"; - if (LAUNCH_DATA_ERRNO == launch_data_get_type(job_response)) { - int job_err = launch_data_get_errno(job_response); - if (job_err != 0) { - // We only keep the last error - if (local_error) { - CFRelease(local_error); - local_error = NULL; - } - switch (job_err) { - case EEXIST: - local_error - = GTMCFLaunchCreateUnlocalizedError(job_err, - CFSTR("%s already loaded"), - job_string); - break; - case ESRCH: - local_error - = GTMCFLaunchCreateUnlocalizedError(job_err, - CFSTR("%s not loaded"), - job_string); - break; - default: - local_error - = GTMCFLaunchCreateUnlocalizedError(job_err, - CFSTR("%s failed to load"), - job_string); - break; - } - } - } - } - break; - case LAUNCH_DATA_ERRNO: { - int e = launch_data_get_errno(resp); - if (e) { - local_error = GTMCFLaunchCreateUnlocalizedError(e, CFSTR("")); - } - break; - } + if (!cf_job) { + local_error + = GTMCFLaunchCreateUnlocalizedError(EINVAL, + CFSTR("NULL Job."), + NULL); + goto exit; + } - default: - local_error - = GTMCFLaunchCreateUnlocalizedError(EINVAL, - CFSTR("unknown response from launchd %d"), - resp_type); - break; - } - launch_data_free(resp); - launch_data_free(msg); - } else { - local_error = GTMCFLaunchCreateUnlocalizedError(errno, CFSTR("")); - } + 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; + } + + fd = mkstemp(fileName); + 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 || WEXITSTATUS(status)) { + local_error + = GTMCFLaunchCreateUnlocalizedError(errno, + CFSTR("Child Process Error.\n" + "Cmd: /bin/launchctl\n" + "pid: %d\n" + "ExitStatus: %d\n"), + childpid, WEXITSTATUS(status)); + 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; @@ -520,7 +625,9 @@ CFDictionaryRef GTMSMJobCheckIn(CFErrorRef *error) { if (error) { *error = local_error; } else if (local_error) { +#ifdef DEBUG CFShow(local_error); +#endif // DEBUG CFRelease(local_error); } return check_in_dict; @@ -556,7 +663,9 @@ Boolean GTMSMJobRemove(CFStringRef jobLabel, CFErrorRef *error) { if (error) { *error = local_error; } else if (local_error) { +#ifdef DEBUG CFShow(local_error); +#endif // DEBUG CFRelease(local_error); } return local_error == NULL; @@ -580,7 +689,9 @@ CFDictionaryRef GTMSMJobCopyDictionary(CFStringRef jobLabel) { launch_data_free(resp); } if (error) { +#ifdef DEBUG CFShow(error); +#endif // DEBUG CFRelease(error); } return dict; @@ -608,10 +719,54 @@ CFDictionaryRef GTMSMCopyAllJobDictionaries(void) { = GTMCFLaunchCreateUnlocalizedError(errno, CFSTR("")); } if (error) { +#ifdef DEBUG CFShow(error); +#endif // DEBUG CFRelease(error); } return dict; } +// Some private SPIs defined by apple in the launchd sources +// http://opensource.apple.com/source/launchd/launchd-258.25/launchd/src/ +// and +// http://opensource.apple.com/source/launchd/launchd-329.3/launchd/src/ +// It turns out that they renamed the enum that I need to use between 10.5 and +// 10.6. Luckily if we request the 10_5 value on 10_6 we get an error +// so we just ask for the 10_5 value first, and then the 10_6 value second. + +typedef enum { + VPROC_GSK_ENVIRONMENT_10_5 = 10, + VPROC_GSK_ENVIRONMENT_10_6 = 11 +} vproc_gsk_t; + +extern vproc_err_t vproc_swap_complex(vproc_t vp, + vproc_gsk_t key, + launch_data_t inval, + launch_data_t *outval); + +CFDictionaryRef GTMCopyLaunchdExports(void) { + launch_data_t resp; + CFDictionaryRef dict = NULL; + vproc_err_t err = vproc_swap_complex(NULL, + VPROC_GSK_ENVIRONMENT_10_5, + NULL, + &resp); + if (err) { + err = vproc_swap_complex(NULL, VPROC_GSK_ENVIRONMENT_10_6, NULL, &resp); + } + if (err == NULL) { + CFErrorRef error = NULL; + dict = GTMCFTypeCreateFromLaunchData(resp, false, &error); + if (error) { +#ifdef DEBUG + CFShow(error); +#endif // DEBUG + CFRelease(error); + } + launch_data_free(resp); + } + return dict; +} + #endif // if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_4 diff --git a/Foundation/GTMServiceManagement.h b/Foundation/GTMServiceManagement.h index dc7f5ff..7a8e6b2 100644 --- a/Foundation/GTMServiceManagement.h +++ b/Foundation/GTMServiceManagement.h @@ -68,6 +68,10 @@ CFTypeRef GTMCFTypeCreateFromLaunchData(launch_data_t ldata, bool convert_non_standard_objects, CFErrorRef *error); +// Returns the list of exports defined by launchd. +// Caller is takes ownership of the returned type. +CFDictionaryRef GTMCopyLaunchdExports(); + GTM_EXTERN_C_END #endif // if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_4 diff --git a/Foundation/GTMServiceManagementTest.m b/Foundation/GTMServiceManagementTest.m index 45bf478..a2eda4a 100644 --- a/Foundation/GTMServiceManagementTest.m +++ b/Foundation/GTMServiceManagementTest.m @@ -22,10 +22,18 @@ #import "GTMSenTestCase.h" #import "GTMGarbageCollection.h" +#import <servers/bootstrap.h> #define STANDARD_JOB_LABEL "com.apple.launchctl.Background" #define OUR_JOB_LABEL "com.google.gtm.GTMServiceManagementTest.job" #define BAD_JOB_LABEL "com.google.gtm.GTMServiceManagementTest.badjob" +#define TEST_HARNESS_LABEL "com.google.gtm.GTMServiceManagementTestHarness" +#define GTM_MACH_PORT_NAME "GTMServiceManagementTestingHarnessMachPort" + +static NSString const *kGTMSocketKey + = @"COM_GOOGLE_GTM_GTMSERVICEMANAGEMENT_TEST_SOCKET"; +static NSString const *kGTMSocketName + = @"GTMServiceManagementTesting"; @interface GTMServiceManagementTest : GTMTestCase @end @@ -114,14 +122,6 @@ STAssertEquals([(NSError *)error code], (NSInteger)EEXIST, nil); CFRelease(error); - NSDictionary *emptyJob - = [NSDictionary dictionaryWithObject:@"Empty Job" - forKey:@LAUNCH_JOBKEY_LABEL]; - isGood = GTMSMJobSubmit((CFDictionaryRef)emptyJob, &error); - STAssertFalse(isGood, nil); - STAssertEquals([(NSError *)error code], (NSInteger)EINVAL, nil); - CFRelease(error); - NSDictionary *goodJob = [NSDictionary dictionaryWithObjectsAndKeys: @OUR_JOB_LABEL, @LAUNCH_JOBKEY_LABEL, @@ -142,16 +142,76 @@ CFRelease(error); } +- (void)testCopyExports { + CFDictionaryRef exports = GTMCopyLaunchdExports(); + STAssertNotNULL(exports, nil); + NSString *user = [(NSDictionary *)exports objectForKey:@"USER"]; + STAssertEqualObjects(user, NSUserName(), nil); + CFRelease(exports); +} + - (void)testCheckin { CFErrorRef error = NULL; - // TODO(dmaclach): to actually test checkin we would need a subprocess to - // launch, which is too complex at this point. Perhaps - // in the future if bugs are found. + // Can't check ourselves in NSDictionary *badTest = GTMCFAutorelease(GTMSMJobCheckIn(&error)); STAssertNil(badTest, nil); STAssertNotNULL(error, nil); CFRelease(error); + + NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; + STAssertNotNil(testBundle, nil); + NSString *testHarnessPath + = [testBundle pathForResource:@"GTMServiceManagementTestingHarness" + ofType:nil]; + STAssertNotNil(testHarnessPath, nil); + NSDictionary *machServices + = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool:YES], @GTM_MACH_PORT_NAME, + nil]; + + NSDictionary *socket + = [NSDictionary dictionaryWithObjectsAndKeys: + kGTMSocketKey,@LAUNCH_JOBSOCKETKEY_SECUREWITHKEY, + nil]; + + NSDictionary *sockets + = [NSDictionary dictionaryWithObjectsAndKeys: + socket, kGTMSocketName, + nil]; + + // LAUNCH_JOBKEY_WAITFORDEBUGGER left commented out + // so that it can easily be reenabled for debugging. + NSDictionary *job = [NSDictionary dictionaryWithObjectsAndKeys: + @TEST_HARNESS_LABEL, @LAUNCH_JOBKEY_LABEL, + testHarnessPath, @LAUNCH_JOBKEY_PROGRAM, + [NSNumber numberWithBool:YES], @LAUNCH_JOBKEY_RUNATLOAD, + [NSNumber numberWithBool:YES], @LAUNCH_JOBKEY_DEBUG, + //[NSNumber numberWithBool:YES], @LAUNCH_JOBKEY_WAITFORDEBUGGER, + machServices, @LAUNCH_JOBKEY_MACHSERVICES, + sockets, @LAUNCH_JOBKEY_SOCKETS, + nil]; + + // This is allowed to fail. + GTMSMJobRemove(CFSTR(TEST_HARNESS_LABEL), NULL); + + BOOL isGood = GTMSMJobSubmit((CFDictionaryRef)job, &error); + STAssertTrue(isGood, @"Error %@", error); + + NSDictionary* exports = GTMCFAutorelease(GTMCopyLaunchdExports()); + STAssertNotNULL(exports, nil); + NSString *socketPath = [exports objectForKey:kGTMSocketKey]; + STAssertNotNULL(socketPath, nil); + STAssertEqualObjects([socketPath lastPathComponent], kGTMSocketName, nil); + + mach_port_t sp = 0; + kern_return_t rt = bootstrap_look_up(bootstrap_port, + GTM_MACH_PORT_NAME, + &sp); + STAssertNotEquals(sp, (mach_port_t)0, nil); + STAssertEquals(rt, KERN_SUCCESS, nil); + isGood = GTMSMJobRemove(CFSTR(TEST_HARNESS_LABEL), &error); + STAssertTrue(isGood, @"Error %@", error); } @end diff --git a/Foundation/GTMServiceManagementTestingHarness.c b/Foundation/GTMServiceManagementTestingHarness.c new file mode 100644 index 0000000..dadff13 --- /dev/null +++ b/Foundation/GTMServiceManagementTestingHarness.c @@ -0,0 +1,31 @@ +// +// GTMServiceManagementTestingHarness.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. +// + +#include "GTMServiceManagement.h" + +#if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_4 + +int main(int argc, const char** argv) { + CFErrorRef error = NULL; + CFDictionaryRef dict = GTMSMJobCheckIn(&error); + if (!dict) { + CFShow(error); + } + return 0; +} +#endif // if MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_4 |