staging.m   [plain text]


/*
 *  staging.m
 *  kext_tools
 *
 *  Copyright 2017 Apple Inc. All rights reserved.
 *
 */
#import <Foundation/Foundation.h>
#import <sys/csr.h>
#import <rootless.h>
#import <copyfile.h>

#import <IOKit/kext/OSKextPrivate.h>

#import "kext_tools_util.h"
#import "security.h"
#import "staging.h"
#import "syspolicy.h"

// This requirement says a bundle must be apple signed or signed by NVIDIA's kernel extension
// signing certificate (a generic kernel extension certificate + NVIDIA's team ID).
#define GPU_BUNDLE_STAGING_REQUIREMENT "anchor apple or (anchor apple generic " \
        "and certificate 1[field.1.2.840.113635.100.6.2.6] " \
        "and certificate leaf[field.1.2.840.113635.100.6.1.13] " \
        "and certificate leaf[field.1.2.840.113635.100.6.1.18] " \
        "and certificate leaf[subject.OU] = \"6KR3T733EC\")"

#define INSECURE_GRAPHICS_BUNDLE_PATH "/System/Library/Extensions"
#define SECURE_GRAPHICS_BUNDLE_STAGING_PATH "/Library/GPUBundles"
#define SECURE_KEXT_STAGING_PATH "/Library/StagedExtensions"

static CFStringRef kCompanionBundlesKey = CFSTR("GPUCompanionBundles");
static CFStringRef kOptionalCompanionBundlesKey = CFSTR("GPUOptionalCompanionBundles");

typedef BOOL (^BundleURLHandler)(NSURL *, NSURL *);


#pragma mark Helper Functions

static BOOL
isSIPDisabled(void) {
    static BOOL sInitialized = NO;
    static BOOL sSIPDisabled = NO;

    if (!sInitialized) {
        sSIPDisabled = csr_check(CSR_ALLOW_UNTRUSTED_KEXTS) == 0 ? YES : NO;
        sInitialized = YES;
    }

    return sSIPDisabled;
}

Boolean
pathIsSecure(NSString *path) {
    Boolean is_secure = false;
    BOOL is_protected_volume = rootless_protected_volume(path.UTF8String) == 1 ? YES : NO;
    BOOL is_trusted_path = rootless_check_trusted_class(path.UTF8String, "KernelExtensionManagement") == 0 ? YES : NO;

    if (isSIPDisabled()) {
        // SIP is disabled so everything is considered secure.
        is_secure = true;
    } else if (!is_protected_volume) {
        // SIP is enabled and the volume is not protected, so it's insecure.
        is_secure = false;
    } else {
        // SIP is enabled and it is a protected volume, so it's only secure if it's trusted.
        is_secure = is_trusted_path;
    }
    return is_secure;
}

static BOOL
gpuBundleValidates(NSURL *bundleURL)
{
    OSStatus result = 0;
    SecStaticCodeRef staticCodeRef = NULL;
    SecRequirementRef requirementRef = NULL;
    BOOL passesRequirement = NO;

    result = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCodeRef);
    if (result != errSecSuccess) {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                          CFSTR("Unable to create SecCode (%d): %@"),
                          (int)result, bundleURL.path);
        goto __out;
    }

    result = SecRequirementCreateWithString(CFSTR(GPU_BUNDLE_STAGING_REQUIREMENT), kSecCSDefaultFlags, &requirementRef);
    if (result) {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                          CFSTR("Error creating requirement: %d"),
                          (int)result);
        goto __out;
    }

    result = SecStaticCodeCheckValidity(staticCodeRef, (SecCSFlags)kSecCSStrictValidate, requirementRef);
    if (result) {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                          CFSTR("GPU Bundle failed validity check (%d): %@"),
                          (int)result, bundleURL.path);
        goto __out;
    }

    passesRequirement = YES;

__out:
    SAFE_RELEASE(requirementRef);
    SAFE_RELEASE(staticCodeRef);
    return passesRequirement;
}

