Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#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;
};
/**
* Helper to launch macOS tasks via NSTask and wait for the launched task to
* terminate.
*/
static void LaunchTask(NSString* aPath, NSArray* aArguments) {
MacAutoreleasePool pool;
NSTask* task = [[NSTask alloc] init];
[task setExecutableURL:[NSURL fileURLWithPath:aPath]];
if (aArguments) {
[task setArguments:aArguments];
}
[task launchAndReturnError:nil];
[task waitUntilExit];
[task release];
}
static void RegisterAppWithLaunchServices(NSString* aBundlePath) {
MacAutoreleasePool pool;
@try {
OSStatus status =
LSRegisterURL((CFURLRef)[NSURL fileURLWithPath:aBundlePath], YES);
if (status != noErr) {
NSLog(@"We failed to register the app in the Launch Services database, "
@"which may lead to a failure to launch the app. Launch path: %@",
aBundlePath);
}
} @catch (NSException* e) {
NSLog(@"%@: %@", e.name, e.reason);
}
}
static void StripQuarantineBit(NSString* aBundlePath) {
MacAutoreleasePool pool;
NSArray* arguments = @[ @"-dr", @"com.apple.quarantine", aBundlePath ];
LaunchTask(@"/usr/bin/xattr", arguments);
}
void LaunchMacApp(int argc, const char** argv) {
MacAutoreleasePool pool;
@try {
NSString* launchPath = [NSString stringWithUTF8String:argv[0]];
NSMutableArray* arguments = [NSMutableArray arrayWithCapacity:argc - 1];
for (int i = 1; i < argc; i++) {
[arguments addObject:[NSString stringWithUTF8String:argv[i]]];
}
if (![launchPath hasSuffix:@".app"]) {
// We only support launching applications inside .app bundles.
NSLog(@"The updater attempted to launch an app that was not a .app "
@"bundle. Please verify launch path: %@",
launchPath);
return;
}
StripQuarantineBit(launchPath);
RegisterAppWithLaunchServices(launchPath);
// We use NSWorkspace to register the application into the
// `TALAppsToRelaunchAtLogin` list and allow for macOS session resume.
// This API only works with `.app`s.
__block dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSWorkspaceOpenConfiguration* config =
[NSWorkspaceOpenConfiguration configuration];
[config setArguments:arguments];
[config setCreatesNewApplicationInstance:YES];
[config setEnvironment:[[NSProcessInfo processInfo] environment]];
[[NSWorkspace sharedWorkspace]
openApplicationAtURL:[NSURL fileURLWithPath:launchPath]
configuration:config
completionHandler:^(NSRunningApplication* aChild, NSError* aError) {
if (aError) {
NSLog(@"launchchild_osx: Failed to run application. Error: %@",
aError);
}
dispatch_semaphore_signal(semaphore);
}];
// We use a semaphore to wait for the application to launch.
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
} @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]];
// Invoke post-update with a minimal environment to avoid environment
// variables intended to relaunch Firefox impacting post-update operations, in
// particular background tasks. The updater will invoke the callback
// application with the current (non-minimal) environment.
[task setEnvironment:@{}];
[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];
// The following call will terminate the current process due to the "remove"
// argument.
LaunchTask(@"/bin/launchctl", @[ @"remove", @"org.mozilla.updater" ]);
}
// Note: Caller is responsible for freeing aArgv.
bool ObtainUpdaterArguments(int* aArgc, char*** aArgv,
MARChannelStringTable* aMARStrings) {
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)];
*aArgc = [updaterArguments count];
char** tempArgv = (char**)malloc(sizeof(char*) * (*aArgc));
for (int i = 0; i < *aArgc; i++) {
int argLen = [[updaterArguments objectAtIndex:i] length] + 1;
tempArgv[i] = (char*)malloc(argLen);
strncpy(tempArgv[i], [[updaterArguments objectAtIndex:i] UTF8String],
argLen);
}
*aArgv = tempArgv;
NSString* channelID =
[updateServer performSelector:@selector(getMARChannelID)];
const char* channelIDStr = [channelID UTF8String];
aMARStrings->MARChannelID =
mozilla::MakeUnique<char[]>(strlen(channelIDStr) + 1);
strcpy(aMARStrings->MARChannelID.get(), channelIDStr);
} @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;
NSString* mMARChannelID;
}
- (id)initWithArgs:(NSArray*)aArgs marChannelID:(NSString*)aMARChannelID;
- (BOOL)runServer;
- (NSArray*)getArguments;
- (NSString*)getMARChannelID;
- (void)abort;
- (BOOL)wasAborted;
- (void)shutdown;
- (BOOL)shouldKeepRunning;
@end
@implementation ElevatedUpdateServer
- (id)initWithArgs:(NSArray*)aArgs marChannelID:(NSString*)aMARChannelID {
self = [super init];
if (!self) {
return nil;
}
mUpdaterArguments = aArgs;
mMARChannelID = aMARChannelID;
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;
}
/**
* The MAR channel ID(s) are stored in the UpdateSettings.framework that ships
* with the updater.app bundle. When an elevated update is occurring, the
* org.mozilla.updater binary is extracted and installed individually as a
* Privileged Helper Tool. This Privileged Helper Tool does not have access to
* the UpdateSettings.framework and we therefore rely on the unelevated updater
* process to pass this information to the elevated updater process in the same
* fashion that the command line arguments are passed to the elevated updater
* process by `getArguments`.
*/
- (NSString*)getMARChannelID {
return mMARChannelID;
}
- (void)abort {
mAborted = YES;
[self shutdown];
}
- (BOOL)wasAborted {
return mAborted;
}
- (void)shutdown {
mShouldKeepRunning = NO;
}
- (BOOL)shouldKeepRunning {
return mShouldKeepRunning;
}
@end
bool ServeElevatedUpdate(int aArgc, const char** aArgv,
const char* aMARChannelID) {
MacAutoreleasePool pool;
NSMutableArray* updaterArguments = [NSMutableArray arrayWithCapacity:aArgc];
for (int i = 0; i < aArgc; i++) {
[updaterArguments addObject:[NSString stringWithUTF8String:aArgv[i]]];
}
NSString* channelID = [NSString stringWithUTF8String:aMARChannelID];
ElevatedUpdateServer* updater =
[[ElevatedUpdateServer alloc] initWithArgs:updaterArguments
marChannelID:channelID];
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;
}
}
}
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]) {
StripQuarantineBit(destPath);
RegisterAppWithLaunchServices(destPath);
return true;
}
return false;
}