aboutsummaryrefslogtreecommitdiff
path: root/Foundation
diff options
context:
space:
mode:
authorGravatar gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2010-11-04 22:00:47 +0000
committerGravatar gtm.daemon <gtm.daemon@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3>2010-11-04 22:00:47 +0000
commit2e0d716e8fe85d33f1b68e2830d2f33c79dd8289 (patch)
tree1cbf70170d2a4c66bc708d02edb47e0bf5f4f95e /Foundation
parent5dcfbf8df7de8cbdc2dbd699cdb980b4d18fb1c7 (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.c295
-rw-r--r--Foundation/GTMServiceManagement.h4
-rw-r--r--Foundation/GTMServiceManagementTest.m82
-rw-r--r--Foundation/GTMServiceManagementTestingHarness.c31
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