static BOOL
kextBundleValidates(NSURL *bundleURL)
{
    BOOL validates = NO;
    OSKextRef kext = NULL;
    OSStatus status = 0;

    kext = OSKextCreate(NULL, (__bridge CFURLRef)bundleURL);
    if (!kext) {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                          CFSTR("Could not create kext to validate: %@"), bundleURL.path);
        goto __out;
    }

    status = checkKextSignature(kext, true, true);
    if (status) {
        if (isInvalidSignatureAllowed()) {
            OSKextLogCFString(NULL,
                              kOSKextLogErrorLevel | kOSKextLogValidationFlag,
                              CFSTR("Kext with invalid signatured (%d) allowed: %@"),
                              (int)status, bundleURL.path);
        } else {
            OSKextLogCFString(NULL,
                              kOSKextLogErrorLevel | kOSKextLogValidationFlag,
                              CFSTR("Kext with invalid signature (%d) denied: %@"),
                              (int)status, bundleURL.path);
            goto __out;
        }
    }

    validates = YES;

__out:
    SAFE_RELEASE(kext);
    return validates;
}

BOOL bundleValidates(NSURL *bundleURL, BOOL isGPUBundle)
{
    return isGPUBundle ? gpuBundleValidates(bundleURL) : kextBundleValidates(bundleURL);
}

Boolean
stageBundle(NSURL *sourceURL, NSURL *destinationURL, BOOL isGPUBundle)
{
    Boolean success = false;
    int copyStatus = 0;
    NSError *error = nil;
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSUUID *uuid = nil;
    NSURL *temporaryURL = nil;
    NSString *dirName = nil;

    // If the target directory already exists, we need to delete it first.
    if ([fileManager fileExistsAtPath:[destinationURL path]]) {
        [fileManager removeItemAtURL:destinationURL error:&error];
        if (error) {
            OSKextLogCFString(NULL,
                              kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                              CFSTR("Error deleting old bundle while staging: %@"),
                              error);
            success = false;
            goto __out;
        }
    }

    // Create a temporary staging directory name to use pre-validation.
    uuid = [NSUUID UUID];
    if (!uuid) {
        OSKextLogMemError();
        success = false;
        goto __out;
    }

    if (isGPUBundle) {
        dirName = uuid.UUIDString;
    } else {
        // Since we use real OSKext objects to assist validation, we need these to have
        // the appropriate kext ending or they get rejected by IOKit.
        dirName = [NSString stringWithFormat:@"%@.%@", uuid.UUIDString, @"kext"];
    }
    if (!dirName) {
        OSKextLogMemError();
        success = false;
        goto __out;
    }

    temporaryURL = [destinationURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:dirName];
    if (!temporaryURL) {
        OSKextLogMemError();
        success = false;
        goto __out;
    }

    // copyfile relies on the parent directory already existing.
    success = [fileManager createDirectoryAtPath:destinationURL.URLByDeletingLastPathComponent.path
                     withIntermediateDirectories:YES
                                      attributes:nil
                                           error:&error];
    if (!success) {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                          CFSTR("Error creating directory during staging: %@"),
                          error);
        success = false;
        goto __out;
    }

    copyStatus = copyfile(sourceURL.path.UTF8String, temporaryURL.path.UTF8String, NULL, COPYFILE_ALL | COPYFILE_NOFOLLOW | COPYFILE_RECURSIVE);
    if (copyStatus < 0) {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                          CFSTR("Error copying bundle during staging: %d"),
                          copyStatus);
        success = false;
        goto __out;
    }

    // Validate bundle and rename or delete, as appropriate.
    if (bundleValidates(temporaryURL, isGPUBundle)) {
        NSURL *resultURL = nil;
        success = [fileManager replaceItemAtURL:destinationURL
                                  withItemAtURL:temporaryURL
                                 backupItemName:nil
                                        options:0
                               resultingItemURL:&resultURL
                                          error:&error];
        if (!success || error) {
            success = false;
            OSKextLogCFString(NULL,
                              kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                              CFSTR("Error renaming bundle during staging: %@"),
                              error);
            [fileManager removeItemAtURL:temporaryURL error:nil];
            goto __out;
        }

        if (![destinationURL.path isEqualToString:resultURL.path]) {
            success = false;
            OSKextLogCFString(NULL,
                              kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                              CFSTR("Error renaming bundle, result ended up in %@. Deleting."),
                              resultURL.path);
            [fileManager removeItemAtURL:resultURL error:nil];
            goto __out;
        }
    } else {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                          CFSTR("Bundle (%@) failed to validate, deleting: %@"),
                          sourceURL.path, temporaryURL.path);
        [fileManager removeItemAtURL:temporaryURL error:nil];
        success = false;
        goto __out;
    }

    success = true;

