Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
strip beside the tabs still resized the dock. The handle now sits above the
preview.

- **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]
Expand Down
8 changes: 3 additions & 5 deletions build/linux/aur/PKGBUILD.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
22 changes: 9 additions & 13 deletions build/linux/nfpm/nfpm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
4 changes: 4 additions & 0 deletions build/linux/nfpm/scripts/postinstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 5 additions & 1 deletion docs/ceilings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/chromium-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
26 changes: 21 additions & 5 deletions shell/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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())
Expand All @@ -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
Expand Down Expand Up @@ -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/"]));
Expand Down
Loading