// // TerminalSBX.m // Terminal (a-Shell 2.3.2 full + worse_query) // // In-process commands and launch-time sandbox escape for a-Shell. // Primary primitive: worse_query (forcequitOS), GPL-3.0. // Fallback primitive: class-13 part-domain traversal (Veilpath / bad_query lineage). // // This file is loaded by the a-Shell binary at launch (weak LC_LOAD_DYLIB) and // through commandDictionary.plist. It is not a standalone application. // #import #import #import #import #import #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Output streams // --------------------------------------------------------------------------- static FILE *sbx_out(void) { typedef FILE *(*ios_stream_fn)(void); static ios_stream_fn out_fn = NULL; static int tried = 0; if (!tried) { out_fn = (ios_stream_fn)dlsym(RTLD_DEFAULT, "ios_stdout"); tried = 1; } if (out_fn) { FILE *f = out_fn(); if (f) return f; } FILE **tls = (FILE **)dlsym(RTLD_DEFAULT, "thread_stdout"); if (tls && *tls) return *tls; return stdout; } static FILE *sbx_err(void) { typedef FILE *(*ios_stream_fn)(void); static ios_stream_fn err_fn = NULL; static int tried = 0; if (!tried) { err_fn = (ios_stream_fn)dlsym(RTLD_DEFAULT, "ios_stderr"); tried = 1; } if (err_fn) { FILE *f = err_fn(); if (f) return f; } FILE **tls = (FILE **)dlsym(RTLD_DEFAULT, "thread_stderr"); if (tls && *tls) return *tls; return stderr; } static void sbx_printf(const char *fmt, ...) { FILE *f = sbx_out(); va_list ap; va_start(ap, fmt); vfprintf(f, fmt, ap); va_end(ap); fflush(f); } static void sbx_eprintf(const char *fmt, ...) { FILE *f = sbx_err(); va_list ap; va_start(ap, fmt); vfprintf(f, fmt, ap); va_end(ap); fflush(f); } // --------------------------------------------------------------------------- // Launch progress UI // --------------------------------------------------------------------------- @interface SBXProgressViewController : UIViewController @property (nonatomic, strong) UIProgressView *progressView; @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UILabel *statusLabel; @property (nonatomic, strong) UILabel *percentLabel; - (void)updateProgress:(double)value status:(NSString *)status; @end @implementation SBXProgressViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.82]; self.view.accessibilityViewIsModal = YES; UIView *card = [[UIView alloc] initWithFrame:CGRectZero]; card.translatesAutoresizingMaskIntoConstraints = NO; card.backgroundColor = [UIColor colorWithWhite:0.12 alpha:0.98]; card.layer.cornerRadius = 18.0; card.layer.masksToBounds = YES; [self.view addSubview:card]; self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; self.titleLabel.text = @"Terminal"; self.titleLabel.font = [UIFont systemFontOfSize:24 weight:UIFontWeightSemibold]; self.titleLabel.textColor = [UIColor whiteColor]; self.titleLabel.textAlignment = NSTextAlignmentCenter; self.statusLabel = [[UILabel alloc] initWithFrame:CGRectZero]; self.statusLabel.text = @"正在获取沙盒扩展…"; self.statusLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightRegular]; self.statusLabel.textColor = [UIColor colorWithWhite:0.88 alpha:1.0]; self.statusLabel.textAlignment = NSTextAlignmentCenter; self.statusLabel.numberOfLines = 0; self.progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault]; self.progressView.translatesAutoresizingMaskIntoConstraints = NO; self.progressView.progressTintColor = [UIColor systemGreenColor]; self.progressView.trackTintColor = [UIColor colorWithWhite:0.30 alpha:1.0]; self.progressView.progress = 0.0; self.progressView.transform = CGAffineTransformMakeScale(1.0, 2.0); self.progressView.layer.cornerRadius = 2.0; self.progressView.clipsToBounds = YES; self.percentLabel = [[UILabel alloc] initWithFrame:CGRectZero]; self.percentLabel.text = @"0%"; self.percentLabel.font = [UIFont monospacedDigitSystemFontOfSize:14 weight:UIFontWeightMedium]; self.percentLabel.textColor = [UIColor colorWithWhite:0.75 alpha:1.0]; self.percentLabel.textAlignment = NSTextAlignmentCenter; UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:@[ self.titleLabel, self.statusLabel, self.progressView, self.percentLabel ]]; stack.translatesAutoresizingMaskIntoConstraints = NO; stack.axis = UILayoutConstraintAxisVertical; stack.alignment = UIStackViewAlignmentFill; stack.spacing = 14.0; [card addSubview:stack]; NSLayoutConstraint *width = [card.widthAnchor constraintEqualToAnchor:self.view.widthAnchor multiplier:0.86]; width.priority = UILayoutPriorityDefaultHigh; [NSLayoutConstraint activateConstraints:@[ [card.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor], [card.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor], width, [card.widthAnchor constraintLessThanOrEqualToConstant:420.0], [stack.topAnchor constraintEqualToAnchor:card.topAnchor constant:28.0], [stack.leadingAnchor constraintEqualToAnchor:card.leadingAnchor constant:24.0], [stack.trailingAnchor constraintEqualToAnchor:card.trailingAnchor constant:-24.0], [stack.bottomAnchor constraintEqualToAnchor:card.bottomAnchor constant:-28.0], [self.progressView.heightAnchor constraintEqualToConstant:8.0], ]]; } - (void)updateProgress:(double)value status:(NSString *)status { if (value < 0.0) value = 0.0; if (value > 1.0) value = 1.0; [self.progressView setProgress:(float)value animated:YES]; self.percentLabel.text = [NSString stringWithFormat:@"%d%%", (int)(value * 100.0 + 0.5)]; if (status.length > 0) self.statusLabel.text = status; } @end static SBXProgressViewController *g_progressVC = nil; static BOOL g_progressPresenting = NO; static BOOL g_progressFinished = NO; static BOOL g_progressEnabled = NO; static double g_progressValue = 0.0; static NSString *g_progressStatus = @"正在获取沙盒扩展…"; static pthread_mutex_t g_progressLock = PTHREAD_MUTEX_INITIALIZER; static void sbx_open_sandbox_list(void); static UIWindow *sbx_key_window(void) { UIApplication *app = [UIApplication sharedApplication]; for (UIScene *scene in app.connectedScenes) { if (![scene isKindOfClass:[UIWindowScene class]]) continue; UIWindowScene *windowScene = (UIWindowScene *)scene; for (UIWindow *window in windowScene.windows) { if (window.isKeyWindow && !window.hidden) return window; } } for (UIScene *scene in app.connectedScenes) { if (![scene isKindOfClass:[UIWindowScene class]]) continue; UIWindowScene *windowScene = (UIWindowScene *)scene; for (UIWindow *window in windowScene.windows) { if (!window.hidden) return window; } } return nil; } static void sbx_present_progress_now(void) { if (g_progressVC != nil || g_progressPresenting || g_progressFinished) return; UIWindow *window = sbx_key_window(); if (window == nil || window.rootViewController == nil) return; UIViewController *root = window.rootViewController; while (root.presentedViewController != nil) root = root.presentedViewController; pthread_mutex_lock(&g_progressLock); double value = g_progressValue; NSString *status = g_progressStatus; pthread_mutex_unlock(&g_progressLock); [window endEditing:YES]; g_progressPresenting = YES; SBXProgressViewController *vc = [[SBXProgressViewController alloc] initWithNibName:nil bundle:nil]; vc.modalPresentationStyle = UIModalPresentationOverFullScreen; vc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; if (@available(iOS 13.0, *)) vc.modalInPresentation = YES; [vc updateProgress:value status:status]; [root presentViewController:vc animated:NO completion:^{ g_progressPresenting = NO; if (g_progressFinished) { [vc dismissViewControllerAnimated:NO completion:nil]; return; } g_progressVC = vc; pthread_mutex_lock(&g_progressLock); double current = g_progressValue; NSString *currentStatus = g_progressStatus; pthread_mutex_unlock(&g_progressLock); [vc updateProgress:current status:currentStatus]; }]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (g_progressVC == nil) g_progressPresenting = NO; }); } static void sbx_present_progress_retry(int attempts) { if (g_progressVC != nil || g_progressFinished || attempts <= 0) return; sbx_present_progress_now(); if (g_progressVC == nil) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ sbx_present_progress_retry(attempts - 1); }); } } static void sbx_show_progress(void) { pthread_mutex_lock(&g_progressLock); g_progressEnabled = YES; g_progressFinished = NO; g_progressValue = 0.01; g_progressStatus = @"正在初始化沙盒环境…"; pthread_mutex_unlock(&g_progressLock); dispatch_async(dispatch_get_main_queue(), ^{ sbx_present_progress_retry(80); }); } static void sbx_progress_set(double value, NSString *status) { pthread_mutex_lock(&g_progressLock); if (!g_progressEnabled) { pthread_mutex_unlock(&g_progressLock); return; } g_progressValue = value; if (status.length > 0) g_progressStatus = [status copy]; pthread_mutex_unlock(&g_progressLock); dispatch_async(dispatch_get_main_queue(), ^{ if (g_progressVC != nil) [g_progressVC updateProgress:value status:status]; }); } static void sbx_progress_finish(void) { pthread_mutex_lock(&g_progressLock); if (!g_progressEnabled) { pthread_mutex_unlock(&g_progressLock); return; } g_progressFinished = YES; g_progressValue = 1.0; g_progressStatus = @"完成,正在进入 _sandbox…"; pthread_mutex_unlock(&g_progressLock); dispatch_async(dispatch_get_main_queue(), ^{ SBXProgressViewController *vc = g_progressVC; if (vc == nil) { sbx_open_sandbox_list(); return; } [vc updateProgress:1.0 status:@"完成,正在进入 _sandbox…"]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.45 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (g_progressVC == nil) { sbx_open_sandbox_list(); return; } SBXProgressViewController *current = g_progressVC; g_progressVC = nil; [current dismissViewControllerAnimated:YES completion:^{ sbx_open_sandbox_list(); }]; }); }); } static void sbx_open_command_url(NSString *command) { if (command.length == 0) return; NSCharacterSet *allowed = [NSCharacterSet characterSetWithCharactersInString: @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"]; NSString *encoded = [command stringByAddingPercentEncodingWithAllowedCharacters:allowed]; NSURL *url = [NSURL URLWithString:[@"ashell:" stringByAppendingString:encoded ?: @""]]; if (url == nil) return; NSLog(@"[TerminalSBX] opening command URL: %@", url.absoluteString); [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) { if (!success) NSLog(@"[TerminalSBX] could not open command URL: %@", command); }]; } static void sbx_open_sandbox_list(void) { // Run both commands through "source" in one URL. This keeps the cwd // change in the current shell session, and avoids relying on semicolon // or "&&" parsing in a-Shell's URL command path. NSString *docs = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"]; NSString *scriptPath = [docs stringByAppendingPathComponent:@".sbx_start"]; NSString *script = @"clear\ncd ~/Documents/_sandbox\nls\n"; [script writeToFile:scriptPath atomically:YES encoding:NSUTF8StringEncoding error:NULL]; sbx_open_command_url(@"source ~/Documents/.sbx_start"); } // --------------------------------------------------------------------------- // Global state // --------------------------------------------------------------------------- static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; // handles / group / state static pthread_mutex_t g_query_lock = PTHREAD_MUTEX_INITIALIZER; // one containermanager query at a time static pthread_mutex_t g_mount_lock = PTHREAD_MUTEX_INITIALIZER; // serialise automount static NSMutableArray *g_handles = nil; static NSMutableSet *g_accessible_roots = nil; static NSString *g_group = nil; static BOOL g_mount_done = NO; static BOOL g_mount_running = NO; static int64_t g_mount_mounted = 0; static int64_t g_mount_failed = 0; static void sbx_mark_root(NSString *root) { pthread_mutex_lock(&g_lock); if (g_accessible_roots == nil) g_accessible_roots = [NSMutableSet set]; [g_accessible_roots addObject:root]; pthread_mutex_unlock(&g_lock); } static BOOL sbx_root_accessible(NSString *root) { pthread_mutex_lock(&g_lock); BOOL value = [g_accessible_roots containsObject:root]; pthread_mutex_unlock(&g_lock); return value; } // --------------------------------------------------------------------------- // Path helpers // --------------------------------------------------------------------------- static NSString *sbx_canonical(NSString *raw) { if (raw == nil) return @""; NSString *path = [raw stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (path.length == 0) return @""; if ([path hasPrefix:@"file://"]) { NSURL *url = [NSURL URLWithString:path]; if (url.path.length > 0) path = url.path; } if (![path hasPrefix:@"/"]) { NSString *cwd = [[NSFileManager defaultManager] currentDirectoryPath]; if (cwd.length == 0) cwd = NSHomeDirectory(); path = [cwd stringByAppendingPathComponent:path]; } path = [path stringByStandardizingPath]; if ([path isEqualToString:@"/var"] || [path hasPrefix:@"/var/"]) { path = [@"/private" stringByAppendingString:path]; } while ([path hasPrefix:@"/private/private/"]) { path = [@"/private" stringByAppendingString:[path substringFromIndex:@"/private".length]]; } return path; } static NSString *sbx_display(NSString *raw) { NSString *path = sbx_canonical(raw); if ([path isEqualToString:@"/private/var"]) return @"/var"; if ([path hasPrefix:@"/private/var/"]) { return [@"/var" stringByAppendingString:[path substringFromIndex:@"/private/var".length]]; } return path; } static BOOL sbx_is_own_path(NSString *path) { NSString *home = sbx_canonical(NSHomeDirectory()); if (path.length == 0 || home.length == 0) return NO; if ([path isEqualToString:home]) return YES; return [path hasPrefix:[home stringByAppendingString:@"/"]]; } static NSString *sbx_mount_root(void) { return [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/_sandbox"]; } static BOOL sbx_path_exists(NSString *path) { struct stat st; return lstat(path.fileSystemRepresentation, &st) == 0; } static BOOL sbx_path_is_dir(NSString *path) { struct stat st; if (lstat(path.fileSystemRepresentation, &st) != 0) return NO; return S_ISDIR(st.st_mode) != 0; } // --------------------------------------------------------------------------- // App Group resolution // --------------------------------------------------------------------------- static NSArray *sbx_groups_from_profile(void) { NSString *profile = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"embedded.mobileprovision"]; NSData *data = [NSData dataWithContentsOfFile:profile]; if (data.length == 0) return @[]; NSData *startTag = [@"" dataUsingEncoding:NSUTF8StringEncoding]; NSRange start = [data rangeOfData:startTag options:0 range:NSMakeRange(0, data.length)]; if (start.location == NSNotFound) return @[]; NSRange end = [data rangeOfData:endTag options:0 range:NSMakeRange(start.location, data.length - start.location)]; if (end.location == NSNotFound) return @[]; NSRange full = NSMakeRange(start.location, NSMaxRange(end) - start.location); NSData *plistData = [data subdataWithRange:full]; id plist = [NSPropertyListSerialization propertyListWithData:plistData options:NSPropertyListImmutable format:NULL error:NULL]; if (![plist isKindOfClass:[NSDictionary class]]) return @[]; id ent = ((NSDictionary *)plist)[@"Entitlements"]; if (![ent isKindOfClass:[NSDictionary class]]) return @[]; id groups = ((NSDictionary *)ent)[@"com.apple.security.application-groups"]; if (![groups isKindOfClass:[NSArray class]]) return @[]; NSMutableArray *out = [NSMutableArray array]; for (id g in (NSArray *)groups) { if ([g isKindOfClass:[NSString class]]) [out addObject:g]; } return out; } static NSArray *sbx_groups_from_entitlements(void) { void *sec = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY); if (!sec) return @[]; typedef void *(*task_create_fn)(CFAllocatorRef); typedef CFTypeRef (*task_copy_fn)(void *, CFStringRef, CFErrorRef *); task_create_fn create = (task_create_fn)dlsym(sec, "SecTaskCreateFromSelf"); task_copy_fn copy = (task_copy_fn)dlsym(sec, "SecTaskCopyValueForEntitlement"); if (!create || !copy) return @[]; void *task = create(NULL); if (!task) return @[]; CFTypeRef value = copy(task, CFSTR("com.apple.security.application-groups"), NULL); NSMutableArray *out = [NSMutableArray array]; if (value) { if (CFGetTypeID(value) == CFArrayGetTypeID()) { NSArray *arr = (__bridge NSArray *)value; for (id g in arr) { if ([g isKindOfClass:[NSString class]]) [out addObject:g]; } } CFRelease(value); } CFRelease(task); return out; } static void sbx_resolve_group(void) { pthread_mutex_lock(&g_lock); if (g_group != nil) { pthread_mutex_unlock(&g_lock); return; } pthread_mutex_unlock(&g_lock); NSMutableArray *candidates = [NSMutableArray array]; void (^add)(id) = ^(id value) { if ([value isKindOfClass:[NSString class]] && [(NSString *)value length] > 0 && ![candidates containsObject:value]) { [candidates addObject:value]; } }; add([[NSBundle mainBundle] objectForInfoDictionaryKey:@"AppGroupIdentifier"]); for (NSString *g in sbx_groups_from_entitlements()) add(g); for (NSString *g in sbx_groups_from_profile()) add(g); // Groups provisioned for the signing identity used by this workspace. add(@"group.com.cert.account.t260930000125.01"); add(@"group.com.cert.account.t260930000125.02"); add(@"group.com.cert.account.t260930000125.03"); add(@"group.com.cert.account.t260930000125.04"); add(@"group.com.cert.account.t260930000125.05"); add(@"group.AsheKube.a-Shell"); NSFileManager *fm = [NSFileManager defaultManager]; // The group container is sometimes not visible during the first few // hundred milliseconds of app launch. Retry briefly so commands issued // immediately by a URL or .profile do not race the sandbox setup. for (int attempt = 0; attempt < 15; attempt++) { for (NSString *group in candidates) { NSURL *url = [fm containerURLForSecurityApplicationGroupIdentifier:group]; if (!url) continue; NSURL *library = [url URLByAppendingPathComponent:@"Library" isDirectory:YES]; [fm createDirectoryAtURL:library withIntermediateDirectories:YES attributes:nil error:NULL]; pthread_mutex_lock(&g_lock); g_group = [group copy]; pthread_mutex_unlock(&g_lock); NSLog(@"[TerminalSBX] app group: %@ at %@", group, url.path); return; } usleep(100000); } NSLog(@"[TerminalSBX] no accessible app group; falling back to class-13 primitive"); } static NSString *sbx_group(void) { pthread_mutex_lock(&g_lock); NSString *group = g_group; pthread_mutex_unlock(&g_lock); if (group == nil) { sbx_resolve_group(); pthread_mutex_lock(&g_lock); group = g_group; pthread_mutex_unlock(&g_lock); } return group; } // --------------------------------------------------------------------------- // worse_query (forcequitOS, GPL-3.0) // --------------------------------------------------------------------------- typedef void *(*container_query_create_fn)(void); typedef void (*container_query_set_class_fn)(void *, uint64_t); typedef void (*container_query_set_identifiers_fn)(void *, xpc_object_t); typedef void (*container_query_set_flags_fn)(void *, uint64_t); typedef void (*container_query_set_part_fn)(void *, uint64_t); typedef void (*container_query_set_part_domain_fn)(void *, const char *); typedef void *(*container_query_get_single_result_fn)(void *); typedef void (*container_query_iterate_results_sync_fn)(void *, void (^)(void *)); typedef void (*container_query_free_fn)(void *); typedef char *(*container_copy_sandbox_token_fn)(void *); typedef const char *(*container_object_get_path_fn)(void *); typedef const char *(*container_object_get_identifier_fn)(void *); typedef int64_t (*sandbox_extension_consume_fn)(const char *); typedef int (*sandbox_extension_release_fn)(int64_t); static void sbx_clean_group_caches(NSString *group) { if (group.length == 0) return; NSFileManager *fm = [NSFileManager defaultManager]; NSURL *root = [fm containerURLForSecurityApplicationGroupIdentifier:group]; if (!root) return; NSURL *caches = [root URLByAppendingPathComponent:@"Library/Caches" isDirectory:YES]; [fm removeItemAtURL:caches error:NULL]; [fm createDirectoryAtURL:caches withIntermediateDirectories:NO attributes:nil error:NULL]; } static int64_t sbx_worse_query(NSString *target, bool alt_flags, bool create_dir) { if (target.length == 0 || ![target hasPrefix:@"/"]) return -255; if (!create_dir) { struct stat st; if (lstat(target.fileSystemRepresentation, &st) != 0) return -255; } NSString *group = sbx_group(); if (group.length == 0) return -1; NSFileManager *fm = [NSFileManager defaultManager]; NSURL *groupRoot = [fm containerURLForSecurityApplicationGroupIdentifier:group]; if (!groupRoot) return -1; NSString *caches = [[groupRoot URLByAppendingPathComponent:@"Library/Caches"] path]; void *mgr = dlopen("/usr/lib/system/libsystem_containermanager.dylib", RTLD_NOW | RTLD_LOCAL); if (!mgr) return -2; container_query_create_fn query_create = (container_query_create_fn)dlsym(mgr, "container_query_create"); container_query_set_class_fn query_set_class = (container_query_set_class_fn)dlsym(mgr, "container_query_set_class"); container_query_set_identifiers_fn query_set_group_identifiers = (container_query_set_identifiers_fn)dlsym(mgr, "container_query_set_group_identifiers"); container_query_set_flags_fn query_set_flags = (container_query_set_flags_fn)dlsym(mgr, "container_query_operation_set_flags"); container_query_set_part_fn query_set_part = (container_query_set_part_fn)dlsym(mgr, "container_query_operation_set_part"); container_query_get_single_result_fn query_get_single_result = (container_query_get_single_result_fn)dlsym(mgr, "container_query_get_single_result"); container_query_free_fn query_free = (container_query_free_fn)dlsym(mgr, "container_query_free"); container_copy_sandbox_token_fn copy_sandbox_token = (container_copy_sandbox_token_fn)dlsym(mgr, "container_copy_sandbox_token"); sandbox_extension_consume_fn consume_extension = (sandbox_extension_consume_fn)dlsym(RTLD_DEFAULT, "sandbox_extension_consume"); if (!query_create || !query_set_class || !query_set_group_identifiers || !query_set_flags || !query_set_part || !query_get_single_result || !query_free || !copy_sandbox_token || !consume_extension) { dlclose(mgr); return -2; } void *query = query_create(); if (!query) { dlclose(mgr); return -3; } xpc_object_t identifier = xpc_string_create(group.UTF8String); query_set_class(query, 7); // app group query_set_group_identifiers(query, identifier); query_set_part(query, 3); // Library/Caches if (alt_flags) { query_set_flags(query, 0x0000000800000000ULL); } else { query_set_flags(query, 0x0000008000000000ULL); } [fm removeItemAtPath:caches error:NULL]; if (![fm createSymbolicLinkAtPath:caches withDestinationPath:target error:NULL]) { query_free(query); dlclose(mgr); return -4; } void *result = query_get_single_result(query); if (!result) { query_free(query); dlclose(mgr); sbx_clean_group_caches(group); return -5; } char *token = copy_sandbox_token(result); if (!token) { query_free(query); dlclose(mgr); sbx_clean_group_caches(group); return -6; } int64_t handle = consume_extension(token); free(token); query_free(query); dlclose(mgr); sbx_clean_group_caches(group); return handle; } static void sbx_release_handle(int64_t handle) { if (handle <= 0) return; sandbox_extension_release_fn release_extension = (sandbox_extension_release_fn)dlsym(RTLD_DEFAULT, "sandbox_extension_release"); if (release_extension) release_extension(handle); } // --------------------------------------------------------------------------- // Class-13 fallback (Veilpath / bad_query lineage) // --------------------------------------------------------------------------- static int64_t sbx_fallback_query(NSString *target, bool alt_flags) { if (target.length == 0 || ![target hasPrefix:@"/"]) return -255; struct stat st; if (lstat(target.fileSystemRepresentation, &st) != 0) return -255; void *mgr = dlopen("/usr/lib/system/libsystem_containermanager.dylib", RTLD_NOW | RTLD_LOCAL); if (!mgr) return -1; container_query_create_fn query_create = (container_query_create_fn)dlsym(mgr, "container_query_create"); container_query_set_class_fn query_set_class = (container_query_set_class_fn)dlsym(mgr, "container_query_set_class"); container_query_set_identifiers_fn query_set_group_identifiers = (container_query_set_identifiers_fn)dlsym(mgr, "container_query_set_group_identifiers"); container_query_set_flags_fn query_set_flags = (container_query_set_flags_fn)dlsym(mgr, "container_query_operation_set_flags"); container_query_set_part_fn query_set_part = (container_query_set_part_fn)dlsym(mgr, "container_query_operation_set_part"); container_query_set_part_domain_fn query_set_part_domain = (container_query_set_part_domain_fn)dlsym(mgr, "container_query_operation_set_part_domain"); container_query_get_single_result_fn query_get_single_result = (container_query_get_single_result_fn)dlsym(mgr, "container_query_get_single_result"); container_query_free_fn query_free = (container_query_free_fn)dlsym(mgr, "container_query_free"); container_copy_sandbox_token_fn copy_sandbox_token = (container_copy_sandbox_token_fn)dlsym(mgr, "container_copy_sandbox_token"); sandbox_extension_consume_fn consume_extension = (sandbox_extension_consume_fn)dlsym(RTLD_DEFAULT, "sandbox_extension_consume"); if (!query_create || !query_set_class || !query_set_group_identifiers || !query_set_flags || !query_set_part || !query_set_part_domain || !query_get_single_result || !query_free || !copy_sandbox_token || !consume_extension) { dlclose(mgr); return -1; } void *query = query_create(); if (!query) { dlclose(mgr); return -3; } xpc_object_t identifier = xpc_string_create("systemgroup.com.apple.mobilegestaltcache"); query_set_class(query, 13); query_set_group_identifiers(query, identifier); query_set_part(query, 3); char *part = NULL; if (asprintf(&part, "../../../../../../../..%s", target.fileSystemRepresentation) == -1) { query_free(query); dlclose(mgr); return -5; } query_set_part_domain(query, part); if (alt_flags) { query_set_flags(query, 0x0000000800000000ULL); } else { query_set_flags(query, 0x0000008000000000ULL); } void *result = query_get_single_result(query); if (!result) { free(part); query_free(query); dlclose(mgr); return -5; } char *token = copy_sandbox_token(result); if (!token) { free(part); query_free(query); dlclose(mgr); return -6; } int64_t handle = consume_extension(token); free(token); free(part); query_free(query); dlclose(mgr); return handle; } // --------------------------------------------------------------------------- // Container enumeration (needed for the Application / daemon / plugin roots, // whose parent directory cannot itself be granted on iOS 27) // --------------------------------------------------------------------------- static NSArray *sbx_enumerate_class(uint64_t container_class) { void *mgr = dlopen("/usr/lib/system/libsystem_containermanager.dylib", RTLD_NOW | RTLD_LOCAL); if (!mgr) return nil; container_query_create_fn query_create = (container_query_create_fn)dlsym(mgr, "container_query_create"); container_query_set_class_fn query_set_class = (container_query_set_class_fn)dlsym(mgr, "container_query_set_class"); container_query_iterate_results_sync_fn iterate = (container_query_iterate_results_sync_fn)dlsym(mgr, "container_query_iterate_results_sync"); container_query_free_fn query_free = (container_query_free_fn)dlsym(mgr, "container_query_free"); container_object_get_path_fn get_path = (container_object_get_path_fn)dlsym(mgr, "container_object_get_path"); container_object_get_identifier_fn get_identifier = (container_object_get_identifier_fn)dlsym(mgr, "container_object_get_identifier"); if (!get_identifier) get_identifier = (container_object_get_identifier_fn)dlsym(mgr, "container_result_get_identifier"); if (!query_create || !query_set_class || !iterate || !query_free || !get_path) { dlclose(mgr); return nil; } void *query = query_create(); if (!query) { dlclose(mgr); return nil; } query_set_class(query, container_class); NSMutableArray *out = [NSMutableArray array]; __block container_object_get_path_fn blk_get_path = get_path; __block container_object_get_identifier_fn blk_get_id = get_identifier; iterate(query, ^(void *result) { if (!result || !blk_get_path) return; const char *path = blk_get_path(result); const char *identifier = blk_get_id ? blk_get_id(result) : NULL; if (!path || path[0] == '\0') return; NSString *p = [NSString stringWithUTF8String:path]; if (p.length == 0) return; p = sbx_canonical(p); NSString *i = (identifier && identifier[0]) ? [NSString stringWithUTF8String:identifier] : nil; [out addObject:@{@"path": p, @"id": i ?: @""}]; }); query_free(query); dlclose(mgr); return out; } // --------------------------------------------------------------------------- // Inode path enumeration (pathleak) // // fsgetpath() resolves a file-system object id to its absolute path without // requiring directory-search permission. Scanning the inode range and // keeping immediate children of a root enumerates containers when a // containermanager query for the parent is refused. Derived from pathleak // (forcequitOS, GPL-3.0). // --------------------------------------------------------------------------- static double sbx_now(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0; } static NSArray *sbx_pathleak(NSString *root, uint64_t max_inode, double seconds) { if (root.length == 0 || max_inode < 1) return nil; NSString *dir = sbx_canonical(root); while (dir.length > 1 && [dir hasSuffix:@"/"]) dir = [dir substringToIndex:dir.length - 1]; struct statfs sfs; if (statfs(dir.fileSystemRepresentation, &sfs) != 0) { NSString *tmp = NSTemporaryDirectory(); if (tmp.length == 0 || statfs(tmp.fileSystemRepresentation, &sfs) != 0) return nil; } fsid_t fsid = sfs.f_fsid; const char *prefix = dir.fileSystemRepresentation; size_t prefix_len = strlen(prefix); NSMutableArray *result = [NSMutableArray array]; char buffer[PATH_MAX]; double start = sbx_now(); for (uint64_t inode = 1; inode <= max_inode; inode++) { if (seconds > 0 && (inode & 0xFFF) == 0 && (sbx_now() - start) > seconds) break; ssize_t n = fsgetpath(buffer, sizeof(buffer), &fsid, inode); if (n <= 0) continue; if (strncmp(buffer, prefix, prefix_len) != 0) continue; if (buffer[prefix_len] != '/') continue; if (strchr(buffer + prefix_len + 1, '/') != NULL) continue; NSString *path = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:buffer length:strlen(buffer)]; if (path.length > 0) [result addObject:path]; } [result sortUsingSelector:@selector(localizedStandardCompare:)]; return result; } // --------------------------------------------------------------------------- // Handle cache // --------------------------------------------------------------------------- static void sbx_store_handle(NSString *path, int64_t handle, NSString *engine) { if (handle <= 0) return; pthread_mutex_lock(&g_lock); if (g_handles == nil) g_handles = [NSMutableArray array]; [g_handles addObject:@{@"path": path, @"handle": @(handle), @"engine": engine ?: @"worse_query"}]; pthread_mutex_unlock(&g_lock); } static int64_t sbx_cached_handle_locked(NSString *path) { if (g_handles == nil) return 0; NSString *prefix = [path stringByAppendingString:@"/"]; for (NSDictionary *entry in g_handles) { NSString *p = entry[@"path"]; if ([path isEqualToString:p] || [path hasPrefix:prefix] || [path hasPrefix:[p stringByAppendingString:@"/"]]) { return [entry[@"handle"] longLongValue]; } } return 0; } static BOOL sbx_has_handle_for(NSString *path) { pthread_mutex_lock(&g_lock); int64_t h = sbx_cached_handle_locked(path); pthread_mutex_unlock(&g_lock); return h > 0; } static BOOL sbx_uses_alt_flags(NSString *path) { NSInteger major = [[NSProcessInfo processInfo] operatingSystemVersion].majorVersion; return major == 26 && [path containsString:@"/var/mobile/Containers/Shared/AppGroup"]; } // --------------------------------------------------------------------------- // Grant // --------------------------------------------------------------------------- static int64_t sbx_grant_path(NSString *raw, BOOL create, NSString **engineOut) { NSString *path = sbx_canonical(raw); if (path.length == 0 || ![path hasPrefix:@"/"]) return -255; if (sbx_is_own_path(path)) { if (engineOut) *engineOut = @"own-sandbox"; return 0; } pthread_mutex_lock(&g_lock); int64_t cached = sbx_cached_handle_locked(path); pthread_mutex_unlock(&g_lock); if (cached > 0) { if (engineOut) *engineOut = @"cached"; return cached; } NSString *group = sbx_group(); bool alt = sbx_uses_alt_flags(path); int64_t handle = 0; NSString *engine = nil; pthread_mutex_lock(&g_query_lock); if (group.length > 0) { handle = sbx_worse_query(path, alt, create); if (handle > 0) engine = @"worse_query"; } if (handle <= 0) { int64_t fallback = sbx_fallback_query(path, alt); if (fallback > 0) { handle = fallback; engine = @"fallback"; } } pthread_mutex_unlock(&g_query_lock); if (handle > 0) { sbx_store_handle(path, handle, engine); if (engineOut) *engineOut = engine; } else if (engineOut) { *engineOut = nil; } return handle; } static int64_t sbx_grant_mount(NSString *path) { int64_t handle = sbx_grant_path(path, NO, NULL); if (handle <= 0) handle = sbx_grant_path(path, YES, NULL); return handle; } static NSString *sbx_symlink_target(NSString *path) { char buf[PATH_MAX]; ssize_t n = readlink(path.fileSystemRepresentation, buf, sizeof(buf) - 1); if (n <= 0) return nil; buf[n] = '\0'; NSString *target = [NSString stringWithUTF8String:buf]; if (target.length == 0) return nil; if (![target hasPrefix:@"/"]) { target = [[path stringByDeletingLastPathComponent] stringByAppendingPathComponent:target]; } return sbx_canonical(target); } static int64_t sbx_grant_item(NSString *raw) { NSString *path = sbx_canonical(raw); if (sbx_is_own_path(path)) return 0; struct stat st; if (lstat(path.fileSystemRepresentation, &st) == 0) { if (S_ISLNK(st.st_mode)) { NSString *target = sbx_symlink_target(path); if (target.length > 0) { int64_t h = sbx_grant_path(target, NO, NULL); if (h > 0) return h; } } if (S_ISDIR(st.st_mode)) return sbx_grant_path(path, NO, NULL); NSString *parent = [path stringByDeletingLastPathComponent]; int64_t h = sbx_grant_path(parent, NO, NULL); return h != 0 ? h : sbx_grant_path(path, NO, NULL); } int64_t h = sbx_grant_path(path, NO, NULL); if (h > 0) return h; return sbx_grant_path([path stringByDeletingLastPathComponent], NO, NULL); } static void sbx_release_all_internal(void) { pthread_mutex_lock(&g_lock); NSArray *handles = [g_handles copy]; [g_handles removeAllObjects]; pthread_mutex_unlock(&g_lock); for (NSDictionary *entry in handles) { sbx_release_handle([entry[@"handle"] longLongValue]); } sbx_printf("released %lu sandbox extension(s)\n", (unsigned long)handles.count); } // --------------------------------------------------------------------------- // Mount points // --------------------------------------------------------------------------- static NSArray *sbx_mount_points(void) { return @[ @{@"name": @"system", @"root": @"/private/var/containers/Data/System", @"children": @NO, @"class": @0}, @{@"name": @"systemgroups", @"root": @"/private/var/containers/Shared/SystemGroup", @"children": @YES, @"class": @11}, @{@"name": @"apps", @"root": @"/private/var/mobile/Containers/Data/Application", @"children": @YES, @"class": @2}, @{@"name": @"daemons", @"root": @"/private/var/mobile/Containers/Data/InternalDaemon", @"children": @YES, @"class": @10}, @{@"name": @"plugins", @"root": @"/private/var/mobile/Containers/Data/PluginKitPlugin", @"children": @YES, @"class": @8}, @{@"name": @"appgroups", @"root": @"/private/var/mobile/Containers/Shared/AppGroup", @"children": @YES, @"class": @5}, @{@"name": @"appgroups27", @"root": @"/private/var/containers/Shared/AppGroup", @"children": @YES, @"class": @5} ]; } static NSArray *sbx_list_dir(NSString *path) { const char *p = path.fileSystemRepresentation; DIR *dir = opendir(p); if (!dir) return nil; NSMutableArray *entries = [NSMutableArray array]; struct dirent *ent = NULL; while ((ent = readdir(dir)) != NULL) { if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) continue; NSString *name = [NSString stringWithUTF8String:ent->d_name]; if (name.length > 0) [entries addObject:name]; } closedir(dir); [entries sortUsingSelector:@selector(localizedStandardCompare:)]; return entries; } static NSString *sbx_bundle_id(NSString *containerPath) { char buf[512]; memset(buf, 0, sizeof(buf)); ssize_t len = getxattr(containerPath.fileSystemRepresentation, "com.apple.containermanager.identifier", buf, sizeof(buf) - 1, 0, 0); if (len > 0) { return [NSString stringWithUTF8String:buf]; } NSString *meta = [containerPath stringByAppendingPathComponent:@".com.apple.mobile_container_manager.metadata.plist"]; NSData *data = [NSData dataWithContentsOfFile:meta]; if (data.length == 0) return nil; id plist = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:NULL]; if (![plist isKindOfClass:[NSDictionary class]]) return nil; id ident = ((NSDictionary *)plist)[@"MCMMetadataIdentifier"]; if ([ident isKindOfClass:[NSString class]] && [(NSString *)ident length] > 0) return ident; return nil; } static NSString *sbx_safe_name(NSString *name) { NSMutableString *out = [NSMutableString stringWithString:name ?: @"unknown"]; [out replaceOccurrencesOfString:@"/" withString:@"_" options:0 range:NSMakeRange(0, out.length)]; [out replaceOccurrencesOfString:@":" withString:@"_" options:0 range:NSMakeRange(0, out.length)]; return out; } static BOOL sbx_symlink(NSString *target, NSString *link) { NSFileManager *fm = [NSFileManager defaultManager]; [fm removeItemAtPath:link error:NULL]; return [fm createSymbolicLinkAtPath:link withDestinationPath:target error:NULL]; } static void sbx_write_manifest(int64_t mounted, int64_t failed) { NSMutableString *text = [NSMutableString string]; [text appendString:@"Terminal sandbox escape manifest\n"]; [text appendFormat:@"OS: %@\n", [[NSProcessInfo processInfo] operatingSystemVersionString]]; pthread_mutex_lock(&g_lock); NSString *group = g_group; NSArray *handles = [g_handles copy]; pthread_mutex_unlock(&g_lock); [text appendFormat:@"App group: %@\n", group ?: @"(none)"]; [text appendFormat:@"Mounted: %lld, failed: %lld\n", mounted, failed]; [text appendFormat:@"Active handles: %lu\n\n", (unsigned long)handles.count]; for (NSDictionary *entry in handles) { [text appendFormat:@" handle %@: %@ (%@)\n", entry[@"handle"], entry[@"path"], entry[@"engine"]]; } NSString *manifest = [sbx_mount_root() stringByAppendingPathComponent:@".mount_info"]; [text writeToFile:manifest atomically:YES encoding:NSUTF8StringEncoding error:NULL]; } static void sbx_automount(BOOL showUI) { @autoreleasepool { pthread_mutex_lock(&g_mount_lock); pthread_mutex_lock(&g_lock); if (g_mount_running) { pthread_mutex_unlock(&g_lock); pthread_mutex_unlock(&g_mount_lock); return; } g_mount_running = YES; g_mount_done = NO; pthread_mutex_unlock(&g_lock); NSFileManager *fm = [NSFileManager defaultManager]; NSString *mountRoot = sbx_mount_root(); [fm createDirectoryAtPath:mountRoot withIntermediateDirectories:YES attributes:nil error:NULL]; // Clear previous mounts, keeping the manifest until it is replaced. NSArray *old = [fm contentsOfDirectoryAtPath:mountRoot error:NULL]; for (NSString *item in old) { if ([item isEqualToString:@".mount_info"]) continue; [fm removeItemAtPath:[mountRoot stringByAppendingPathComponent:item] error:NULL]; } if (sbx_group() == nil) { NSLog(@"[TerminalSBX] warning: no app group available; using class-13 fallback only"); } int64_t mounted = 0; int64_t failed = 0; sbx_progress_set(0.06, @"正在准备挂载点…"); NSArray *mountPoints = sbx_mount_points(); NSUInteger mountPointCount = mountPoints.count; NSUInteger mountPointIndex = 0; for (NSDictionary *mp in mountPoints) { NSString *name = mp[@"name"]; double baseProgress = 0.06 + 0.84 * ((double)mountPointIndex / (double)MAX(mountPointCount, (NSUInteger)1)); double spanProgress = 0.84 / (double)MAX(mountPointCount, (NSUInteger)1); mountPointIndex++; sbx_progress_set(baseProgress, [NSString stringWithFormat:@"正在处理 %@…", name]); NSString *root = mp[@"root"]; BOOL children = [mp[@"children"] boolValue]; BOOL exists = sbx_path_exists(root); int64_t handle = sbx_grant_mount(root); NSString *dest = [mountRoot stringByAppendingPathComponent:name]; if (!children) { if (handle <= 0) { failed++; NSLog(@"[TerminalSBX] cannot grant %@ (handle=%lld, exists=%d)", root, handle, exists); continue; } if (sbx_symlink(root, dest)) { mounted++; sbx_mark_root(root); NSLog(@"[TerminalSBX] mounted %@ -> %@", name, root); } else { failed++; } sbx_progress_set(baseProgress + spanProgress, [NSString stringWithFormat:@"完成 %@", name]); continue; } NSMutableArray *childItems = [NSMutableArray array]; NSArray *entries = handle > 0 ? sbx_list_dir(root) : nil; if (entries != nil) { for (NSString *entry in entries) { NSString *child = [root stringByAppendingPathComponent:entry]; [childItems addObject:@{@"path": child, @"id": @""}]; } } else { // iOS 27 refuses a token for the parent of Application, // InternalDaemon, PluginKitPlugin and SystemGroup. Enumerate // the individual containers with fsgetpath instead. double seconds = [name isEqualToString:@"apps"] ? 12.0 : 6.0; NSArray *leaked = sbx_pathleak(root, 2000000, seconds); for (NSString *child in leaked ?: @[]) { [childItems addObject:@{@"path": child, @"id": @""}]; } if (leaked.count > 0) { NSLog(@"[TerminalSBX] fsgetpath found %lu container(s) under %@", (unsigned long)leaked.count, root); } } if (childItems.count == 0) { failed++; NSLog(@"[TerminalSBX] cannot enumerate %@", root); sbx_progress_set(baseProgress + spanProgress, [NSString stringWithFormat:@"跳过 %@", name]); continue; } sbx_mark_root(root); sbx_progress_set(baseProgress + spanProgress * 0.25, [NSString stringWithFormat:@"发现 %lu 个 %@ 容器", (unsigned long)childItems.count, name]); [fm createDirectoryAtPath:dest withIntermediateDirectories:YES attributes:nil error:NULL]; int64_t childMounted = 0; NSUInteger childIndex = 0; NSUInteger childCount = childItems.count; for (NSDictionary *childInfo in childItems) { if ((childIndex & 0x1F) == 0) { double p = baseProgress + spanProgress * (0.25 + 0.70 * ((double)childIndex / (double)MAX(childCount, (NSUInteger)1))); sbx_progress_set(p, [NSString stringWithFormat:@"%@ %lu/%lu", name, (unsigned long)childIndex, (unsigned long)childCount]); } childIndex++; NSString *child = childInfo[@"path"]; int64_t childHandle = sbx_grant_mount(child); if (childHandle <= 0) { failed++; continue; } if (!sbx_path_is_dir(child)) continue; NSString *fallbackName = child.lastPathComponent; NSString *bundleId = childInfo[@"id"]; if (bundleId.length == 0) bundleId = sbx_bundle_id(child); if (bundleId.length == 0) bundleId = fallbackName; bundleId = sbx_safe_name(bundleId); NSString *childDest = [dest stringByAppendingPathComponent:bundleId]; if ([fm fileExistsAtPath:childDest]) { childDest = [dest stringByAppendingPathComponent: [NSString stringWithFormat:@"%@_%@", bundleId, [fallbackName substringToIndex:MIN((NSUInteger)8, fallbackName.length)]]]; } [fm createDirectoryAtPath:childDest withIntermediateDirectories:YES attributes:nil error:NULL]; NSArray *subdirs = @[@"Documents", @"Library", @"tmp", @"SystemData", @"StoreKit"]; for (NSString *sub in subdirs) { NSString *target = [child stringByAppendingPathComponent:sub]; if (!sbx_path_exists(target)) continue; sbx_grant_mount(target); sbx_symlink(target, [childDest stringByAppendingPathComponent:sub]); } sbx_symlink(child, [childDest stringByAppendingPathComponent:@"_data"]); childMounted++; } mounted += childMounted; NSLog(@"[TerminalSBX] mounted %lld container(s) under %@", childMounted, name); sbx_progress_set(baseProgress + spanProgress, [NSString stringWithFormat:@"完成 %@ (%lld)", name, childMounted]); } sbx_progress_set(0.90, @"正在挂载便捷目录…"); // Mount Terminal's own container by bundle ID as well. fsgetpath() // does not always report the calling process's own container during // the scan, but no extension is needed for it. NSString *ownRoot = NSHomeDirectory(); NSString *ownBid = [[NSBundle mainBundle] bundleIdentifier]; if (ownBid.length == 0) ownBid = @"com.apple.Terminal"; NSString *appsDest = [mountRoot stringByAppendingPathComponent:@"apps"]; [fm createDirectoryAtPath:appsDest withIntermediateDirectories:YES attributes:nil error:NULL]; NSString *ownDest = [appsDest stringByAppendingPathComponent:sbx_safe_name(ownBid)]; if (![fm fileExistsAtPath:ownDest]) { [fm createDirectoryAtPath:ownDest withIntermediateDirectories:YES attributes:nil error:NULL]; NSArray *subdirs = @[@"Documents", @"Library", @"tmp", @"SystemData", @"StoreKit"]; for (NSString *sub in subdirs) { NSString *target = [ownRoot stringByAppendingPathComponent:sub]; if (sbx_path_exists(target)) { sbx_symlink(target, [ownDest stringByAppendingPathComponent:sub]); } } sbx_symlink(ownRoot, [ownDest stringByAppendingPathComponent:@"_data"]); mounted++; } // Convenience mounts inside the system group tree. NSString *gestalt = @"/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.mobilegestaltcache/Library/Caches"; if (sbx_path_exists(gestalt) && sbx_grant_mount(gestalt) > 0) { if (sbx_symlink(gestalt, [mountRoot stringByAppendingPathComponent:@"gestalt"])) mounted++; } NSString *profiles = @"/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles"; if (sbx_path_exists(profiles) && sbx_grant_mount(profiles) > 0) { if (sbx_symlink(profiles, [mountRoot stringByAppendingPathComponent:@"configprofiles"])) mounted++; } sbx_progress_set(0.97, @"正在写入挂载清单…"); sbx_write_manifest(mounted, failed); pthread_mutex_lock(&g_lock); g_mount_mounted = mounted; g_mount_failed = failed; g_mount_done = YES; g_mount_running = NO; pthread_mutex_unlock(&g_lock); NSLog(@"[TerminalSBX] auto-mount complete: %lld mounted, %lld failed", mounted, failed); sbx_progress_set(1.0, @"完成"); if (showUI) sbx_progress_finish(); pthread_mutex_unlock(&g_mount_lock); } } static void *sbx_automount_thread(void *arg) { (void)arg; sbx_show_progress(); usleep(400000); sbx_automount(YES); return NULL; } __attribute__((constructor)) static void sbx_initialize(void) { // Every launch starts with a fresh terminal and no restored command // history. a-Shell checks keep_content before restoring terminal data, // cwd and the history array. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setBool:NO forKey:@"keep_content"]; [defaults synchronize]; // Do not touch Foundation before the app's run loop exists; mount on a // detached thread shortly after launch. pthread_t thread; if (pthread_create(&thread, NULL, sbx_automount_thread, NULL) == 0) { pthread_detach(thread); } } // --------------------------------------------------------------------------- // Error descriptions // --------------------------------------------------------------------------- static const char *sbx_error_text(int64_t code) { switch (code) { case -255: return "target does not exist (use -c to bypass the existence check)"; case -254: return "not an absolute path"; case -1: return "no usable App Group / container functions unavailable"; case -2: return "failed to resolve containermanager functions"; case -3: return "failed to create container query"; case -4: return "failed to create the Library/Caches symlink"; case -5: return "query rejected by containermanagerd"; case -6: return "kernel refused to issue a sandbox extension"; default: return "unknown error"; } } // --------------------------------------------------------------------------- // Commands // --------------------------------------------------------------------------- __attribute__((visibility("default"))) int sbx_badshell(int argc, char **argv) { (void)argc; (void)argv; @autoreleasepool { sbx_printf( "Terminal — a-Shell 2.3.2 full + worse_query sandbox escape\n" "\n" "The launch-time mount runs in the background and takes ~40 seconds.\n" "Use \"sbx_status\" to check progress.\n" "\n" "Auto-mounted at ~/Documents/_sandbox/ by the launch-time escape:\n" " _sandbox/system/ /var/containers/Data/System\n" " _sandbox/systemgroups/ /var/containers/Shared/SystemGroup/*\n" " _sandbox/apps/ /var/mobile/Containers/Data/Application/*\n" " _sandbox/daemons/ /var/mobile/Containers/Data/InternalDaemon/*\n" " _sandbox/plugins/ /var/mobile/Containers/Data/PluginKitPlugin/*\n" " _sandbox/appgroups/ /var/mobile/Containers/Shared/AppGroup/*\n" " _sandbox/appgroups27/ /var/containers/Shared/AppGroup/* (iOS 27)\n" " _sandbox/gestalt/ MobileGestalt cache\n" " _sandbox/configprofiles/ ConfigurationProfiles\n" "\n" "Every app container is exposed by bundle ID:\n" " _sandbox/apps/com.example.app/Documents/\n" " _sandbox/apps/com.example.app/_data/ full container root\n" "\n" "Use standard commands (ls, cat, cp, grep, find, tar, python3...) on these\n" "paths, or use the sbx_* commands for manual control:\n" "\n" " sbxhelp Show this help and mount overview\n" " neofetch System information (actual device)\n" " sbx_grant [-c] Consume a sandbox extension for a path\n" " sbx_release Release sandbox extension(s)\n" " sbx_status [-v] Show app group, mount state and active handles\n" " sbx_remount Re-run the complete auto-mount\n" " sbx_ls List a directory (or file metadata)\n" " sbx_cat Print a file\n" " sbx_cp Copy a file or directory\n" " sbx_write [-a] Write/append text\n" " sbx_rm [-r] Remove a file (or directory with -r)\n" " sbx_hexdump [-n N] Hex dump\n" " sbx_containers [root] List containers and bundle IDs\n" " sbx_id Read a container's bundle ID\n" " sbx_sysgroups [filter] List system group containers\n" " sbx_probe Probe every supported escape path\n" " sbx_paths Show the supported path table\n" " bswelcome Banner + wait for the mount to finish\n" "\n" "Native plugin environment (installed in ~/Documents/.terminal):\n" " termenv init Create the prefix and local repository\n" " termenv selftest Test apt/dpkg with the hello package\n" " apt update Refresh the package index\n" " apt install Install a Terminal-compatible .deb\n" " dpkg -i Install a .deb directly\n" " dpkg -l List installed packages\n" " mkdeb [arch]\n" " bash