Files
terminal_worse/src/TerminalSBX.m
T

2399 lines
98 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#import <Security/Security.h>
#import <UIKit/UIKit.h>
#import <Metal/Metal.h>
#include <dlfcn.h>
#include <dirent.h>
#include <errno.h>
#include <limits.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fsgetpath.h>
#include <sys/mount.h>
#include <sys/sysctl.h>
#include <sys/utsname.h>
#include <mach/mach.h>
#include <ifaddrs.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <time.h>
#include <unistd.h>
#include <xpc/xpc.h>
// ---------------------------------------------------------------------------
// 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<NSDictionary *> *g_handles = nil;
static NSMutableSet<NSString *> *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<NSString *> *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 = [@"<?xml" dataUsingEncoding:NSUTF8StringEncoding];
NSData *endTag = [@"</plist>" 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<NSString *> *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<NSString *> *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<NSDictionary *> *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<NSDictionary *> *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<NSString *> *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<NSString *> *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<NSDictionary *> *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<NSString *> *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<NSString *> *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<NSDictionary *> *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<NSDictionary *> *childItems = [NSMutableArray array];
NSArray<NSString *> *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<NSString *> *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<NSString *> *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<NSString *> *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] <path> Consume a sandbox extension for a path\n"
" sbx_release <handle|path|all> 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 <path> List a directory (or file metadata)\n"
" sbx_cat <file> Print a file\n"
" sbx_cp <src> <dst> Copy a file or directory\n"
" sbx_write [-a] <file> <text...> Write/append text\n"
" sbx_rm [-r] <path> Remove a file (or directory with -r)\n"
" sbx_hexdump [-n N] <file> Hex dump\n"
" sbx_containers [root] List containers and bundle IDs\n"
" sbx_id <container> 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 <package> Install a Terminal-compatible .deb\n"
" dpkg -i <file.deb> Install a .deb directly\n"
" dpkg -l List installed packages\n"
" mkdeb <name> <version> <payload-dir> [arch]\n"
" bash <script> a-Shell sh compatibility launcher\n"
"\n"
"Quick start:\n"
" cd ~/Documents/_sandbox/apps\n"
" ls\n"
" cat com.example.app/Documents/file.txt\n"
"\n");
return 0;
}
}
__attribute__((visibility("default")))
int sbx_status(int argc, char **argv) {
@autoreleasepool {
BOOL verbose = NO;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) verbose = YES;
}
pthread_mutex_lock(&g_lock);
BOOL done = g_mount_done;
BOOL running = g_mount_running;
int64_t mounted = g_mount_mounted;
int64_t failed = g_mount_failed;
NSString *group = g_group;
NSArray *handles = [g_handles copy];
pthread_mutex_unlock(&g_lock);
NSProcessInfo *pi = [NSProcessInfo processInfo];
NSOperatingSystemVersion v = pi.operatingSystemVersion;
sbx_printf("Terminal sandbox escape status\n");
sbx_printf(" OS: iOS %ld.%ld.%ld\n", (long)v.majorVersion, (long)v.minorVersion, (long)v.patchVersion);
sbx_printf(" App group: %s\n", group ? group.UTF8String : "(none)");
sbx_printf(" Mount root: %s\n", sbx_display(sbx_mount_root()).UTF8String);
sbx_printf(" Auto-mount: %s\n", done ? "complete" : (running ? "running (paths appear gradually, ~40s)" : "pending"));
if (done) sbx_printf(" Mounted: %lld (failed %lld)\n", mounted, failed);
sbx_printf(" Handles: %lu\n", (unsigned long)handles.count);
sbx_printf("\nSupported paths:\n");
for (NSDictionary *mp in sbx_mount_points()) {
NSString *path = mp[@"root"];
BOOL has = sbx_has_handle_for(path) || sbx_root_accessible(path);
sbx_printf(" [%s] %-58s %s\n",
has ? "OK" : "--",
sbx_display(path).UTF8String,
sbx_path_exists(path) ? "" : "(not present)");
}
if (handles.count > 0) {
sbx_printf("\nActive sandbox extensions:\n");
NSUInteger limit = verbose ? handles.count : MIN(handles.count, (NSUInteger)24);
for (NSUInteger i = 0; i < limit; i++) {
NSDictionary *e = handles[i];
sbx_printf(" %8s %-10s %s\n",
[e[@"handle"] stringValue].UTF8String,
[e[@"engine"] UTF8String],
sbx_display(e[@"path"]).UTF8String);
}
if (limit < handles.count) {
sbx_printf(" ... %lu more (use sbx_status -v)\n",
(unsigned long)(handles.count - limit));
}
}
return 0;
}
}
__attribute__((visibility("default")))
int sbx_grant(int argc, char **argv) {
@autoreleasepool {
BOOL create = NO;
NSString *path = nil;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-c") == 0) create = YES;
else path = [NSString stringWithUTF8String:argv[i]];
}
if (path.length == 0) {
sbx_eprintf("usage: sbx_grant [-c] <path>\n"
" Consume a sandbox extension for <path> via worse_query.\n"
" -c bypass the target-existence check\n");
sbx_eprintf("\nSupported escape roots:\n");
for (NSDictionary *mp in sbx_mount_points()) {
sbx_eprintf(" %s\n", sbx_display(mp[@"root"]).UTF8String);
}
return 1;
}
NSString *engine = nil;
NSString *canonical = sbx_canonical(path);
int64_t handle = sbx_grant_path(canonical, create, &engine);
if (handle > 0) {
sbx_printf("granted %s (handle %lld, %s)\n",
sbx_display(canonical).UTF8String, handle, engine.UTF8String);
return 0;
}
if (handle == 0 && [engine isEqualToString:@"own-sandbox"]) {
sbx_printf("%s is already inside Terminal's own sandbox (no extension needed)\n",
sbx_display(canonical).UTF8String);
return 0;
}
sbx_eprintf("sbx_grant: %s: %s (%lld)\n",
sbx_display(canonical).UTF8String, sbx_error_text(handle), handle);
return 1;
}
}
__attribute__((visibility("default")))
int sbx_release(int argc, char **argv) {
@autoreleasepool {
if (argc < 2) {
sbx_eprintf("usage: sbx_release <handle|path|all>\n");
pthread_mutex_lock(&g_lock);
NSArray *handles = [g_handles copy];
pthread_mutex_unlock(&g_lock);
if (handles.count == 0) {
sbx_eprintf("no active sandbox extensions\n");
return 1;
}
sbx_eprintf("\nActive extensions:\n");
NSUInteger limit = MIN(handles.count, (NSUInteger)200);
for (NSUInteger i = 0; i < limit; i++) {
NSDictionary *e = handles[i];
sbx_eprintf(" %s %s\n",
[e[@"handle"] stringValue].UTF8String,
sbx_display(e[@"path"]).UTF8String);
}
if (limit < handles.count) sbx_eprintf(" ... %lu more\n", (unsigned long)(handles.count - limit));
return 1;
}
NSString *target = [NSString stringWithUTF8String:argv[1]];
if ([target isEqualToString:@"all"]) {
sbx_release_all_internal();
return 0;
}
pthread_mutex_lock(&g_lock);
NSDictionary *found = nil;
NSUInteger index = NSNotFound;
NSString *canonical = sbx_canonical(target);
for (NSUInteger i = 0; i < g_handles.count; i++) {
NSDictionary *e = g_handles[i];
if ([e[@"handle"] stringValue].longLongValue == target.longLongValue && target.longLongValue > 0) {
found = e; index = i; break;
}
if ([e[@"path"] isEqualToString:canonical]) {
found = e; index = i; break;
}
}
if (found) [g_handles removeObjectAtIndex:index];
pthread_mutex_unlock(&g_lock);
if (!found) {
sbx_eprintf("sbx_release: '%s' not found in active extensions\n", target.UTF8String);
return 1;
}
int64_t handle = [found[@"handle"] longLongValue];
sbx_release_handle(handle);
sbx_printf("released handle %lld (%s)\n", handle, sbx_display(found[@"path"]).UTF8String);
return 0;
}
}
__attribute__((visibility("default")))
int sbx_remount(int argc, char **argv) {
(void)argc; (void)argv;
@autoreleasepool {
sbx_printf("releasing existing handles...\n");
sbx_release_all_internal();
sbx_printf("re-mounting all supported sandbox paths...\n");
sbx_automount(NO);
pthread_mutex_lock(&g_lock);
int64_t mounted = g_mount_mounted;
int64_t failed = g_mount_failed;
NSUInteger handles = g_handles.count;
pthread_mutex_unlock(&g_lock);
sbx_printf("done: %lld mounted, %lld failed, %lu handle(s) active\n",
mounted, failed, (unsigned long)handles);
return 0;
}
}
__attribute__((visibility("default")))
int sbx_ls(int argc, char **argv) {
@autoreleasepool {
if (argc < 2) {
sbx_eprintf("usage: sbx_ls <path>\n");
return 1;
}
NSString *path = sbx_canonical([NSString stringWithUTF8String:argv[1]]);
int64_t handle = sbx_grant_item(path);
if (handle <= 0 && !sbx_is_own_path(path)) {
sbx_eprintf("sbx_ls: cannot access %s (%s)\n", sbx_display(path).UTF8String, sbx_error_text(handle));
return 1;
}
struct stat st;
if (stat(path.fileSystemRepresentation, &st) != 0) {
sbx_eprintf("sbx_ls: %s: %s\n", sbx_display(path).UTF8String, strerror(errno));
return 1;
}
if (!S_ISDIR(st.st_mode)) {
sbx_printf("%s\t%lld bytes\n", sbx_display(path).UTF8String, (long long)st.st_size);
return 0;
}
NSArray<NSString *> *entries = sbx_list_dir(path);
if (entries == nil) {
sbx_eprintf("sbx_ls: %s: %s\n", sbx_display(path).UTF8String, strerror(errno));
return 1;
}
for (NSString *name in entries) {
NSString *full = [path stringByAppendingPathComponent:name];
struct stat child;
if (lstat(full.fileSystemRepresentation, &child) != 0) {
sbx_printf("%s\n", name.UTF8String);
continue;
}
if (S_ISDIR(child.st_mode)) {
sbx_printf("%s/\n", name.UTF8String);
} else {
sbx_printf("%s\t%lld bytes\n", name.UTF8String, (long long)child.st_size);
}
}
return 0;
}
}
__attribute__((visibility("default")))
int sbx_cat(int argc, char **argv) {
@autoreleasepool {
if (argc < 2) {
sbx_eprintf("usage: sbx_cat <file>\n");
return 1;
}
NSString *path = sbx_canonical([NSString stringWithUTF8String:argv[1]]);
int64_t handle = sbx_grant_item(path);
if (handle <= 0 && !sbx_is_own_path(path)) {
sbx_eprintf("sbx_cat: cannot access %s (%s)\n", sbx_display(path).UTF8String, sbx_error_text(handle));
return 1;
}
FILE *f = fopen(path.fileSystemRepresentation, "rb");
if (!f) {
sbx_eprintf("sbx_cat: %s: %s\n", sbx_display(path).UTF8String, strerror(errno));
return 1;
}
unsigned char buf[65536];
size_t n;
FILE *out = sbx_out();
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
if (fwrite(buf, 1, n, out) != n) break;
}
int err = ferror(f);
fclose(f);
fflush(out);
if (err) {
sbx_eprintf("sbx_cat: read error\n");
return 1;
}
return 0;
}
}
__attribute__((visibility("default")))
int sbx_cp(int argc, char **argv) {
@autoreleasepool {
if (argc < 3) {
sbx_eprintf("usage: sbx_cp <source> <destination>\n");
return 1;
}
NSString *src = sbx_canonical([NSString stringWithUTF8String:argv[1]]);
NSString *dst = sbx_canonical([NSString stringWithUTF8String:argv[2]]);
int64_t hs = sbx_grant_item(src);
if (hs <= 0 && !sbx_is_own_path(src)) {
sbx_eprintf("sbx_cp: cannot access source (%s)\n", sbx_error_text(hs));
return 1;
}
int64_t hd = sbx_grant_item([dst stringByDeletingLastPathComponent]);
if (hd <= 0 && !sbx_is_own_path(dst)) {
sbx_eprintf("sbx_cp: cannot access destination (%s)\n", sbx_error_text(hd));
return 1;
}
NSFileManager *fm = [NSFileManager defaultManager];
BOOL dstIsDir = NO;
if ([fm fileExistsAtPath:dst isDirectory:&dstIsDir] && dstIsDir) {
dst = [dst stringByAppendingPathComponent:src.lastPathComponent];
}
if ([fm fileExistsAtPath:dst]) {
[fm removeItemAtPath:dst error:NULL];
}
NSError *error = nil;
if (![fm copyItemAtPath:src toPath:dst error:&error]) {
sbx_eprintf("sbx_cp: %s\n", error.localizedDescription.UTF8String);
return 1;
}
sbx_printf("copied %s -> %s\n", sbx_display(src).UTF8String, sbx_display(dst).UTF8String);
return 0;
}
}
__attribute__((visibility("default")))
int sbx_write(int argc, char **argv) {
@autoreleasepool {
BOOL append = NO;
int first = 1;
if (argc > 1 && strcmp(argv[1], "-a") == 0) { append = YES; first = 2; }
if (argc <= first + 1) {
sbx_eprintf("usage: sbx_write [-a] <file> <text...>\n");
return 1;
}
NSString *path = sbx_canonical([NSString stringWithUTF8String:argv[first]]);
NSMutableString *text = [NSMutableString string];
for (int i = first + 1; i < argc; i++) {
if (text.length > 0) [text appendString:@" "];
[text appendString:[NSString stringWithUTF8String:argv[i]]];
}
[text appendString:@"\n"];
int64_t handle = sbx_grant_item([path stringByDeletingLastPathComponent]);
if (handle <= 0 && !sbx_is_own_path(path)) {
sbx_eprintf("sbx_write: cannot access parent directory (%s)\n", sbx_error_text(handle));
return 1;
}
NSError *error = nil;
BOOL ok = NO;
if (append) {
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
ok = [text writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:&error];
} else {
NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:path];
if (fh) {
@try {
[fh seekToEndOfFile];
[fh writeData:[text dataUsingEncoding:NSUTF8StringEncoding]];
[fh closeFile];
ok = YES;
} @catch (NSException *e) {
error = [NSError errorWithDomain:@"TerminalSBX" code:1 userInfo:@{NSLocalizedDescriptionKey: e.reason ?: @"write failed"}];
}
}
}
} else {
ok = [text writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:&error];
}
if (!ok) {
sbx_eprintf("sbx_write: %s\n", error.localizedDescription.UTF8String);
return 1;
}
sbx_printf("wrote %lu bytes to %s\n", (unsigned long)[text lengthOfBytesUsingEncoding:NSUTF8StringEncoding],
sbx_display(path).UTF8String);
return 0;
}
}
__attribute__((visibility("default")))
int sbx_rm(int argc, char **argv) {
@autoreleasepool {
BOOL recursive = NO;
BOOL force = NO;
NSString *path = nil;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-r") == 0 || strcmp(argv[i], "-R") == 0) recursive = YES;
else if (strcmp(argv[i], "-f") == 0) force = YES;
else path = [NSString stringWithUTF8String:argv[i]];
}
if (path.length == 0) {
sbx_eprintf("usage: sbx_rm [-r] [-f] <path>\n");
return 1;
}
NSString *canonical = sbx_canonical(path);
int64_t handle = sbx_grant_item([canonical stringByDeletingLastPathComponent]);
if (handle <= 0 && !sbx_is_own_path(canonical)) {
sbx_eprintf("sbx_rm: cannot access parent directory (%s)\n", sbx_error_text(handle));
return 1;
}
struct stat st;
if (lstat(canonical.fileSystemRepresentation, &st) != 0) {
if (force) return 0;
sbx_eprintf("sbx_rm: %s: %s\n", sbx_display(canonical).UTF8String, strerror(errno));
return 1;
}
if (S_ISDIR(st.st_mode) && !recursive) {
sbx_eprintf("sbx_rm: %s is a directory (use -r)\n", sbx_display(canonical).UTF8String);
return 1;
}
NSError *error = nil;
if (![[NSFileManager defaultManager] removeItemAtPath:canonical error:&error]) {
sbx_eprintf("sbx_rm: %s\n", error.localizedDescription.UTF8String);
return 1;
}
sbx_printf("removed %s\n", sbx_display(canonical).UTF8String);
return 0;
}
}
__attribute__((visibility("default")))
int sbx_hexdump(int argc, char **argv) {
@autoreleasepool {
long long maxBytes = 256;
NSString *path = nil;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
maxBytes = strtoll(argv[++i], NULL, 10);
if (maxBytes < 0) maxBytes = 0;
} else {
path = [NSString stringWithUTF8String:argv[i]];
}
}
if (path.length == 0) {
sbx_eprintf("usage: sbx_hexdump [-n count] <file>\n");
return 1;
}
NSString *canonical = sbx_canonical(path);
int64_t handle = sbx_grant_item(canonical);
if (handle <= 0 && !sbx_is_own_path(canonical)) {
sbx_eprintf("sbx_hexdump: cannot access %s (%s)\n", sbx_display(canonical).UTF8String, sbx_error_text(handle));
return 1;
}
FILE *f = fopen(canonical.fileSystemRepresentation, "rb");
if (!f) {
sbx_eprintf("sbx_hexdump: %s: %s\n", sbx_display(canonical).UTF8String, strerror(errno));
return 1;
}
unsigned char *data = malloc((size_t)maxBytes + 1);
if (!data) { fclose(f); return 1; }
size_t n = fread(data, 1, (size_t)maxBytes, f);
fclose(f);
for (size_t offset = 0; offset < n; offset += 16) {
char hex[3 * 16 + 1];
char ascii[17];
size_t k = 0;
for (size_t j = 0; j < 16; j++) {
if (offset + j < n) {
unsigned char b = data[offset + j];
snprintf(hex + k, 4, "%02x ", b);
ascii[j] = (b >= 0x20 && b < 0x7f) ? (char)b : '.';
} else {
snprintf(hex + k, 4, " ");
ascii[j] = ' ';
}
k += 3;
}
hex[k] = '\0';
ascii[16] = '\0';
sbx_printf("%08zx %s |%s|\n", offset, hex, ascii);
}
if (n == (size_t)maxBytes) {
struct stat st;
if (stat(canonical.fileSystemRepresentation, &st) == 0 && (long long)st.st_size > maxBytes) {
sbx_printf("... (%lld bytes total, showing first %lld)\n", (long long)st.st_size, maxBytes);
}
}
free(data);
return 0;
}
}
__attribute__((visibility("default")))
int sbx_containers(int argc, char **argv) {
@autoreleasepool {
NSString *root = @"/private/var/mobile/Containers/Data/Application";
if (argc >= 2) root = sbx_canonical([NSString stringWithUTF8String:argv[1]]);
int64_t handle = sbx_grant_mount(root);
NSMutableArray<NSDictionary *> *children = [NSMutableArray array];
if (handle > 0) {
NSArray<NSString *> *entries = sbx_list_dir(root);
for (NSString *entry in entries ?: @[]) {
[children addObject:@{@"path": [root stringByAppendingPathComponent:entry], @"id": @""}];
}
}
if (children.count == 0) {
NSArray<NSDictionary *> *enumerated = sbx_enumerate_class(2);
if (enumerated.count > 0) [children addObjectsFromArray:enumerated];
}
if (children.count == 0) {
NSArray<NSString *> *leaked = sbx_pathleak(root, 2000000, 8.0);
for (NSString *child in leaked ?: @[]) {
[children addObject:@{@"path": child, @"id": @""}];
}
}
if (children.count == 0) {
sbx_eprintf("sbx_containers: cannot access or enumerate %s (%s)\n",
sbx_display(root).UTF8String, sbx_error_text(handle));
return 1;
}
int count = 0;
for (NSDictionary *info in children) {
NSString *child = info[@"path"];
if (!sbx_path_is_dir(child)) continue;
sbx_grant_mount(child);
NSString *bid = info[@"id"];
if (bid.length == 0) bid = sbx_bundle_id(child);
if (bid.length == 0) bid = child.lastPathComponent;
sbx_printf("%s\t%s\n", bid.UTF8String, sbx_display(child).UTF8String);
count++;
}
if (count == 0) {
sbx_eprintf("sbx_containers: no containers found\n");
return 1;
}
return 0;
}
}
__attribute__((visibility("default")))
int sbx_id(int argc, char **argv) {
@autoreleasepool {
if (argc < 2) {
sbx_eprintf("usage: sbx_id <container_path>\n");
return 1;
}
NSString *path = sbx_canonical([NSString stringWithUTF8String:argv[1]]);
NSFileManager *fm = [NSFileManager defaultManager];
NSString *dataLink = [path stringByAppendingPathComponent:@"_data"];
if ([fm fileExistsAtPath:dataLink]) path = sbx_canonical(dataLink);
NSString *linkTarget = sbx_symlink_target(path);
if (linkTarget.length > 0) path = linkTarget;
int64_t handle = sbx_grant_mount(path);
if (handle <= 0 && !sbx_is_own_path(path)) {
sbx_eprintf("sbx_id: cannot access %s (%s)\n", sbx_display(path).UTF8String, sbx_error_text(handle));
return 1;
}
NSString *bid = sbx_bundle_id(path);
if (bid.length == 0) {
sbx_eprintf("sbx_id: could not read container metadata\n");
return 1;
}
sbx_printf("%s\n", bid.UTF8String);
return 0;
}
}
__attribute__((visibility("default")))
int sbx_sysgroups(int argc, char **argv) {
@autoreleasepool {
NSString *filter = argc >= 2 ? [NSString stringWithUTF8String:argv[1]] : nil;
NSString *root = @"/private/var/containers/Shared/SystemGroup";
int64_t handle = sbx_grant_mount(root);
NSMutableArray<NSDictionary *> *children = [NSMutableArray array];
if (handle > 0) {
NSArray<NSString *> *entries = sbx_list_dir(root);
for (NSString *entry in entries ?: @[]) {
[children addObject:@{@"path": [root stringByAppendingPathComponent:entry], @"id": @""}];
}
}
if (children.count == 0) {
NSArray<NSDictionary *> *enumerated = sbx_enumerate_class(11);
if (enumerated.count > 0) [children addObjectsFromArray:enumerated];
}
if (children.count == 0) {
NSArray<NSString *> *leaked = sbx_pathleak(root, 2000000, 8.0);
for (NSString *child in leaked ?: @[]) {
[children addObject:@{@"path": child, @"id": @""}];
}
}
if (children.count == 0) {
sbx_eprintf("sbx_sysgroups: cannot access or enumerate %s (%s)\n",
sbx_display(root).UTF8String, sbx_error_text(handle));
return 1;
}
int found = 0;
for (NSDictionary *info in children) {
NSString *child = info[@"path"];
if (!sbx_path_is_dir(child)) continue;
sbx_grant_mount(child);
NSString *entry = child.lastPathComponent;
NSString *bid = info[@"id"];
if (bid.length == 0) bid = sbx_bundle_id(child);
if (bid.length == 0) bid = entry;
if (filter.length > 0 &&
[bid rangeOfString:filter options:NSCaseInsensitiveSearch].location == NSNotFound &&
[entry rangeOfString:filter options:NSCaseInsensitiveSearch].location == NSNotFound) {
continue;
}
sbx_printf("%s\t%s\n", bid.UTF8String, sbx_display(child).UTF8String);
found++;
}
if (found == 0) {
sbx_eprintf("sbx_sysgroups: no entries%s%s\n", filter ? " matching " : "",
filter ? filter.UTF8String : "");
return 1;
}
return 0;
}
}
__attribute__((visibility("default")))
int sbx_enum(int argc, char **argv) {
@autoreleasepool {
if (argc < 2) {
sbx_eprintf("usage: sbx_enum <container-class> [filter]\n"
" classes: 2=app data, 5=app group, 8=plugin, 10=daemon, 11=system group\n");
return 1;
}
uint64_t cls = strtoull(argv[1], NULL, 0);
NSString *filter = argc >= 3 ? [NSString stringWithUTF8String:argv[2]] : nil;
NSArray<NSDictionary *> *items = sbx_enumerate_class(cls);
if (items.count == 0) {
sbx_eprintf("sbx_enum: class %llu returned no containers\n", cls);
return 1;
}
int shown = 0;
for (NSDictionary *item in items) {
NSString *path = item[@"path"];
NSString *bid = item[@"id"];
if (filter.length > 0 &&
[path rangeOfString:filter options:NSCaseInsensitiveSearch].location == NSNotFound &&
[bid rangeOfString:filter options:NSCaseInsensitiveSearch].location == NSNotFound) {
continue;
}
sbx_printf("%s\t%s\n", bid.length ? bid.UTF8String : "-", sbx_display(path).UTF8String);
shown++;
}
sbx_printf("%d/%lu container(s)\n", shown, (unsigned long)items.count);
return 0;
}
}
__attribute__((visibility("default")))
int sbx_enumclasses(int argc, char **argv) {
(void)argc; (void)argv;
@autoreleasepool {
sbx_printf("containermanager enumeration by class:\n");
for (uint64_t cls = 1; cls <= 20; cls++) {
NSArray<NSDictionary *> *items = sbx_enumerate_class(cls);
if (items.count > 0) {
sbx_printf(" class %-2llu: %lu container(s)\n", cls, (unsigned long)items.count);
NSUInteger limit = MIN(items.count, (NSUInteger)5);
for (NSUInteger i = 0; i < limit; i++) {
sbx_printf(" %s\n", sbx_display(items[i][@"path"]).UTF8String);
}
}
}
return 0;
}
}
__attribute__((visibility("default")))
int sbx_leak(int argc, char **argv) {
@autoreleasepool {
if (argc < 2) {
sbx_eprintf("usage: sbx_leak <path> [max_inode] [seconds]\n");
return 1;
}
NSString *path = sbx_canonical([NSString stringWithUTF8String:argv[1]]);
uint64_t maxInode = 2000000;
double seconds = 30.0;
if (argc >= 3) maxInode = strtoull(argv[2], NULL, 10);
if (argc >= 4) seconds = strtod(argv[3], NULL);
double start = sbx_now();
NSArray<NSString *> *items = sbx_pathleak(path, maxInode, seconds);
if (items.count == 0) {
sbx_eprintf("sbx_leak: no immediate children found under %s\n", sbx_display(path).UTF8String);
return 1;
}
for (NSString *item in items) sbx_printf("%s\n", sbx_display(item).UTF8String);
sbx_printf("%lu path(s) in %.2fs\n", (unsigned long)items.count, sbx_now() - start);
return 0;
}
}
__attribute__((visibility("default")))
int sbx_lsapps(int argc, char **argv) {
(void)argc; (void)argv;
@autoreleasepool {
Class cls = NSClassFromString(@"LSApplicationWorkspace");
if (!cls) {
sbx_eprintf("sbx_lsapps: LSApplicationWorkspace unavailable\n");
return 1;
}
id ws = [cls performSelector:NSSelectorFromString(@"defaultWorkspace")];
if (!ws) {
sbx_eprintf("sbx_lsapps: defaultWorkspace unavailable\n");
return 1;
}
NSArray *apps = [ws performSelector:NSSelectorFromString(@"allInstalledApplications")];
if (![apps isKindOfClass:[NSArray class]]) {
sbx_eprintf("sbx_lsapps: allInstalledApplications unavailable\n");
return 1;
}
int count = 0;
for (id app in apps) {
NSString *bid = [app performSelector:NSSelectorFromString(@"bundleIdentifier")];
NSURL *url = [app performSelector:NSSelectorFromString(@"dataContainerURL")];
if ([bid isKindOfClass:[NSString class]] && [url isKindOfClass:[NSURL class]]) {
sbx_printf("%s\t%s\n", bid.UTF8String, sbx_display(url.path).UTF8String);
count++;
}
}
sbx_printf("%d app data container(s)\n", count);
return count > 0 ? 0 : 1;
}
}
__attribute__((visibility("default")))
int sbx_probe(int argc, char **argv) {
(void)argc; (void)argv;
@autoreleasepool {
int ok = 0;
sbx_printf("Probing every path supported by worse_query:\n");
for (NSDictionary *mp in sbx_mount_points()) {
NSString *root = mp[@"root"];
NSString *name = mp[@"name"];
int64_t handle = sbx_grant_mount(root);
NSArray *entries = handle > 0 ? sbx_list_dir(root) : nil;
NSArray *enumerated = nil;
if (entries == nil) {
enumerated = sbx_pathleak(root, 2000000, 6.0);
}
if (handle > 0 || entries.count > 0 || enumerated.count > 0) {
sbx_printf(" [OK] %-14s %-58s", name.UTF8String,
sbx_display(root).UTF8String);
if (handle > 0) sbx_printf(" handle=%lld", handle);
if (entries) sbx_printf(" entries=%lu", (unsigned long)entries.count);
if (enumerated) sbx_printf(" fsgetpath=%lu", (unsigned long)enumerated.count);
sbx_printf("\n");
sbx_mark_root(root);
ok++;
} else {
sbx_printf(" [FAIL] %-14s %-58s %s (%lld)\n", name.UTF8String,
sbx_display(root).UTF8String, sbx_error_text(handle), handle);
}
}
sbx_printf("%d/%lu supported roots accessible\n", ok, (unsigned long)sbx_mount_points().count);
return ok > 0 ? 0 : 1;
}
}
__attribute__((visibility("default")))
int sbx_paths(int argc, char **argv) {
(void)argc; (void)argv;
@autoreleasepool {
sbx_printf("worse_query supported sandbox paths (iOS 26.0–26.7.2 / 27.2):\n");
sbx_printf(" %-20s %-56s %s\n", "MOUNT", "PATH", "STATUS");
for (NSDictionary *mp in sbx_mount_points()) {
NSString *path = mp[@"root"];
sbx_printf(" %-20s %-56s %s\n",
[mp[@"name"] UTF8String],
sbx_display(path).UTF8String,
sbx_has_handle_for(path) ? "granted" : (sbx_path_exists(path) ? "not granted" : "not present"));
}
return 0;
}
}
__attribute__((visibility("default")))
int sbx_bswelcome(int argc, char **argv) {
(void)argc; (void)argv;
@autoreleasepool {
sbx_printf("\033[95mTerminal\033[0m — a-Shell + worse_query\n");
sbx_printf("waiting for sandbox escape to finish...\n");
for (int i = 0; i < 150; i++) {
pthread_mutex_lock(&g_lock);
BOOL done = g_mount_done;
pthread_mutex_unlock(&g_lock);
if (done) break;
usleep(100000);
}
pthread_mutex_lock(&g_lock);
int64_t mounted = g_mount_mounted;
int64_t failed = g_mount_failed;
pthread_mutex_unlock(&g_lock);
sbx_printf("\033[95m[ready]\033[0m %lld mounted, %lld failed. Sandbox at ~/Documents/_sandbox/\n",
mounted, failed);
return 0;
}
}
// ---------------------------------------------------------------------------
// neofetch
// ---------------------------------------------------------------------------
static NSString *sbx_sysctl_string(const char *name) {
size_t size = 0;
if (sysctlbyname(name, NULL, &size, NULL, 0) != 0 || size == 0) return nil;
char *buffer = (char *)malloc(size + 1);
if (!buffer) return nil;
if (sysctlbyname(name, buffer, &size, NULL, 0) != 0) {
free(buffer);
return nil;
}
buffer[size] = '\0';
NSString *value = [NSString stringWithUTF8String:buffer];
free(buffer);
return value;
}
static NSString *sbx_format_size(unsigned long long bytes) {
double gib = (double)bytes / 1073741824.0;
if (gib >= 1.0) return [NSString stringWithFormat:@"%.1f GiB", gib];
double mib = (double)bytes / 1048576.0;
return [NSString stringWithFormat:@"%.0f MiB", mib];
}
static NSDictionary *sbx_device_snapshot(void) {
__block NSString *name = @"iPhone";
__block NSString *model = @"iPhone";
__block NSString *system = @"iOS";
__block float batteryLevel = -1.0f;
__block NSInteger batteryState = 0;
__block CGSize screenSize = CGSizeZero;
__block CGFloat screenScale = 1.0;
void (^snapshot)(void) = ^{
UIDevice *device = [UIDevice currentDevice];
device.batteryMonitoringEnabled = YES;
if (device.name.length > 0) name = [device.name copy];
if (device.model.length > 0) model = [device.model copy];
if (device.systemVersion.length > 0) system = [device.systemVersion copy];
batteryLevel = device.batteryLevel;
batteryState = (NSInteger)device.batteryState;
UIScreen *screen = [UIScreen mainScreen];
if (screen != nil) {
screenSize = screen.bounds.size;
screenScale = screen.scale;
}
};
if ([NSThread isMainThread]) snapshot();
else dispatch_sync(dispatch_get_main_queue(), snapshot);
return @{
@"name": name,
@"model": model,
@"system": system,
@"batteryLevel": @(batteryLevel),
@"batteryState": @(batteryState),
@"screenWidth": @(screenSize.width * screenScale),
@"screenHeight": @(screenSize.height * screenScale),
@"screenScale": @(screenScale),
};
}
static NSString *sbx_cpu_arch(void) {
int32_t cputype = 0;
size_t size = sizeof(cputype);
if (sysctlbyname("hw.cputype", &cputype, &size, NULL, 0) != 0) return @"arm64";
int32_t subtype = 0;
size = sizeof(subtype);
sysctlbyname("hw.cpusubtype", &subtype, &size, NULL, 0);
if (cputype == 0x0100000C) { // CPU_TYPE_ARM64
return subtype == 2 ? @"arm64e" : @"arm64";
}
return [NSString stringWithFormat:@"cputype %d", cputype];
}
static NSString *sbx_local_ip(void) {
struct ifaddrs *interfaces = NULL;
if (getifaddrs(&interfaces) != 0) return nil;
NSString *result = nil;
for (struct ifaddrs *ifa = interfaces; ifa != NULL; ifa = ifa->ifa_next) {
if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_INET) continue;
NSString *name = [NSString stringWithUTF8String:ifa->ifa_name];
if (![name isEqualToString:@"en0"] && ![name isEqualToString:@"en1"]) continue;
char host[NI_MAXHOST];
if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in),
host, sizeof(host), NULL, 0, NI_NUMERICHOST) == 0) {
result = [NSString stringWithUTF8String:host];
break;
}
}
freeifaddrs(interfaces);
return result;
}
__attribute__((visibility("default")))
int sbx_neofetch(int argc, char **argv) {
(void)argv;
@autoreleasepool {
if (argc > 1 && (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0)) {
sbx_printf("usage: neofetch\n"
" Compact system information for Terminal / a-Shell.\n");
return 0;
}
NSProcessInfo *pi = [NSProcessInfo processInfo];
NSDictionary *device = sbx_device_snapshot();
NSString *machine = sbx_sysctl_string("hw.machine") ?: @"arm64";
NSString *hardware = sbx_sysctl_string("hw.model") ?: @"unknown";
NSString *build = sbx_sysctl_string("kern.osversion") ?: @"";
NSString *product = sbx_sysctl_string("kern.osproductversion") ?: device[@"system"];
NSString *bundleVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"2.3.2";
struct utsname uts;
memset(&uts, 0, sizeof(uts));
uname(&uts);
unsigned long long totalMemory = pi.physicalMemory;
unsigned long long appMemory = 0;
struct mach_task_basic_info taskInfo;
mach_msg_type_number_t taskCount = MACH_TASK_BASIC_INFO_COUNT;
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO,
(task_info_t)&taskInfo, &taskCount) == KERN_SUCCESS) {
appMemory = taskInfo.resident_size;
}
struct statfs sfs;
unsigned long long diskTotal = 0, diskFree = 0;
if (statfs(NSHomeDirectory().fileSystemRepresentation, &sfs) == 0) {
diskTotal = (unsigned long long)sfs.f_blocks * (unsigned long long)sfs.f_bsize;
diskFree = (unsigned long long)sfs.f_bavail * (unsigned long long)sfs.f_bsize;
}
long long uptime = (long long)pi.systemUptime;
long long days = uptime / 86400;
long long hours = (uptime % 86400) / 3600;
long long minutes = (uptime % 3600) / 60;
NSString *uptimeText;
if (days > 0) {
uptimeText = [NSString stringWithFormat:@"%lld day%@, %lld hour%@, %lld min%@",
days, days == 1 ? @"" : @"s",
hours, hours == 1 ? @"" : @"s",
minutes, minutes == 1 ? @"" : @"s"];
} else if (hours > 0) {
uptimeText = [NSString stringWithFormat:@"%lld hour%@, %lld min%@",
hours, hours == 1 ? @"" : @"s",
minutes, minutes == 1 ? @"" : @"s"];
} else {
uptimeText = [NSString stringWithFormat:@"%lld min%@", minutes, minutes == 1 ? @"" : @"s"];
}
NSString *thermal = @"Nominal";
switch (pi.thermalState) {
case NSProcessInfoThermalStateFair: thermal = @"Fair"; break;
case NSProcessInfoThermalStateSerious: thermal = @"Serious"; break;
case NSProcessInfoThermalStateCritical: thermal = @"Critical"; break;
default: break;
}
float battery = [device[@"batteryLevel"] floatValue];
NSInteger batteryState = [device[@"batteryState"] integerValue];
NSString *batteryText = @"unknown";
if (battery >= 0.0f) {
NSString *state = @"unplugged";
if (batteryState == UIDeviceBatteryStateCharging) state = @"charging";
else if (batteryState == UIDeviceBatteryStateFull) state = @"full";
else if (batteryState == UIDeviceBatteryStateUnknown) state = @"unknown";
batteryText = [NSString stringWithFormat:@"%d%% (%@)", (int)(battery * 100.0f + 0.5f), state];
}
CGFloat width = [device[@"screenWidth"] doubleValue];
CGFloat height = [device[@"screenHeight"] doubleValue];
CGFloat scale = [device[@"screenScale"] doubleValue];
NSString *ip = sbx_local_ip();
// Apple mark from neofetch's compact "mac_small" ASCII art.
sbx_printf("\033[1;32m .:'\033[0m\n");
sbx_printf("\033[1;32m _ :'_\033[0m\n");
sbx_printf("\033[1;33m .'`_`-'_``.\033[0m\n");
sbx_printf("\033[1;33m:________.-'\033[0m\n");
sbx_printf("\033[1;35m:_______:\033[0m\n");
sbx_printf("\033[1;35m:_______:\033[0m\n");
sbx_printf("\033[1;34m :_______:\033[0m\n");
sbx_printf("\033[1;34m `._.-._.'\033[0m\n");
sbx_printf("\033[1;36m Terminal · a-Shell %s\033[0m\n\n", bundleVersion.UTF8String);
sbx_printf("\033[1;35m%s@%s\033[0m\n", NSUserName().UTF8String,
[(NSString *)device[@"name"] UTF8String]);
sbx_printf("\033[1;36m-----------------------------\033[0m\n");
NSString *osName = [product hasPrefix:@"iOS"] ? product : [@"iOS " stringByAppendingString:product];
if (build.length > 0) {
sbx_printf("\033[1;36m %-10s\033[0m %s (%s)\n", "OS", osName.UTF8String, build.UTF8String);
} else {
sbx_printf("\033[1;36m %-10s\033[0m %s\n", "OS", osName.UTF8String);
}
sbx_printf("\033[1;36m %-10s\033[0m %s\n", "Host",
[(NSString *)device[@"name"] UTF8String]);
if (hardware.length > 0 && ![hardware isEqualToString:@"unknown"]) {
sbx_printf("\033[1;36m %-10s\033[0m %s (%s)\n", "Model",
machine.UTF8String, hardware.UTF8String);
} else {
sbx_printf("\033[1;36m %-10s\033[0m %s\n", "Model", machine.UTF8String);
}
sbx_printf("\033[1;36m %-10s\033[0m %s %s\n", "Kernel", uts.sysname, uts.release);
sbx_printf("\033[1;36m %-10s\033[0m %s\n", "Uptime", uptimeText.UTF8String);
sbx_printf("\033[1;36m %-10s\033[0m a-Shell %s · Terminal\n", "Shell", bundleVersion.UTF8String);
sbx_printf("\033[1;36m %-10s\033[0m %.0fx%.0f @%.0fx\n", "Resolution", width, height, scale);
sbx_printf("\033[1;36m %-10s\033[0m %lu cores · %s\n", "CPU",
(unsigned long)pi.processorCount, sbx_cpu_arch().UTF8String);
id<MTLDevice> gpu = MTLCreateSystemDefaultDevice();
if (gpu.name.length > 0) {
sbx_printf("\033[1;36m %-10s\033[0m %s\n", "GPU", gpu.name.UTF8String);
}
sbx_printf("\033[1;36m %-10s\033[0m %s total · %s app\n", "Memory",
sbx_format_size(totalMemory).UTF8String, sbx_format_size(appMemory).UTF8String);
if (diskTotal > 0) {
sbx_printf("\033[1;36m %-10s\033[0m %s free / %s\n", "Disk",
sbx_format_size(diskFree).UTF8String, sbx_format_size(diskTotal).UTF8String);
}
sbx_printf("\033[1;36m %-10s\033[0m %s\n", "Battery", batteryText.UTF8String);
sbx_printf("\033[1;36m %-10s\033[0m %s%s\n", "Thermal", thermal.UTF8String,
pi.lowPowerModeEnabled ? " · low power" : "");
sbx_printf("\033[1;36m %-10s\033[0m %s\n", "Locale",
[[NSLocale currentLocale].localeIdentifier UTF8String]);
sbx_printf("\033[1;36m %-10s\033[0m %s\n", "Timezone",
[[NSTimeZone localTimeZone].name UTF8String]);
if (ip.length > 0) {
sbx_printf("\033[1;36m %-10s\033[0m %s\n", "IP", ip.UTF8String);
}
return 0;
}
}