personalize.m   [plain text]


//
//  personalize.m
//

#import <string.h>
#import <stdlib.h>
#import <stdbool.h>
#import <unistd.h>
#import <paths.h>
#import <sys/param.h>
#import <sys/mount.h>
#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#import <OSPersonalization/OSPersonalization.h>
#import <APFS/APFS.h>
#import <Bom/Bom.h>
#import <IOKit/storage/IOMedia.h>
#import "bless.h"
#import "bless_private.h"
#import "protos.h"


#define EX_BADNETWORK		129
#define EX_SERVERREFUSED	130
#define EX_OTHERPERSONALIZE	131


static int HandleSpecialVolume(BLContextPtr context, const char *rootPath, const char *volumeDev, int role);
static int EnsureSpecialVolumeUUIDPath(BLContextPtr context, const char *volumeDev, int role, char *mountPoint, int mountLen,
									   bool *didMount);
static int CopyRootToDir(const char *rootPath, const char *volumePath);
static int GetRoledVolumeBSDForContainerBSD(const char *containerBSD, char *volumeBSD, int volumeLen, int role);


int PersonalizeOSVolume(BLContextPtr context, const char *volumePath, const char *prFile, bool suppressACPrompt)
{
	__block int                 ret = 0;
	NSAutoreleasePool			*pool = [[NSAutoreleasePool alloc] init];
	OSPersonalizationController *pc = [OSPersonalizationController sharedController];
	char						prebootMount[MAXPATHLEN];
	NSURL                       *vURL;
	NSURL						*pURL = nil;
	char						*installEnv;
	NSURL                       *rootsURL;
	dispatch_semaphore_t        wait = dispatch_semaphore_create(0);
	NSDictionary                *prOptions;
	NSDictionary                *restoreOptions = nil;
	NSMutableDictionary         *options;
	char                        tmpLoc[MAXPATHLEN];
	__block NSArray<OSPersonalizedManifestRootType> *types;
	const char                  *pRoot;
	struct statfs               sfs;
	bool						mustCleanupRoots = false;
	bool						mustUnmountPreboot = false;

	if (statfs(volumePath, &sfs) < 0) {
		ret = errno;
		goto exit;
	}
	vURL = [NSURL fileURLWithFileSystemRepresentation:volumePath isDirectory:YES relativeToURL:nil];
	if ([pc personalizationRequiredForVolumeAtMountPoint:vURL]) {
		installEnv = getenv("__OSINSTALL_ENVIRONMENT");
		if (installEnv && (atoi(installEnv) > 0 || strcasecmp(installEnv, "yes") == 0 || strcasecmp(installEnv, "true") == 0)) {
			strlcpy(tmpLoc, "/var/tmp/RecoveryTemp", sizeof tmpLoc);
		} else {
			if (!confstr(_CS_DARWIN_USER_TEMP_DIR, tmpLoc, sizeof tmpLoc)) {
				// We couldn't get our path in /var/folders, so just try /var/tmp.
				strlcpy(tmpLoc, "/var/tmp/", sizeof tmpLoc);
			}
		}
		
		if (strcmp(sfs.f_fstypename, "apfs") == 0) {
			ret = EnsureSpecialVolumeUUIDPath(context, sfs.f_mntfromname, APFS_VOL_ROLE_PREBOOT, prebootMount, sizeof prebootMount,
											  &mustUnmountPreboot);
			if (ret) goto exit;
			pURL = [NSURL fileURLWithFileSystemRepresentation:prebootMount isDirectory:YES relativeToURL:nil];
		}
		if ([pc volumeHasBeenPersonalized:vURL prebootFolder:pURL]) {
			// This is already done.  Let's get out of here.
			ret = 0;
			goto exit;
		}

		rootsURL = [[NSURL fileURLWithFileSystemRepresentation:tmpLoc isDirectory:YES relativeToURL:nil] URLByAppendingPathComponent:@"roots"];
		
		options = [NSMutableDictionary dictionary];
		[options setObject:[NSNumber numberWithBool:YES] forKey:OSPersonalizationOptionUseRunningDeviceIdentity];
		if (suppressACPrompt) [options setObject:[NSNumber numberWithBool:NO] forKey:OSPersonalizationOptionShowUI];
		if (prFile) {
			prOptions = [NSDictionary dictionaryWithContentsOfFile:[NSString stringWithUTF8String:prFile]];
			if (prOptions) {
				restoreOptions = [prOptions objectForKey:@"RestoreOptions"];
				if (!restoreOptions) restoreOptions = prOptions;
			}
		}
		if (restoreOptions) [options addEntriesFromDictionary:restoreOptions];
		if ([pc networkAvailableForPersonalizationWithOptions:options]) {
			[pc personalizeVolumeAtMountPointForInstall:vURL outputDirectory:rootsURL options:options completionHandler:^(NSArray<OSPersonalizedManifestRootType> * _Nonnull manifestTypes, NSError * _Nullable error) {
				 if (error) {
					 if (OSPErrorIsNetworkingRelated(error)) {
						 ret = EX_BADNETWORK;
					 } else if ([error.domain isEqualToString:OSPErrorDomain] && error.code == OSPTATSUDeclinedAuthorizationError) {
						 ret = EX_SERVERREFUSED;
					 } else {
						 ret = EX_OTHERPERSONALIZE;
					 }
				 } else {
					 ret = 0;
				 }
				 if (!ret) types = manifestTypes;
				 dispatch_semaphore_signal(wait);
			 }];
			dispatch_semaphore_wait(wait, DISPATCH_TIME_FOREVER);
			if (ret) goto exit;
			mustCleanupRoots = true;
			ret = CopyRootToDir([[rootsURL URLByAppendingPathComponent:OSPersonalizedManifestRootTypeBoot] fileSystemRepresentation],
								volumePath);
			if (ret) goto exit;
			if (strcmp(sfs.f_fstypename, "apfs") == 0) {
				pRoot = [[rootsURL URLByAppendingPathComponent:OSPersonalizedManifestRootTypePreboot] fileSystemRepresentation];
				if (access(pRoot, R_OK) < 0) goto exit;
				ret = HandleSpecialVolume(context, pRoot, sfs.f_mntfromname, APFS_VOL_ROLE_PREBOOT);
				if (ret) goto exit;
				pRoot = [[rootsURL URLByAppendingPathComponent:OSPersonalizedManifestRootTypeRecoveryBoot] fileSystemRepresentation];
				if (access(pRoot, R_OK) < 0) goto exit;
				ret = HandleSpecialVolume(context, pRoot, sfs.f_mntfromname, APFS_VOL_ROLE_RECOVERY);
				if (ret) goto exit;
			}
		} else {
			ret = EX_BADNETWORK;
			goto exit;
		}
	}
	
exit:
	if (mustUnmountPreboot) {
		BLUnmountContainerVolume(context, prebootMount);
	}
	if (!ret && mustCleanupRoots) {
		DeleteFileOrDirectory([rootsURL fileSystemRepresentation]);
	}
	dispatch_release(wait);
	[pool release];
	return ret;
}