__out:
    return success;
}

NSData *
copyIdentifierFromBundle(NSURL *url)
{
    OSStatus result = 0;
    SecStaticCodeRef staticCodeRef = NULL;
    SecRequirementRef requirementRef = NULL;
    CFDictionaryRef signingInfo = NULL;
    NSData *cdhashData = nil;

    // This function must compute some identifier from a URL to identify the bundle at the URL
    // such that we can insecurely determine if two copies are identical are not.
    //  - For signed code, we use the cdhash data blob directly out of the signing information.
    //  - For unsigned code, we use a string of the cdhash generated using the legacy adhoc cdhash
    // generation function that was used to generate the legacy allow list.

    result = SecStaticCodeCreateWithPath((__bridge CFURLRef)url, kSecCSDefaultFlags, &staticCodeRef);
    if (result) {
        OSKextLogMemError();
        goto __out;
    }

    result = SecStaticCodeCheckValidity(staticCodeRef, (SecCSFlags)kSecCSBasicValidateOnly, NULL);
    if (result && (result != errSecCSUnsigned)) {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                          CFSTR("Basic validation of bundle failed (%d): %@"),
                          (int)result, url.path);
        goto __out;
    }

    if (result == errSecCSUnsigned) {
        // The unsigned case - generate the adhoc signature and package up the string version
        char *cdhashCString = NULL;

        getAdhocSignatureHash((__bridge CFURLRef)url, &cdhashCString, NULL);
        if (!cdhashCString) {
            OSKextLogCFString(NULL,
                              kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                              CFSTR("Unable to create adhoc cdhashfor bundle: %@"),
                              url.path);
            goto __out;
        }

        cdhashData = [NSData dataWithBytes:cdhashCString length:strlen(cdhashCString)];
        free(cdhashCString);
    } else {
        // The signed case - copy the cdhash data blob out of the codesigning dictionary
        result = SecCodeCopySigningInformation(staticCodeRef, kSecCSDefaultFlags, &signingInfo);
        if (result) {
            OSKextLogCFString(NULL,
                              kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                              CFSTR("Failed to copy signing information (%d): %@"),
                              (int)result, url.path);
            goto __out;
        }

        cdhashData = (__bridge NSData *)CFDictionaryGetValue(signingInfo, kSecCodeInfoUnique);
    }

__out:
    SAFE_RELEASE(requirementRef);
    SAFE_RELEASE(signingInfo);
    SAFE_RELEASE(staticCodeRef);
    return cdhashData;
}

BOOL
bundleNeedsStaging(NSURL *sourceURL, NSURL *destinationURL)
{
    BOOL needsStaging = NO;
    NSFileManager *fileManager = [NSFileManager defaultManager];

    if (pathIsSecure(sourceURL.path)) {
        // If the source location is "secure", no need for any staging or further checks.
        needsStaging = NO;
    } else if ([fileManager fileExistsAtPath:destinationURL.path]) {
        // Given the source is insecure, look for an already staged copy and then we only need
        // to re-stage if they no longer match content.
        // NOTE: it is ok to use an insecure comparison here because we will re-validate the bundle once
        // its been moved into a SIP protected location, so the worst an attacker can do here is
        // prevent us from copying a new version, which is still safe.
        NSData *sourceIdentifier = copyIdentifierFromBundle(sourceURL);
        NSData *destinationIdentifier = copyIdentifierFromBundle(destinationURL);

        if (!sourceIdentifier || ![sourceIdentifier isEqualToData:destinationIdentifier]) {
            needsStaging = YES;
        }
    } else {
        // This bundle has never been staged and is in an insecure location, so stage it.
        needsStaging = YES;
    }

    return needsStaging;
}

