nix: only install the Unity stdio hooks in a Unity player - #120
Merged
ManlyMarco merged 1 commit intoSep 25, 2026
Merged
Conversation
colefuerth
force-pushed
the
fix/nix-stdio-hooks-non-unity
branch
2 times, most recently
from
September 24, 2026 19:23
de00522 to
3461435
Compare
Contributor
|
I tested on native linux Valheim which uses Unity v6000.0.75f1 and this appears to be working good. I completely removed the workaround from the run.sh during testing. |
LD_PRELOAD is inherited by every child process, so Doorstop is also
loaded into the launcher scripts that start the game. When no UnityPlayer
module is present, doorstop_ctor() falls back to hooking the main
executable's PLT and installs fclose_hook and dup2_hook there.
Both hooks deliberately suppress redirection of stdout/stderr, which is
what a shell uses to implement command substitution and file
redirection, so any /bin/sh in the launch chain silently breaks:
$ LD_PRELOAD=libdoorstop.so DOORSTOP_ENABLED=1 \
DOORSTOP_TARGET_ASSEMBLY=... \
sh -c 'x="$(echo hello)"; echo "got=[$x]"'
hello
got=[]
This breaks BepInEx on every Steam Linux Runtime game: the runtime's
_v2-entry-point parses its arguments with `getopt_temp="$(getopt ...)"`,
gets an empty string back and exits with "Error: A command to run is
required", so the game never starts.
Only install the stdio hooks when the PLT we opened actually belongs to
a Unity player -- either because we hooked UnityPlayer directly, or
because the main executable ships a Unity data folder next to it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
colefuerth
force-pushed
the
fix/nix-stdio-hooks-non-unity
branch
from
September 25, 2026 06:31
3461435 to
dc5be57
Compare
ManlyMarco
approved these changes
Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #88.
Problem
LD_PRELOADis inherited by every child process, so Doorstop is also loaded into the launcher scripts that start the game. When noUnityPlayermodule is present,doorstop_ctor()falls back to hooking the main executable's PLT and installsfclose_hookanddup2_hookthere.Both hooks deliberately suppress redirection of
stdout/stderr— which is what a shell uses for$(...)and>— so any/bin/shin the launch chain silently breaks:That is the cause behind the
SteamLaunchre-exec dance inassets/nix/run.sh, which exists specifically to delay adding Doorstop toLD_PRELOADuntil after the Steam Linux Runtime's shell scripts have parsed their arguments.SteamLinuxRuntime_soldier/_v2-entry-pointdoesgetopt_temp="$(getopt ...)", gets an empty string back, and exits withError: A command to run is required.Change
Only install the stdio hooks when the PLT we opened actually belongs to a Unity player:
UnityPlayeris mapped, or<name>_Data).The check fails open: if the executable path cannot be read at all (no
/proc, non-dumpable process) the hooks stay installed, because a false negative silently costs a real player its console output while a false positive is loud and easy to diagnose. It also probes the full file name before the extension-stripped one, so a player calledGame.ClientfindsGame.Client_Data.macOS is left exactly as it is today: bundles lay the data folder out differently and I have no Mac to verify against, so
main_module_is_unity_player()returnsTRUEthere and behaviour is unchanged. Happy to extend it if someone can test.dlsymstays hooked, since it is how injection works for older Unity games where the player is the main executable. Note that it is not inert in a non-Unity process that resolves one of the redirected symbol names —REDIRECT_INITmatches on the symbol name alone — but that is existing behaviour this PR does not change; gating it is a separate discussion. Theboot_config_overridefopen/fopen64hooks are likewise left in place; they only gained aNULLfilename guard, since they are installed process-wide and the realfopenmerely fails on aNULLpath rather than crashing.Testing
Built from source and verified on Linux x64 (Unity 2021.3.45f2 Mono, BepInEx 5.4.23.5, STRAFTAT), against
master(8e66ca0) as the control:1. The shell bug is fixed — same command as above:
mastergot=[]got=[]got=[hello]got=[redir]2. Detection behaves in both directions. A test binary placed in various layouts, reporting whether
dup2(fd, 1)was suppressed:Game.x86_64+Game_Data/Game.Client+Game.Client_Data/Mr.Prepper.x86_64+Mr.Prepper_Data/_Datafolder3. The end-to-end case works. Running the Steam Linux Runtime chain with the Doorstop environment exported around it, which is what a mod manager wants to do instead of the
run.shre-exec:4.
fopen(NULL, "r")withDOORSTOP_BOOT_CONFIG_OVERRIDEset: segfault onmaster, returnsNULLwith this PR.There is no test harness in the repo, so the above is manual; the one-line
sh -crepro from #88 is the cheapest regression check if you ever add one.🤖 Generated with Claude Code
https://claude.ai/code/session_01TTHrLRGBdwxEMboXdCRmdj