static int HandleSpecialVolume(BLContextPtr context, const char *rootPath, const char *volumeDev, int role)
{
	int				ret;
	char			specialMountPointPath[MAXPATHLEN];
	bool			mustUnmount;

	ret = EnsureSpecialVolumeUUIDPath(context, volumeDev, role, specialMountPointPath, sizeof specialMountPointPath, &mustUnmount);
	if (ret) goto exit;
	
	ret = CopyRootToDir(rootPath, specialMountPointPath);
	if (ret) goto exit;

exit:
	if (mustUnmount) BLUnmountContainerVolume(context, specialMountPointPath);
	return ret;
}




static int EnsureSpecialVolumeUUIDPath(BLContextPtr context, const char *volumeDev, int role, char *mountPoint, int mountLen,
									   bool *didMount)
{
	int				ret;
	char			specialDevPath[64];
	struct statfs	*mnts;
	int				mntsize;
	int				i;
	io_service_t	service = IO_OBJECT_NULL;
	CFStringRef		uuid = NULL;
	int				len;

	strlcpy(specialDevPath, _PATH_DEV, sizeof specialDevPath);
	ret = GetRoledVolumeBSDForContainerBSD(volumeDev, specialDevPath + strlen(_PATH_DEV),
										   sizeof specialDevPath - strlen(_PATH_DEV), role);
	if (ret) goto exit;
	
	// Check if the given volume is mounted.
	mntsize = getmntinfo(&mnts, MNT_NOWAIT);
	if (!mntsize) {
		ret = 5;
		goto exit;
	}
	for (i = 0; i < mntsize; i++) {
		if (strcmp(mnts[i].f_mntfromname, specialDevPath) == 0) break;
	}
	if (i < mntsize) {
		strlcpy(mountPoint, mnts[i].f_mntonname, mountLen);
		*didMount = false;
	} else {
		// The preboot volume isn't mounted right now.  We'll have to mount it.
		ret = BLMountContainerVolume(context, specialDevPath + strlen(_PATH_DEV), mountPoint, mountLen, false);
		if (ret) goto exit;
		*didMount = true;
	}
	ret = BLGetIOServiceForDeviceName(context, volumeDev + strlen(_PATH_DEV), &service);
	if (ret) goto exit;
	uuid = IORegistryEntryCreateCFProperty(service, CFSTR(kIOMediaUUIDKey), kCFAllocatorDefault, 0);
	if (!uuid) {
		ret = EINVAL;
		goto exit;
	}
	len = strlen(mountPoint);
	if (mountLen <= len + 1) {
		ret = EINVAL;
		goto exit;
	}
	mountPoint[len] = '/';
	CFStringGetCString(uuid, mountPoint + len + 1, mountLen - len - 1, kCFStringEncodingUTF8);

exit:
	if (service) IOObjectRelease(service);
	if (uuid) CFRelease(uuid);
	return ret;
}



static int CopyRootToDir(const char *rootPath, const char *volumePath)
{
	int			ret;
	BOMCopier	copier;
	
	copier = BOMCopierNew();
	if (!copier) return ENOMEM;
	ret = BOMCopierCopy(copier, rootPath, volumePath);
	BOMCopierFree(copier);
	return ret;
}




static int GetRoledVolumeBSDForContainerBSD(const char *containerBSD, char *volumeBSD, int volumeLen, int role)
{
	int                 ret;
	CFMutableArrayRef   volumes = NULL;
	CFStringRef         volume;
	
	*volumeBSD = '\0';
	ret = APFSVolumeRoleFind(containerBSD, role, &volumes);
	if (ret == unix_err(ENOATTR) || !volumes || CFArrayGetCount(volumes) == 0) {
		ret = 0;
	} else if (!ret) {
		volume = CFArrayGetValueAtIndex(volumes, 0);
		CFStringGetCString(volume, volumeBSD, volumeLen, kCFStringEncodingUTF8);
		memmove(volumeBSD, volumeBSD + strlen(_PATH_DEV), strlen(volumeBSD) - strlen(_PATH_DEV) + 1);
	}
	if (volumes) CFRelease(volumes);
	return ret;
}