// A helper function that iterates through an array of bundle names and calls a callback function
// for each bundle that is found to be in an insecure location or where the currently staged
// version no longer matches the unstaged version.
void
forEachInsecureBundleHelper(NSArray *bundles, BundleURLHandler callbackHandler, NSURL *sourceBaseURL, NSURL *targetBaseURL)
{
    NSFileManager *fileManager = [NSFileManager defaultManager];

    for (NSString *bundleName in bundles) {
        NSURL *sourceURL = [[sourceBaseURL URLByAppendingPathComponent:bundleName] URLByStandardizingPath];
        NSURL *targetURL = [[targetBaseURL URLByAppendingPathComponent:bundleName] URLByStandardizingPath];
        if ([fileManager fileExistsAtPath:sourceURL.path]) {
            NSDictionary *attributes = nil;
            NSError *error = nil;

            // There's definitely a usermode bundle present.
            BOOL needsStaging = NO;

            // Ensure URL's are still prefixed by their expected base to prevent any ../-style filesystem escapes.
            if (![sourceURL.path hasPrefix:sourceBaseURL.path]) {
                OSKextLogCFString(NULL,
                                  kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                                  CFSTR("Source URL is no longer contained in expected directory: %@"),
                                  sourceURL.path);
                continue;
            } else if (![targetURL.path hasPrefix:targetBaseURL.path]) {
                OSKextLogCFString(NULL,
                                  kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                                  CFSTR("Target URL is no longer contained in expected directory: %@"),
                                  targetURL.path);
                continue;
            }

            // Validate the sourceURL as a directory and ensure it's not a symlink.
            attributes = [fileManager attributesOfItemAtPath:sourceURL.path error:&error];
            if (!attributes || error) {
                // Error, do not stage this if we can't check its attributes.
                OSKextLogCFString(NULL,
                                  kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                                  CFSTR("Failed to get attributes of bundle source (%@): %@"),
                                  sourceURL.path, error);
                continue;
            }

            if (![NSFileTypeDirectory isEqualToString:attributes[NSFileType]]) {
                // If this isn't a directory, log the error and skip it.
                OSKextLogCFString(NULL,
                                  kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                                  CFSTR("Bundle is not a directory (type = %@), skipping %@"),
                                  attributes[NSFileType], bundleName);
                continue;
            }

            needsStaging = bundleNeedsStaging(sourceURL, targetURL);
            if (needsStaging) {
                BOOL shouldContinue = callbackHandler(sourceURL, targetURL);
                if (!shouldContinue) {
                    break;
                }
            }
        }
    }
}

static void
forEachInsecureBundle(NSArray *bundles, BundleURLHandler callbackHandler)
{
    NSURL *sourceBaseURL = [NSURL fileURLWithPath:@INSECURE_GRAPHICS_BUNDLE_PATH];
    NSURL *targetBaseURL = [NSURL fileURLWithPath:@SECURE_GRAPHICS_BUNDLE_STAGING_PATH];

    forEachInsecureBundleHelper(bundles, callbackHandler, sourceBaseURL, targetBaseURL);
}

static Boolean
arrayHasInsecureBundles(NSArray *bundles) {
    __block BOOL insecure = NO;
    forEachInsecureBundle(bundles, ^ BOOL (NSURL * __unused sourceURL, NSURL * __unused targetURL) {
        insecure = YES;
        return NO;
    });
    return insecure;
}

NSURL *
createStagingURL(NSURL *sourceURL)
{
    static NSArray *stagingURLComponents = nil;

    if (!stagingURLComponents) {
        stagingURLComponents = [NSURL fileURLWithPath:@SECURE_KEXT_STAGING_PATH isDirectory:YES].pathComponents;
    }

    // To create the full URL, we start with the staging URL, add in all the path components of the source URL
    // without the initial /, then add add a trailing / to ensure it's a directory URL.
    NSMutableArray *pathComponents = [NSMutableArray arrayWithArray:stagingURLComponents];
    [pathComponents addObjectsFromArray:[sourceURL.pathComponents subarrayWithRange:NSMakeRange(1, sourceURL.pathComponents.count - 1)]];
    [pathComponents addObject:@"/"];

    // NOTE: The output URL MUST be a directory and contain a trailing /
    // IOKit function calls rely on this or the OSKext object doesn't fill out properly.
    return [NSURL fileURLWithPathComponents:pathComponents];
}

