Discovered while debugging the Exceptions/ForeignThread/ForeignThreadExceptions/ForeignThreadExceptions.dll test.
The test creates a new thread which invokes JIT managed code.
https://github.com/dotnet/runtime/blob/00d0dba14ed9927b22de224de94d812449529f17/src/tests/Exceptions/ForeignThread/ForeignThreadExceptionsNative.cpp#L49-L60
(lldb) bt
* thread #1, queue = 'com.apple.main-thread'
* frame #0: 0x00000001d51db3d0 libsystem_kernel.dylib`__ulock_wait + 8
frame #1: 0x00000001d528ec3c libsystem_pthread.dylib`_pthread_join + 452
frame #2: 0x0000000102377ed8 libForeignThreadExceptionsNative.dylib`::InvokeCallbackOnNewThread(callback=(0x000000028003c210)) at ForeignThreadExceptionsNative.cpp:60:10
frame #3: 0x000000028137ce34
frame #4: 0x000000028137cbac
frame #5: 0x000000028137c914
frame #6: 0x000000010163f40c libcoreclr.dylib`CallDescrWorkerInternal at calldescrworkerarm64.S:71
The new thread is failing when it tries to execute the managed code.
* thread #9, stop reason = EXC_BAD_ACCESS (code=2, address=0x28003c210)
frame #0: 0x000000028003c210
* frame #1: 0x0000000102377e38 libForeignThreadExceptionsNative.dylib`InvokeCallbackUnix(callback=0x000000028003c210) at ForeignThreadExceptionsNative.cpp:31:5
frame #2: 0x00000001d528d0f8 libsystem_pthread.dylib`_pthread_start + 320
(lldb) mem region 0x000000028003c210
[0x000000028003c000-0x0000000280040000) rwx
This is apparently occurring because the new thread does not default to pthread_jit_write_protect_np(true).
I believe we should try to have Apple set a default which allows new threads to execute JIT code.
/cc @janvorli
I think we can handle that ourselves by calling the pthread_jit_write_protect_np in TheUMEntryPrestubWorker or maybe CreateThreadBlockThrow to establish the initial state.
I don't see how that can happen on the clients new thread which gets a pointer to our managed code.
InvokeCallbackOnNewThread(callback=(0x000000028003c210))
Unless all delegates get that as a wrapper and the wrapper is not in JIT generated code
I did wonder if we could hijack the exception and set the JIT write enable state in this case. I suspect we can, but it may introduce unnecessary security risk. A sane default seems safer.
All the callbacks go through the asm TheUMEntryPrestub that then calls the c++ TheUMEntryPrestubWorker.
UnmanagedCallersOnly methods don't go through TheUMEntryPrestub. They instead go through PreStubWorker_Preemptive IIRC.
Right, I was referring to regular callbacks.
I didn't find a test which failed due to PreStubWorker_Preemptive call from a foreign thread, perhaps there is no test coverage?
Did you run src\tests\Interop\UnmanagedCallersOnly ?
I think it was run.... I just reran it now too. Looks like there should be coverage for the new thread case.
The entry path is a little different, so perhaps it was getting JIT execute enabled someplace else....
Re-opened based on comments here https://github.com/dotnet/runtime/pull/40435#discussion_r485935856