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 - fileoff style base calculation misbehaving when dataoff == linkedit.fileoff.
- The header being read before
__LINKEDIT is 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_name and launch via run_bepinex.sh.
- Observe: Doorstop debug output appears, game starts normally, no
BepInEx/config or LogOutput.log is 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.dylib are universal.
- Install layout — files at game root alongside the
.app, not inside the bundle; .app code signature verifies clean.
- Quarantine —
com.apple.quarantine stripped from all extracted files.
DOORSTOP_MONO_LIB_PATH — present in the 4.5.0 binary's strings; pointing it directly at Contents/Frameworks/libmonobdwgc-2.0.dylib changes 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.
Summary
On macOS/Apple Silicon, Doorstop loads successfully into a Unity 6000.3.8f1 Mono game but never installs its
dlsymhook, somono_jit_init_versionis never intercepted and the target assembly never runs. BepInEx produces noLogOutput.logand noconfig/— it fails completely silently.I traced this to
read_chained_fixups()inplthook_osx.creturning an all-zerodyld_chained_fixups_headerforUnityPlayer.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
libmonobdwgc-2.0.dylib)Both the game executable and
UnityPlayer.dylibare universal (x86_64 arm64).libdoorstop.dylibfrom the be.785 build is also universal, so it loads natively on arm64.Symptom
Doorstop injects fine — its
PLTHOOK_DEBUG_ADDRoutput 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:Diagnosis — Doorstop 4.5.0
plthook_osx.ctakes theLC_DYLD_CHAINED_FIXUPSpath for this binary (it has noLC_DYLD_INFO/LC_DYLD_INFO_ONLY). ThePLTHOOK_DEBUG_FIXUPSdump shows the header parsing as effectively all zeros:With
imports_count == 0no imports are enumerated, sodlsymis never located in the GOT and no hook is installed. Notably__gotitself 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.dylibin the same run reportsimports_count 1, also wrong.Relevant binary layout (arm64 slice of
UnityPlayer.dylib)Note
LC_DYLD_CHAINED_FIXUPS.dataoffis exactly equal to__LINKEDIT.fileoff— the header sits at the very start of__LINKEDIT. That makes this look like alinkedit_basecomputation problem: reading at the intended address lands on zeroed memory. Two candidates I could not distinguish from outside:slide + vmaddr - fileoffstyle base calculation misbehaving whendataoff == linkedit.fileoff.__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:
At the v4.3.0 tag,
plthook_osx.conly walks__LINKEDITand__DATA, and only matches sections flaggedS_LAZY_SYMBOL_POINTERS. It has noLC_DYLD_CHAINED_FIXUPShandling at all. Since this binary keeps__gotin__DATA_CONSTand 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
executable_nameand launch viarun_bepinex.sh.BepInEx/configorLogOutput.logis ever created.To launch outside Steam without the game restarting itself, set
SteamAppId=<appid>soSteamAPI_RestartAppIfNecessaryreturns false.Ruled out
libdoorstop.dylibare universal..app, not inside the bundle;.appcode signature verifies clean.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).SteamAppId; game reaches full init either way.Why this matters
Unity 6.3 builds ship
UnityPlayer.dylibas chained-fixups-only with__gotin__DATA_CONST. As more titles move to Unity 6.3, this will affect every macOS Mono game, on both Intel and Apple Silicon.