Repository navigation
Unix: stdio hooks are installed in non-Unity processes, breaking shells in the launch chain #119
Description
Summary
On Unix, Doorstop installs dup2_hook and fclose_hook into the main executable's PLT whenever no UnityPlayer module is present. Since LD_PRELOAD is inherited by every child process, that includes the launcher scripts that start the game — and both hooks deliberately suppress redirection of stdout/stderr, which is exactly what a shell uses to implement $(...) and >.
The result is that any /bin/sh in the launch chain silently stops working.
Reproduction
No Unity or Steam needed — any shell will do:
$ LD_PRELOAD=./libdoorstop.so DOORSTOP_ENABLED=1 DOORSTOP_TARGET_ASSEMBLY=/path/to/BepInEx.Preloader.dll \
sh -c 'x="$(echo hello)"; echo "substitution: [$x]"; echo redir > /tmp/t; echo "redirect: [$(cat /tmp/t)]"'
hello
substitution: []
redirect: []Expected:
substitution: [hello]
redirect: [redir]
Reproduced against master (8e66ca0) built from source, and against the shipped 4.5.0 release.
Real-world impact: BepInEx is broken on every Steam Linux Runtime game
Steam launches native Linux games through the Steam Linux Runtime:
_v2-entry-point --verb=waitforexitandrun -- scout-on-soldier-entry-point-v2 -- <game>
Both entry points are shell scripts. SteamLinuxRuntime_soldier/_v2-entry-point parses its arguments with:
getopt_temp="$(getopt -o '' --long "$getopt_temp" -n "$me" -- "$@")"
eval "set -- $getopt_temp"With Doorstop preloaded that command substitution returns an empty string, so $@ is destroyed and the script exits at its own guard:
if [ "$#" -eq 0 ] || [ "$1" = -- ]; then
log "Error: A command to run is required"
usage 125
fiThe game never starts. Demonstrated with STRAFTAT (native Linux, Unity 2021.3.45f2, Mono):
# Doorstop environment set around the runtime chain
$ LD_PRELOAD=libdoorstop.so DOORSTOP_ENABLED=1 DOORSTOP_TARGET_ASSEMBLY=... \
_v2-entry-point --verb=waitforexitandrun -- scout-on-soldier-entry-point-v2 -- STRAFTAT.x86_64
[27623]: Error: A command to run is requiredThis is, I believe, why Linux mod managers have to work around Doorstop rather than simply exporting its environment — e.g. ebkr/r2modmanPlus#1848.
Cause
doorstop_ctor() in src/nix/entrypoint.c:
void *unity_player = plthook_handle_by_name("UnityPlayer");
if (unity_player && PLTHOOK_OPEN_BY_HANDLE_OR_ADDRESS(&hook, unity_player) == 0) {
LOG("Found UnityPlayer, hooking into it instead");
} else if (plthook_open(&hook, NULL) != 0) { // <-- the main executable
...
}
...
plthook_replace(hook, "fclose", &fclose_hook, NULL);
plthook_replace(hook, "dup2", &dup2_hook, NULL);In a shell there is no UnityPlayer, so the fallback patches the shell's own dup2/fclose. Both hooks are written for Unity's behaviour specifically and are unconditionally destructive elsewhere:
int dup2_hook(int od, int nd) {
// Newer versions of Unity redirect stdout to player.log, we don't want that
if (nd == fileno(stdout) || nd == fileno(stderr))
return F_OK; // silently refuses the redirection
return dup2(od, nd);
}Note the fallback itself is legitimate — older Unity games have no separate UnityPlayer.so and the player is the main executable — so it can't simply be removed; it needs to distinguish a player from a launcher.
Related
#110 proposed this same scoping ("scope Unix stdout/fclose hooks to UnityPlayer so inherited LD_PRELOAD no longer breaks shell command substitution or file redirection") but bundled a large set of unrelated cross-platform changes and was closed with a request to split it up. This issue covers only the stdio scoping; I have a focused PR for it.
Activity
arrowmaster commentedon Sep 24, 2026
Duplicate of #88. There seems to be a misunderstanding about ebkr/r2modmanPlus#1848 because mod managers do NOT work around this issue. The work around exists in UnityDoorstops run.sh, but many older repacks of BepInEx do not contain a working version of the workaround.
While I very much welcome a fix for this long standing issue, I have concerns due to how wrong this description of the problem is.
colefuerth commentedon Sep 24, 2026
You're right on both counts — thanks for the correction, and sorry for the noise.
I found #110 when searching and missed #88, which is the same bug with essentially the same repro. Closing this as a duplicate and repointing the PR at #88.
And the r2modman framing was wrong: the workaround is
run.sh'sSteamLaunchre-exec, in this repo, not something mod managers carry. What I actually hit is a profile whose BepInEx repack predates that workaround, which is a packaging problem, not a Doorstop one.The PR itself stands on the
sh -crepro from #88 and is unchanged in substance; I've rewritten its description to drop the bad framing.colefuerth commentedon Sep 24, 2026
Duplicate of #88.