From 426a643b0ea06e3627f28f0bd62e8edd42dfe084 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 16 Sep 2026 15:20:33 +0000 Subject: [PATCH] Bundled engine: probe that it can start before using it; install the AppArmor userns profile on Ubuntu First launch after v3.13.0 on bonita crashed in ZygoteHostImpl::Init (SIGTRAP): Ubuntu 23.10+ blocks unprivileged user namespaces through AppArmor, so a Chromium that is not a distro package cannot set up its sandbox and aborts with No usable sandbox. The Flatpak never hit it (bwrap has its own allowance) and the container test hid it (--no-sandbox). Two fixes. The launcher now runs a throwaway headless start of the bundled engine once per engine version (engine_usable), remembers success in engine/.usable, and on failure falls through to a system or Flatpak Ungoogled Chromium with a note saying why; a missing library or missing profile is a fallback, never a crash. And install.sh writes /etc/apparmor.d/tronbrowser-engine, granting userns to the engine binary path, with one sudo when the kernel restricts user namespaces (Chromium docs/security/apparmor-userns-restrictions.md; the same mechanism Ubuntu uses for its own browsers), on install and on tron upgrade, skipped where the kernel does not restrict, with a plain warning and the retry command when no sudo is available. Co-Authored-By: Claude Fable 5.1 --- apps/desktop/launcher/tronbrowser | 31 ++++++++++++++-- apps/web/public/install.sh | 59 ++++++++++++++++++++++++++++++- docs/moshpit-pit-toggle.md | 16 ++++++++- 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/apps/desktop/launcher/tronbrowser b/apps/desktop/launcher/tronbrowser index c986aa2..3bd984a 100755 --- a/apps/desktop/launcher/tronbrowser +++ b/apps/desktop/launcher/tronbrowser @@ -62,11 +62,38 @@ ver_gt() { exit 1 }' } +# The bundled engine must be able to START here, not merely exist: a shared +# library can be missing, and on Ubuntu 23.10+ AppArmor blocks the unprivileged +# user namespaces Chromium's sandbox needs unless install.sh's profile is in +# place — either way Chromium aborts in ZygoteHostImpl::Init ("No usable +# sandbox!"), which must never be what a click on the app icon does. So probe +# with a throwaway headless run once per engine version and remember success +# beside the engine; a failure is re-probed on every launch (a second or so, +# only while it is broken) and falls through to the system candidates. +engine_usable() { # engine_dir + [ -x "$1/chrome" ] || return 1 + _ev="$(cat "$1/VERSION" 2>/dev/null)" + if [ -n "$_ev" ] && [ "$(cat "$1/.usable" 2>/dev/null)" = "$_ev" ]; then return 0; fi + _pd="$(mktemp -d 2>/dev/null || echo "/tmp/tron-engine-probe.$$")" + if timeout 25 "$1/chrome" --headless=new --disable-gpu --no-first-run --user-data-dir="$_pd" \ + --dump-dom about:blank >/dev/null 2>&1; then + rm -rf "$_pd" + printf '%s\n' "$_ev" > "$1/.usable" 2>/dev/null + return 0 + fi + rm -rf "$_pd" + echo "TronBrowser: the bundled engine ($1/chrome) cannot start on this machine — using a system or Flatpak Ungoogled Chromium instead." >&2 + if [ "$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2>/dev/null)" = "1" ]; then + echo " This distro restricts unprivileged user namespaces, which the engine's sandbox needs. Run 'tron upgrade': it installs the AppArmor profile that allows it (asks for sudo once)." >&2 + fi + return 1 +} + if [ -n "${TRONBROWSER_BROWSER:-}" ]; then BROWSER="$TRONBROWSER_BROWSER" elif [ -x "$DIR/chrome" ]; then BROWSER="$DIR/chrome" # future: bundled native fork binary -elif [ -x "$DIR/engine/chrome" ] && "$DIR/engine/chrome" --version >/dev/null 2>&1; then +elif engine_usable "$DIR/engine"; then # TronBrowser's own engine: the portable ungoogled-chromium the installer # fetches next to this shim (install.sh ensure_engine), pinned per release. # Preferred over anything the distro or Flathub provides because it is the @@ -74,8 +101,6 @@ elif [ -x "$DIR/engine/chrome" ] && "$DIR/engine/chrome" --version >/dev/null 2> # per-user NSS trust store, which the Flathub ungoogled-chromium does not # (a Moshpit name's certificate sat in the exact database that build opened # and it still refused it), so https on pit names works here with no flags. - # The --version probe skips it when a shared library is missing, falling - # through to the system candidates rather than failing to start. BROWSER="$DIR/engine/chrome" else # Ungoogled Chromium ONLY. Never regular Chromium/Chrome, never snap (snap diff --git a/apps/web/public/install.sh b/apps/web/public/install.sh index 50e25a0..5eac690 100755 --- a/apps/web/public/install.sh +++ b/apps/web/public/install.sh @@ -732,13 +732,69 @@ download_engine() { # dest_dir rm -rf "$tmp"; return 1 } +# Ubuntu 23.10+ (kernel.apparmor_restrict_unprivileged_userns=1) blocks the +# unprivileged user namespaces Chromium's sandbox needs unless an AppArmor +# profile grants them to the binary. Ubuntu ships such profiles for its own +# browsers (/etc/apparmor.d/chrome); a third-party engine brings its own, and +# without it the engine aborts at start with "No usable sandbox!". Chromium's +# docs/security/apparmor-userns-restrictions.md is the reference. One sudo, +# written once per engine path, survives upgrades. The launcher probes the +# engine before using it, so a missing profile means a fallback, not a crash. +ENGINE_PROFILE="${TRONBROWSER_ENGINE_PROFILE:-/etc/apparmor.d/tronbrowser-engine}" + +ensure_engine_sandbox() { # engine_dir + [ "$(uname -s)" = "Linux" ] || return 0 + [ "$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2>/dev/null)" = "1" ] || return 0 + bin="$1/chrome" + if grep -qs "profile tronbrowser-engine $bin " "$ENGINE_PROFILE"; then return 0; fi + uid="$(id -u 2>/dev/null || echo 0)" + SUDO="" + if [ "$uid" -ne 0 ] && command -v sudo >/dev/null 2>&1 && { [ -t 1 ] || [ -t 2 ]; }; then SUDO="sudo"; fi + if [ "$uid" -ne 0 ] && [ -z "$SUDO" ]; then + warn "This distro restricts unprivileged user namespaces; TronBrowser's engine needs an AppArmor profile to sandbox itself. Re-run this in a terminal (it asks for sudo once): curl -fsSL $INSTALL_URL | sh -s -- ensure-engine" + return 1 + fi + if ! command -v apparmor_parser >/dev/null 2>&1; then + warn "apparmor_parser not found; cannot install the engine's AppArmor profile." + return 1 + fi + info "Allowing the engine to sandbox itself (AppArmor profile at $ENGINE_PROFILE, asks for sudo once)…" + tmpf="$(mktemp)" + cat > "$tmpf" <, +include + +profile tronbrowser-engine $bin flags=(unconfined) { + userns, + + # Site-specific additions and overrides. See local/README for details. + include if exists +} +PROFILE + if $SUDO install -m 0644 "$tmpf" "$ENGINE_PROFILE" && $SUDO apparmor_parser -r -T -W "$ENGINE_PROFILE"; then + rm -f "$tmpf" + info "Installed the engine's AppArmor profile." + rm -f "$1/.usable" # let the launcher re-probe now that the sandbox should work + return 0 + fi + rm -f "$tmpf" + warn "Couldn't install the engine's AppArmor profile; the launcher will use a system or Flatpak Ungoogled Chromium instead." + return 1 +} + ensure_engine() { [ "${TB_NO_ENGINE_INSTALL:-0}" = "1" ] && return 0 [ "$(uname -s)" = "Linux" ] || return 0 endest="$APP_DIR/engine" _ldir="$(find "$APP_DIR" -maxdepth 3 -type f -name tronbrowser 2>/dev/null | head -n1)" [ -n "$_ldir" ] && endest="$(dirname "$_ldir")/engine" - if [ -x "$endest/chrome" ] && [ "$(cat "$endest/VERSION" 2>/dev/null)" = "$ENGINE_VERSION" ]; then return 0; fi + if [ -x "$endest/chrome" ] && [ "$(cat "$endest/VERSION" 2>/dev/null)" = "$ENGINE_VERSION" ]; then + ensure_engine_sandbox "$endest" || true + return 0 + fi if ! tar --help 2>/dev/null | grep -q -- '-J\|xz'; then if ! command -v xz >/dev/null 2>&1; then warn "Couldn't install TronBrowser's engine: 'tar' here cannot read .xz and 'xz' is not installed (Debian/Ubuntu: sudo apt install xz-utils). Falling back to the system or Flatpak Ungoogled Chromium." @@ -748,6 +804,7 @@ ensure_engine() { info "Setting up TronBrowser's engine (ungoogled-chromium ${ENGINE_VERSION})…" if download_engine "$endest"; then info "Installed the engine to $endest" + ensure_engine_sandbox "$endest" || true return 0 fi warn "Couldn't install TronBrowser's engine; falling back to the system or Flatpak Ungoogled Chromium (https on Moshpit names may warn there). Retry with: curl -fsSL $INSTALL_URL | sh -s -- ensure-engine" diff --git a/docs/moshpit-pit-toggle.md b/docs/moshpit-pit-toggle.md index 19ccbe3..fc704d4 100644 --- a/docs/moshpit-pit-toggle.md +++ b/docs/moshpit-pit-toggle.md @@ -106,7 +106,21 @@ or Flatpak Chromium (after a `--version` probe, so a machine missing a shared library falls back rather than failing to start). On that engine the per-name import above is all that is needed: no flag, no bar, no relaunch. If the pit is turned on while a Flatpak engine is still running, the sidebar says so and -points at `tron upgrade`. A `--ignore-certificate-errors-spki-list` workaround +points at `tron upgrade`. + +Two things the engine needs to actually start. Ubuntu 23.10+ sets +`kernel.apparmor_restrict_unprivileged_userns=1`, which blocks the user +namespaces Chromium's sandbox needs; without help the engine aborts in +`ZygoteHostImpl::Init` ("No usable sandbox!", SIGTRAP), which is what bonita +hit first. Ubuntu's answer for third-party browsers is an AppArmor profile +granting `userns` to the binary path (Chromium's +`docs/security/apparmor-userns-restrictions.md`), so `install.sh` writes +`/etc/apparmor.d/tronbrowser-engine` once, with one `sudo`, and loads it. +And the launcher never takes the engine on faith: `engine_usable` runs a +throwaway headless start once per engine version, remembers success in +`engine/.usable`, and on failure falls through to a system or Flatpak +Chromium with a note saying why. A missing library or a missing profile is a +fallback, never a crash. A `--ignore-certificate-errors-spki-list` workaround was tried and reverted: it works, but Chromium flags it as an unsupported switch at every start.