static OSKextRef
createRefreshedKext(NSURL *url)
{
    CFURLRef kextURL = (__bridge CFURLRef)url;
    OSKextRef theKext = NULL;
    CFArrayRef kexts = NULL;

    // Create all kexts from the URL so IOKit can create objects for the
    // kext itself and any bundled plug-ins.  This guarantees there are
    // new OSKext objects for this kext URL and all plug-ins for use in future
    // API calls.
    kexts = OSKextCreateKextsFromURL(kCFAllocatorDefault, kextURL);
    if (!kexts) {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                          CFSTR("Could not create all kexts from url: %@"),
                          url.path);
        goto finish;
    }

    // Create an OSKext object for this kext URL directly so we can return it
    // for future use by the caller.  Internally, this will just be an already
    // created object from the list above, but this saves us the trouble of
    // walking that list to find it.
    theKext = OSKextCreate(kCFAllocatorDefault, kextURL);
    if (!theKext) {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                          CFSTR("Could not create refreshed kext at url: %@"),
                          url.path);
        goto finish;
    }

finish:
    SAFE_RELEASE(kexts);
    return theKext;
}

#pragma mark External Functions
OSKextRef
createStagedKext(OSKextRef theKext)
{
    OSKextRef finalKext = NULL;

    if (!stagingEnabled()) {
        finalKext = (OSKextRef)CFRetain(theKext);
        goto finish;
    }

    if (kextRequiresStaging(theKext)) {
        NSURL *sourceURL = (__bridge NSURL *)OSKextGetURL(theKext);
        NSURL *destinationURL = createStagingURL(sourceURL);

        // Even if the source kext "requires staging", the bundle itself may have already
        // been staged properly (from a previous load, etc) and not need any more work.
        if (bundleNeedsStaging(sourceURL, destinationURL)) {
            if (!stageBundle(sourceURL, destinationURL, NO)) {
                OSKextLog(NULL,
                          kOSKextLogErrorLevel | kOSKextLogArchiveFlag |
                          kOSKextLogValidationFlag | kOSKextLogGeneralFlag,
                          "Unable to stage kext (%s) to secure location.",
                          sourceURL.path.UTF8String);
                finalKext = NULL;
                goto finish;
            }
        }

        // Regardless of whether the copy happened now or previously, we still need to update
        // the OSKext object to be based on the staged URL.
        finalKext = createRefreshedKext(destinationURL);
    } else {
        // The incoming kext is already secure, so just retain it for the output.
        finalKext = (OSKextRef)CFRetain(theKext);
    }

finish:
    return finalKext;
}

Boolean
stagingEnabled(void)
{
    return !isSIPDisabled();
}

Boolean
kextIsInSecureLocation(OSKextRef theKext)
{
    NSURL *url = (__bridge NSURL *)OSKextGetURL(theKext);
    if (!url) {
        return false;
    }
    return pathIsSecure(url.path);
}

Boolean
kextRequiresStaging(OSKextRef theKext)
{
    return !kextIsInSecureLocation(theKext);
}

Boolean
needsGPUBundlesStaged(OSKextRef theKext)
{
    CFTypeRef companionBundlesValue = NULL;
    CFTypeRef optionalBundlesValue = NULL;
    Boolean hasInsecureBundles = false;

    if (!stagingEnabled()) {
        return false;
    }

    companionBundlesValue = OSKextGetValueForInfoDictionaryKey(theKext, kCompanionBundlesKey);
    optionalBundlesValue = OSKextGetValueForInfoDictionaryKey(theKext, kOptionalCompanionBundlesKey);

    // If neither key exists, it definitely doesn't need any staging.
    if (!companionBundlesValue && !optionalBundlesValue) {
        return false;
    }

    // If either key is malformed, we'll print a warning and not copy anything.
    if ((CFGetTypeID(companionBundlesValue) != CFArrayGetTypeID()) ||
        (CFGetTypeID(companionBundlesValue) != CFArrayGetTypeID())) {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogLoadFlag,
                          CFSTR("GPU bundle keys malformed - wrong type."));
        return false;
    }

    // If either array has any insecure bundles, then the kext needs something staged.
    hasInsecureBundles = arrayHasInsecureBundles((__bridge NSArray*)companionBundlesValue);
    hasInsecureBundles = hasInsecureBundles || arrayHasInsecureBundles((__bridge NSArray*)optionalBundlesValue);
    return hasInsecureBundles;
}

