Bug 394984: Enable any admin user on OSX to update Firefox, native OSX changes. r=mstange
authorStephen A Pohl <spohl.mozilla.bugs@gmail.com>
Fri, 13 May 2016 23:27:24 -0400
changeset 491534 7a3e03cd333da58cad74254b4dfb0bc1db3a8e0c
parent 491533 0cee74c3de68994ee53c4640c5bebc0644c121d1
child 491535 9448761166f47ab9557f674909f90318a86e3d72
push id47343
push userbmo:dothayer@mozilla.com
push dateWed, 01 Mar 2017 22:58:58 +0000
reviewersmstange
bugs394984
milestone49.0a1
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;
+}