Skip to content

Commit dc5be57

Browse files
colefuerthclaude
andcommitted
nix: only install the Unity stdio hooks in a Unity player
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>
1 parent 8e66ca0 commit dc5be57

1 file changed

Lines changed: 61 additions & 8 deletions

File tree

‎src/nix/entrypoint.c‎

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ char_t *default_boot_config_path = NULL;
6464
FILE *fopen64_hook(char *filename, char *mode) {
6565
char *actual_file_name = filename;
6666

67-
if (strcmp(filename, default_boot_config_path) == 0) {
67+
if (filename && strcmp(filename, default_boot_config_path) == 0) {
6868
actual_file_name = config.boot_config_override;
6969
LOG("Overriding boot.config to %s", actual_file_name);
7070
}
@@ -76,7 +76,7 @@ FILE *fopen64_hook(char *filename, char *mode) {
7676
FILE *fopen_hook(char *filename, char *mode) {
7777
char *actual_file_name = filename;
7878

79-
if (strcmp(filename, default_boot_config_path) == 0) {
79+
if (filename && strcmp(filename, default_boot_config_path) == 0) {
8080
actual_file_name = config.boot_config_override;
8181
LOG("Overriding boot.config to %s", actual_file_name);
8282
}
@@ -92,6 +92,50 @@ int dup2_hook(int od, int nd) {
9292
return dup2(od, nd);
9393
}
9494

95+
#if defined(__APPLE__)
96+
#define main_module_is_unity_player() TRUE
97+
#else
98+
static bool_t data_folder_exists(const char_t *dir, const char_t *name) {
99+
size_t size = strlen(dir) + strlen(name) + STR_LEN(TEXT("/_Data"));
100+
char_t *data_dir = (char_t *)malloc(size * sizeof(char_t));
101+
if (!data_dir)
102+
return FALSE;
103+
104+
snprintf(data_dir, size, TEXT("%s/%s_Data"), dir, name);
105+
bool_t result = folder_exists(data_dir);
106+
107+
free(data_dir);
108+
return result;
109+
}
110+
111+
// LD_PRELOAD reaches every child, so the stdio hooks above would land in the
112+
// launcher shells too. A player is identified by its data folder; anything we
113+
// cannot determine keeps the hooks, as before. (macOS bundles differ --
114+
// unchanged there.)
115+
static bool_t main_module_is_unity_player(void) {
116+
char_t *exe = program_path();
117+
if (!exe || !*exe) {
118+
free(exe);
119+
return TRUE;
120+
}
121+
122+
char_t *dir = get_folder_name(exe);
123+
// get_file_name() strips at the last dot: Game.Client needs
124+
// Game.Client_Data.
125+
char_t *full_name = get_file_name(exe, TRUE);
126+
char_t *base_name = get_file_name(exe, FALSE);
127+
128+
bool_t result = data_folder_exists(dir, full_name) ||
129+
data_folder_exists(dir, base_name);
130+
131+
free(base_name);
132+
free(full_name);
133+
free(dir);
134+
free(exe);
135+
return result;
136+
}
137+
#endif
138+
95139
__attribute__((constructor)) void doorstop_ctor() {
96140
init_logger();
97141
load_config();
@@ -105,6 +149,8 @@ __attribute__((constructor)) void doorstop_ctor() {
105149

106150
void *unity_player = plthook_handle_by_name("UnityPlayer");
107151

152+
bool_t hook_unity_stdio = TRUE;
153+
108154
if (unity_player &&
109155
PLTHOOK_OPEN_BY_HANDLE_OR_ADDRESS(&hook, unity_player) == 0) {
110156
LOG("Found UnityPlayer, hooking into it instead");
@@ -114,6 +160,12 @@ __attribute__((constructor)) void doorstop_ctor() {
114160
"%s\n",
115161
plthook_error());
116162
return;
163+
} else {
164+
// UnityPlayer is mapped but unhookable -- still a player.
165+
hook_unity_stdio =
166+
unity_player != NULL || main_module_is_unity_player();
167+
if (!hook_unity_stdio)
168+
LOG("Main module is not a Unity player; skipping stdio hooks");
117169
}
118170

119171
if (plthook_replace(hook, "dlsym", &dlsym_hook, NULL) != 0)
@@ -145,13 +197,14 @@ __attribute__((constructor)) void doorstop_ctor() {
145197
}
146198
}
147199

148-
if (plthook_replace(hook, "fclose", &fclose_hook, NULL) != 0)
149-
LOG("Failed to hook fclose, ignoring it. Error: %s",
150-
plthook_error());
200+
if (hook_unity_stdio) {
201+
if (plthook_replace(hook, "fclose", &fclose_hook, NULL) != 0)
202+
LOG("Failed to hook fclose, ignoring it. Error: %s",
203+
plthook_error());
151204

152-
if (plthook_replace(hook, "dup2", &dup2_hook, NULL) != 0)
153-
LOG("Failed to hook dup2, ignoring it. Error: %s",
154-
plthook_error());
205+
if (plthook_replace(hook, "dup2", &dup2_hook, NULL) != 0)
206+
LOG("Failed to hook dup2, ignoring it. Error: %s", plthook_error());
207+
}
155208

156209
#if defined(__APPLE__)
157210
/*

0 commit comments

Comments
 (0)