Repository navigation
macOS: Doorstop cannot hook Unity 6000.3 Mono games — chained-fixups header parses as all zeros #108
Description
Summary
On macOS/Apple Silicon, Doorstop loads successfully into a Unity 6000.3.8f1 Mono game but never installs its dlsym hook, so mono_jit_init_version is never intercepted and the target assembly never runs. BepInEx produces no LogOutput.log and no config/ — it fails completely silently.
I traced this to read_chained_fixups() in plthook_osx.c returning an all-zero dyld_chained_fixups_header for UnityPlayer.dylib. I also tested Doorstop 4.3.0, which fails at a different point for a different reason — details below, since the pair of failures narrows things down.
Environment
| OS | macOS 27.0 (build 26A5388g), Darwin 27.0.0 |
| CPU | Apple M4 (arm64) |
| Game | Ages of Conflict: World War Simulator (Steam appid 2186320) |
| Unity | 6000.3.8f1, Mono backend (libmonobdwgc-2.0.dylib) |
| Doorstop | 4.5.0, via BepInEx 6.0.0-be.785+6abdba4 (Unity.Mono macos-x64) |
| Also tested | Doorstop 4.3.0, via BepInEx 5.4.23.2 (macos_x64) |
Both the game executable and UnityPlayer.dylib are universal (x86_64 arm64). libdoorstop.dylib from the be.785 build is also universal, so it loads natively on arm64.
Symptom
Doorstop injects fine — its PLTHOOK_DEBUG_ADDR output is emitted from inside the game process, and the game itself starts normally (Metal device created, PhysX initialised, Steam API connected). But no hook is ever installed, and no BepInEx artifacts are created:
BepInEx/
├── core/
├── patchers/
└── plugins/ <- config/ and LogOutput.log never appear
Diagnosis — Doorstop 4.5.0
plthook_osx.c takes the LC_DYLD_CHAINED_FIXUPS path for this binary (it has no LC_DYLD_INFO/LC_DYLD_INFO_ONLY). The PLTHOOK_DEBUG_FIXUPS dump shows the header parsing as effectively all zeros:
MEMORY MAP(.../Ages of Conflict.app/Contents/Frameworks/UnityPlayer.dylib)
dyld_chained_fixups_header
fixups_version 0
starts_offset 0
imports_offset 0
symbols_offset 0
imports_count 0 <- UnityPlayer.dylib has ~5000 symbols
imports_format 0
symbols_format 0
With imports_count == 0 no imports are enumerated, so dlsym is never located in the GOT and no hook is installed. Notably __got itself is found — the "__got section is not found in %s" path is never hit, so this is specifically the fixups parse, not section lookup.
For reference, the same dump for libdoorstop.dylib in the same run reports imports_count 1, also wrong.
Relevant binary layout (arm64 slice of UnityPlayer.dylib)
LC_DYLD_CHAINED_FIXUPS
dataoff 28934144
datasize 32304
segname __LINKEDIT
vmaddr 0x1d44000
vmsize 0xc4000
fileoff 28934144
filesize 799840
sectname __got
segname __DATA_CONST
addr 0x1ac0000
size 0x1cf0
offset 28049408
Note LC_DYLD_CHAINED_FIXUPS.dataoff is exactly equal to __LINKEDIT.fileoff — the header sits at the very start of __LINKEDIT. That makes this look like a linkedit_base computation problem: reading at the intended address lands on zeroed memory. Two candidates I could not distinguish from outside:
- The
slide + vmaddr - fileoffstyle base calculation misbehaving whendataoff == linkedit.fileoff. - The header being read before
__LINKEDITis mapped/valid for this image.
I'm reporting the observation rather than asserting the cause — the exact offsets above should make it reproducible.
Second data point — Doorstop 4.3.0 fails differently
Testing BepInEx 5.4.23.2 (Doorstop 4.3.0, x86_64-only dylib, run under Rosetta) is useful because it fails at an earlier stage:
Image name: .../UnityPlayer.dylib
symbol count: 138e <- parses the symbol table correctly
...
__DATA section __objc_const
__DATA section __data
__DATA section __bss
__DATA section __common <- only __DATA sections enumerated
At the v4.3.0 tag, plthook_osx.c only walks __LINKEDIT and __DATA, and only matches sections flagged S_LAZY_SYMBOL_POINTERS. It has no LC_DYLD_CHAINED_FIXUPS handling at all. Since this binary keeps __got in __DATA_CONST and uses chained fixups (so there are no lazy-pointer sections), 4.3.0 can never find a hook target either.
So 4.5.0's __DATA_CONST/__got/chained-fixups support is clearly the right direction — it just doesn't produce a usable header on this binary.
Reproduction
- On Apple Silicon macOS, install BepInEx 6.0.0-be.785 (Unity.Mono macos-x64) into a Unity 6000.3.x Mono game.
- Set
executable_nameand launch viarun_bepinex.sh. - Observe: Doorstop debug output appears, game starts normally, no
BepInEx/configorLogOutput.logis ever created.
To launch outside Steam without the game restarting itself, set SteamAppId=<appid> so SteamAPI_RestartAppIfNecessary returns false.
Ruled out
- Architecture — identical failure natively on arm64 and forced to x86_64 under Rosetta. All of the game's dylibs and
libdoorstop.dylibare universal. - Install layout — files at game root alongside the
.app, not inside the bundle;.appcode signature verifies clean. - Quarantine —
com.apple.quarantinestripped from all extracted files. DOORSTOP_MONO_LIB_PATH— present in the 4.5.0 binary's strings; pointing it directly atContents/Frameworks/libmonobdwgc-2.0.dylibchanges nothing (appears to be Windows-only in effect).- Steam relaunch interference — suppressed via
SteamAppId; game reaches full init either way.
Why this matters
Unity 6.3 builds ship UnityPlayer.dylib as chained-fixups-only with __got in __DATA_CONST. As more titles move to Unity 6.3, this will affect every macOS Mono game, on both Intel and Apple Silicon.
Activity
poitee commentedon Aug 2, 2026
Confirming this affects a second title, same failure mode.
Environment
libmonobdwgc-2.0.dylib,MonoBleedingEdge/present)citag'sdoorstop_macos_verbose_4.5.0.zip, via BepInEx 5.4.23.5 macos_universalSymptom
Identical to the report above: Doorstop loads and its debug memory-map dump prints for
UnityPlayer.dylibandlibdoorstop.dylib, the game boots and runs completely normally, butBepInEx/config,BepInEx/pluginscontents, andLogOutput.logare never created — Preloader never gets control.Swapping in the verbose CI build (
doorstop_macos_verbose_4.5.0.zip) surfaced the same failure explicitly:which matches the all-zero
dyld_chained_fixups_header(imports_count 0,imports_format 0) described above —UnityPlayer.dylibin this build has ~thousands of symbols, so this is clearly the same parse failure, not a genuinely-empty import table.Ruled out (same as above, independently reproduced)
ARCHPREFERENCE) — identical failure both ways.com.apple.quarantine— stripped from all extracted BepInEx files before testing.steam_appid.txtcontaining the App ID; game reaches full init regardless.GR.app, not inside the bundle.No workaround found on our end either. +1 that this looks like it'll affect any Unity 6.3+ Mono game on macOS going forward, given both affected titles are otherwise unrelated (different devs/publishers) and hit the exact same zeroed chained-fixups header.
Pik-4 commentedon Aug 4, 2026
I encountered the same problem in macOS 26.6 with Hearthstone(Unity 6000.3.11f1). Doorstop (4.5.0 CI build) reports :
[Doorstop] Failed to open current process PLT! Cannot run Doorstop! Error: unknown imports format 0Celtech commentedon Aug 4, 2026
libdoorstop.dylib.zip
Oh I already got it working on macOS again, you have to pull github.com/NeighTools/UnityDoorstop and patch src/nix/plthook/plthook_osx.c (Fix chained-fixups parsing (linkedit-relative address) + map imports to real GOT slots by reading chains from the file) and src/nix/entrypoint.c (Hook dlsym via DYLD interpose instead of (only) GOT patching) . Build it then yoink libdoorstop.dylib and you're golden.
Attached a working lib you can test that fix with
plthook_osx.c
entrypoint.c
ManlyMarco commentedon Aug 5, 2026
Have you considered making a PR?
piratedota commentedon Aug 6, 2026
``Application paused: False
=================================================================
Native Crash Reporting
Got a segv while executing native code. This usually indicates
a fatal error in the mono runtime or one of the native libraries
used by your application.
Obtained 23 stack frames.
=================================================================
Native stacktrace:
=================================================================
Telemetry Dumper:
#0 0x00000111d58320 in (Unknown)
Thread 0x700010a99000 may have been prematurely finalized* Assertion at mono-threads.c:702, condition `info' not met, function:mono_thread_info_current,
An error has occured in the native fault reporting. Some diagnostic information will be unavailable.
=================================================================
Native stacktrace:
#1 0x00000111cd1d0d in (Unknown)
0x11a7b1292 - /Volumes/Game/Hearthstone/Hearthstone.app/Contents/Frameworks/libmonobdwgc-2.0.dylib : mono_breakpoint_clean_code
0x11a90d1d3 - /Volumes/Game/Hearthstone/Hearthstone.app/Contents/Frameworks/libmonobdwgc-2.0.dylib : monoeg_assertion_message
0x11a901a38 - /Volumes/Game/Hearthstone/Hearthstone.app/Contents/Frameworks/libmonobdwgc-2.0.dylib : mono_thread_info_current_unchecked
0x11a902fd9 - /Volumes/Game/Hearthstone/Hearthstone.app/Contents/Frameworks/libmonobdwgc-2.0.dylib : mono_thread_info_set_flags
0x11a8ab35d - /Volumes/Game/Hearthstone/Hearthstone.app/Contents/Frameworks/libmonobdwgc-2.0.dylib : mono_threads_detach_coop
0x11a7b1549 - /Volumes/Game/Hearthstone/Hearthstone.app/Contents/Frameworks/libmonobdwgc-2.0.dylib : mono_breakpoint_clean_code
0x11a7573c6 - /Volumes/Game/Hearthstone/Hearthstone.app/Contents/Frameworks/libmonobdwgc-2.0.dylib : mono_unity_backtrace_from_context
0x11a6d3585 - /Volumes/Game/Hearthstone/Hearthstone.app/Contents/Frameworks/libmonobdwgc-2.0.dylib : mono_jit_set_domain
0x7ff81b473dfd - /usr/lib/system/libsystem_platform.dylib : _sigtramp
0x6000003d3400 - Unknown
0x112614814 - /Volumes/Game/Hearthstone/Hearthstone.app/Contents/Frameworks/UnityPlayer.dylib :
0x11261441a - /Volumes/Game/Hearthstone/Hearthstone.app/Contents/Frameworks/UnityPlayer.dylib :
0x1125af5c8 - /Volumes/Game/Hearthstone/Hearthstone.app/Contents/Frameworks/UnityPlayer.dylib :
0x7ff81b45e4e1 - /usr/lib/system/libsystem_pthread.dylib : _pthread_start
0x7ff81b459f6b - /usr/lib/system/libsystem_pthread.dylib : thread_start
#2 0x000001128dd3de in unity_operator_delete_dealloc
#3 0x000001128e053a in unity_operator_delete_dealloc
#4 0x007ff81e6166d3 in -[_NSOpenGLViewBackingLayer display]
#5 0x007ff8225f762b in CA::Layer::display_if_needed(CA::Transaction*)
=================================================================
External Debugger Dump:
#6 0x007ff82274ef86 in CA::Context::commit_transaction(CA::Transaction*, double, double*)
#7 0x007ff8225d8a89 in CA::Transaction::commit()
#8 0x007ff81e0cdd71 in __62+[CATransaction(NSCATransaction) NS_setFlushesWithDisplayLink]_block_invoke
Exiting early due to double fault.
Docs: macOS does not work yet because Doorstop cannot hook Unity 6.3
Add an experimental macOS install script
cdobbyn commentedon Sep 22, 2026
Root cause found.
read_chained_fixupsreads the header at(mach_header + dataoff), butdataoffis a file offset and__LINKEDITis mapped at a different delta, so the read lands in the string table. Hence the zeros.Two fixes, either works:
__got-slot mapping, which is wrong on this image (dlsymis import 546, slot 556).With either, Ages of Conflict 6000.3.8f1 reaches
Chainloader startup completeon arm64.Renee-Napoliello commentedon Sep 25, 2026
Confirming on a second title: Dressmaker (Unity 6000.3.4f1), macOS 26, Apple M3.
Stock BepInEx 5.4.23.5 (Doorstop 4.5.0): libdoorstop.dylib maps into the process and the game runs normally, but the hook never installs — no LogOutput.log, no config/. dyld_chained_fixups_header parses as all zeros, imports_count 0.
Swapping in the CI build of master (PR #117) fixes it: Chainloader startup complete, plugin loaded.
It also works when running the x86_64 slice under Rosetta (ARCHPREFERENCE=x86_64), not just native arm64 — possibly useful to know since BepInEx 5.4.23.5 separately loads no plugins on native arm64 (PR BepInEx/BepInEx#1402 isn't in a release yet)... so Rosetta is currently the practical configuration on Apple Silicon.
cdobbyn commentedon Sep 26, 2026
Should be fixed once doorstop's next release, my code was merged. Bepinex will need a small change to update the doorstop version and it's own release.
macOS installer (experimental): installs the workaround that runs the…