Files
vphone-cli/scripts/tweakloader/TweakLoader.m
T
zqxwceandClaude Opus 4.7 9b4e25569b camera: libvcamcaptured + cameracaptured TweakLoader allowlist + install wiring
libvcamcaptured.dylib is loaded into /usr/libexec/cameracaptured via
TweakLoader and registers a synthetic FigCaptureSource backed by the
vphone shm region. From AVF's point of view there is now a normal
"vphone:vcam:0" camera device that streams BGRA frames at the
session's requested width/height.

  scripts/vcamcaptured/                .gitignore (drop built .dylib),
                                       Makefile, libvcamcaptured.m,
                                       libvcamcaptured.plist (filter:
                                       Executables=["cameracaptured"]).

  scripts/tweakloader/TweakLoader.m    add /usr/libexec/cameracaptured
                                       to kVPhoneAllowedDaemonPaths so
                                       TweakLoader engages in a daemon
                                       (not just .app/) processes.

  scripts/cfw_install_exp.sh           build_libvcamcaptured() helper
                                       (clang + CoreMedia/CoreVideo/
                                       Foundation, ldid-signed) and
                                       new [JB-4.1] section that
                                       scp's the dylib + plist into
                                       procursus/Library/MobileSubstrate/
                                       DynamicLibraries.

Pairs with the host vphone-cli camera server + vphoned vcam vsock
listener already in this branch.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-20 01:12:16 +03:00

175 lines
6.1 KiB
Objective-C

#import <Foundation/Foundation.h>
#import <dlfcn.h>
#import <fcntl.h>
#import <stdarg.h>
#import <unistd.h>
static NSString *const kTweakDir = @"/var/jb/Library/MobileSubstrate/DynamicLibraries";
static NSString *const kLogDir = @"/var/jb/var/mobile/Library/TweakLoader";
static NSString *const kLogPath = @"/var/jb/var/mobile/Library/TweakLoader/tweakloader.log";
static void TLLog(NSString *format, ...) {
va_list args;
va_start(args, format);
NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
va_end(args);
if (!message.length) return;
[[NSFileManager defaultManager] createDirectoryAtPath:kLogDir
withIntermediateDirectories:YES
attributes:nil
error:nil];
NSString *line = [NSString stringWithFormat:@"%@ [TweakLoader] %@\n",
[NSDate.date description],
message];
NSData *data = [line dataUsingEncoding:NSUTF8StringEncoding];
if (!data.length) return;
int fd = open(kLogPath.fileSystemRepresentation, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd < 0) return;
(void)write(fd, data.bytes, data.length);
close(fd);
}
static NSString *TLExecutableName(void) {
NSString *argv0 = NSProcessInfo.processInfo.arguments.firstObject;
if (argv0.length) return argv0.lastPathComponent;
return NSProcessInfo.processInfo.processName ?: @"unknown";
}
static NSString *TLExecutablePath(void) {
NSString *argv0 = NSProcessInfo.processInfo.arguments.firstObject;
return argv0 ?: @"";
}
// Daemons the loader is explicitly allowed to load tweaks into, even though
// they're not under a `.app/` path. Keep this list narrow — every daemon
// added here is a process where a buggy tweak can crash a launchd-managed
// service and destabilize boot.
//
// This list ONLY applies to tweaks WITHOUT Filter.Frameworks. Framework-
// filtered tweaks engage in every process and self-limit based on which
// frameworks are actually loaded — they don't need per-daemon entries here.
static NSString *const kVPhoneAllowedDaemonPaths[] = {
@"/usr/libexec/cameracaptured", // libvcamcaptured (Filter.Executables match)
};
static BOOL TLShouldRunInCurrentProcess(void) {
NSString *execPath = TLExecutablePath();
if (!execPath.length) return NO;
// vphone's hook runtime injects broadly, including launch-critical daemons
// like xpcproxy, logd, notifyd, sshd, shells, and helper tools. Restrict the
// user tweak loader to app binaries only — plus an allowlist of daemons
// that explicitly opt in (see `kVPhoneAllowedDaemonPaths`).
if ([execPath containsString:@".app/"]) return YES;
for (size_t i = 0;
i < sizeof(kVPhoneAllowedDaemonPaths) /
sizeof(kVPhoneAllowedDaemonPaths[0]);
i++) {
if ([execPath isEqualToString:kVPhoneAllowedDaemonPaths[i]]) return YES;
}
return NO;
}
static BOOL TLArrayContainsString(id obj, NSString *value) {
if (![obj isKindOfClass:[NSArray class]] || !value.length) return NO;
for (id item in (NSArray *)obj) {
if ([item isKindOfClass:[NSString class]] &&
[(NSString *)item isEqualToString:value]) {
return YES;
}
}
return NO;
}
static BOOL TLFilterMatches(NSDictionary *plist, NSString *bundleID, NSString *executableName) {
NSDictionary *filter = [plist isKindOfClass:[NSDictionary class]] ? plist[@"Filter"] : nil;
if (![filter isKindOfClass:[NSDictionary class]]) {
return YES;
}
id bundles = filter[@"Bundles"];
if ([bundles isKindOfClass:[NSArray class]]) {
if (!bundleID.length || !TLArrayContainsString(bundles, bundleID)) {
return NO;
}
}
id executables = filter[@"Executables"];
if ([executables isKindOfClass:[NSArray class]]) {
if (!executableName.length || !TLArrayContainsString(executables, executableName)) {
return NO;
}
}
return YES;
}
static void TLLoadTweaks(void) {
NSFileManager *fm = NSFileManager.defaultManager;
NSString *execPath = TLExecutablePath();
NSString *bundleID = NSBundle.mainBundle.bundleIdentifier ?: @"";
NSString *executableName = TLExecutableName();
if (!TLShouldRunInCurrentProcess()) {
return;
}
NSArray<NSString *> *files = [fm contentsOfDirectoryAtPath:kTweakDir error:nil];
if (!files.count) {
TLLog(@"No tweak files found for bundle=%@ exec=%@ path=%@",
bundleID, executableName, execPath);
return;
}
TLLog(@"Scanning %lu tweak entries for bundle=%@ exec=%@ path=%@",
(unsigned long)files.count, bundleID, executableName, execPath);
for (NSString *filename in files) {
if (![filename.pathExtension isEqualToString:@"plist"]) continue;
NSString *plistPath = [kTweakDir stringByAppendingPathComponent:filename];
NSDictionary *plist = [NSDictionary dictionaryWithContentsOfFile:plistPath];
if (![plist isKindOfClass:[NSDictionary class]]) {
TLLog(@"Skipping unreadable plist %@", plistPath);
continue;
}
if (!TLFilterMatches(plist, bundleID, executableName)) {
continue;
}
NSString *baseName = filename.stringByDeletingPathExtension;
NSString *dylibPath = [[kTweakDir stringByAppendingPathComponent:baseName]
stringByAppendingPathExtension:@"dylib"];
if (![fm isExecutableFileAtPath:dylibPath]) {
TLLog(@"Skipping %@ because dylib is missing or not executable", dylibPath);
continue;
}
void *handle = dlopen(dylibPath.fileSystemRepresentation, RTLD_NOW | RTLD_GLOBAL);
if (handle) {
TLLog(@"Loaded %@", dylibPath);
} else {
const char *err = dlerror();
TLLog(@"Failed to load %@: %s", dylibPath, err ?: "unknown error");
}
}
}
__attribute__((constructor))
static void TweakLoaderInit(void) {
@autoreleasepool {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
TLLoadTweaks();
});
}
}