Skip to content

macOS: Doorstop cannot hook Unity 6000.3 Mono games — chained-fixups header parses as all zeros #108

Description

@Ramanathan0310

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:

  1. The slide + vmaddr - fileoff style base calculation misbehaving when dataoff == linkedit.fileoff.
  2. 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

  1. On Apple Silicon macOS, install BepInEx 6.0.0-be.785 (Unity.Mono macos-x64) into a Unity 6000.3.x Mono game.
  2. Set executable_name and launch via run_bepinex.sh.
  3. 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.

Activity

  1. poitee commented on Aug 2, 2026

    @poitee

    Confirming this affects a second title, same failure mode.

    Environment

    OS macOS 26.5.2 (25F84), Darwin 25.5.0
    CPU Apple M-series (arm64), Mac16,11
    Game Global Rescue (Steam appid 2873660)
    Unity 6000.3.12f1, Mono backend (libmonobdwgc-2.0.dylib, MonoBleedingEdge/ present)
    Doorstop 4.5.0 (release build) and the ci tag's doorstop_macos_verbose_4.5.0.zip, via BepInEx 5.4.23.5 macos_universal

    Symptom

    Identical to the report above: Doorstop loads and its debug memory-map dump prints for UnityPlayer.dylib and libdoorstop.dylib, the game boots and runs completely normally, but BepInEx/config, BepInEx/plugins contents, and LogOutput.log are never created — Preloader never gets control.

    Swapping in the verbose CI build (doorstop_macos_verbose_4.5.0.zip) surfaced the same failure explicitly:

    [Doorstop] Failed to open current process PLT! Cannot run Doorstop! Error: unknown imports format 0
    

    which matches the all-zero dyld_chained_fixups_header (imports_count 0, imports_format 0) described above — UnityPlayer.dylib in 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)

    • Native arm64 vs. forced x86_64/Rosetta (ARCHPREFERENCE) — identical failure both ways.
    • com.apple.quarantine — stripped from all extracted BepInEx files before testing.
    • Steam relaunch — suppressed with a steam_appid.txt containing the App ID; game reaches full init regardless.
    • Install layout — BepInEx files placed at game root alongside 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.

  2. Pik-4 commented on Aug 4, 2026

    @Pik-4

    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 0

  3. Celtech commented on Aug 4, 2026

    @Celtech

    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

  4. ManlyMarco commented on Aug 5, 2026

    @ManlyMarco
    Collaborator

    Have you considered making a PR?

  5. piratedota commented on Aug 6, 2026

    @piratedota

    ``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:

    0x11a7b1292 - /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
    

    =================================================================
    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.

  6. cdobbyn commented on Sep 22, 2026

    @cdobbyn
    Contributor

    Root cause found. read_chained_fixups reads the header at (mach_header + dataoff), but dataoff is a file offset and __LINKEDIT is mapped at a different delta, so the read lands in the string table. Hence the zeros.

    Two fixes, either works:

    With either, Ages of Conflict 6000.3.8f1 reaches Chainloader startup complete on arm64.

  7. Renee-Napoliello commented on Sep 25, 2026

    @Renee-Napoliello

    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.

  8. cdobbyn commented on Sep 26, 2026

    @cdobbyn
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions