Skip to content

Commit

Permalink
Add support for running framework and CLI as root (#2119)
Browse files Browse the repository at this point in the history
When the framework or sparkle-cli is run as root, we always launch the installer (Autoupdate) in the system domain.

For the progress tool agent, we chown() the copied Updater.app so the Updater app has proper ownership of it (and clean it up later). The progress tool app may not be able to clean up the parent directory, but that's okay and will be garbage collected on a later run.

To get the username and home directory for the user session when running the framework as root, we use the SecurityConfiguration framework. Sparkle needs and depends on an active GUI user login session. If a user ssh's in to install an update, that user must be the same as the active logged in user.

Installing interactive based package updates as root is not supported. For sparkle-cli, we disallow using --interactive when running as root.

An additional error exit code is added for sparkle-cli when ran as root and trying to install a interactive based package update, which is unsupported.
  • Loading branch information
zorgiepoo authored May 7, 2022
1 parent 9042c8d commit 339e269
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 65 deletions.
112 changes: 81 additions & 31 deletions InstallerLauncher/SUInstallerLauncher.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
#import <ImageIO/ImageIO.h>
#import <ServiceManagement/ServiceManagement.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#import <SystemConfiguration/SystemConfiguration.h>


#include "AppKitPrevention.h"

Expand Down Expand Up @@ -71,6 +73,8 @@ - (BOOL)submitProgressToolAtPath:(NSString *)progressToolPath withHostBundle:(NS
}
}

// If we are running as the root user, there is no need to explicitly set the UserName / GroupName keys
// because we are submitting under the user domain, which should automatically use the the console user.
NSMutableDictionary *jobDictionary = [[NSMutableDictionary alloc] init];
jobDictionary[@"Label"] = label;
jobDictionary[@"ProgramArguments"] = arguments;
Expand Down Expand Up @@ -101,7 +105,7 @@ - (BOOL)submitProgressToolAtPath:(NSString *)progressToolPath withHostBundle:(NS
return (submittedJob == true);
}