Boolean
stageGPUBundles(OSKextRef theKext)
{
    NSArray<NSString *> *companionBundles = nil;
    NSArray<NSString *> *optionalBundles = nil;
    BundleURLHandler handler = NULL;
    __block Boolean success = true;

    if (!stagingEnabled()) {
        return false;
    }

    handler = ^ BOOL (NSURL *sourceURL, NSURL *targetURL) {
        OSKextLogCFString(NULL,
                          kOSKextLogBasicLevel | kOSKextLogLoadFlag,
                          CFSTR("Staging insecure bundle: %@"),
                          sourceURL.path);
        if (!stageBundle(sourceURL, targetURL, YES)) {
            OSKextLogCFString(NULL,
                              kOSKextLogBasicLevel | kOSKextLogLoadFlag,
                              CFSTR("Staging failed for bundle: %@"),
                              sourceURL.path);
            success = false;
        }
        return YES;
    };

    companionBundles = (__bridge NSArray*)OSKextGetValueForInfoDictionaryKey(theKext, kCompanionBundlesKey);
    optionalBundles = (__bridge NSArray*)OSKextGetValueForInfoDictionaryKey(theKext, kOptionalCompanionBundlesKey);

    forEachInsecureBundle(companionBundles, handler);
    forEachInsecureBundle(optionalBundles, handler);

    return success;
}

CFArrayRef
createStagedKextsFromURLs(CFArrayRef kextURLs, Boolean includeUnstaged)
{
    CFArrayRef unstagedKexts = NULL;
    CFMutableArrayRef finalKexts = NULL;
    CFIndex count = 0;

    if (!kextURLs) {
        goto finish;
    }

    if (!createCFMutableArray(&finalKexts, &kCFTypeArrayCallBacks)) {
        OSKextLogMemError();
        goto finish;
    }

    unstagedKexts = OSKextCreateKextsFromURLs(NULL, kextURLs);
    if (!unstagedKexts) {
        OSKextLogCFString(NULL,
                          kOSKextLogErrorLevel | kOSKextLogGeneralFlag,
                          CFSTR("Unable to read extensions from URLs: %@"),
                          kextURLs);
        goto finish;
    }

    count = CFArrayGetCount(unstagedKexts);
    for(CFIndex i = 0; i < count; i++) {
        OSKextRef unstagedKext = (OSKextRef)CFArrayGetValueAtIndex(unstagedKexts, i);
        OSKextRef stagedKext = createStagedKext(unstagedKext);
        if (!stagedKext) {
            continue;
        }

        CFArrayAppendValue(finalKexts, stagedKext);
        if (includeUnstaged && (stagedKext != unstagedKext)) {
            CFArrayAppendValue(finalKexts, unstagedKext);
        }
        SAFE_RELEASE(stagedKext);
    }

finish:
    SAFE_RELEASE(unstagedKexts);
    return finalKexts;
}

NSURL *
createURLWithoutPrefix(NSURL *url, NSString *prefix)
{
    if (![url.path hasPrefix:prefix]) {
        return url;
    }

    NSMutableArray *components = [NSMutableArray arrayWithObject:@"/"];
    NSRange prefixRange = NSMakeRange(prefix.pathComponents.count,
                                      url.pathComponents.count - prefix.pathComponents.count);
    NSArray *sourceComponents = [url.pathComponents subarrayWithRange:prefixRange];
    [components addObjectsFromArray:sourceComponents];
    return [NSURL fileURLWithPathComponents:components];
}

