8000 gadget: Use ForkMonitor by wertyutreethgfd · Pull Request #1263 · frida/frida-core · GitHub
[go: up one dir, main page]

Skip to content

gadget: Use ForkMonitor - #1263

Open
wertyutreethgfd wants to merge 5 commits into
frida:mainfrom
wertyutreethgfd:fix/gadget-fork-monitor
Open

wertyutreethgfd wants to merge 5 commits into
frida:mainfrom
wertyutreethgfd:fix/gadget-fork-monitor

Conversation

@wertyutreethgfd
Copy link
Copy Markdown

oleavr asked for ForkMonitor in Gadget instead of skipping JS in unrecovered children (frida/frida-gum#1165).

Gadget never installed ForkMonitor. Agent already does, which is why this hang shows up on Gadget.

This is a thin ForkHandler: no frida-server, no child-gating, no session rebuild. On fork it pr 8000 epare/recover-s GLib, GIO, Gum, GumJS and recreates the Gadget worker. The old supports_async_exit check existed because that worker dies across an unnoticed fork.

Also:

  • clone / clone3 / libc syscall without CLONE_VM are treated as process creation (pthread clone with CLONE_VM is ignored)
  • nested clone inside libc fork is ignored (already FORKING)
  • vfork is still replaced with fork, same as the agent

Needs the gumjs recover follow-up so a mutex owned by a vanished thread, and fork-from-JS, actually recover instead of just restarting a scheduler that is still pointing at a dead thread.

JS listeners run in the child after recover. A short-lived helper that then calls Java.perform still needs a live ART; we do not skip JS there.

Fixes frida/frida-gum#1163

Gadget never prepared/recovered GumJS around fork(), so a JS interceptor
in a fork-without-exec child deadlocked on the QuickJS mutex and the
parent sat in waitpid.

Install a thin ForkHandler (no agent controller or child-gating):
prepare/recover GLib, GIO, Gum, GumJS and recreate the Gadget worker.

Also treat clone/clone3/syscall without CLONE_VM as process creation.
vfork is still redirected to fork, same as the agent.
wertyutreethgfd and others added 3 commits October 6, 2026 06:28
libc clone() and bionic __clone have different ABIs; treating flags as a
function pointer skipped process-creating clones. Mark the hooks
unignorable, install pthread_atfork as a fallback, and recover in the
clone(fn) child trampoline.
Joining the Gadget worker or gum-js-loop in the parent around fork()
wedges the host. Child recovery must also require a new pid so a
clone on_leave of 0 in the still-live parent does not reinitialize
locks. Ignore SYS_vfork; it shares the parent's address space.
Short-lived helpers are not a live session; waiting for the worker
leaves _exit stuck on g_cond_wait.
@wertyutreethgfd
Copy link
Copy Markdown
Author

Pushed a follow-up so short-lived fork children skip async exit teardown.

A helper is not a live Gadget session. supports_async_exit() posting unload to the worker leaves _exit stuck on g_cond_wait. Fork children now take the empty sync path (getpid() vs the pid captured at Gadget init). The host process is unchanged: parent still does not join gum-js-loop / the Gadget worker around fork().

@oleavr
oleavr commented Oct 9, 2026
Copy link
Copy Markdown
Member

Thanks for wiring this up, much appreciated!

One fundamental issue: GadgetForkHandler.prepare_to_fork() is empty, but the child still calls every recover_from_fork_in_child(). Those functions assume prepare ran. GLib's thread pool, for instance, resumes from state it only records in prepare. Skipping prepare is also what pushes all the extra complexity into frida/frida-gum#1169.

Could the handler mirror the agent's suspend_subsystems()/resume_subsystems(), plus stopping and restarting the Gadget worker? As for the wedge when joining gum-js-loop, I'd love to find the root cause. My guess is that the JS thread is waiting on the main thread, e.g. via Java.perform, while the main thread waits for it to stop. A stack trace of that would be great.

The clone/clone3/syscall() hooks, pthread_atfork and the clone trampoline would be better as a separate PR, if at all. Hooking every syscall() is a big cost to pay, and it isn't needed for frida/frida-gum#1163.

Thanks again! 🙏

@wertyutreethgfd
Copy link
Copy Markdown
Author

Tried mirroring Agent suspend_subsystems() / resume_subsystems() on Gadget: GLib/GIO/Gum/GumJS prepare, without joining gum-js-loop.

g_thread_pool_pause_all() in prepare copies pool mutexes into the child still looking held. Child recover then dies here:

g_mutex_lock_slowpath
g_thread_pool_resume_all
GadgetForkHandler.recover_from_fork_in_child
ForkMonitor.finish_process_creation

The helper never reaches _exit. Joining gum-js-loop from the forking thread is worse — JS in Java.perform waits for the main thread, main waits for JS to stop, which matches your guess.

So Gadget cannot use the Agent prepare/recover pairing. Empty prepare_to_fork() plus child-only recover (reset copied Gum locks) is what actually returns from waitpid.

clone() without CLONE_VM is what Android uses for this hang, so those hooks stay. Happy to move the syscall() hook to a follow-up.

pause_all copies held pool mutexes; child resume_all then blocks
in g_mutex_lock. Empty prepare plus child Gum recover is required.
@wertyutreethgfd
wertyutreethgfd force-pushed the fix/gadget-fork-monitor branch from 27f941f to a29f359 Compare October 11, 2026 22:02
@wertyutreethgfd
Copy link
Copy Markdown
Author

debuggerd -b of the leftover fork children after Gadget ran GLib prepare_to_fork() / g_thread_pool_pause_all() (no gum-js-loop join). Each dump is complete — the child only has the forking thread. Host package path redacted.

----- pid 17106 at 2026-10-12 05:39:36.842478437+0800 -----
Cmd line: <host>
ABI: 'arm64'

"Thread-1118" sysTid=17106
    #00 pc 0000000000b6e2b0  /data/app/.../lib/arm64/libfrida-gadget.so
    #01 pc 0000000000c290ac  /data/app/.../lib/arm64/libfrida-gadget.so
    #02 pc 0000000000c14c30  /data/app/.../lib/arm64/libfrida-gadget.so
    #03 pc 0000000000a85660  /data/app/.../lib/arm64/libfrida-gadget.so
    #04 pc 0000000000b6a594  /data/app/.../lib/arm64/libfrida-gadget.so
    #05 pc 0000000000a94a08  /data/app/.../lib/arm64/libfrida-gadget.so
    #06 pc 0000000000000200  <anonymous:71fd895000>

----- end 17106 -----

Second leftover, same frames:

----- pid 17108 at 2026-10-12 05:39:37.134592008+0800 -----
Cmd line: <host>
ABI: 'arm64'

"Thread-1118" sysTid=17108
    #00 pc 0000000000b6e2b0  /data/app/.../lib/arm64/libfrida-gadget.so
    #01 pc 0000000000c290ac  /data/app/.../lib/arm64/libfrida-gadget.so
    #02 pc 0000000000c14c30  /data/app/.../lib/arm64/libfrida-gadget.so
    #03 pc 0000000000a85660  /data/app/.../lib/arm64/libfrida-gadget.so
    #04 pc 0000000000b6a594  /data/app/.../lib/arm64/libfrida-gadget.so
    #05 pc 0000000000a94a08  /data/app/.../lib/arm64/libfrida-gadget.so
    #06 pc 0000000000000200  <anonymous:71fd895000>

----- end 17108 -----

llvm-addr2line on those PCs (unstripped Gadget):

#00 syscall                                    frida-core/lib/payload/syscall-linux-arm64.S:47
#01 g_mutex_lock_slowpath                      glib/gthread-posix.c:1602
#02 g_thread_pool_resume_all                   glib/gthreadpool.c:1423
#03 GadgetForkHandler.recover_from_fork_in_child  frida-core/lib/gadget/gadget.vala:785
#04 ForkMonitor.finish_process_creation        frida-core/lib/payload/fork-monitor.vala:277
#05 _gum_function_context_end_invocation       frida-gum/gum/guminterceptor.c:2254

Helpers never reach _exit. Pixel 5 / Android 14 / arm64, Gadget 17.22.0 QuickJS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gadget JS Interceptor deadlocks in fork() child; parent hangs in waitpid (Android arm64, 17.22)

2 participants

0