- (SUInstallerLauncherStatus)submitInstallerAtPath:(NSString *)installerPath withHostBundle:(NSBundle *)hostBundle updaterIdentifier:(NSString *)updaterIdentifier authorizationPrompt:(NSString *)authorizationPrompt inSystemDomain:(BOOL)systemDomain
- (SUInstallerLauncherStatus)submitInstallerAtPath:(NSString *)installerPath withHostBundle:(NSBundle *)hostBundle updaterIdentifier:(NSString *)updaterIdentifier userName:(NSString *)userName homeDirectory:(NSString *)homeDirectory authorizationPrompt:(NSString *)authorizationPrompt inSystemDomain:(BOOL)systemDomain rootUser:(BOOL)rootUser
{
SUFileManager *fileManager = [[SUFileManager alloc] init];

Expand All @@ -111,12 +115,6 @@ - (SUInstallerLauncherStatus)submitInstallerAtPath:(NSString *)installerPath wit
NSString *hostBundleIdentifier = hostBundle.bundleIdentifier;
assert(hostBundleIdentifier != nil);

NSString *homeDirectory = NSHomeDirectory();
assert(homeDirectory != nil);

NSString *userName = NSUserName();
assert(userName != nil);

// The first argument has to be the path to the program, and the second is a host identifier so that the installer knows what mach services to host
// The third and forth arguments are for home directory and user name which only pkg installer scripts may need
// We intentionally do not pass any more arguments. Anything else should be done via IPC.
Expand All @@ -134,7 +132,7 @@ - (SUInstallerLauncherStatus)submitInstallerAtPath:(NSString *)installerPath wit

BOOL canceledAuthorization = NO;
BOOL failedToUseSystemDomain = NO;
if (auth != NULL && systemDomain) {
if (auth != NULL && systemDomain && !rootUser) {
// See Apple's 'EvenBetterAuthorizationSample' sample code and
// https://developer.apple.com/library/mac/technotes/tn2095/_index.html#//apple_ref/doc/uid/DTS10003110-CH1-SECTION7
// We can set a custom right name for authenticating as an administrator
Expand Down Expand Up @@ -368,38 +366,46 @@ BOOL SPUSystemNeedsAuthorizationAccessForBundlePath(NSString *bundlePath)
return needsAuthorization;
}

static BOOL SPUSystemNeedsAuthorizationAccess(NSString *path, NSString *installationType)
static BOOL SPUUsesSystemDomainForBundlePath(NSString *path, NSString *installationType, BOOL rootUser)
{
BOOL needsAuthorization;
if ([installationType isEqualToString:SPUInstallationTypeGuidedPackage]) {
needsAuthorization = YES;
} else if ([installationType isEqualToString:SPUInstallationTypeInteractivePackage]) {
needsAuthorization = NO;
if (!rootUser) {
if ([installationType isEqualToString:SPUInstallationTypeGuidedPackage]) {
return YES;
} else if ([installationType isEqualToString:SPUInstallationTypeInteractivePackage]) {
return NO;
} else {
return SPUSystemNeedsAuthorizationAccessForBundlePath(path);
}
} else {
needsAuthorization = SPUSystemNeedsAuthorizationAccessForBundlePath(path);
// If we are the root user we use the system domain even if we don't need escalated authorization.
// Note interactive package installations are not supported as root.
return YES;
}
return needsAuthorization;
}


// Note: do not pass untrusted information such as paths to the installer and progress agent tools, when we can find them ourselves here
- (void)launchInstallerWithHostBundlePath:(NSString *)hostBundlePath updaterIdentifier:(NSString *)updaterIdentifier authorizationPrompt:(NSString *)authorizationPrompt installationType:(NSString *)installationType allowingDriverInteraction:(BOOL)allowingDriverInteraction completion:(void (^)(SUInstallerLauncherStatus, BOOL))completionHandler
{
dispatch_async(dispatch_get_main_queue(), ^{
BOOL needsSystemAuthorization = SPUSystemNeedsAuthorizationAccess(hostBundlePath, installationType);
// We could do a sort of preflight Authorization test instead of testing if we are running as root,
// but I think this is not necessarily a better approach. We have to chown() the launcher cache directory later on,
// and that is not necessarily related to a preflight test. It's more related to being ran under a root / different user from the active GUI session
BOOL rootUser = (geteuid() == 0);

BOOL inSystemDomain = SPUUsesSystemDomainForBundlePath(hostBundlePath, installationType, rootUser);

NSBundle *hostBundle = [NSBundle bundleWithPath:hostBundlePath];
if (hostBundle == nil) {
SULog(SULogLevelError, @"InstallerLauncher failed to create bundle at %@", hostBundlePath);
SULog(SULogLevelError, @"Please make sure InstallerLauncher is not sandboxed and do not sign your app by passing --deep. Check: codesign -d --entitlements :- \"%@\"", NSBundle.mainBundle.bundlePath);
SULog(SULogLevelError, @"More information regarding sandboxing: https://sparkle-project.org/documentation/sandboxing/");
completionHandler(SUInstallerLauncherFailure, needsSystemAuthorization);
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
return;
}

// if we need to use the system domain and we aren't allowed interaction, then try sometime later when interaction is allowed
if (needsSystemAuthorization && !allowingDriverInteraction) {
completionHandler(SUInstallerLauncherAuthorizeLater, needsSystemAuthorization);
// if we need to use the system authorization from non-root and we aren't allowed interaction, then try sometime later when interaction is allowed
if (inSystemDomain && !rootUser && !allowingDriverInteraction) {
completionHandler(SUInstallerLauncherAuthorizeLater, inSystemDomain);
return;
}

Expand All @@ -414,7 +420,7 @@ - (void)launchInstallerWithHostBundlePath:(NSString *)hostBundlePath updaterIden
NSString *installerPath = [self pathForBundledTool:@""SPARKLE_RELAUNCH_TOOL_NAME extension:@"" fromBundle:ourBundle];
if (installerPath == nil) {
SULog(SULogLevelError, @"Error: Cannot submit installer because the installer could not be located");
completionHandler(SUInstallerLauncherFailure, needsSystemAuthorization);
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
return;
}

Expand All @@ -423,7 +429,7 @@ - (void)launchInstallerWithHostBundlePath:(NSString *)hostBundlePath updaterIden

if (progressToolResourcePath == nil) {
SULog(SULogLevelError, @"Error: Cannot submit progress tool because the progress tool could not be located");
completionHandler(SUInstallerLauncherFailure, needsSystemAuthorization);
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
return;
}

Expand All @@ -439,25 +445,69 @@ - (void)launchInstallerWithHostBundlePath:(NSString *)hostBundlePath updaterIden
NSString *launcherCachePath = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:rootLauncherCachePath];
if (launcherCachePath == nil) {
SULog(SULogLevelError, @"Failed to create cache directory for progress tool in %@", rootLauncherCachePath);
completionHandler(SUInstallerLauncherFailure, needsSystemAuthorization);
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
return;
}

SUFileManager *fileManager = [[SUFileManager alloc] init];

NSString *userName;
NSString *homeDirectory;
if (!rootUser) {
// Normal path
homeDirectory = NSHomeDirectory();
assert(homeDirectory != nil);

userName = NSUserName();
assert(userName != nil);
} else {
// As the root user we need to obtain the user name and home directory reflecting
// the user's console session.

uid_t uid = 0;
gid_t gid = 0;
CFStringRef userNameRef = SCDynamicStoreCopyConsoleUser(NULL, &uid, &gid);
if (userNameRef == NULL) {
SULog(SULogLevelError, @"Failed to retrieve user name from the console user");
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
return;
}

userName = (NSString *)CFBridgingRelease(userNameRef);
homeDirectory = NSHomeDirectoryForUser(userName);
if (homeDirectory == nil) {
SULog(SULogLevelError, @"Failed to retrieve home directory for user: %@", userName);

completionHandler(SUInstallerLauncherFailure, inSystemDomain);
return;
}

// Ensure the console user has ownership of the launcher cache directory
// Otherwise the updater may not launch and not be able to clean up itself
NSError *changeOwnerAndGroupError = nil;
if (![fileManager changeOwnerAndGroupOfItemAtURL:[NSURL fileURLWithPath:launcherCachePath] ownerID:uid groupID:gid error:&changeOwnerAndGroupError]) {
SULog(SULogLevelError, @"Failed to change owner and group for launcher cache directory: %@", changeOwnerAndGroupError);

completionHandler(SUInstallerLauncherFailure, inSystemDomain);
return;
}
}

NSString *progressToolPath = [launcherCachePath stringByAppendingPathComponent:@""SPARKLE_INSTALLER_PROGRESS_TOOL_NAME@".app"];

NSError *copyError = nil;
// SUFileManager is more reliable for copying files around
if (![[[SUFileManager alloc] init] copyItemAtURL:[NSURL fileURLWithPath:progressToolResourcePath] toURL:[NSURL fileURLWithPath:progressToolPath] error:&copyError]) {
if (![fileManager copyItemAtURL:[NSURL fileURLWithPath:progressToolResourcePath] toURL:[NSURL fileURLWithPath:progressToolPath] error:&copyError]) {
SULog(SULogLevelError, @"Failed to copy progress tool to cache: %@", copyError);
completionHandler(SUInstallerLauncherFailure, needsSystemAuthorization);
completionHandler(SUInstallerLauncherFailure, inSystemDomain);
return;
}

SUInstallerLauncherStatus installerStatus = [self submitInstallerAtPath:installerPath withHostBundle:hostBundle updaterIdentifier:updaterIdentifier authorizationPrompt:authorizationPrompt inSystemDomain:needsSystemAuthorization];
SUInstallerLauncherStatus installerStatus = [self submitInstallerAtPath:installerPath withHostBundle:hostBundle updaterIdentifier:updaterIdentifier userName:userName homeDirectory:homeDirectory authorizationPrompt:authorizationPrompt inSystemDomain:inSystemDomain rootUser:rootUser];

BOOL submittedProgressTool = NO;
if (installerStatus == SUInstallerLauncherSuccess) {
submittedProgressTool = [self submitProgressToolAtPath:progressToolPath withHostBundle:hostBundle inSystemDomainForInstaller:needsSystemAuthorization];
submittedProgressTool = [self submitProgressToolAtPath:progressToolPath withHostBundle:hostBundle inSystemDomainForInstaller:inSystemDomain];

if (!submittedProgressTool) {
SULog(SULogLevelError, @"Failed to submit progress tool job");
Expand All @@ -468,9 +518,9 @@ - (void)launchInstallerWithHostBundlePath:(NSString *)hostBundlePath updaterIden
}

if (installerStatus == SUInstallerLauncherCanceled) {
completionHandler(installerStatus, needsSystemAuthorization);
completionHandler(installerStatus, inSystemDomain);
} else {
completionHandler(submittedProgressTool ? SUInstallerLauncherSuccess : SUInstallerLauncherFailure, needsSystemAuthorization);
completionHandler(submittedProgressTool ? SUInstallerLauncherSuccess : SUInstallerLauncherFailure, inSystemDomain);
}
});
}
Expand Down
37 changes: 24 additions & 13 deletions Sparkle/SPUBasicUpdateDriver.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#import "SPUResumableUpdate.h"
#import "SPUAppcastItemState.h"
#import "SUAppcastItem+Private.h"
#import "SPUInstallationType.h"


#include "AppKitPrevention.h"
Expand Down Expand Up @@ -139,21 +140,31 @@ - (void)didFinishLoadingAppcast:(SUAppcast *)appcast
- (void)notifyFoundValidUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem systemDomain:(NSNumber * _Nullable)systemDomain resuming:(BOOL)resuming
{
if (!self.aborted) {
// If the update is not being resumed from a prior session, give the delegate a chance to bail
NSError *shouldNotProceedError = nil;
if (!resuming && [self.updaterDelegate respondsToSelector:@selector(updater:shouldProceedWithUpdate:updateCheck:error:)] && ![self.updaterDelegate updater:self.updater shouldProceedWithUpdate:updateItem updateCheck:self.updateCheck error:&shouldNotProceedError]) {
[self.delegate basicDriverIsRequestingAbortUpdateWithError:shouldNotProceedError];
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterDidFindValidUpdateNotification
object:self.updater
userInfo:@{ SUUpdaterAppcastItemNotificationKey: updateItem }];

if ([self.updaterDelegate respondsToSelector:@selector((updater:didFindValidUpdate:))]) {
[self.updaterDelegate updater:self.updater didFindValidUpdate:updateItem];
if (!resuming) {
// interactive pkg based updates are not supported under root user
if ([updateItem.installationType isEqualToString:SPUInstallationTypeInteractivePackage] && geteuid() == 0) {
[self.delegate basicDriverIsRequestingAbortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationRootInteractiveError userInfo:@{ NSLocalizedDescriptionKey: SULocalizedString(@"Interactive based packages cannot be installed as the root user.", nil) }]];
return;
} else {
// Give the delegate a chance to bail

NSError *shouldNotProceedError = nil;
if ([self.updaterDelegate respondsToSelector:@selector(updater:shouldProceedWithUpdate:updateCheck:error:)] && ![self.updaterDelegate updater:self.updater shouldProceedWithUpdate:updateItem updateCheck:self.updateCheck error:&shouldNotProceedError]) {
[self.delegate basicDriverIsRequestingAbortUpdateWithError:shouldNotProceedError];
return;
}
}

[self.delegate basicDriverDidFindUpdateWithAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem systemDomain:systemDomain];
}

[[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterDidFindValidUpdateNotification
object:self.updater
userInfo:@{ SUUpdaterAppcastItemNotificationKey: updateItem }];

if ([self.updaterDelegate respondsToSelector:@selector((updater:didFindValidUpdate:))]) {
[self.updaterDelegate updater:self.updater didFindValidUpdate:updateItem];
}

[self.delegate basicDriverDidFindUpdateWithAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem systemDomain:systemDomain];
}
}

Expand Down
1 change: 1 addition & 0 deletions Sparkle/SUErrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ typedef NS_ENUM(OSStatus, SUError) {
SUInstallationAuthorizeLaterError = 4008,
SUNotValidUpdateError = 4009,
SUAgentInvalidationError = 4010,
SUInstallationRootInteractiveError = 4011,

// API misuse errors.
SUIncorrectAPIUsageError = 5000
Expand Down
12 changes: 12 additions & 0 deletions Sparkle/SUFileManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL *)matchURL error:(NSError **)error;

/**
Changes the owner and group ID of an item at a specified target URL
@param targetURL A URL pointing to the target item whose owner and group IDs to alter. The item at this URL must exist.
@param ownerID The new owner ID to set on the item.
@param groupID The new group ID to set on the item.
@param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL.
@return YES if the target item's owner and group IDs have changed, otherwise NO along with a populated error object.
Unlike -changeOwnerAndGroupOfItemAtRootURL:toMatchURL:error: this method does not recursively try to change the owner and group IDs if the target item is a directory.
*/
- (BOOL)changeOwnerAndGroupOfItemAtURL:(NSURL *)targetURL ownerID:(uid_t)ownerID groupID:(gid_t)groupID error:(NSError * __autoreleasing *)error;

/**
* Updates the modification and access time of an item at a specified target URL to the current time
* @param targetURL A URL pointing to the target item whose modification and access time to update. The item at this URL must exist.
Expand Down
Loading

0 comments on commit 339e269

Please sign in to comment.