From 779482598f45f3d0ce0a1bb76d062c341b33a47a Mon Sep 17 00:00:00 2001 From: lesseradmin Date: Sat, 29 Aug 2026 15:20:02 -0500 Subject: [PATCH] fix(desktop): pass --disable-setuid-sandbox on the userns launch path When chrome-sandbox is present but not root-owned 4755, Chromium can still abort via setuid_sandbox_host even though the namespace sandbox works. After the userns probe skips sudo, append --disable-setuid-sandbox so .desktop/no-TTY launches keep the namespace sandbox without a privilege prompt. Does not add --no-sandbox. Fixes #51327 --- hermes_cli/main.py | 25 +++++++++++ tests/hermes_cli/test_gui_command.py | 12 ++++- tests/hermes_cli/test_linux_sandbox_fixup.py | 47 ++++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index c5da3b99ee..f0361ae6c1 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -8260,6 +8260,29 @@ def _desktop_linux_sandbox_fixup(packaged_executable: Path) -> bool: return True +def _desktop_linux_needs_disable_setuid_sandbox(packaged_executable: Path) -> bool: + """Return True when Chromium should skip the present-but-non-setuid helper. + + A user-owned ``chrome-sandbox`` still makes Chromium abort with + ``setuid_sandbox_host`` even when the namespace sandbox works. Passing + ``--disable-setuid-sandbox`` keeps the userns sandbox and avoids sudo. + Call only after ``_desktop_linux_sandbox_fixup`` succeeded without making + the helper root-owned 4755 (the userns path). Does not re-probe userns. + """ + if sys.platform != "linux": + return False + sandbox = packaged_executable.parent / "chrome-sandbox" + try: + sandbox_lstat = sandbox.lstat() + except OSError: + return False + if not stat.S_ISREG(sandbox_lstat.st_mode): + return False + if sandbox_lstat.st_uid == 0 and stat.S_IMODE(sandbox_lstat.st_mode) == 0o4755: + return False + return True + + _LINUX_PASSWORD_STORES = frozenset({"gnome-libsecret", "kwallet", "kwallet5", "kwallet6", "basic"}) @@ -8654,6 +8677,8 @@ def cmd_gui(args: argparse.Namespace): launch_command.append("--no-sandbox") else: sys.exit(1) + elif _desktop_linux_needs_disable_setuid_sandbox(packaged_executable): + launch_command.append("--disable-setuid-sandbox") launch_command.extend(config_electron_flags) print(f"→ Launching packaged Hermes Desktop: {' '.join(launch_command)}") diff --git a/tests/hermes_cli/test_gui_command.py b/tests/hermes_cli/test_gui_command.py index a3480f96d1..85348f9a6e 100644 --- a/tests/hermes_cli/test_gui_command.py +++ b/tests/hermes_cli/test_gui_command.py @@ -130,7 +130,11 @@ def test_gui_installs_packages_and_launches_desktop_app(tmp_path, monkeypatch): assert install_env is not None and "PATH" in install_env assert mock_run.call_args_list[0].args[0] == ["/usr/bin/npm", "run", "pack"] assert mock_run.call_args_list[0].kwargs["cwd"] == desktop_dir - assert mock_run.call_args_list[1].args[0] == [str(packaged_exe)] + launched = mock_run.call_args_list[1].args[0] + if sys.platform.startswith("linux"): + assert launched == [str(packaged_exe), "--disable-setuid-sandbox"] + else: + assert launched == [str(packaged_exe)] assert mock_run.call_args_list[1].kwargs["cwd"] == desktop_dir @@ -968,7 +972,11 @@ def test_gui_launches_even_when_desktop_entry_install_fails(tmp_path, monkeypatc cli_main.cmd_gui(_ns()) assert exc.value.code == 0 - assert mock_run.call_args.args[0] == [str(packaged_exe)] + launched = mock_run.call_args.args[0] + if sys.platform.startswith("linux"): + assert launched == [str(packaged_exe), "--disable-setuid-sandbox"] + else: + assert launched == [str(packaged_exe)] @pytest.mark.macos_only diff --git a/tests/hermes_cli/test_linux_sandbox_fixup.py b/tests/hermes_cli/test_linux_sandbox_fixup.py index 9f7e6d1815..d1c820b536 100644 --- a/tests/hermes_cli/test_linux_sandbox_fixup.py +++ b/tests/hermes_cli/test_linux_sandbox_fixup.py @@ -114,3 +114,50 @@ class TestDesktopLinuxSandboxFixup: ) as probe: assert cli_main._desktop_linux_sandbox_fixup(exe) is True probe.assert_not_called() + + +class TestDesktopLinuxNeedsDisableSetuidSandbox: + def _fake_packaged_app(self, tmp_path): + unpacked = tmp_path / "linux-unpacked" + unpacked.mkdir() + exe = unpacked / "Hermes" + exe.write_text("", encoding="utf-8") + sandbox = unpacked / "chrome-sandbox" + sandbox.write_text("", encoding="utf-8") + sandbox.chmod(0o755) + return exe + + def test_true_for_user_owned_helper_when_userns_works(self, monkeypatch, tmp_path): + monkeypatch.setattr(sys, "platform", "linux") + exe = self._fake_packaged_app(tmp_path) + with patch.object( + cli_main, "_desktop_linux_userns_sandbox_available", return_value=True + ): + assert cli_main._desktop_linux_needs_disable_setuid_sandbox(exe) is True + + def test_false_for_root_owned_setuid_helper(self, monkeypatch, tmp_path): + monkeypatch.setattr(sys, "platform", "linux") + exe = self._fake_packaged_app(tmp_path) + real_lstat = (exe.parent / "chrome-sandbox").lstat() + + class _RootSetuidStat: + st_mode = stat.S_IFREG | 0o4755 + st_uid = 0 + + def __getattr__(self, name): + return getattr(real_lstat, name) + + with patch.object(cli_main.Path, "lstat", return_value=_RootSetuidStat()), \ + patch.object( + cli_main, "_desktop_linux_userns_sandbox_available", return_value=True + ) as probe: + assert cli_main._desktop_linux_needs_disable_setuid_sandbox(exe) is False + probe.assert_not_called() + + def test_false_when_helper_missing(self, monkeypatch, tmp_path): + monkeypatch.setattr(sys, "platform", "linux") + unpacked = tmp_path / "linux-unpacked" + unpacked.mkdir() + exe = unpacked / "Hermes" + exe.write_text("", encoding="utf-8") + assert cli_main._desktop_linux_needs_disable_setuid_sandbox(exe) is False