Boolean
pruneStagingDirectoryHelper(NSString *stagingRoot)
{
    Boolean success = true;
    NSError *error = nil;
    NSFileManager *fm = [NSFileManager defaultManager];
    NSDirectoryEnumerator *enumerator = nil;
    NSArray<NSURLResourceKey> *keys = @[NSURLIsDirectoryKey];
    NSMutableArray *kextsToCheck = [NSMutableArray array];

    NSURL *stagingBaseURL = [NSURL fileURLWithPath:stagingRoot isDirectory:YES];

    enumerator = [fm enumeratorAtURL:stagingBaseURL
          includingPropertiesForKeys:keys
                             options:(NSDirectoryEnumerationOptions)0
                        errorHandler:^(NSURL *url, NSError *error) {
                            OSKextLogCFString(NULL,
                                              kOSKextLogErrorLevel | kOSKextLogGeneralFlag,
                                              CFSTR("Error pruning staging area: %@, %@"),
                                              url.path, error);
                            return YES;
                        }];

    for (NSURL *url in enumerator) {
        NSNumber *isDirectory = nil;

        if ([url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]) {
            if (isDirectory && isDirectory.boolValue) {
                if ([url.lastPathComponent.pathExtension isEqualToString:@"kext"]) {
                    [kextsToCheck addObject:url];
                    [enumerator skipDescendants];
                }
            }
        }
    }

    for (NSURL *stagedURL in kextsToCheck) {
        NSURL *parentURL = nil;
        NSURL *unstagedURL = nil;

        // Check if it still exists on the filesystem.
        unstagedURL = createURLWithoutPrefix(stagedURL, stagingRoot);
        if ([fm fileExistsAtPath:unstagedURL.path]) {
            continue;
        }

        // If not, remove it.
        OSKextLogCFString(NULL,
                          kOSKextLogDetailLevel | kOSKextLogGeneralFlag,
                          CFSTR("Pruning deleted kernel extension: %@"),
                          unstagedURL.path);
        if (![fm removeItemAtURL:stagedURL error:&error]) {
            OSKextLogCFString(NULL,
                              kOSKextLogErrorLevel | kOSKextLogGeneralFlag,
                              CFSTR("Unable to delete kernel extension: %@, %@"),
                              stagedURL.path, error);
        }

        // Then walk up directories to see if we need to remove empty parents.
        parentURL = [stagedURL URLByDeletingLastPathComponent];
        while ([stagedURL isNotEqualTo:parentURL]) {
            NSArray *contents = [fm contentsOfDirectoryAtPath:parentURL.path error:&error];

            // If parent is now empty, remove it...otherwise, move on to the next kext.
            if (contents.count == 0) {
                if (![fm removeItemAtURL:parentURL error:&error]) {
                    OSKextLogCFString(NULL,
                                      kOSKextLogErrorLevel | kOSKextLogGeneralFlag,
                                      CFSTR("Unable to delete empty staging directory: %@, %@"),
                                      parentURL.path, error);
                }
                parentURL = [parentURL URLByDeletingLastPathComponent];
            } else {
                break;
            }
        }
    }

    return success;
}

Boolean
pruneStagingDirectory(void)
{
    return pruneStagingDirectoryHelper(@SECURE_KEXT_STAGING_PATH);
}

Boolean
clearStagingDirectoryHelper(NSString *stagingRoot)
{
    NSFileManager *fm = [NSFileManager defaultManager];
    NSURL *baseURL = [NSURL fileURLWithPath:stagingRoot isDirectory:YES];

    // Enumerate all directories in the top-level staging directory and remove them.
    NSArray *contents = [fm contentsOfDirectoryAtPath:stagingRoot error:nil];
    for (NSString *itemName in contents) {
        NSError *error = nil;
        NSURL *itemURL = [baseURL URLByAppendingPathComponent:itemName];

        if (![fm removeItemAtURL:itemURL error:&error]) {
            OSKextLogCFString(NULL,
                              kOSKextLogErrorLevel | kOSKextLogGeneralFlag,
                              CFSTR("Unable to delete extension: %@, %@"),
                              itemURL.path, error);
        }
    }
    return true;
}

Boolean
clearStagingDirectory(void)
{
    return clearStagingDirectoryHelper(@SECURE_KEXT_STAGING_PATH);
}