Repository navigation
Conversation
Capture the backtraces of all threads through the debugger, so it works
even when all of Frida's threads in the target are blocked. Frames are
expressed as module path plus offset so they can be symbolicated
offline.
The result is a dictionary with a `threads` array, one entry per thread:
```json
{
"threads": [
{
"id": "0x4dee1",
"name": "gum-js-loop",
"registers": { "x0": "0x...", ..., "cpsr": "0x..." },
"frames": [
{ "address": "0x1a2b3c4d0", "module": "/a/b", "offset": 46 },
{ "address": "0x110a28210" }
]
}
]
}
```
Addresses, thread IDs and register values are hex strings, so the result
survives JSON serialization. Frames are innermost first: the PC, the LR,
then the frame-pointer chain. A frame has `module` and `offset` only if
it falls inside a known module. Frames inside Gadget are attributed to
the local Gadget file that was injected, or to `FridaGadget.dylib` if it
was already loaded. The registers are the general-purpose ones reported
by debugserver. If a thread can't be fully walked, it also has an
`error` and keeps the frames collected so far.
Gadget only routes `EXC_BREAKPOINT` to the debugger, so debugserver's
interrupt never arrives once it's loaded. When spawned with the new
`interruptible` option, Gadget therefore starts a thread that executes
a breakpoint whenever someone connects to it, passing along its own
address range so its frames can be attributed. The injector finds the
thread through its name. It makes its socket calls as raw syscalls and
guards its state with atomics, so hooks on libc functions can't drag it
into a blocked Frida thread.
The breakpoint is disarmed when Gadget detaches, so it can't fire
without a debugger to handle it, and the injector re-arms it when
reattaching to an existing Gadget.
mrmacete
marked this pull request as draft
November 12, 2026 12:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Capture the backtraces of all threads through the debugger, so it works even when all of Frida's threads in the target are blocked. Frames are expressed as module path plus offset so they can be symbolicated offline.
The result is a dictionary with a
threadsarray, one entry per thread:{ "threads": [ { "id": "0x4dee1", "name": "gum-js-loop", "registers": { "x0": "0x...", ..., "cpsr": "0x..." }, "frames": [ { "address": "0x1a2b3c4d0", "module": "/a/b", "offset": 46 }, { "address": "0x110a28210" } ] } ] }Addresses, thread IDs and register values are hex strings, so the result survives JSON serialization. Frames are innermost first: the PC, the LR, then the frame-pointer chain. A frame has
moduleandoffsetonly if it falls inside a known module. Frames inside Gadget are attributed to the local Gadget file that was injected, or toFridaGadget.dylibif it was already loaded. The registers are the general-purpose ones reported by debugserver. If a thread can't be fully walked, it also has anerrorand keeps the frames collected so far.Gadget only routes
EXC_BREAKPOINTto the debugger, so debugserver's interrupt never arrives once it's loaded. When spawned with the newinterruptibleoption, Gadget therefore starts a thread that executes a breakpoint whenever someone connects to it, passing along its own address range so its frames can be attributed. The injector finds the thread through its name. It makes its socket calls as raw syscalls and guards its state with atomics, so hooks on libc functions can't drag it into a blocked Frida thread.The breakpoint is disarmed when Gadget detaches, so it can't fire without a debugger to handle it, and the injector re-arms it when reattaching to an existing Gadget.