If you try to use frida-gadget on jailed iOS (or macOS with hardened runtime) without a debugger attached, the process crashes at launch. This happens even if the config file has "code_signing": "required" as specified in the documentation.
The cause is an initialization order issue. There are two relevant constructor functions: frida_libc_shim_init and frida_on_load. frida-core/lib/gadget/meson.build explicitly orders frida_libc_shim_init before frida_on_load. frida_on_load (-> frida_gadget_load) is responsible for loading the config file and setting gum_code_signing_policy and gum_teardown_requirement. But frida_libc_shim_init calls gum_init_embedded, which triggers several steps that try to patch code or execute unsigned code:
gum_try_drive_registry_via_dyld_internals -> gum_create_resident_notifier (image-load hook)
gum_try_drive_registry_via_dyld_internals -> interceptor API (image-unload hook)
_gum_unwind_broker_backend_activate -> interceptor API
Since frida_gadget_load has not run yet, gum_code_signing_policy and gum_teardown_requirement are both at their default values from gumprocess.c, namely GUM_CODE_SIGNING_OPTIONAL and GUM_TEARDOWN_REQUIREMENT_FULL. It doesn’t matter what the config file says since it hasn’t been loaded yet.
If a debugger is attached then the process starts successfully.
If I change the defaults in gumprocess.c to GUM_CODE_SIGNING_REQUIRED and GUM_TEARDOWN_REQUIREMENT_MINIMAL, then the process starts successfully without a debugger. Of course this is not a real fix.
Reproduction
I'm pretty sure I'm using frida-gadget the way it's meant to be used, but if I'm doing something wrong, let me know and I will hang my head in shame. :)
I reproduced this on iOS (27 beta, 24A5355q) as well as macOS (26.5, 25F71) using the hardened runtime.
For iOS, I used the tool Cyan to inject frida-gadget.dylib and frida-gadget.config into a preexisting app. The tool adds a LC_LOAD_WEAK_DYLIB command to the main executable to load frida-gadget.dylib, and re-signs both binaries.
For macOS, I created an artificial test case as follows. For this to work properly, you will need SIP enabled, and the codesigning certificate should be the same one used to build frida-gadget.
mkdir test Frameworks
echo 'int main() {}' > test/test.c
echo '<dict><key>com.apple.security.get-task-allow</key><true/></dict>' > test/entitlements.plist
rsync ~/frida/build/subprojects/frida-core/lib/gadget/frida-gadget.dylib Frameworks/FridaGadget.dylib
echo '{"interaction":{"type":"connect","address":"127.0.0.1","port":27052},"code_signing":"required"}' > Frameworks/FridaGadget.config
clang -o test/test test/test.c Frameworks/FridaGadget.dylib
codesign -f -s 8477F0AB655B6F922C20C214CE0E5A19AB97B222 -o runtime test/test --entitlements test/entitlements.plist
test/test
This should show test being killed, but it should succeed if launched under lldb.
Backtrace
With unmodified Frida (831acfffc87483c4a1d665b9b6ae89e66bcefaa2), the first crash happens here (.ips crashlog symbolicated with LLDB):
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=50, address=0x104de8000)
* frame #0: 0x000000018ee1067c libsystem_platform.dylib`sys_icache_invalidate + 40 [synthetic]
frame #1: 0x0000000109177763 FridaGadget.dylib`gum_clear_cache + 23 [synthetic]
frame #2: 0x000000010915b917 FridaGadget.dylib`gum_code_allocator_commit + 211 [synthetic]
frame #3: 0x0000000109161327 FridaGadget.dylib`gum_interceptor_transaction_end + 75 [synthetic]
frame #4: 0x000000010916186f FridaGadget.dylib`gum_interceptor_replace_with_type + 155 [synthetic]
frame #5: 0x00000001091617c7 FridaGadget.dylib`gum_interceptor_replace + 63 [synthetic]
frame #6: 0x000000010917c89b FridaGadget.dylib`_gum_unwind_broker_backend_activate + 119 [synthetic]
frame #7: 0x0000000109160143 FridaGadget.dylib`gum_unwind_broker_obtain at gumunwindbroker.c:126:5 [synthetic] [opt]
frame #8: 0x0000000109160e27 FridaGadget.dylib`gum_interceptor_obtain + 123 [synthetic]
frame #9: 0x0000000109159317 FridaGadget.dylib`gum_init_embedded + 219 [synthetic]
frame #10: 0x000000010924b1d3 FridaGadget.dylib`frida_libc_shim_init + 35 [synthetic]
frame #11: 0x000000018ea74e2f dyld`invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const::$_0::operator()() const + 179 [synthetic]
frame #12: 0x000000018ea7fef3 dyld`invocation function for block in dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const + 171 [synthetic]
frame #13: 0x000000018eab685f dyld`invocation function for block in mach_o::UnsafeHeader::forEachSection(void (mach_o::UnsafeHeader::SectionInfo const&, bool&) block_pointer) const + 311 [synthetic]
frame #14: 0x000000018eab3393 dyld`mach_o::UnsafeHeader::forEachLoadCommand(void (load_command const*, bool&) block_pointer) const + 207 [synthetic]
frame #15: 0x000000018eab4c3b dyld`mach_o::UnsafeHeader::forEachSection(void (mach_o::UnsafeHeader::SectionInfo const&, bool&) block_pointer) const + 123 [synthetic]
frame #16: 0x000000018ea86af3 dyld`dyld3::MachOFile::forEachInitializerPointerSection(Diagnostics&, void (unsigned int, unsigned int, bool&) block_pointer) const + 159 [synthetic]
frame #17: 0x000000018ea7fbb3 dyld`dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const + 431 [synthetic]
frame #18: 0x000000018ea6f7d3 dyld`dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const + 527 [synthetic]
frame #19: 0x000000018ea6840b dyld`dyld4::JustInTimeLoader::runInitializers(dyld4::RuntimeState&) const + 35 [synthetic]
frame #20: 0x000000018ea6feef dyld`dyld4::Loader::runInitializersBottomUp(dyld4::RuntimeState&, lsl::Vector<AuthenticatedValue<dyld4::Loader const*>>&, lsl::Vector<AuthenticatedValue<dyld4::Loader const*>>&) const + 567 [synthetic]
frame #21: 0x000000018ea6fde3 dyld`dyld4::Loader::runInitializersBottomUp(dyld4::RuntimeState&, lsl::Vector<AuthenticatedValue<dyld4::Loader const*>>&, lsl::Vector<AuthenticatedValue<dyld4::Loader const*>>&) const + 299 [synthetic]
frame #22: 0x000000018ea74ec7 dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&) const::$_0::operator()() const + 115 [synthetic]
frame #23: 0x000000018ea701d7 dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&) const + 715 [synthetic]
frame #24: 0x000000018ea4938f dyld`dyld4::prepare(dyld4::APIs&, mach_o::UnsafeHeader const*) + 3879 [synthetic]
frame #25: 0x000000018ea4844b dyld`dyld4::start(dyld4::KernelArgs*, void*, void*, unsigned long long)::$_1::operator()() const + 319 [synthetic]
frame #26: 0x000000018ea47da7 dyld`start + 6903 [synthetic]
If you try to use frida-gadget on jailed iOS (or macOS with hardened runtime) without a debugger attached, the process crashes at launch. This happens even if the config file has
"code_signing": "required"as specified in the documentation.The cause is an initialization order issue. There are two relevant constructor functions:
frida_libc_shim_initandfrida_on_load.frida-core/lib/gadget/meson.buildexplicitly ordersfrida_libc_shim_initbeforefrida_on_load.frida_on_load(->frida_gadget_load) is responsible for loading the config file and settinggum_code_signing_policyandgum_teardown_requirement. Butfrida_libc_shim_initcallsgum_init_embedded, which triggers several steps that try to patch code or execute unsigned code:gum_try_drive_registry_via_dyld_internals->gum_create_resident_notifier(image-load hook)gum_try_drive_registry_via_dyld_internals-> interceptor API (image-unload hook)_gum_unwind_broker_backend_activate-> interceptor APISince
frida_gadget_loadhas not run yet,gum_code_signing_policyandgum_teardown_requirementare both at their default values fromgumprocess.c, namelyGUM_CODE_SIGNING_OPTIONALandGUM_TEARDOWN_REQUIREMENT_FULL. It doesn’t matter what the config file says since it hasn’t been loaded yet.If a debugger is attached then the process starts successfully.
If I change the defaults in
gumprocess.ctoGUM_CODE_SIGNING_REQUIREDandGUM_TEARDOWN_REQUIREMENT_MINIMAL, then the process starts successfully without a debugger. Of course this is not a real fix.Reproduction
I'm pretty sure I'm using frida-gadget the way it's meant to be used, but if I'm doing something wrong, let me know and I will hang my head in shame. :)
I reproduced this on iOS (27 beta, 24A5355q) as well as macOS (26.5, 25F71) using the hardened runtime.
For iOS, I used the tool Cyan to inject
frida-gadget.dylibandfrida-gadget.configinto a preexisting app. The tool adds aLC_LOAD_WEAK_DYLIBcommand to the main executable to loadfrida-gadget.dylib, and re-signs both binaries.For macOS, I created an artificial test case as follows. For this to work properly, you will need SIP enabled, and the codesigning certificate should be the same one used to build frida-gadget.
This should show
testbeing killed, but it should succeed if launched underlldb.Backtrace
With unmodified Frida (831acfffc87483c4a1d665b9b6ae89e66bcefaa2), the first crash happens here (.ips crashlog symbolicated with LLDB):