Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
618d29f
Disable watchdoghook installation for A/B panic test
7xj1998-cell Aug 12, 2026
a533eb4
Disable launchdhook initializer for A/B panic test
7xj1998-cell Aug 12, 2026
4fe3b31
Preserve boomerang handoff and skip launchd hooks for A/B test
7xj1998-cell Aug 12, 2026
2960352
Create opainject-cleanup.patch
7xj1998-cell Aug 13, 2026
5fd81a7
Update opainject-build.patch
7xj1998-cell Aug 13, 2026
3292c8b
Fix opainject unified diff hunk counts
7xj1998-cell Aug 13, 2026
17f6f68
Fix opainject failure cleanup before rebuild
7xj1998-cell Aug 13, 2026
ca23ee7
Fix opainject failure cleanup before rebuild
7xj1998-cell Aug 13, 2026
7d1746c
Fix opainject patch hunk counts
7xj1998-cell Aug 13, 2026
af67247
Normalize opainject unified diff formatting
7xj1998-cell Aug 13, 2026
456428d
Fix final opainject diff hunk count
7xj1998-cell Aug 13, 2026
561abc4
Fix arbCall wait hunk context
7xj1998-cell Aug 13, 2026
c7f04d2
Correct final diff hunk count after arbCall fix
7xj1998-cell Aug 13, 2026
e2fc687
Fix opainject remote stack lifetime before thread restore
7xj1998-cell Aug 13, 2026
93d6b95
Fix opainject patch hunk context
7xj1998-cell Aug 13, 2026
719b1d6
Fix literal tab in opainject patch context
7xj1998-cell Aug 13, 2026
bf28139
Normalize opainject remote stack reorder hunk
7xj1998-cell Aug 13, 2026
ab8009e
Fix opainject patch indentation context
7xj1998-cell Aug 13, 2026
bfc7a0f
Fix opainject launchd handoff cleanup
7xj1998-cell Aug 14, 2026
56d891f
Prevent launchd reset during jailbreak handoff
7xj1998-cell Aug 14, 2026
6aa144a
Fix ARC boomerang server cleanup
7xj1998-cell Aug 14, 2026
8f6b609
Prepare Dopamine V6.0 build
7xj1998-cell Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 37 additions & 17 deletions .github/patches/opainject-build.patch
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,17 @@ diff --git a/rop_inject.m b/rop_inject.m
index 77acb84..f125a72 100644
--- a/rop_inject.m
+++ b/rop_inject.m
@@ -293,12 +293,22 @@ kern_return_t arbCall(task_t task, thread_act_t targetThread, uint64_t* retOut,
@@ -293,13 +293,22 @@ kern_return_t arbCall(task_t task, thread_act_t targetThread, uint64_t* retOut,
thread_act_array_t cachedThreads;
mach_msg_type_number_t cachedThreadCount;
kr = task_threads(task, &cachedThreads, &cachedThreadCount);
- if (kr != KERN_SUCCESS) return kr;
+ if (kr != KERN_SUCCESS) {
+ free(origThreadFullState);
+ // The target thread was already rewritten for the remote call.
+ // Restore it before returning or launchd may continue with a bad PC/SP.
+ thread_abort(targetThread);
+ thread_restore_state_arm64(targetThread, origThreadFullState);
+ origThreadFullState = NULL; // thread_restore_state_arm64 consumes it
+ vm_deallocate(task, remoteStack, STACK_SIZE);
+ thread_resume(targetThread);
+ return kr;
Expand All @@ -132,15 +136,19 @@ index 77acb84..f125a72 100644
printf("[arbCall] Started thread, waiting for it to finish...\n");

// wait for arbitary call to finish (or not)
@@ -310,6 +320,11 @@ kern_return_t arbCall(task_t task, thread_act_t targetThread, uint64_t* retOut,
@@ -310,7 +320,15 @@ kern_return_t arbCall(task_t task, thread_act_t targetThread, uint64_t* retOut,
if(kr != KERN_SUCCESS)
{
free(origThreadFullState);
printf("[arbCall] ERROR: failed to wait for thread to finish: %s\n", mach_error_string(kr));
+ if (kr == KERN_OPERATION_TIMED_OUT) {
+ printf("[arbCall] Remote call still active; preserving its stack and thread state\n");
+ return kr;
+ }
- free(origThreadFullState);
+ // The target was resumed before waiting. Fail closed: abort the
+ // remote call, restore the original context, then release its stack.
+ thread_suspend(targetThread);
+ thread_abort(targetThread);
+ thread_restore_state_arm64(targetThread, origThreadFullState);
+ origThreadFullState = NULL; // thread_restore_state_arm64 consumes it
+ vm_deallocate(task, remoteStack, STACK_SIZE);
+ thread_resume(targetThread);
printf("[arbCall] ERROR: failed to wait for thread to finish: %s\n", mach_error_string(kr));
return kr;
}

Expand Down Expand Up @@ -187,8 +195,11 @@ index 77acb84..f125a72 100644
- arbCall(task, pthread, (uint64_t*)&readExtensionRet, true, sandbox_extension_consumeAddr, 1, remoteExtString);
- vm_deallocate(task, remoteExtString, remoteExtStringSize);
+ kern_return_t kr = arbCall(task, pthread, (uint64_t*)&readExtensionRet, true, sandbox_extension_consumeAddr, 1, remoteExtString);
+ if (kr != KERN_OPERATION_TIMED_OUT) vm_deallocate(task, remoteExtString, remoteExtStringSize);
+ if (kr != KERN_SUCCESS) return kr;
+ vm_deallocate(task, remoteExtString, remoteExtStringSize);
+ if (kr != KERN_SUCCESS) {
+ thread_terminate(pthread);
+ return kr;
+ }

printf("[sandboxFixup] sandbox_extension_consume returned %lld for read extension\n", (int64_t)readExtensionRet);
- retval |= (readExtensionRet <= 0);
Expand All @@ -212,8 +223,11 @@ index 77acb84..f125a72 100644
- arbCall(task, pthread, (uint64_t*)&executableExtensionRet, true, sandbox_extension_consumeAddr, 1, remoteExtString);
- vm_deallocate(task, remoteExtString, remoteExtStringSize);
+ kern_return_t kr = arbCall(task, pthread, (uint64_t*)&executableExtensionRet, true, sandbox_extension_consumeAddr, 1, remoteExtString);
+ if (kr != KERN_OPERATION_TIMED_OUT) vm_deallocate(task, remoteExtString, remoteExtStringSize);
+ if (kr != KERN_SUCCESS) return kr;
+ vm_deallocate(task, remoteExtString, remoteExtStringSize);
+ if (kr != KERN_SUCCESS) {
+ thread_terminate(pthread);
+ return kr;
+ }

printf("[sandboxFixup] sandbox_extension_consume returned %lld for executable extension\n", (int64_t)executableExtensionRet);
- retval |= (executableExtensionRet <= 0);
Expand Down Expand Up @@ -243,7 +257,7 @@ index 77acb84..f125a72 100644
+ kr = sandboxFixup(task, pthread, pid, dylibPath, allImageInfoAddr);
+ if (kr != KERN_SUCCESS) {
+ printf("[injectDylibViaRop] ERROR: sandbox fixup failed: %s\n", mach_error_string(kr));
+ if (kr != KERN_OPERATION_TIMED_OUT) thread_terminate(pthread);
+ thread_terminate(pthread);
+ return kr;
+ }

Expand All @@ -268,8 +282,11 @@ index 77acb84..f125a72 100644
- vm_deallocate(task, remoteDylibPath, remoteDylibPathSize);
+ void* dlopenRet = NULL;
+ kr = arbCall(task, pthread, (uint64_t*)&dlopenRet, true, dlopenAddr, 2, remoteDylibPath, RTLD_NOW);
+ if (kr != KERN_OPERATION_TIMED_OUT) vm_deallocate(task, remoteDylibPath, remoteDylibPathSize);
+ if (kr != KERN_SUCCESS) return kr;
+ vm_deallocate(task, remoteDylibPath, remoteDylibPathSize);
+ if (kr != KERN_SUCCESS) {
+ thread_terminate(pthread);
+ return kr;
+ }

if (dlopenRet) {
printf("[injectDylibViaRop] dlopen succeeded, library handle: %p\n", dlopenRet);
Expand All @@ -278,7 +295,10 @@ index 77acb84..f125a72 100644
uint64_t remoteErrorString = 0;
- arbCall(task, pthread, (uint64_t*)&remoteErrorString, true, dlerrorAddr, 0);
+ kr = arbCall(task, pthread, (uint64_t*)&remoteErrorString, true, dlerrorAddr, 0);
+ if (kr != KERN_SUCCESS) return kr;
+ if (kr != KERN_SUCCESS) {
+ thread_terminate(pthread);
+ return kr;
+ }
char *errorString = task_copy_string(task, remoteErrorString);
printf("[injectDylibViaRop] dlopen failed, error:\n%s\n", errorString);
free(errorString);
Expand Down
25 changes: 25 additions & 0 deletions .github/patches/opainject-cleanup.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
diff --git a/rop_inject.m b/rop_inject.m
--- a/rop_inject.m
+++ b/rop_inject.m
@@
kr = task_threads(task, &cachedThreads, &cachedThreadCount);
if (kr != KERN_SUCCESS) {
+ // The target thread was already rewritten for the remote call.
+ // Restore it before returning or launchd may continue with a bad PC/SP.
+ thread_abort(targetThread);
+ thread_restore_state_arm64(targetThread, origThreadFullState);
free(origThreadFullState);
vm_deallocate(task, remoteStack, STACK_SIZE);
thread_resume(targetThread);
@@
if(kr != KERN_SUCCESS)
{
+ if (kr != KERN_OPERATION_TIMED_OUT) {
+ // All non-timeout errors must restore the hijacked target thread.
+ thread_suspend(targetThread);
+ thread_abort(targetThread);
+ thread_restore_state_arm64(targetThread, origThreadFullState);
+ thread_resume(targetThread);
+ }
free(origThreadFullState);
printf("[arbCall] ERROR: failed to wait for thread to finish: %s\n", mach_error_string(kr));
12 changes: 12 additions & 0 deletions .github/patches/opainject-serial-cleanup.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
diff --git a/rop_inject.m b/rop_inject.m
index f125a72..d7c0b1a 100644
--- a/rop_inject.m
+++ b/rop_inject.m
@@ -349,3 +349,0 @@
- // release fake stack as it's no longer needed
- vm_deallocate(task, remoteStack, STACK_SIZE);
-
@@ -369,0 +367,3 @@
+ // Keep the temporary stack mapped until the target has been restored.
+ vm_deallocate(task, remoteStack, STACK_SIZE);
+
3 changes: 2 additions & 1 deletion .github/workflows/roothide.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ jobs:
run: |
set -euo pipefail
git -C BaseBin/XPF apply "$GITHUB_WORKSPACE/.github/patches/XPF-roothide.patch"
git -C BaseBin/opainject apply "$GITHUB_WORKSPACE/.github/patches/opainject-build.patch"
git -C BaseBin/opainject apply --recount "$GITHUB_WORKSPACE/.github/patches/opainject-build.patch"
git -C BaseBin/opainject apply --unidiff-zero "$GITHUB_WORKSPACE/.github/patches/opainject-serial-cleanup.patch"

# Xcode 15.4 is the last toolchain used by the original Dopamine 3
# project layout. Newer Xcodes treat some command-line targets as
Expand Down
66 changes: 51 additions & 15 deletions Application/Dopamine/Jailbreak/DOJailbreaker.m
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,10 @@ - (NSError *)loadBasebinTrustcache
{
while (true) {
xpc_object_t xdict = nil;
if (!xpc_pipe_receive(info->serverPort, &xdict)) {
if (xpc_pipe_receive(info->serverPort, &xdict)) {
break;
}
if (xdict) {
if (jbserver_received_boomerang_xpc_message(&gBoomerangServer, xdict) == JBS_BOOMERANG_DONE) {
dispatch_semaphore_signal(info->boomerangDone);
break;
Expand All @@ -389,51 +392,84 @@ - (NSError *)injectLaunchdHook
{
// Host a boomerang server that will be used by launchdhook to get the jailbreak primitives from this app
mach_port_t serverPort = MACH_PORT_NULL;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &serverPort);
mach_port_insert_right(mach_task_self(), serverPort, serverPort, MACH_MSG_TYPE_MAKE_SEND);

struct boomerang_info info;
info.serverPort = serverPort;
info.boomerangDone = dispatch_semaphore_create(0);
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &serverPort);
if (kr != KERN_SUCCESS) {
return [NSError errorWithDomain:JBErrorDomain code:JBErrorCodeFailedLaunchdInjection userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Allocating boomerang server port failed: %s", mach_error_string(kr)]}];
}
kr = mach_port_insert_right(mach_task_self(), serverPort, serverPort, MACH_MSG_TYPE_MAKE_SEND);
if (kr != KERN_SUCCESS) {
mach_port_destroy(mach_task_self(), serverPort);
return [NSError errorWithDomain:JBErrorDomain code:JBErrorCodeFailedLaunchdInjection userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Creating boomerang send right failed: %s", mach_error_string(kr)]}];
}

pthread_t boomerangThread;
pthread_create(&boomerangThread, NULL, (void *(*)(void *))boomerang_server, &info);
pthread_detach(boomerangThread);
struct boomerang_info *info = calloc(1, sizeof(*info));
info->serverPort = serverPort;
info->boomerangDone = dispatch_semaphore_create(0);

// Stash port to server in launchd's initPorts[2]
// Since we don't have the neccessary entitlements, we need to do it over jbctl
posix_spawnattr_t attr;
posix_spawnattr_init(&attr);
posix_spawnattr_set_registered_ports_np(&attr, (mach_port_t[]){MACH_PORT_NULL, MACH_PORT_NULL, serverPort}, 3);
int attrError = posix_spawnattr_set_registered_ports_np(&attr, (mach_port_t[]){MACH_PORT_NULL, MACH_PORT_NULL, serverPort}, 3);
if (attrError != 0) {
posix_spawnattr_destroy(&attr);
free(info);
mach_port_destroy(mach_task_self(), serverPort);
return [NSError errorWithDomain:JBErrorDomain code:JBErrorCodeFailedLaunchdInjection userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Preparing launchd port handoff failed with error code %d", attrError]}];
}
pid_t spawnedPid = 0;
const char *jbctlPath = JBROOT_PATH("/basebin/jbctl");
int spawnError = posix_spawn(&spawnedPid, jbctlPath, NULL, &attr, (char *const *)(const char *[]){ jbctlPath, "internal", "launchd_stash_port", NULL }, NULL);
posix_spawnattr_destroy(&attr);
if (spawnError != 0) {
free(info);
mach_port_destroy(mach_task_self(), serverPort);
return [NSError errorWithDomain:JBErrorDomain code:JBErrorCodeFailedLaunchdInjection userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Spawning jbctl failed with error code %d", spawnError]}];
}
posix_spawnattr_destroy(&attr);
int status = 0;
do {
if (waitpid(spawnedPid, &status, 0) == -1) {
free(info);
mach_port_destroy(mach_task_self(), serverPort);
return [NSError errorWithDomain:JBErrorDomain code:JBErrorCodeFailedLaunchdInjection userInfo:@{NSLocalizedDescriptionKey : @"Waiting for jbctl failed"}];;
}
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
free(info);
mach_port_destroy(mach_task_self(), serverPort);
return [NSError errorWithDomain:JBErrorDomain code:JBErrorCodeFailedLaunchdInjection userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"jbctl failed while stashing launchd port (status 0x%x)", status]}];
}

pthread_t boomerangThread;
int threadError = pthread_create(&boomerangThread, NULL, (void *(*)(void *))boomerang_server, info);
if (threadError != 0) {
free(info);
mach_port_destroy(mach_task_self(), serverPort);
return [NSError errorWithDomain:JBErrorDomain code:JBErrorCodeFailedLaunchdInjection userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Starting boomerang server failed with error code %d", threadError]}];
}

// Inject launchdhook.dylib into launchd via opainject
printf("[launchdhook-build] launchd-availability-v3\n");
int r = exec_cmd(JBROOT_PATH("/basebin/opainject"), "1", JBROOT_PATH("/basebin/launchdhook.dylib"), NULL);
if (r != 0) {
mach_port_destroy(mach_task_self(), serverPort);
pthread_cancel(boomerangThread);
pthread_detach(boomerangThread);
return [NSError errorWithDomain:JBErrorDomain code:JBErrorCodeFailedLaunchdInjection userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"opainject failed with error code %d", r]}];
}

// A broken launchd handoff must return an error instead of leaving the UI
// blocked forever after opainject has already exited.
dispatch_time_t handoffDeadline = dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC);
if (dispatch_semaphore_wait(info.boomerangDone, handoffDeadline) != 0) {
mach_port_deallocate(mach_task_self(), serverPort);
if (dispatch_semaphore_wait(info->boomerangDone, handoffDeadline) != 0) {
mach_port_destroy(mach_task_self(), serverPort);
pthread_cancel(boomerangThread);
pthread_detach(boomerangThread);
return [NSError errorWithDomain:JBErrorDomain code:JBErrorCodeFailedLaunchdInjection userInfo:@{NSLocalizedDescriptionKey : @"Timed out waiting for launchd primitive handoff"}];
}
mach_port_deallocate(mach_task_self(), serverPort);
pthread_join(boomerangThread, NULL);
free(info);
mach_port_destroy(mach_task_self(), serverPort);

return nil;
}
Expand Down
6 changes: 5 additions & 1 deletion BaseBin/launchdhook/src/main.m
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@ int sysctlbyname_hook(const char *name, void *oldp, size_t *oldlenp, void *newp,
if (err != 0) {
char msg[1000];
snprintf(msg, 1000, "Dopamine: Failed to recover primitives (error %d), cannot continue.", err);
abort_with_reason(7, 1, msg, 0);
// launchd is PID 1. Never terminate it because the handoff failed:
// doing so makes the kernel panic with "initproc exited" and reboots
// the device. Leave launchd untouched and report the failed attempt.
fprintf(stderr, "%s\n", msg);
return;
}

Expand Down Expand Up @@ -190,3 +193,4 @@ int sysctlbyname_hook(const char *name, void *oldp, size_t *oldlenp, void *newp,
roothide_launchd_postinit(firstLoad);
/********** roothide specfic ********/
}

6 changes: 3 additions & 3 deletions BaseBin/watchdoghook/src/main.m
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ kern_return_t IOConnectCallStructMethod_hook(mach_port_t connection, uint32_t se
{
/////////////////////////////
if(access("/var/log/.disable_watchdoghook", F_OK) == 0) {
return;
}
return;
}
///////////////////////////////

MSHookFunction(IOServiceOpen, (void *)&IOServiceOpen_hook, (void **)&IOServiceOpen_orig);
MSHookFunction(IOConnectCallStructMethod, (void *)&IOConnectCallStructMethod_hook, (void **)&IOConnectCallStructMethod_orig);
}
}