Bug 394984: Enable any admin user on OSX to update Firefox, native OSX changes. r=mstange
authorStephen A Pohl <spohl.mozilla.bugs@gmail.com>
Tue, 24 May 2016 22:25:16 -0400
changeset 337882 a468fe7af93708141cbbc201890909adf4acad83
parent 337881 e33952e456ea3a7371c66b1a702ef8a0e17efb8b
child 337883 0cd11745dc4526123de7a67d80c0084e5e089e85
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmstange
bugs394984
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 394984: Enable any admin user on OSX to update Firefox, native OSX changes. r=mstange
toolkit/mozapps/update/updater/launchchild_osx.mm
toolkit/xre/MacLaunchHelper.h
toolkit/xre/MacLaunchHelper.mm
toolkit/xre/updaterfileutils_osx.h
toolkit/xre/updaterfileutils_osx.mm
--- a/toolkit/mozapps/update/updater/launchchild_osx.mm
+++ b/toolkit/mozapps/update/updater/launchchild_osx.mm
@@ -5,114 +5,82 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include <Cocoa/Cocoa.h>
 #include <CoreServices/CoreServices.h>
 #include <crt_externs.h>
 #include <stdlib.h>
 #include <stdio.h>
 #include <spawn.h>
+#include <SystemConfiguration/SystemConfiguration.h>
 #include "readstrings.h"
 
