diff --git a/tools/linuxfs/preload/binfmt.c b/tools/linuxfs/preload/binfmt.c index eb912efe1..dbe297726 100644 --- a/tools/linuxfs/preload/binfmt.c +++ b/tools/linuxfs/preload/binfmt.c @@ -18,6 +18,8 @@ typedef int (*spawn_fn)(pid_t *, const char *, const posix_spawn_file_actions_t *, const posix_spawnattr_t *, char *const[], char *const[]); +__attribute__((visibility("hidden"))) +char *const *bl_webhelper_argv(const char *path, char *const argv[], char *const envp[]); #if defined(__aarch64__) || defined(BINFMT_TEST) @@ -141,21 +143,26 @@ __attribute__((visibility("hidden"))) int bl_binfmt_spawn(pid_t *pid, const char } int execvpe(const char *file, char *const argv[], char *const envp[]) { + char resolved[PATH_MAX]; + const char *path = lookup(file, resolved, sizeof resolved) ? resolved : file; + char *const *adjusted = bl_webhelper_argv(path, argv, envp); static int (*real)(const char *, char *const[], char *const[]); if (!real) real = (int (*)(const char *, char *const[], char *const[]))dlsym(RTLD_NEXT, "execvpe"); - bl_binfmt_before_search(file, argv, envp); - real(file, argv, envp); - return bl_binfmt_after_search(file, argv, envp, errno); + bl_binfmt_before_search(file, (char *const *)adjusted, envp); + real(file, (char *const *)adjusted, envp); + return bl_binfmt_after_search(file, (char *const *)adjusted, envp, errno); } int posix_spawnp(pid_t *pid, const char *file, const posix_spawn_file_actions_t *fa, const posix_spawnattr_t *attr, char *const argv[], char *const envp[]) { + char path[PATH_MAX]; + int found = lookup(file, path, sizeof path); + char *const *adjusted = bl_webhelper_argv(found ? path : file, argv, envp); static spawn_fn real, spawn; if (!real) real = (spawn_fn)dlsym(RTLD_NEXT, "posix_spawnp"); if (!spawn) spawn = (spawn_fn)dlsym(RTLD_NEXT, "posix_spawn"); - char path[PATH_MAX]; - if (!handing_off() || !lookup(file, path, sizeof path)) return real(pid, file, fa, attr, argv, envp); - return bl_binfmt_spawn(pid, path, fa, attr, argv, envp, spawn); + if (!handing_off() || !found) return real(pid, file, fa, attr, (char *const *)adjusted, envp); + return bl_binfmt_spawn(pid, path, fa, attr, (char *const *)adjusted, envp, spawn); } #else diff --git a/tools/linuxfs/preload/net.c b/tools/linuxfs/preload/net.c index 4946db7f9..e45ed9c5c 100644 --- a/tools/linuxfs/preload/net.c +++ b/tools/linuxfs/preload/net.c @@ -271,7 +271,8 @@ __attribute__((visibility("hidden"))) void bl_binfmt_before(const char *path, ch __attribute__((visibility("hidden"))) int bl_binfmt_retry(const char *path, char *const argv[], char *const envp[], int error); __attribute__((visibility("hidden"))) void bl_binfmt_before_search(const char *file, char *const argv[], char *const envp[]); __attribute__((visibility("hidden"))) int bl_binfmt_after_search(const char *file, char *const argv[], char *const envp[], int error); -__attribute__((visibility("hidden"))) char *const *bl_webhelper_argv(const char *path, char *const argv[]); +__attribute__((visibility("hidden"))) +char *const *bl_webhelper_argv(const char *path, char *const argv[], char *const envp[]); __attribute__((visibility("hidden"))) int bl_binfmt_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *fa, const posix_spawnattr_t *attr, char *const argv[], char *const envp[], spawn_fn real); @@ -341,7 +342,7 @@ static char **web_helper_redirect(const char *path, char *const argv[], const ch } int execve(const char *path, char *const argv_in[], char *const envp[]) { - char *const *argv = bl_webhelper_argv(path, argv_in); + char *const *argv = bl_webhelper_argv(path, argv_in, envp); static int (*real)(const char *, char *const[], char *const[]); if (!real) real = (int (*)(const char *, char *const[], char *const[]))dlsym(RTLD_NEXT, "execve"); maybe_answer_lsof(path, argv); @@ -372,7 +373,7 @@ static int answer_lsof_via_spawn(const char *path, char *const argv[], pid_t *pi } int execvp(const char *file, char *const argv_in[]) { - char *const *argv = bl_webhelper_argv(file, argv_in); + char *const *argv = bl_webhelper_argv(file, argv_in, environ); static int (*real)(const char *, char *const[]); if (!real) real = (int (*)(const char *, char *const[]))dlsym(RTLD_NEXT, "execvp"); maybe_answer_lsof(file, argv); @@ -382,7 +383,7 @@ int execvp(const char *file, char *const argv_in[]) { } int execv(const char *path, char *const argv_in[]) { - char *const *argv = bl_webhelper_argv(path, argv_in); + char *const *argv = bl_webhelper_argv(path, argv_in, environ); static int (*real)(const char *, char *const[]); if (!real) real = (int (*)(const char *, char *const[]))dlsym(RTLD_NEXT, "execv"); maybe_answer_lsof(path, argv); @@ -406,5 +407,6 @@ int posix_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t * } static spawn_fn real; if (!real) real = (spawn_fn)dlsym(RTLD_NEXT, "posix_spawn"); - return bl_binfmt_spawn(pid, path, fa, attr, argv, envp, real); + char *const *adjusted = bl_webhelper_argv(path, argv, envp); + return bl_binfmt_spawn(pid, path, fa, attr, (char *const *)adjusted, envp, real); } diff --git a/tools/linuxfs/preload/webhelper.c b/tools/linuxfs/preload/webhelper.c index 949487279..7642a3463 100644 --- a/tools/linuxfs/preload/webhelper.c +++ b/tools/linuxfs/preload/webhelper.c @@ -26,6 +26,14 @@ static char *join_features(const char *have, const char *add) { return strcat(out, add); } +static int electron_node_mode(char *const envp[]) { + static const char variable[] = "ELECTRON_RUN_AS_NODE="; + for (int i = 0; envp && envp[i]; i++) + if (!strncmp(envp[i], variable, sizeof variable - 1)) + return envp[i][sizeof variable - 1] != 0; + return 0; +} + static char *const *chromium_argv(const char *path, char *const argv[]) { const char *slash = path ? strrchr(path, '/') : NULL; if (!slash || !argv || !argv[0]) return argv; @@ -48,7 +56,9 @@ static char *const *chromium_argv(const char *path, char *const argv[]) { return out; } -__attribute__((visibility("hidden"))) char *const *bl_webhelper_argv(const char *path, char *const argv[]) { +__attribute__((visibility("hidden"))) +char *const *bl_webhelper_argv(const char *path, char *const argv[], char *const envp[]) { + if (electron_node_mode(envp)) return argv; if (!is_webhelper_browser(path, argv)) return chromium_argv(path, argv); const char *spec = getenv("BL_WEBHELPER_ARGS"); if (!spec || !*spec) spec = WEBHELPER_DEFAULT; diff --git a/tools/tests/test_binfmt_preload.py b/tools/tests/test_binfmt_preload.py index 8314d39e4..9966b2850 100644 --- a/tools/tests/test_binfmt_preload.py +++ b/tools/tests/test_binfmt_preload.py @@ -22,22 +22,29 @@ extern char **environ; int main(int argc, char **argv) { if (argc < 3) return 2; const char *how = argv[1], *file = argv[2]; + char *node_env[] = {"ELECTRON_RUN_AS_NODE=1", NULL}; + char *const *child_env = !strcmp(how, "execve-node") || !strcmp(how, "execvpe-node") || + !strcmp(how, "spawn-node") || !strcmp(how, "spawnp-node") ? node_env : environ; char **args = argv + 2; if (!strcmp(how, "access")) { int result = access(file, X_OK); printf("access %d %d\n", result, result ? errno : 0); return 0; } - if (!strcmp(how, "spawn") || !strcmp(how, "spawnp")) { + if (!strcmp(how, "spawn") || !strcmp(how, "spawnp") || !strcmp(how, "spawn-node") || + !strcmp(how, "spawnp-node")) { pid_t pid; - int error = how[5] ? posix_spawnp(&pid, file, NULL, NULL, args, environ) : posix_spawn(&pid, file, NULL, NULL, args, environ); + int spawnp = !strcmp(how, "spawnp") || !strcmp(how, "spawnp-node"); + int error = spawnp ? posix_spawnp(&pid, file, NULL, NULL, args, child_env) + : posix_spawn(&pid, file, NULL, NULL, args, child_env); if (error) { printf("errno %d\n", error); return 3; } int status; waitpid(pid, &status, 0); printf("exit %d\n", WEXITSTATUS(status)); return 0; } - if (!strcmp(how, "execve")) execve(file, args, environ); + if (!strcmp(how, "execve") || !strcmp(how, "execve-node")) execve(file, args, (char *const *)child_env); + else if (!strcmp(how, "execvpe-node")) execvpe(file, args, (char *const *)child_env); else if (!strcmp(how, "execv")) execv(file, args); else if (!strcmp(how, "execvp")) execvp(file, args); else if (!strcmp(how, "execvpe")) execvpe(file, args, environ); @@ -88,6 +95,15 @@ class BinfmtPreloadTest(unittest.TestCase): path.chmod(mode) return path + def chromium_script(self): + folder = self.dir / "vscode" + folder.mkdir() + executable = folder / "code" + executable.write_text("#!/bin/sh\nprintf '%s\\n' \"$@\"\n") + executable.chmod(0o755) + (folder / "chrome-sandbox").touch() + return executable + def environment(self, desktop=True, **extra): env = {"PATH": "%s:/usr/bin:/bin" % self.dir, "LD_PRELOAD": str(self.library), "HANDLER_LOG": str(self.log)} if desktop: @@ -161,6 +177,17 @@ class BinfmtPreloadTest(unittest.TestCase): self.file("script.sh", b"#!/bin/sh\nexit 5\n") self.assertEqual(self.run_program("spawnp", "script.sh"), (0, "exit 5", None)) + def test_explicit_node_child_environment_skips_injection_for_execve_and_spawn(self): + code = self.chromium_script() + self.assertEqual(self.run_program("execve-node", code, "--version"), (0, "--version", None)) + self.assertEqual(self.run_program("execvpe-node", code, "--version"), (0, "--version", None)) + self.assertEqual(self.run_program("spawn-node", code, "--version"), (0, "--version\nexit 0", None)) + self.assertEqual(self.run_program("spawnp-node", code, "--version"), (0, "--version\nexit 0", None)) + + def test_chromium_gui_launch_without_node_mode_still_receives_no_sandbox(self): + code = self.chromium_script() + self.assertEqual(self.run_program("execve", code, "--version"), (0, "--no-sandbox\n--version", None)) + def test_the_hand_off_switch_works_without_the_desktop(self): tool = self.x86() for how in ("execve", "execvp"): diff --git a/tools/tests/test_webhelper_preload.py b/tools/tests/test_webhelper_preload.py index 7c49e1d76..a7476945c 100644 --- a/tools/tests/test_webhelper_preload.py +++ b/tools/tests/test_webhelper_preload.py @@ -9,9 +9,16 @@ PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload" PROGRAM = r""" #include -char *const *bl_webhelper_argv(const char *path, char *const argv[]); +#include +char *const *bl_webhelper_argv(const char *path, char *const argv[], char *const envp[]); int main(int argc, char **argv) { - char *const *out = bl_webhelper_argv(argv[1], argv + 1); + const char *mode = getenv("TEST_NODE_MODE_CHILD"); + static char node[64]; + snprintf(node, sizeof node, "ELECTRON_RUN_AS_NODE=%s", mode ? mode : ""); + char *node_env[] = {node, NULL}; + char *plain_env[] = {"PATH=/usr/bin", NULL}; + char *const *envp = mode ? node_env : plain_env; + char *const *out = bl_webhelper_argv(argv[1], argv + 1, (char *const *)envp); for (int i = 0; out[i]; i++) printf("%s\n", out[i]); return 0; } @@ -35,13 +42,28 @@ class WebhelperPreloadTest(unittest.TestCase): def tearDownClass(cls): shutil.rmtree(cls.tmp, ignore_errors=True) - def argv(self, *args, spec=None): + def argv(self, *args, spec=None, node_child=False, parent_node=False): env = {k: v for k, v in os.environ.items() if k != "BL_WEBHELPER_ARGS"} + env.pop("ELECTRON_RUN_AS_NODE", None) if spec is not None: env["BL_WEBHELPER_ARGS"] = spec + if node_child is not False: + env["TEST_NODE_MODE_CHILD"] = "1" if node_child is True else node_child + else: + env.pop("TEST_NODE_MODE_CHILD", None) + if parent_node: + env["ELECTRON_RUN_AS_NODE"] = "1" result = subprocess.run([str(self.program), *args], env=env, capture_output=True, text=True, check=True) return result.stdout.splitlines() + def chromium(self, directory, name): + root = self.tmp / directory + root.mkdir(exist_ok=True) + executable = root / name + executable.touch() + (root / "chrome-sandbox").touch() + return str(executable) + def test_the_browser_process_keeps_its_helpers_in_process(self): out = self.argv(WEBHELPER, "-lang=en_US", "--enable-features=A", "--no-sandbox", "--disable-features=B,C") self.assertEqual(out, [WEBHELPER, "-lang=en_US", "--no-sandbox", "--no-zygote", "--in-process-gpu", @@ -57,6 +79,31 @@ class WebhelperPreloadTest(unittest.TestCase): other = ["/usr/bin/steam", "--enable-features=A"] self.assertEqual(self.argv(*other), other) + def test_node_mode_from_the_child_environment_skips_sandbox_injection(self): + code = self.chromium("vscode", "code") + self.assertEqual(self.argv(code, node_child=True), [code]) + + def test_any_nonempty_node_mode_value_skips_sandbox_injection(self): + code = self.chromium("vscode-true", "code") + for value in ("true", "0", "yes"): + self.assertEqual(self.argv(code, node_child=value), [code]) + + def test_empty_node_mode_value_still_gets_sandbox_flag(self): + chrome = self.chromium("chromium-empty-mode", "chrome") + self.assertEqual(self.argv(chrome, node_child=""), [chrome, "--no-sandbox"]) + + def test_non_browser_helper_beside_chrome_sandbox_skips_injection_in_node_mode(self): + helper = self.chromium("vscode-helper", "remote-cli") + self.assertEqual(self.argv(helper, node_child=True), [helper]) + + def test_chromium_gui_launch_still_gets_sandbox_flag_from_explicit_environment(self): + chrome = self.chromium("chromium", "chrome") + self.assertEqual(self.argv(chrome), [chrome, "--no-sandbox"]) + + def test_parent_node_mode_does_not_override_the_explicit_child_environment(self): + chrome = self.chromium("chromium-parent-mode", "chrome") + self.assertEqual(self.argv(chrome, parent_node=True), [chrome, "--no-sandbox"]) + def test_stock_turns_it_off(self): args = [WEBHELPER, "--enable-features=A"] self.assertEqual(self.argv(*args, spec="stock"), args)