--- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -8146,6 +8146,44 @@ return False +def _desktop_linux_userns_sandbox_available() -> bool: + """True when the unprivileged userns sandbox works (probed with ``unshare``, fails closed) — then + the setuid ``chrome-sandbox`` helper is never consulted and no sudo prompt is needed.""" + if sys.platform != "linux": + return False + unshare = shutil.which("unshare") + if not unshare: + return False + try: + return ( + subprocess.run( + [unshare, "--user", "--map-root-user", "true"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5, check=False, + ).returncode + == 0) + except (OSError, subprocess.TimeoutExpired): + return False + +def _sandbox_helper_lstat(packaged_executable: Path) -> tuple[Path, Optional[os.stat_result]]: + """``(chrome-sandbox path, lstat or None)`` — lstat so a symlink is inspected, not followed.""" + sandbox = packaged_executable.parent / "chrome-sandbox" + try: + return sandbox, sandbox.lstat() + except OSError: + return sandbox, None + +def _sandbox_helper_is_setuid_root(st: os.stat_result) -> bool: + return st.st_uid == 0 and stat.S_IMODE(st.st_mode) == 0o4755 + +def _desktop_linux_needs_disable_setuid_sandbox(packaged_executable: Path) -> bool: + """True when a present, non-setuid ``chrome-sandbox`` would make Chromium abort with + ``setuid_sandbox_host`` despite a working userns sandbox (call after the fixup's userns path).""" + if sys.platform != "linux": + return False + _sandbox, st = _sandbox_helper_lstat(packaged_executable) + return st is not None and stat.S_ISREG(st.st_mode) and not _sandbox_helper_is_setuid_root(st) + + def _desktop_linux_sandbox_helper_is_regular_file(packaged_executable: Path) -> bool: """Return True when ``chrome-sandbox`` exists as a regular file.""" if sys.platform != "linux": @@ -8182,6 +8220,10 @@ return False if sandbox_lstat.st_uid == 0 and stat.S_IMODE(sandbox_lstat.st_mode) == 0o4755: + return True + + if _desktop_linux_userns_sandbox_available(): + print("✓ Using Chromium's user-namespace sandbox (setuid helper not needed).") return True sudo = shutil.which("sudo") @@ -8591,6 +8633,9 @@ 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)}") --- a/scripts/desktop-update/posix.sh +++ b/scripts/desktop-update/posix.sh @@ -317,6 +317,8 @@ if [ ! -e "$sb" ]; then GATE=relaunch; return; fi if [ -u "$sb" ] && [ "$(stat -c %u "$sb" 2>/dev/null)" = "0" ]; then GATE=relaunch; return; fi + if unshare --user --map-root-user true 2>/dev/null; then GATE=relaunch; return; fi + case "${ELECTRON_DISABLE_SANDBOX:-}" in 1|true|TRUE|True) GATE=relaunch; return ;; esac [ "$SANDBOX_FALLBACK" -eq 1 ] && { GATE=relaunch; return; } for arg in ${RELAUNCH_ARGS[@]+"${RELAUNCH_ARGS[@]}"}; do