-// Prefer the currently running architecture (this is the same as the
-// architecture that launched the updater) and fallback to CPU_TYPE_ANY if it
-// is no longer available after the update.
-static cpu_type_t pref_cpu_types[2] = {
-#if defined(__i386__)
-                                 CPU_TYPE_X86,
-#elif defined(__x86_64__)
-                                 CPU_TYPE_X86_64,
-#elif defined(__ppc__)
-                                 CPU_TYPE_POWERPC,
-#endif
-                                 CPU_TYPE_ANY };
-
-void LaunchChild(int argc, char **argv)
-{
-  // Initialize spawn attributes.
-  posix_spawnattr_t spawnattr;
-  if (posix_spawnattr_init(&spawnattr) != 0) {
-    printf("Failed to init posix spawn attribute.");
-    return;
+class MacAutoreleasePool {
+public:
+  MacAutoreleasePool()
+  {
+    mPool = [[NSAutoreleasePool alloc] init];
+  }
+  ~MacAutoreleasePool()
+  {
+    [mPool release];
   }
 
-  // Set spawn attributes.
-  size_t attr_count = 2;
-  size_t attr_ocount = 0;
-  if (posix_spawnattr_setbinpref_np(&spawnattr, attr_count, pref_cpu_types, &attr_ocount) != 0 ||
-      attr_ocount != attr_count) {
-    printf("Failed to set binary preference on posix spawn attribute.");
-    posix_spawnattr_destroy(&spawnattr);
-    return;
-  }
+private:
+  NSAutoreleasePool* mPool;
+};
+
+void LaunchChild(int argc, const char** argv)
+{
+  MacAutoreleasePool pool;
 
-  // "posix_spawnp" uses null termination for arguments rather than a count.
-  // Note that we are not duplicating the argument strings themselves.
-  char** argv_copy = (char**)malloc((argc + 1) * sizeof(char*));
-  if (!argv_copy) {
-    printf("Failed to allocate memory for arguments.");
-    posix_spawnattr_destroy(&spawnattr);
-    return;
-  }
-  for (int i = 0; i < argc; i++) {
-    argv_copy[i] = argv[i];
-  }
-  argv_copy[argc] = NULL;
-
-  // Pass along our environment.
-  char** envp = NULL;
-  char*** cocoaEnvironment = _NSGetEnviron();
-  if (cocoaEnvironment) {
-    envp = *cocoaEnvironment;
-  }
-
-  int result = posix_spawnp(NULL, argv_copy[0], NULL, &spawnattr, argv_copy, envp);
-
-  free(argv_copy);
-  posix_spawnattr_destroy(&spawnattr);
-
-  if (result != 0) {
-    printf("Process spawn failed with code %d!", result);
+  @try {
+    NSString* launchPath = [NSString stringWithUTF8String:argv[0]];
+    NSMutableArray* arguments = [NSMutableArray arrayWithCapacity:argc];
+    for (int i = 1; i < argc; i++) {
+      [arguments addObject:[NSString stringWithUTF8String:argv[i]]];
+    }
+    [NSTask launchedTaskWithLaunchPath:launchPath
+                             arguments:arguments];
+  } @catch (NSException* e) {
+    // Ignore any exception.
   }
 }
 
 void
 LaunchMacPostProcess(const char* aAppBundle)
 {
+  MacAutoreleasePool pool;
+
   // Launch helper to perform post processing for the update; this is the Mac
   // analogue of LaunchWinPostProcess (PostUpdateWin).
-  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
-
   NSString* iniPath = [NSString stringWithUTF8String:aAppBundle];
   iniPath =
     [iniPath stringByAppendingPathComponent:@"Contents/Resources/updater.ini"];
 
   NSFileManager* fileManager = [NSFileManager defaultManager];
   if (![fileManager fileExistsAtPath:iniPath]) {
     // the file does not exist; there is nothing to run
-    [pool release];
     return;
   }
 
   int readResult;
   char values[2][MAX_TEXT_LEN];
   readResult = ReadStrings([iniPath UTF8String],
                            "ExeRelPath\0ExeArg\0",
                            2,
                            values,
                            "PostUpdateMac");
   if (readResult) {
-    [pool release];
     return;
   }
 
   NSString *exeRelPath = [NSString stringWithUTF8String:values[0]];
   NSString *exeArg = [NSString stringWithUTF8String:values[1]];
   if (!exeArg || !exeRelPath) {
-    [pool release];
     return;
   }
 
   NSString* exeFullPath = [NSString stringWithUTF8String:aAppBundle];
   exeFullPath = [exeFullPath stringByAppendingPathComponent:exeRelPath];
 
   char optVals[1][MAX_TEXT_LEN];
   readResult = ReadStrings([iniPath UTF8String],
@@ -128,11 +96,258 @@ LaunchMacPostProcess(const char* aAppBun
   if (!readResult) {
     NSString *exeAsync = [NSString stringWithUTF8String:optVals[0]];
     if ([exeAsync isEqualToString:@"false"]) {
       [task waitUntilExit];
     }
   }
   // ignore the return value of the task, there's nothing we can do with it
   [task release];
+}
 
-  [pool release];
+void CleanupElevatedMacUpdate(bool aFailureOccurred)
+{
+  MacAutoreleasePool pool;
+
+  id updateServer = nil;
+  @try {
+    updateServer = (id)[NSConnection
+      rootProxyForConnectionWithRegisteredName:
+        @"org.mozilla.updater.server"
+      host:nil
+      usingNameServer:[NSSocketPortNameServer sharedInstance]];
+    if (aFailureOccurred &&
+        updateServer &&
+        [updateServer respondsToSelector:@selector(abort)]) {
+      [updateServer performSelector:@selector(abort)];
+    }
+    else if (updateServer &&
+             [updateServer respondsToSelector:@selector(shutdown)]) {
+      [updateServer performSelector:@selector(shutdown)];
+    }
+  } @catch (NSException* e) {
+    // Ignore exceptions.
+  }
+  NSFileManager* manager = [NSFileManager defaultManager];
+  [manager removeItemAtPath:@"/Library/PrivilegedHelperTools/org.mozilla.updater"
+                      error:nil];
+  [manager removeItemAtPath:@"/Library/LaunchDaemons/org.mozilla.updater.plist"
+                      error:nil];
+  const char* launchctlArgs[] = {"/bin/launchctl",
+                                 "remove",
+                                 "org.mozilla.updater"};
+  // The following call will terminate the current process due to the "remove"
+  // argument in launchctlArgs.
+  LaunchChild(3, launchctlArgs);
+}
+
+// Note: Caller is responsible for freeing argv.
+bool ObtainUpdaterArguments(int* argc, char*** argv)
+{
+  MacAutoreleasePool pool;
+
+  id updateServer = nil;
+  @try {
+    updateServer = (id)[NSConnection
+      rootProxyForConnectionWithRegisteredName:
+        @"org.mozilla.updater.server"
+      host:nil
+      usingNameServer:[NSSocketPortNameServer sharedInstance]];
+    if (!updateServer ||
+        ![updateServer respondsToSelector:@selector(getArguments)] ||
+        ![updateServer respondsToSelector:@selector(shutdown)]) {
+      NSLog(@"Server doesn't exist or doesn't provide correct selectors.");
+      // Let's try our best and clean up.
+      CleanupElevatedMacUpdate(true);
+      return false; // Won't actually get here due to CleanupElevatedMacUpdate.
+    }
+    NSArray* updaterArguments =
+      [updateServer performSelector:@selector(getArguments)];
+    *argc = [updaterArguments count];
+    char** tempArgv = (char**)malloc(sizeof(char*) * (*argc));
+    for (int i = 0; i < *argc; i++) {
+      int argLen = [[updaterArguments objectAtIndex:i] length] + 1;
+      tempArgv[i] = (char*)malloc(argLen);
+      strncpy(tempArgv[i], [[updaterArguments objectAtIndex:i] UTF8String],
+              argLen);
+    }
+    *argv = tempArgv;
+  } @catch (NSException* e) {
+    // Let's try our best and clean up.
+    CleanupElevatedMacUpdate(true);
+    return false; // Won't actually get here due to CleanupElevatedMacUpdate.
+  }
+  return true;
+}
+
+/**
+ * The ElevatedUpdateServer is launched from a non-elevated updater process.
+ * It allows an elevated updater process (usually a privileged helper tool) to
+ * connect to it and receive all the necessary arguments to complete a
+ * successful update.
+ */
+@interface ElevatedUpdateServer : NSObject
+{
+  NSArray* mUpdaterArguments;
+  BOOL mShouldKeepRunning;
+  BOOL mAborted;
+}
+- (id)initWithArgs:(NSArray*)args;
+- (BOOL)runServer;
+- (NSArray*)getArguments;
+- (void)abort;
+- (BOOL)wasAborted;
+- (void)shutdown;
+- (BOOL)shouldKeepRunning;
+@end
+
+@implementation ElevatedUpdateServer
+
+- (id)initWithArgs:(NSArray*)args
+{
+  self = [super init];
+  if (!self) {
+    return nil;
+  }
+  mUpdaterArguments = args;
+  mShouldKeepRunning = YES;
+  mAborted = NO;
+  return self;
 }
+
+- (BOOL)runServer
+{
+  NSPort* serverPort = [NSSocketPort port];
+  NSConnection* server = [NSConnection connectionWithReceivePort:serverPort
+                                                        sendPort:serverPort];
+  [server setRootObject:self];
+  if ([server registerName:@"org.mozilla.updater.server"
+            withNameServer:[NSSocketPortNameServer sharedInstance]] == NO) {
+    NSLog(@"Unable to register as DirectoryServer.");
+    NSLog(@"Is another copy running?");
+    return NO;
+  }
+
+  while ([self shouldKeepRunning] &&
+         [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
+                                  beforeDate:[NSDate distantFuture]]);
+  return ![self wasAborted];
+}
+
+- (NSArray*)getArguments
+{
+  return mUpdaterArguments;
+}
+
+- (void)abort
+{
+  mAborted = YES;
+  [self shutdown];
+}
+
+- (BOOL)wasAborted
+{
+  return mAborted;
+}
+
+- (void)shutdown
+{
+  mShouldKeepRunning = NO;
+}
+
+- (BOOL)shouldKeepRunning
+{
+  return mShouldKeepRunning;
+}
+
+@end
+
+bool ServeElevatedUpdate(int argc, const char** argv)
+{
+  MacAutoreleasePool pool;
+
+  NSMutableArray* updaterArguments = [NSMutableArray arrayWithCapacity:argc];
+  for (int i = 0; i < argc; i++) {
+    [updaterArguments addObject:[NSString stringWithUTF8String:argv[i]]];
+  }
+
+  ElevatedUpdateServer* updater =
+    [[ElevatedUpdateServer alloc] initWithArgs:[updaterArguments copy]];
+  bool didSucceed = [updater runServer];
+
+  [updater release];
+  return didSucceed;
+}
+
+bool IsOwnedByGroupAdmin(const char* aAppBundle)
+{
+  MacAutoreleasePool pool;
+
+  NSString* appDir = [NSString stringWithUTF8String:aAppBundle];
+  NSFileManager* fileManager = [NSFileManager defaultManager];
+
+  NSDictionary* attributes = [fileManager attributesOfItemAtPath:appDir
+                                                           error:nil];
+  bool isOwnedByAdmin = false;
+  if (attributes &&
+      [[attributes valueForKey:NSFileGroupOwnerAccountID] intValue] == 80) {
+    isOwnedByAdmin = true;
+  }
+  return isOwnedByAdmin;
+}
+
+void SetGroupOwnershipAndPermissions(const char* aAppBundle)
+{
+  MacAutoreleasePool pool;
+
+  NSString* appDir = [NSString stringWithUTF8String:aAppBundle];
+  NSFileManager* fileManager = [NSFileManager defaultManager];
+  NSError* error = nil;
+  NSArray* paths =
+    [fileManager subpathsOfDirectoryAtPath:appDir
+                                     error:&error];
+  if (error) {
+    return;
+  }
+
+  // Set group ownership of Firefox.app to 80 ("admin") and permissions to
+  // 0775.
+  if (![fileManager setAttributes:@{ NSFileGroupOwnerAccountID: @(80),
+                                     NSFilePosixPermissions: @(0775) }
+                     ofItemAtPath:appDir
+                            error:&error] || error) {
+    return;
+  }
+
+  NSArray* permKeys = [NSArray arrayWithObjects:NSFileGroupOwnerAccountID,
+                                                NSFilePosixPermissions,
+                                                nil];
+  // For all descendants of Firefox.app, set group ownership to 80 ("admin") and
+  // ensure write permission for the group.
+  for (NSString* currPath in paths) {
+    NSString* child = [appDir stringByAppendingPathComponent:currPath];
+    NSDictionary* oldAttributes =
+      [fileManager attributesOfItemAtPath:child
+                                    error:&error];
+    if (error) {
+      return;
+    }
+    // Skip symlinks, since they could be pointing to files outside of the .app
+    // bundle.
+    if ([oldAttributes fileType] == NSFileTypeSymbolicLink) {
+      continue;
+    }
+    NSNumber* oldPerms =
+      (NSNumber*)[oldAttributes valueForKey:NSFilePosixPermissions];
+    NSArray* permObjects =
+      [NSArray arrayWithObjects:
+        [NSNumber numberWithUnsignedLong:80],
+        [NSNumber numberWithUnsignedLong:[oldPerms shortValue] | 020],
+        nil];
+    NSDictionary* attributes = [NSDictionary dictionaryWithObjects:permObjects
+                                                           forKeys:permKeys];
+    if (![fileManager setAttributes:attributes
+                       ofItemAtPath:child
+                              error:&error] || error) {
+      return;
+    }
+  }
+}
--- a/toolkit/xre/MacLaunchHelper.h
+++ b/toolkit/xre/MacLaunchHelper.h
@@ -8,11 +8,13 @@
 
 #include <stdint.h>
 
 #include <unistd.h>
 
 extern "C" {
   void LaunchChildMac(int aArgc, char** aArgv, uint32_t aRestartType = 0,
                       pid_t *pid = 0);
+  bool LaunchElevatedUpdate(int argc, char** argv, uint32_t aRestartType = 0,
+                            pid_t* pid = 0);
 }
 
 #endif
--- a/toolkit/xre/MacLaunchHelper.mm
+++ b/toolkit/xre/MacLaunchHelper.mm
@@ -1,22 +1,26 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "MacLaunchHelper.h"
 
-#include "nsMemory.h"
+#include "MacAutoreleasePool.h"
+#include "mozilla/UniquePtr.h"
 #include "nsIAppStartup.h"
-#include "mozilla/UniquePtr.h"
+#include "nsMemory.h"
 
-#include <stdio.h>
+#include <Cocoa/Cocoa.h>
+#include <crt_externs.h>
+#include <ServiceManagement/ServiceManagement.h>
+#include <Security/Authorization.h>
 #include <spawn.h>
-#include <crt_externs.h>
+#include <stdio.h>
 
 using namespace mozilla;
 
 namespace {
 cpu_type_t pref_cpu_types[2] = {
 #if defined(__i386__)
                                  CPU_TYPE_X86,
 #elif defined(__x86_64__)
@@ -83,8 +87,91 @@ void LaunchChildMac(int aArgc, char** aA
   int result = posix_spawnp(pid, argv_copy[0], NULL, &spawnattr, argv_copy.get(), envp);
 
   posix_spawnattr_destroy(&spawnattr);
 
   if (result != 0) {
     printf("Process spawn failed with code %d!", result);
   }
 }
+
+BOOL InstallPrivilegedHelper()
+{
+  AuthorizationRef authRef = NULL;
+  OSStatus status = AuthorizationCreate(NULL,
+                                        kAuthorizationEmptyEnvironment,
+                                        kAuthorizationFlagDefaults |
+                                        kAuthorizationFlagInteractionAllowed,
+                                        &authRef);
+  if (status != errAuthorizationSuccess) {
+    // AuthorizationCreate really shouldn't fail.
+    NSLog(@"AuthorizationCreate failed! NSOSStatusErrorDomain / %d",
+          (int)status);
+    return NO;
+  }
+
+  BOOL result = NO;
+  AuthorizationItem authItem = { kSMRightBlessPrivilegedHelper, 0, NULL, 0 };
+  AuthorizationRights authRights = { 1, &authItem };
+  AuthorizationFlags flags = kAuthorizationFlagDefaults |
+                             kAuthorizationFlagInteractionAllowed |
+                             kAuthorizationFlagPreAuthorize |
+                             kAuthorizationFlagExtendRights;
+
+  // Obtain the right to install our privileged helper tool.
+  status = AuthorizationCopyRights(authRef,
+                                   &authRights,
+                                   kAuthorizationEmptyEnvironment,
+                                   flags,
+                                   NULL);
+  if (status != errAuthorizationSuccess) {
+    NSLog(@"AuthorizationCopyRights failed! NSOSStatusErrorDomain / %d",
+          (int)status);
+  } else {
+    CFErrorRef cfError;
+    // This does all the work of verifying the helper tool against the
+    // application and vice-versa. Once verification has passed, the embedded
+    // launchd.plist is extracted and placed in /Library/LaunchDaemons and then
+    // loaded. The executable is placed in /Library/PrivilegedHelperTools.
+    result = (BOOL)SMJobBless(kSMDomainSystemLaunchd,
+                              (CFStringRef)@"org.mozilla.updater",
+                              authRef,
+                              &cfError);
+    if (!result) {
+      NSLog(@"Unable to install helper!");
+      CFRelease(cfError);
+    }
+  }
+
+  return result;
+}
+
+void AbortElevatedUpdate()
+{
+  mozilla::MacAutoreleasePool pool;
+  id updateServer = nil;
+  @try {
+    updateServer = (id)[NSConnection
+      rootProxyForConnectionWithRegisteredName:
+        @"org.mozilla.updater.server"
+      host:nil
+      usingNameServer:[NSSocketPortNameServer sharedInstance]];
+    if (updateServer &&
+        [updateServer respondsToSelector:@selector(abort)]) {
+      [updateServer performSelector:@selector(abort)];
+    } else {
+      NSLog(@"Unable to clean up updater.");
+    }
+  } @catch (NSException* e) {
+    // Ignore exceptions.
+  }
+}
+
+bool LaunchElevatedUpdate(int argc, char** argv, uint32_t aRestartType,
+                          pid_t* pid)
+{
+  LaunchChildMac(argc, argv, aRestartType, pid);
+  bool didSucceed = InstallPrivilegedHelper();
+  if (!didSucceed) {
+    AbortElevatedUpdate();
+  }
+  return didSucceed;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/xre/updaterfileutils_osx.h
@@ -0,0 +1,13 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef updaterfileutils_osx_h_
+#define updaterfileutils_osx_h_
+
+extern "C" {
+  bool IsRecursivelyWritable(const char* aPath);
+}
+
+#endif
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/xre/updaterfileutils_osx.mm
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "updaterfileutils_osx.h"
+
+#include <Cocoa/Cocoa.h>
+
+bool IsRecursivelyWritable(const char* aPath)
+{
+  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+
+  NSString* rootPath = [NSString stringWithUTF8String:aPath];
+  NSFileManager* fileManager = [NSFileManager defaultManager];
+  NSError* error = nil;
+  NSArray* subPaths =
+    [fileManager subpathsOfDirectoryAtPath:rootPath
+                                     error:&error];
+  NSMutableArray* paths =
+    [NSMutableArray arrayWithCapacity:[subPaths count] + 1];
+  [paths addObject:@""];
+  [paths addObjectsFromArray:subPaths];
+
+  if (error) {
+    [pool drain];
+    return false;
+  }
+
+  for (NSString* currPath in paths) {
+    NSString* child = [rootPath stringByAppendingPathComponent:currPath];
+
+    NSDictionary* attributes =
+      [fileManager attributesOfItemAtPath:child
+                                    error:&error];
+    if (error) {
+      [pool drain];
+      return false;
+    }
+
+    // Don't check for writability of files pointed to by symlinks, as they may
+    // not be descendants of the root path.
+    if ([attributes fileType] != NSFileTypeSymbolicLink &&
+        [fileManager isWritableFileAtPath:child] == NO) {
+      [pool drain];
+      return false;
+    }
+  }
+
+  [pool drain];
+  return true;
+}