forked from mirrors/gecko-dev
For some reason, its value in the Google style we use is 80... except for Objective-C, where it's 100, which led to things like: https://hg.mozilla.org/mozilla-central/rev/31bf68247e6e https://hg.mozilla.org/mozilla-central/rev/64ceb33533a4. There's probably a discussion to have about whether 80 is the right limit, but since it's what's used for everything except ObjC, let's roll with it. # ignore-this-changeset Differential Revision: https://phabricator.services.mozilla.com/D187409
519 lines
17 KiB
Text
519 lines
17 KiB
Text
/* -*- 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) {
|
|
NSTask* task = [[NSTask alloc] init];
|
|
[task setExecutableURL:[NSURL fileURLWithPath:aPath]];
|
|
if (aArguments) {
|
|
[task setArguments:aArguments];
|
|
}
|
|
[task launchAndReturnError:nil];
|
|
[task release];
|
|
}
|
|
|
|
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;
|
|
}
|