Bug 394984: Enable any admin user on OSX to update Firefox, native OSX changes. r=mstange
authorStephen A Pohl <spohl.mozilla.bugs@gmail.com>
Wed, 06 Apr 2016 16:10:48 -0400
changeset 491494 bdc7720fcf3c233a174ed37e7a1da7d3e84ed7d2
parent 491493 acbd16915de8df6db1ce2c3a9e3bcb735c001560
child 491495 94cc6d062da29b6e93255bd8c39477bec31992d5
push id47343
push userbmo:dothayer@mozilla.com
push dateWed, 01 Mar 2017 22:58:58 +0000
reviewersmstange
bugs394984
milestone48.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,86 +5,42 @@
  * 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)
+void LaunchChild(int argc, const char** argv)
 {
-  // Initialize spawn attributes.
-  posix_spawnattr_t spawnattr;
-  if (posix_spawnattr_init(&spawnattr) != 0) {
-    printf("Failed to init posix spawn attribute.");
-    return;
+  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+  @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.
   }
-
-  // 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;
-  }
-
-  // "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);
-  }
+  [pool release];
 }
 
 void
 LaunchMacPostProcess(const char* aAppBundle)
 {
   // Launch helper to perform post processing for the update; this is the Mac
   // analogue of LaunchWinPostProcess (PostUpdateWin).
-  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+  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
@@ -131,8 +87,258 @@ LaunchMacPostProcess(const char* aAppBun
       [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)
+{
+  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+
+  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);
+
+  [pool release];
+}
+
+// Note: Caller is responsible for freeing argv.
+bool ObtainUpdaterArguments(int* argc, char*** argv)
+{
+  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)
+{
+  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)
+{
+  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+
+  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;
+  }
+
+  [pool drain];
+  return isOwnedByAdmin;
+}
+
+void SetGroupOwnershipAndPermissions(const char* aAppBundle)
+{
+  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+
+  NSString* appDir = [NSString stringWithUTF8String:aAppBundle];
+  NSFileManager* fileManager = [NSFileManager defaultManager];
+  NSError* error = nil;
+  NSArray* paths =
+    [fileManager subpathsOfDirectoryAtPath:appDir
+                                     error:&error];
+  if (error) {
+    [pool drain];
+    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) {
+    [pool drain];
+    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) {
+      [pool drain];
+      return;
+    }
+    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) {
+      [pool drain];
+      return;
+    }
+  }
+
+  [pool drain];
+}
--- 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,61 @@
+/* -*- 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>
+#include <pwd.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;
+    }
+    NSNumber* accountID =
+      (NSNumber*)[attributes valueForKey:NSFileOwnerAccountID];
+    NSNumber* groupID =
+      (NSNumber*)[attributes valueForKey:NSFileGroupOwnerAccountID];
+    NSNumber* permissions =
+      (NSNumber*)[attributes valueForKey:NSFilePosixPermissions];
+    int perms = [permissions shortValue];
+    // Check if the user has write access to the file. This is the case when
+    // the user is the owner of the file and the owner has write access, or if
+    // the user belongs to a group that has write access to the file (typically
+    // "admin").
+    if (!((unsigned int)[accountID shortValue] == getuid() && (perms & 0x80)) &&
+        !((unsigned int)[groupID shortValue] == (getpwuid(getuid()))->pw_gid &&
+            (perms & 0x10))) {
+      [pool drain];
+      return false;
+    }
+  }
+
+  [pool drain];
+  return true;
+}