Some NSObject methods crash if used from constructor functions
liuyi12138 opened this issue · 14 comments
I use constructor function in my dylib like this:
#import <Foundation/Foundation.h>
__attribute__((constructor)) static void insert_entry() {
NSLog(@"hello!");
}
When I try to insert dylib into ls use: HC_INSERT_LIBRARY=insert.dylib ls
then segmentation fault happened, i find that ls call _malloc_zone_malloc_instrumented_or_legacy
recursively
[1] 38152 segmentation fault HC_INSERT_LIBRARY= ls -G
but if i use printf, everything is ok. I think maybe Hookcase caued malloc error?
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 libsystem_malloc.dylib 0x7ff81525404a _malloc_zone_malloc_instrumented_or_legacy + 84
1 libsystem_malloc.dylib 0x7ff81525404e _malloc_zone_malloc_instrumented_or_legacy + 88
2 libsystem_malloc.dylib 0x7ff81525404e _malloc_zone_malloc_instrumented_or_legacy + 88
3 libsystem_malloc.dylib 0x7ff81525404e _malloc_zone_malloc_instrumented_or_legacy + 88
4 libsystem_malloc.dylib 0x7ff81525404e _malloc_zone_malloc_instrumented_or_legacy + 88
5 libsystem_malloc.dylib 0x7ff81525404e _malloc_zone_malloc_instrumented_or_legacy + 88
6 libsystem_malloc.dylib 0x7ff81525404e _malloc_zone_malloc_instrumented_or_legacy + 88
7 libsystem_malloc.dylib 0x7ff81525404e _malloc_zone_malloc_instrumented_or_legacy + 88
8 libsystem_malloc.dylib 0x7ff81525404e _malloc_zone_malloc_instrumented_or_legacy + 88
9 libsystem_malloc.dylib 0x7ff81525404e _malloc_zone_malloc_instrumented_or_legacy + 88
10 libsystem_malloc.dylib 0x7ff81525404e _malloc_zone_malloc_instrumented_or_legacy + 88
I can reproduce your problem, even with a minimal hook library (one that cuts out most of the code from my hook library template, and contains just one hook, an interpose hook for NSPushAutoreleasePool()
). I can't reproduce it with Apple's DYLD_INSERT_LIBRARIES
(after slightly altering my minimal hook library to use that functionality). So I guess this must really be a bug in HookCase. But it will be difficult to figure out, and I probably won't be able to fix it anytime soon.
Fortunately there's an easy workaround -- use LogWithFormat()
instead of NSLog()
.
You've already discovered another workaround, printf()
. But NSLog()
is much more powerful, and LogWithFormat()
has all that power. I made LogWithFormat()
emulate NSLog()
, so it's very puzzling that LogWithFormat()
doesn't trigger the bug. But it's also very fortunate.
Almost nothing now works with DYLD_INSERT_LIBRARIES
. I ended up having to create a simple command line utility to test it with.
Here's the minimal hook library that I tested with.
There are also no problems just loading my minimal hook library into a simple command line utility using dlopen()
-- NSLog()
works without any problems. Though, of course, no hooks get loaded when you do this.
Another workaround:
If you're using my hook library template, add a call to Initialize_CF_If_Needed()
just before your call to NSLog()
. Then it works fine.
So the reason NSLog()
crashes is that the CoreFoundation framework hasn't yet been initialized. Initialize_CF_If_Needed()
is called from loadHandler::loadHandler()
. But apparently it hasn't yet been called when your insert_entry()
runs.
I'm going to close this. The timing of CoreFoundation framework initialization is complicated, but it isn't really a bug. You just need to know that it might be an issue.
Another workaround:
If you're using my hook library template, add a call to
Initialize_CF_If_Needed()
just before your call toNSLog()
. Then it works fine.So the reason
NSLog()
crashes is that the CoreFoundation framework hasn't yet been initialized.Initialize_CF_If_Needed()
is called fromloadHandler::loadHandler()
. But apparently it hasn't yet been called when yourinsert_entry()
runs.
this way works, i think maybe HookCase insert earlier than use DYLD_INSERT_LIBRARIES
cause that.
Anyway, it solved my problem, thanks.
If anything, C++ initializers run a little later with HookCase than with DYLD_INSERT_LIBRARIES
. But (to work properly) HookCase needs to interrupt the process whereby dyld
initializes each executable after it loads, load its hooks, then allow dyld
to resume afterwards. This has always been complicated, and has only gotten more so over time (with newer versions of macOS). I've done my best not to mess things up, and I've generally succeeded in this. But there has been some fallout -- for example the CoreFoundation framework doesn't get initialized until later than normal, or perhaps at all. I've worked around this with Initialize_CF_If_Needed()
.
On macOS 12 (Monterey) and up, I need to stop the hook library's C++ initializers running when they normally would, then run them explicitly later (after HookCase's hooks have been set). But just now, playing around with my minimal test library in lldb
(using both HookCase and DYLD_INSERT_LIBRARIES
), I noticed that there's a dyld4::RuntimeState::notifyObjCInit()
method that doesn't run again when the C++ initializers are run explicitly. Maybe it would help if I ran that explicitly, too.
I'll try this out and see what happens. But I may find that it has other, undesirable side effects. In which case it will be best to keep things as they are, since there's already an easy workaround.
Sorry, I'm afriad the workaround doesn‘t apply to every situation.
I use [NSBundle mainBundle]
in constructor function, and it crash again.
I think maybe another framework doesn't get initialized. Is there a way to load all the framework I need ?
Termination Reason: Namespace SIGNAL, Code 11 Segmentation fault: 11
Terminating Process: exc handler [12427]
VM Region Info: 0x7ff7bf31bff8 is in 0x7ff7bbb1c000-0x7ff7bf31c000; bytes after start: 58720248 bytes before end: 7
REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL
MALLOC_SMALL 7fd589800000-7fd58a000000 [ 8192K] rw-/rwx SM=PRV
GAP OF 0x2231b1c000 BYTES
---> STACK GUARD 7ff7bbb1c000-7ff7bf31c000 [ 56.0M] ---/rwx SM=NUL ... for thread 0
Stack 7ff7bf31c000-7ff7bfb1c000 [ 8192K] rw-/rwx SM=PRV thread 0
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 Foundation 0x7ff802a406db +[NSString stringWithFormat:] + 124
1 Foundation 0x7ff80314e92c _NSRequestConcreteObject + 81
2 Foundation 0x7ff8030796ed -[NSString initWithFormat:locale:arguments:] + 14
3 Foundation 0x7ff802a406f8 +[NSString stringWithFormat:] + 153
4 Foundation 0x7ff80314e92c _NSRequestConcreteObject + 81
5 Foundation 0x7ff8030796ed -[NSString initWithFormat:locale:arguments:] + 14
6 Foundation 0x7ff802a406f8 +[NSString stringWithFormat:] + 153
7 Foundation 0x7ff80314e92c _NSRequestConcreteObject + 81
And when I only use NSLog to console a NSString, I find the result is not what i except like this:
2023-03-15 14:32:46.829 ls[12694:309501] <CFString 0x6000032a6840 [0x7ff844bd3f80]>{contents = "ls"}
I think it is casud by the same reason.
I use
[NSBundle mainBundle]
in constructor function, and it crash again.
I'll try this and let you know my results. But yes, it does look like my initializer problem is deeper than it first appeared.
What version of macOS are you using? I've found that the original bug you reported doesn't happen on macOS 11 (BigSur) -- only on macOS 12 (Monterey) and (presumably) macOS 13 (Ventura). This fits with my hunch that there are some initializers that I'm not running explicitly, on macOS 12 and 13. I didn't need to do this on macOS 11 and earlier.
There are comments about "Objective-C +load methods" in Apple's dyld
and libobjc.A.dylib
source code. I suspect these are what I'm missing. I'll be working on this.
I use
[NSBundle mainBundle]
in constructor function, and it crash again.
Yes, I crash if I put this in your constructor function (on macOS 12). I get the crash you reported if I first call Initialize_CF_If_Needed()
. Otherwise I get the _malloc_zone_malloc
stack exhaustion crash you originally reported.
I also see the following error in the console after I run ls
(and get your _NSRequestConcreteObject
crash). This tends to confirm that I'm missing some "Objective-C initializers".
2023-03-15 10:37:35.238 ls[1171:36162] -[__NSCFString _stringByResolvingSymlinksInPathUsingCache:]: unrecognized selector sent to instance 0x60000352c9a0
I'm still working on this. It'll be a while before I finish. But in the meantime I've found another workaround:
Call [NSObject load]
in your constructor function before you call anything else. That seems to work for both NSLog()
and [NSBundle mainBundle]
.
As best I can tell the problem is that (as you suspected) I'm not calling (or triggering calls to) enough initializers. So far I've been doing it only for the hook library itself. I also need to do it (or arrange it to happen) for all its dependencies. Without intervention, Monterey's and Ventura's dyld
ignores these, because they're all loaded "before" the main executable. That is if they're loaded via HC_INSERT_LIBRARY
and not DYLD_INSERT_LIBRARIES
.
thanks, it works for me.
I've just released HookCase 7.1.2, which should fix this problem. You should no longer need the [NSObject load]
workaround.
I've just released HookCase 7.1.2, which should fix this problem. You should no longer need the
[NSObject load]
workaround.
It's proven to work for me, thanks!