#import "MCMBridge.h" #import #import #import typedef void *(*MCMQueryCreate)(void); typedef void (*MCMQuerySetU64)(void *, uint64_t); typedef void (*MCMQuerySetXPC)(void *, xpc_object_t); typedef void (*MCMQuerySetCString)(void *, const char *); typedef void *(*MCMQueryGetPointer)(void *); typedef bool (*MCMQueryIterate)(void *, bool (^)(void *)); typedef void (*MCMQueryFree)(void *); typedef const char *(*MCMObjectGetPath)(void *); typedef const char *(*MCMObjectGetIdentifier)(void *); typedef void *(*MCMObjectCopy)(void *); typedef char *(*MCMObjectCopyToken)(void *); typedef bool (*MCMObjectActivate)(void *, bool); typedef void (*MCMObjectFree)(void *); typedef int (*MCMErrorGetInt)(void *); typedef const char *(*MCMErrorGetString)(void *); typedef struct { void *handle; MCMQueryCreate queryCreate; MCMQuerySetU64 querySetClass; MCMQuerySetXPC querySetIdentifiers; MCMQuerySetXPC querySetGroupIdentifiers; MCMQuerySetU64 querySetFlags; MCMQuerySetU64 querySetPart; MCMQuerySetCString querySetPartDomain; MCMQueryGetPointer queryGetSingle; MCMQueryGetPointer queryGetLastError; MCMQueryIterate queryIterate; MCMQueryFree queryFree; MCMObjectGetPath objectGetPath; MCMObjectGetIdentifier objectGetIdentifier; MCMObjectCopy objectCopy; MCMObjectCopyToken objectCopyToken; MCMObjectActivate objectActivate; MCMObjectFree objectFree; MCMErrorGetInt errorGetPOSIX; MCMErrorGetString errorGetMessage; } MCMAPI; static MCMAPI *MCMSharedAPI(void) { static MCMAPI api; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ api.handle = dlopen("/usr/lib/system/libsystem_containermanager.dylib", RTLD_NOW | RTLD_LOCAL); void *handle = api.handle != NULL ? api.handle : RTLD_DEFAULT; #define LOAD(field, symbol) api.field = (__typeof(api.field))dlsym(handle, symbol) LOAD(queryCreate, "container_query_create"); LOAD(querySetClass, "container_query_set_class"); LOAD(querySetIdentifiers, "container_query_set_identifiers"); LOAD(querySetGroupIdentifiers, "container_query_set_group_identifiers"); LOAD(querySetFlags, "container_query_operation_set_flags"); LOAD(querySetPart, "container_query_operation_set_part"); LOAD(querySetPartDomain, "container_query_operation_set_part_domain"); LOAD(queryGetSingle, "container_query_get_single_result"); LOAD(queryGetLastError, "container_query_get_last_error"); LOAD(queryIterate, "container_query_iterate_results_sync"); LOAD(queryFree, "container_query_free"); LOAD(objectGetPath, "container_object_get_path"); LOAD(objectGetIdentifier, "container_object_get_identifier"); LOAD(objectCopy, "container_object_copy"); LOAD(objectCopyToken, "container_copy_sandbox_token"); LOAD(objectActivate, "container_object_sandbox_extension_activate"); LOAD(objectFree, "container_object_free"); LOAD(errorGetPOSIX, "container_error_get_posix_errno"); LOAD(errorGetMessage, "container_error_get_message"); #undef LOAD }); return &api; } NSArray *MCMEnumerateIdentifiersForClass( uint64_t containerClass, NSUInteger limit, NSString **error) { MCMAPI *api = MCMSharedAPI(); if (!MCMBridgeAvailable() || !api->queryIterate || !api->objectGetIdentifier || limit == 0) { if (error) *error = @"ContainerManager enumeration API unavailable"; return @[]; } void *query = api->queryCreate(); if (!query) { if (error) *error = @"container_query_create returned NULL"; return @[]; } api->querySetClass(query, containerClass); // Metadata-only, no-create lookup. Extensions are requested individually // only after an identifier passes validation. api->querySetFlags(query, 0x100000000ULL); // iOS 18 does not export the part API. A new query already defaults to // part 0, so only set it when the newer API is present. if (api->querySetPart) api->querySetPart(query, 0); NSMutableOrderedSet *identifiers = [NSMutableOrderedSet orderedSet]; BOOL iterated = api->queryIterate(query, ^bool(void *object) { const char *raw = object ? api->objectGetIdentifier(object) : NULL; NSString *identifier = raw ? [NSString stringWithUTF8String:raw] : nil; if (identifier.length) [identifiers addObject:identifier]; return identifiers.count < limit; }); if (!iterated && identifiers.count < limit) { void *queryError = api->queryGetLastError(query); int posix = queryError && api->errorGetPOSIX ? api->errorGetPOSIX(queryError) : 0; const char *message = queryError && api->errorGetMessage ? api->errorGetMessage(queryError) : NULL; if (error) *error = [NSString stringWithFormat:@"enumeration denied posix=%d message=%s", posix, message ?: "unknown"]; } api->queryFree(query); return identifiers.array; } BOOL MCMBridgeAvailable(void) { MCMAPI *api = MCMSharedAPI(); return api->queryCreate != NULL && api->querySetClass != NULL && api->querySetIdentifiers != NULL && api->querySetGroupIdentifiers != NULL && api->querySetFlags != NULL && api->queryGetSingle != NULL && api->queryGetLastError != NULL && api->queryFree != NULL && api->objectGetPath != NULL && api->objectCopy != NULL && api->objectCopyToken != NULL && api->objectActivate != NULL && api->objectFree != NULL; } @interface MCMLease () { void *_query; void *_activation; } @property(nonatomic, readwrite) uint64_t containerClass; @property(nonatomic, copy, readwrite) NSString *identifier; @property(nonatomic, copy, readwrite) NSString *rootPath; @property(nonatomic, readwrite) BOOL groupIdentifier; @property(nonatomic, readwrite) BOOL tokenPresent; @property(nonatomic, readwrite) BOOL activated; @end @implementation MCMLease + (instancetype)leaseForClass:(uint64_t)containerClass identifier:(NSString *)identifier group:(BOOL)group part:(uint64_t)part flags:(uint64_t)flags error:(NSString **)error { return [self leaseForClass:containerClass identifier:identifier group:group part:part partDomain:nil flags:flags error:error]; } + (instancetype)leaseForClass:(uint64_t)containerClass identifier:(NSString *)identifier group:(BOOL)group part:(uint64_t)part partDomain:(NSString *)partDomain flags:(uint64_t)flags error:(NSString **)error { if (!MCMBridgeAvailable() || identifier.length == 0) { if (error) *error = @"ContainerManager bridge unavailable or identifier empty"; return nil; } MCMAPI *api = MCMSharedAPI(); void *query = api->queryCreate(); if (query == NULL) { if (error) *error = @"container_query_create returned NULL"; return nil; } api->querySetClass(query, containerClass); xpc_object_t value = xpc_string_create(identifier.UTF8String); if (group) api->querySetGroupIdentifiers(query, value); else api->querySetIdentifiers(query, value); #if !OS_OBJECT_USE_OBJC xpc_release(value); #endif api->querySetFlags(query, flags); if (part != 0 && !api->querySetPart) { if (error) *error = @"part API unavailable on this OS"; api->queryFree(query); return nil; } if (api->querySetPart) api->querySetPart(query, part); if (partDomain.length != 0) { if (!api->querySetPartDomain) { if (error) *error = @"part-domain API unavailable"; api->queryFree(query); return nil; } api->querySetPartDomain(query, partDomain.fileSystemRepresentation); } void *object = api->queryGetSingle(query); if (object == NULL) { void *queryError = api->queryGetLastError(query); int posix = queryError && api->errorGetPOSIX ? api->errorGetPOSIX(queryError) : 0; const char *message = queryError && api->errorGetMessage ? api->errorGetMessage(queryError) : NULL; if (error) *error = [NSString stringWithFormat:@"lookup denied posix=%d message=%s", posix, message ?: "unknown"]; api->queryFree(query); return nil; } const char *rawPath = api->objectGetPath(object); NSString *root = rawPath ? [NSString stringWithUTF8String:rawPath] : nil; if (root.length == 0 || !root.isAbsolutePath) { if (error) *error = @"MCM returned no absolute container path"; api->queryFree(query); return nil; } if ([root isEqualToString:@"/var"] || [root hasPrefix:@"/var/"]) root = [@"/private" stringByAppendingString:root]; MCMLease *lease = [MCMLease new]; lease->_query = query; lease.containerClass = containerClass; lease.identifier = identifier; lease.groupIdentifier = group; lease.rootPath = root; return lease; } - (BOOL)activate:(NSString **)error { if (self.activated) return YES; if (!_query) { if (error) *error = @"lease invalidated"; return NO; } MCMAPI *api = MCMSharedAPI(); void *object = api->queryGetSingle(_query); _activation = object ? api->objectCopy(object) : NULL; char *token = _activation ? api->objectCopyToken(_activation) : NULL; self.tokenPresent = token && token[0] != '\0'; free(token); self.activated = self.tokenPresent && api->objectActivate(_activation, false); if (!self.activated && error) *error = self.tokenPresent ? @"sandbox extension activation failed" : @"MCM object contained no sandbox token"; return self.activated; } - (void)invalidate { MCMAPI *api = MCMSharedAPI(); if (_activation) { api->objectFree(_activation); _activation = NULL; } if (_query) { api->queryFree(_query); _query = NULL; } self.activated = NO; } - (void)dealloc { [self invalidate]; } @end