diff options
Diffstat (limited to '')
-rw-r--r-- | toolkit/mozapps/update/updater/launchchild_osx.mm | 500 |
1 files changed, 500 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/updater/launchchild_osx.mm b/toolkit/mozapps/update/updater/launchchild_osx.mm new file mode 100644 index 0000000000..3d28a064c7 --- /dev/null +++ b/toolkit/mozapps/update/updater/launchchild_osx.mm @@ -0,0 +1,500 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 <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 <sys/types.h> +#include <sys/sysctl.h> +#include "readstrings.h" + +#define ARCH_PATH "/usr/bin/arch" +#if defined(__x86_64__) +// Work around the fact that this constant is not available in the macOS SDK +# define kCFBundleExecutableArchitectureARM64 0x0100000c +#endif + +class MacAutoreleasePool { + public: + MacAutoreleasePool() { mPool = [[NSAutoreleasePool alloc] init]; } + ~MacAutoreleasePool() { [mPool release]; } + + private: + NSAutoreleasePool* mPool; +}; + +#if defined(__x86_64__) +/* + * Returns true if the process is running under Rosetta translation. Returns + * false if running natively or if an error was encountered. We use the + * `sysctl.proc_translated` sysctl which is documented by Apple to be used + * for this purpose. + */ +bool IsProcessRosettaTranslated() { + int ret = 0; + size_t size = sizeof(ret); + if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == -1) { + if (errno != ENOENT) { + fprintf(stderr, "Failed to check for translation environment\n"); + } + return false; + } + return (ret == 1); +} + +// Returns true if the binary at |executablePath| can be executed natively +// on an arm64 Mac. Returns false otherwise or if an error occurred. +bool IsBinaryArmExecutable(const char* executablePath) { + bool isArmExecutable = false; + + CFURLRef url = ::CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, (const UInt8*)executablePath, strlen(executablePath), false); + if (!url) { + return false; + } + + CFArrayRef archs = ::CFBundleCopyExecutableArchitecturesForURL(url); + if (!archs) { + CFRelease(url); + return false; + } + + CFIndex archCount = ::CFArrayGetCount(archs); + for (CFIndex i = 0; i < archCount; i++) { + CFNumberRef currentArch = static_cast<CFNumberRef>(::CFArrayGetValueAtIndex(archs, i)); + int currentArchInt = 0; + if (!::CFNumberGetValue(currentArch, kCFNumberIntType, ¤tArchInt)) { + continue; + } + if (currentArchInt == kCFBundleExecutableArchitectureARM64) { + isArmExecutable = true; + break; + } + } + + CFRelease(url); + CFRelease(archs); + + return isArmExecutable; +} + +// Returns true if the executable provided in |executablePath| should be +// launched with a preference for arm64. After updating from an x64 version +// running under Rosetta, if the update is to a universal binary with arm64 +// support we want to switch to arm64 execution mode. Returns true if those +// conditions are met and the arch(1) utility at |archPath| is executable. +// It should be safe to always launch with arch and fallback to x64, but we +// limit its use to the only scenario it is necessary to minimize risk. +bool ShouldPreferArmLaunch(const char* archPath, const char* executablePath) { + // If not running under Rosetta, we are not on arm64 hardware. + if (!IsProcessRosettaTranslated()) { + return false; + } + + // Ensure the arch(1) utility is present and executable. + NSFileManager* fileMgr = [NSFileManager defaultManager]; + NSString* archPathString = [NSString stringWithUTF8String:archPath]; + if (![fileMgr isExecutableFileAtPath:archPathString]) { + return false; + } + + // Ensure the binary can be run natively on arm64. + return IsBinaryArmExecutable(executablePath); +} +#endif // __x86_64__ + +void LaunchChild(int argc, const char** argv) { + MacAutoreleasePool pool; + + @try { + bool preferArmLaunch = false; + +#if defined(__x86_64__) + // When running under Rosetta, child processes inherit the architecture + // preference of their parent and therefore universal binaries launched + // by an emulated x64 process will launch in x64 mode. If we are running + // under Rosetta, launch the child process with a preference for arm64 so + // that we will switch to arm64 execution if we have just updated from + // x64 to a universal build. This includes if we were already a universal + // build and the user is intentionally running under Rosetta. + preferArmLaunch = ShouldPreferArmLaunch(ARCH_PATH, argv[0]); +#endif // __x86_64__ + + NSString* launchPath; + NSMutableArray* arguments; + + if (preferArmLaunch) { + launchPath = [NSString stringWithUTF8String:ARCH_PATH]; + + // Size the arguments array to include all the arguments + // in |argv| plus two arguments to pass to the arch(1) utility. + arguments = [NSMutableArray arrayWithCapacity:argc + 2]; + [arguments addObject:[NSString stringWithUTF8String:"-arm64"]]; + [arguments addObject:[NSString stringWithUTF8String:"-x86_64"]]; + + // Add the first argument from |argv|. The rest are added below. + [arguments addObject:[NSString stringWithUTF8String:argv[0]]]; + } else { + launchPath = [NSString stringWithUTF8String:argv[0]]; + arguments = [NSMutableArray arrayWithCapacity:argc - 1]; + } + + for (int i = 1; i < argc; i++) { + [arguments addObject:[NSString stringWithUTF8String:argv[i]]]; + } + [NSTask launchedTaskWithLaunchPath:launchPath arguments:arguments]; + } @catch (NSException* e) { + NSLog(@"%@: %@", e.name, e.reason); + } +} + +void LaunchMacPostProcess(const char* aAppBundle) { + MacAutoreleasePool pool; + + // Launch helper to perform post processing for the update; this is the Mac + // analogue of LaunchWinPostProcess (PostUpdateWin). + 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 + return; + } + + int readResult; + mozilla::UniquePtr<char[]> values[2]; + readResult = + ReadStrings([iniPath UTF8String], "ExeRelPath\0ExeArg\0", 2, values, "PostUpdateMac"); + if (readResult) { + return; + } + + NSString* exeRelPath = [NSString stringWithUTF8String:values[0].get()]; + NSString* exeArg = [NSString stringWithUTF8String:values[1].get()]; + if (!exeArg || !exeRelPath) { + return; + } + + // The path must not traverse directories and it must be a relative path. + if ([exeRelPath isEqualToString:@".."] || [exeRelPath hasPrefix:@"/"] || + [exeRelPath hasPrefix:@"../"] || [exeRelPath hasSuffix:@"/.."] || + [exeRelPath containsString:@"/../"]) { + return; + } + + NSString* exeFullPath = [NSString stringWithUTF8String:aAppBundle]; + exeFullPath = [exeFullPath stringByAppendingPathComponent:exeRelPath]; + + mozilla::UniquePtr<char[]> optVal; + readResult = ReadStrings([iniPath UTF8String], "ExeAsync\0", 1, &optVal, "PostUpdateMac"); + + NSTask* task = [[NSTask alloc] init]; + [task setLaunchPath:exeFullPath]; + [task setArguments:[NSArray arrayWithObject:exeArg]]; + [task launch]; + if (!readResult) { + NSString* exeAsync = [NSString stringWithUTF8String:optVal.get()]; + if ([exeAsync isEqualToString:@"false"]) { + [task waitUntilExit]; + } + } + // ignore the return value of the task, there's nothing we can do with it + [task release]; +} + +id ConnectToUpdateServer() { + MacAutoreleasePool pool; + + id updateServer = nil; + BOOL isConnected = NO; + int currTry = 0; + const int numRetries = 10; // Number of IPC connection retries before + // giving up. + while (!isConnected && currTry < numRetries) { + @try { + updateServer = (id)[NSConnection + rootProxyForConnectionWithRegisteredName:@"org.mozilla.updater.server" + host:nil + usingNameServer:[NSSocketPortNameServer sharedInstance]]; + if (!updateServer || ![updateServer respondsToSelector:@selector(abort)] || + ![updateServer respondsToSelector:@selector(getArguments)] || + ![updateServer respondsToSelector:@selector(shutdown)]) { + NSLog(@"Server doesn't exist or doesn't provide correct selectors."); + sleep(1); // Wait 1 second. + currTry++; + } else { + isConnected = YES; + } + } @catch (NSException* e) { + NSLog(@"Encountered exception, retrying: %@: %@", e.name, e.reason); + sleep(1); // Wait 1 second. + currTry++; + } + } + if (!isConnected) { + NSLog(@"Failed to connect to update server after several retries."); + return nil; + } + return updateServer; +} + +void CleanupElevatedMacUpdate(bool aFailureOccurred) { + MacAutoreleasePool pool; + + id updateServer = ConnectToUpdateServer(); + if (updateServer) { + @try { + if (aFailureOccurred) { + [updateServer performSelector:@selector(abort)]; + } else { + [updateServer performSelector:@selector(shutdown)]; + } + } @catch (NSException* e) { + } + } + + 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 = ConnectToUpdateServer(); + if (!updateServer) { + // Let's try our best and clean up. + CleanupElevatedMacUpdate(true); + return false; // Won't actually get here due to CleanupElevatedMacUpdate. + } + + @try { + 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; + } + } +} + +/** + * Helper to launch macOS tasks via NSTask. + */ +static void LaunchTask(NSString* aPath, NSArray* aArguments) { + if (@available(macOS 10.13, *)) { + NSTask* task = [[NSTask alloc] init]; + [task setExecutableURL:[NSURL fileURLWithPath:aPath]]; + if (aArguments) { + [task setArguments:aArguments]; + } + [task launchAndReturnError:nil]; + [task release]; + } else { + NSArray* arguments = aArguments; + if (!arguments) { + arguments = @[]; + } + [NSTask launchedTaskWithLaunchPath:aPath arguments:arguments]; + } +} + +static void RegisterAppWithLaunchServices(NSString* aBundlePath) { + NSArray* arguments = @[ @"-f", aBundlePath ]; + LaunchTask(@"/System/Library/Frameworks/CoreServices.framework/Frameworks/" + @"LaunchServices.framework/Support/lsregister", + arguments); +} + +static void StripQuarantineBit(NSString* aBundlePath) { + NSArray* arguments = @[ @"-d", @"com.apple.quarantine", aBundlePath ]; + LaunchTask(@"/usr/bin/xattr", arguments); +} + +bool PerformInstallationFromDMG(int argc, char** argv) { + MacAutoreleasePool pool; + if (argc < 4) { + return false; + } + NSString* bundlePath = [NSString stringWithUTF8String:argv[2]]; + NSString* destPath = [NSString stringWithUTF8String:argv[3]]; + if ([[NSFileManager defaultManager] copyItemAtPath:bundlePath toPath:destPath error:nil]) { + RegisterAppWithLaunchServices(destPath); + StripQuarantineBit(destPath); + return true; + } + return false; +} |