From 8a9fdc3308bfaedb2ae6d8a3f0d35fd5f9864edf Mon Sep 17 00:00:00 2001 From: omartelo Date: Fri, 11 Sep 2026 13:20:32 -0300 Subject: [PATCH] fix: set the sandbox helper Chromium actually reads setuid Chromium looks for chrome-sandbox in cef/, beside libcef.so, not beside lich-shell. The deb, rpm and AUR packages installed a root-owned 4755 copy beside lich-shell and left the one in cef/ at 755, so on a desktop that denies unprivileged user namespaces (Ubuntu's AppArmor policy) Chromium found an unusable helper and aborted the window with SIGTRAP. nfpm cannot mark a file inside the window's tree setuid: a second entry for the same destination collides, and the archlinux packager drops a setuid bit read from the source file. The mode is set from the install script instead, wired to post_upgrade for pacman too. The AUR PKGBUILD chmods the copy in cef/, and lich-shell's --no-sandbox decision checks that same file. --- CHANGELOG.md | 9 +++++++++ build/linux/aur/PKGBUILD.tpl | 8 +++----- build/linux/nfpm/nfpm.yaml | 22 +++++++++------------ build/linux/nfpm/scripts/postinstall.sh | 4 ++++ docs/ceilings.md | 6 +++++- docs/chromium-shell.md | 2 +- shell/src/main.rs | 26 ++++++++++++++++++++----- 7 files changed, 52 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcb6a4ddb..6ecb6d72d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 combining accent underlined the wrong columns. The match now runs over the whole logical line and maps back to the cells the characters really occupy. +- **The window opens on Ubuntu instead of dying at launch.** Where the desktop + denies unprivileged user namespaces, which is Ubuntu's AppArmor policy, + Chromium confines the window through its setuid sandbox helper. The deb, rpm + and AUR packages installed that helper root-owned and 4755 beside the + window's binary, but Chromium reads the copy beside its own library, found it + without the setuid bit and aborted: no window, and `trace/breakpoint trap` in + lich's log. The packages now set the mode on the copy Chromium reads; + installing the release over the previous one is enough. + ## [0.49.0] - 2026-09-09 > [!WARNING] diff --git a/build/linux/aur/PKGBUILD.tpl b/build/linux/aur/PKGBUILD.tpl index e5f9d50bb..f69b6c5e9 100644 --- a/build/linux/aur/PKGBUILD.tpl +++ b/build/linux/aur/PKGBUILD.tpl @@ -32,11 +32,9 @@ package() { # /usr/bin/lich looks for it: /usr/lib/lich/shell. install -d "${pkgdir}/usr/lib/lich" cp -a shell "${pkgdir}/usr/lib/lich/shell" - # Chromium's setuid sandbox helper, at the only path its zygote reads: - # beside lich-shell, root-owned (install under fakeroot) and 4755. Without - # it a desktop that denies unprivileged user namespaces opens the window - # with --no-sandbox. The tarball's own copy under cef/ is read by nothing. - install -m4755 shell/cef/chrome-sandbox "${pkgdir}/usr/lib/lich/shell/chrome-sandbox" + # Chromium reads its setuid sandbox helper from cef/, beside libcef.so, and + # aborts the window when the one there is not root-owned (fakeroot) and 4755. + chmod 4755 "${pkgdir}/usr/lib/lich/shell/cef/chrome-sandbox" install -Dm644 "lich-${pkgver}.desktop" "${pkgdir}/usr/share/applications/lich.desktop" install -Dm644 "lich-${pkgver}.png" "${pkgdir}/usr/share/icons/hicolor/128x128/apps/lich.png" } diff --git a/build/linux/nfpm/nfpm.yaml b/build/linux/nfpm/nfpm.yaml index abcc3fe81..66da57cbc 100644 --- a/build/linux/nfpm/nfpm.yaml +++ b/build/linux/nfpm/nfpm.yaml @@ -21,22 +21,12 @@ contents: - src: "./bin/lich" dst: "/usr/local/bin/lich" # The window, where /usr/local/bin/lich looks for it: the lib directory - # sibling to its bin (internal/chromium/shell.go). + # sibling to its bin (internal/chromium/shell.go). Its cef/chrome-sandbox gets + # 4755 from the install scripts: a second entry for it collides with the tree, + # and the archlinux packager drops a setuid bit read from the source file. - src: "./bin/shell" dst: "/usr/local/lib/lich/shell" type: tree - # Chromium's setuid sandbox helper, at the only path its zygote reads — - # beside the executable, root-owned and 4755 (shell/src/main.rs). Without it - # a desktop that denies unprivileged user namespaces, which is Ubuntu's - # AppArmor policy, opens the window with --no-sandbox. Copied rather than - # moved: the tree above carries the same binary under cef/, where the - # tarball keeps it and nothing reads it. - - src: "./bin/shell/cef/chrome-sandbox" - dst: "/usr/local/lib/lich/shell/chrome-sandbox" - file_info: - mode: 0o4755 - owner: root - group: root - src: "./build/appicon.png" dst: "/usr/share/icons/hicolor/128x128/apps/lich.png" - src: "./build/linux/lich.desktop" @@ -147,3 +137,9 @@ rpm: scripts: postinstall: "./build/linux/nfpm/scripts/postinstall.sh" + +# pacman runs post_install on the first install only, and every upgrade rewrites +# the sandbox helper with the package's own mode. +archlinux: + scripts: + postupgrade: "./build/linux/nfpm/scripts/postinstall.sh" diff --git a/build/linux/nfpm/scripts/postinstall.sh b/build/linux/nfpm/scripts/postinstall.sh index 4bbb815a3..5a8ee5dd0 100644 --- a/build/linux/nfpm/scripts/postinstall.sh +++ b/build/linux/nfpm/scripts/postinstall.sh @@ -18,4 +18,8 @@ else echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2 fi +# Chromium reads its setuid sandbox helper from cef/ and aborts the window when +# it is not root-owned 4755; nfpm.yaml says why the package cannot set the mode. +chmod 4755 /usr/local/lib/lich/shell/cef/chrome-sandbox + exit 0 diff --git a/docs/ceilings.md b/docs/ceilings.md index 832369143..271d4de40 100644 --- a/docs/ceilings.md +++ b/docs/ceilings.md @@ -574,7 +574,11 @@ work when nobody knows it and that the call site never shows. The mechanism and `lich doctor` names the window a launch would open. - **The window's own sandbox needs an install a package manager made** (`shell/src/main.rs`, the kurogane fork's `no_sandbox`): Chromium confines the window's subprocesses in a user namespace, or through the - setuid helper beside `lich-shell`. Where it has neither, the browser process would abort at its zygote, so + setuid helper in `cef/`, beside `libcef.so` (not beside `lich-shell`: a helper there that is not root-owned + 4755 aborts the browser process whatever sits elsewhere). The packages nfpm builds (deb, rpm, archlinux) set + that mode from their install script, since nfpm cannot mark one file of the window's tree setuid, so `rpm -V` + and `pacman -Qkk` report a mode mismatch on it; the AUR package carries the mode itself. Where it has + neither, the browser process would abort at its zygote, so the shell asks first, the way Chromium does (a fork trying `CLONE_NEWUSER`, the helper checked for root and 4755, never as root; the `CHROME_DEVEL_SANDBOX` helper Chromium also accepts for a binary the user owns is not asked) and opens with `--no-sandbox`. Only a package can own that helper root: a tarball unpacked as diff --git a/docs/chromium-shell.md b/docs/chromium-shell.md index 4c0428b6d..bf834dd6d 100644 --- a/docs/chromium-shell.md +++ b/docs/chromium-shell.md @@ -205,7 +205,7 @@ taskbar group the running window under the pinned icon. The sandbox stays off there, as kurogane runs it, so the binary is a plain exe rather than CEF's `bootstrap.exe` loading a DLL; Linux is the one platform whose window runs sandboxed — from a package, where the deb, rpm and AUR install Chromium's -setuid helper root-owned 4755 beside `lich-shell` for the desktops that deny +setuid helper root-owned 4755 beside `libcef.so` for the desktops that deny unprivileged user namespaces (`docs/ceilings.md`). Built and smoke-tested on the CI runner only. diff --git a/shell/src/main.rs b/shell/src/main.rs index 49a72d2ed..1e555552b 100644 --- a/shell/src/main.rs +++ b/shell/src/main.rs @@ -279,12 +279,12 @@ fn wait_for_eof(mut stdin: impl Read) -> bool { /// Whether Chromium can confine its subprocesses here, decided the way /// Chromium decides it at its zygote (content/browser/zygote_host): never as -/// root; otherwise a user namespace, or the setuid helper beside the executable. +/// root; otherwise a user namespace, or the setuid helper beside libcef.so. /// With neither the browser process aborts at startup, and Chromium's own /// advice is --no-sandbox; Ubuntu denies unprivileged user namespaces through -/// AppArmor, and the packages do not ship the helper setuid, so that is every -/// Ubuntu desktop. The "stability and security will suffer" bar Chrome draws -/// over the switch is then the truth about that machine. +/// AppArmor, so there only a package, which can own the helper root, keeps the +/// sandbox. The "stability and security will suffer" bar Chrome draws over the +/// switch is then the truth about that machine. #[cfg(target_os = "linux")] fn sandbox_available() -> bool { // SAFETY: geteuid has no preconditions. @@ -296,7 +296,7 @@ fn sandbox_available() -> bool { } std::env::current_exe() .ok() - .and_then(|exe| exe.with_file_name("chrome-sandbox").metadata().ok()) + .and_then(|exe| helper_path(&exe).metadata().ok()) .is_some_and(|meta| { use std::os::unix::fs::MetadataExt; helper_usable(meta.uid(), meta.mode()) @@ -310,6 +310,13 @@ fn helper_usable(uid: u32, mode: u32) -> bool { uid == 0 && mode & libc::S_ISUID != 0 && mode & libc::S_IXOTH != 0 } +/// Chromium looks for the helper beside libcef.so, not beside this executable: +/// its FATAL names cef/chrome-sandbox even with a usable copy next to lich-shell. +#[cfg(target_os = "linux")] +fn helper_path(exe: &std::path::Path) -> std::path::PathBuf { + exe.with_file_name("cef").join("chrome-sandbox") +} + /// Chromium's own probe (sandbox::Credentials::CanCreateProcessInNewUserNS): /// a child tries to enter a new user namespace, and its exit status is the /// answer. Asked from a process, not read from a sysctl, because what denies @@ -489,6 +496,15 @@ mod tests { assert!(!helper_usable(0, 0o104750), "not executable by others"); } + #[cfg(target_os = "linux")] + #[test] + fn looks_for_the_helper_beside_libcef_where_chromium_does() { + assert_eq!( + helper_path(std::path::Path::new("/usr/lib/lich/shell/lich-shell")), + std::path::Path::new("/usr/lib/lich/shell/cef/chrome-sandbox") + ); + } + #[test] fn ignores_what_is_not_a_switch() { let launch = parse(args(&["run", "-x", "--", "http://h/"]));