Skip to content

Commit 6cfd696

Browse files
YellowSnnowmannCodeGhost21
authored andcommitted
fix(tauri): resolve Linux CEF init panic — root/container + SingletonLock + display-server guards (OPENHUMAN-TAURI-K1) (tinyhumansai#2103)
1 parent 9fd9ac7 commit 6cfd696

3 files changed

Lines changed: 233 additions & 9 deletions

File tree

app/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ cef = { version = "=146.4.1", default-features = false }
129129
openhuman_core = { path = "../..", package = "openhuman", default-features = false }
130130

131131
[target.'cfg(unix)'.dependencies]
132-
nix = { version = "0.29", default-features = false, features = ["signal"] }
132+
nix = { version = "0.29", default-features = false, features = ["signal", "user"] }
133133

134134
[target.'cfg(target_os = "macos")'.dependencies]
135135
objc2 = "0.6"

app/src-tauri/src/cef_preflight.rs

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
//! CEF cache-lock preflight check (macOS).
1+
//! CEF cache-lock preflight check (macOS and Linux).
22
//!
33
//! When another OpenHuman instance is already running, it holds an exclusive
4-
//! lock on the CEF user-data-dir at `~/Library/Caches/com.openhuman.app/cef`.
4+
//! lock on the CEF user-data-dir. On macOS this is
5+
//! `~/Library/Caches/com.openhuman.app/cef`; on Linux it is the path in
6+
//! `OPENHUMAN_CEF_CACHE_PATH` (set by `cef_profile::prepare_process_cache_path`
7+
//! before this module runs), falling back to `$XDG_CACHE_HOME/<id>/cef` or
8+
//! `$HOME/.cache/<id>/cef` when the env var is absent.
9+
//!
510
//! The vendored `tauri-runtime-cef` crate calls `cef::initialize()` and
611
//! asserts the result equals `1`; on lock collision it returns `0` and the
712
//! assertion panics with a Rust backtrace and no actionable message
8-
//! (see issue #864).
13+
//! (Sentry OPENHUMAN-TAURI-K1 on Linux, issue #864 on macOS).
914
//!
1015
//! This module runs *before* the Tauri builder constructs the runtime.
1116
//! It detects the lock-holder PID via Chromium's `SingletonLock` symlink and
@@ -72,7 +77,12 @@ impl fmt::Display for CefLockError {
7277

7378
impl std::error::Error for CefLockError {}
7479

75-
/// Resolves the macOS default CEF cache directory and runs the preflight.
80+
/// Resolves the platform default CEF cache directory and runs the preflight.
81+
///
82+
/// Checks `OPENHUMAN_CEF_CACHE_PATH` first (always set by
83+
/// `cef_profile::prepare_process_cache_path` before this runs). Falls back
84+
/// to the platform-specific default: `~/Library/Caches/<id>/cef` on macOS,
85+
/// `$XDG_CACHE_HOME/<id>/cef` or `$HOME/.cache/<id>/cef` on Linux.
7686
pub fn check_default_cache() -> Result<(), CefLockError> {
7787
if let Some(configured) = std::env::var_os("OPENHUMAN_CEF_CACHE_PATH") {
7888
let configured = PathBuf::from(configured);
@@ -84,10 +94,22 @@ pub fn check_default_cache() -> Result<(), CefLockError> {
8494
}
8595

8696
let home = std::env::var_os("HOME").ok_or(CefLockError::NoHomeDir)?;
87-
let cache_path = PathBuf::from(home)
88-
.join("Library/Caches")
97+
let home = PathBuf::from(home);
98+
99+
#[cfg(target_os = "macos")]
100+
let cache_path = home.join("Library/Caches").join(APP_IDENTIFIER).join("cef");
101+
102+
// On Linux: $XDG_CACHE_HOME/<id>/cef or $HOME/.cache/<id>/cef.
103+
// This matches the fallback path in tauri-runtime-cef's CefRuntime::init
104+
// (via `dirs::cache_dir()`).
105+
#[cfg(target_os = "linux")]
106+
let cache_path = std::env::var_os("XDG_CACHE_HOME")
107+
.map(PathBuf::from)
108+
.filter(|p| p.is_absolute())
109+
.unwrap_or_else(|| home.join(".cache"))
89110
.join(APP_IDENTIFIER)
90111
.join("cef");
112+
91113
log::debug!("[cef-preflight] cache_path={}", cache_path.display());
92114
check_cef_cache_lock(&cache_path)
93115
}
@@ -201,6 +223,12 @@ mod tests {
201223
use super::*;
202224
use std::os::unix::fs::symlink;
203225

226+
// Shared lock for all tests that mutate process-global env vars.
227+
// Each test previously had its own local `static ENV_LOCK`, allowing
228+
// concurrent test threads to race on OPENHUMAN_CEF_CACHE_PATH /
229+
// XDG_CACHE_HOME. A single module-level lock serialises them.
230+
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
231+
204232
#[test]
205233
fn parse_target_simple() {
206234
assert_eq!(
@@ -310,4 +338,86 @@ mod tests {
310338
);
311339
let _ = fs::remove_dir_all(&tmp);
312340
}
341+
342+
/// `check_default_cache` must use `OPENHUMAN_CEF_CACHE_PATH` when set —
343+
/// on both macOS and Linux the profile module always sets this before the
344+
/// preflight runs, so the platform-specific fallback paths are irrelevant
345+
/// in production, but the configured-path branch must work on all platforms.
346+
#[test]
347+
fn check_default_cache_uses_configured_env_path() {
348+
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
349+
350+
let prior = std::env::var_os("OPENHUMAN_CEF_CACHE_PATH");
351+
let tmp = fresh_tmp("default-cache-env");
352+
353+
std::env::set_var("OPENHUMAN_CEF_CACHE_PATH", &tmp);
354+
let result = check_default_cache();
355+
356+
match prior {
357+
Some(v) => std::env::set_var("OPENHUMAN_CEF_CACHE_PATH", v),
358+
None => std::env::remove_var("OPENHUMAN_CEF_CACHE_PATH"),
359+
}
360+
361+
assert!(result.is_ok(), "expected Ok with no lock, got {result:?}");
362+
let _ = fs::remove_dir_all(&tmp);
363+
}
364+
365+
/// `check_default_cache` with env-path pointing to a dir holding a live lock
366+
/// must return `CefLockError::Held`.
367+
#[test]
368+
fn check_default_cache_env_path_held_returns_err() {
369+
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
370+
371+
let prior = std::env::var_os("OPENHUMAN_CEF_CACHE_PATH");
372+
let tmp = fresh_tmp("default-cache-held");
373+
let me = std::process::id() as i32;
374+
symlink(format!("testhost-{me}"), tmp.join("SingletonLock")).unwrap();
375+
376+
std::env::set_var("OPENHUMAN_CEF_CACHE_PATH", &tmp);
377+
let result = check_default_cache();
378+
379+
match prior {
380+
Some(v) => std::env::set_var("OPENHUMAN_CEF_CACHE_PATH", v),
381+
None => std::env::remove_var("OPENHUMAN_CEF_CACHE_PATH"),
382+
}
383+
384+
match result {
385+
Err(CefLockError::Held { pid, .. }) => assert_eq!(pid, me),
386+
other => panic!("expected Held, got {other:?}"),
387+
}
388+
let _ = fs::remove_dir_all(&tmp);
389+
}
390+
391+
/// On Linux, `check_default_cache` without `OPENHUMAN_CEF_CACHE_PATH` set
392+
/// must fall back to `$XDG_CACHE_HOME/<id>/cef` and return Ok when no lock
393+
/// is present.
394+
#[cfg(target_os = "linux")]
395+
#[test]
396+
fn check_default_cache_linux_xdg_fallback_no_lock() {
397+
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
398+
399+
let prior_cache = std::env::var_os("OPENHUMAN_CEF_CACHE_PATH");
400+
let prior_xdg = std::env::var_os("XDG_CACHE_HOME");
401+
std::env::remove_var("OPENHUMAN_CEF_CACHE_PATH");
402+
403+
// Redirect XDG_CACHE_HOME to a temp dir we control.
404+
let tmp = fresh_tmp("linux-xdg-fallback");
405+
std::env::set_var("XDG_CACHE_HOME", &tmp);
406+
407+
let result = check_default_cache();
408+
409+
std::env::remove_var("XDG_CACHE_HOME");
410+
match prior_cache {
411+
Some(v) => std::env::set_var("OPENHUMAN_CEF_CACHE_PATH", v),
412+
None => {}
413+
}
414+
match prior_xdg {
415+
Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
416+
None => {}
417+
}
418+
419+
// No SingletonLock under tmp/<id>/cef — should be Ok.
420+
assert!(result.is_ok(), "expected Ok with no lock, got {result:?}");
421+
let _ = fs::remove_dir_all(&tmp);
422+
}
313423
}

app/src-tauri/src/lib.rs

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
compile_error!("src-tauri host is desktop-only. Non-desktop targets are not supported.");
33

44
mod cdp;
5-
#[cfg(target_os = "macos")]
5+
#[cfg(any(target_os = "macos", target_os = "linux"))]
66
mod cef_preflight;
77
mod cef_profile;
88
mod core_process;
@@ -1593,8 +1593,57 @@ fn warn_if_wsl_x11_desktop_launch() {
15931593
#[cfg(not(target_os = "linux"))]
15941594
fn warn_if_wsl_x11_desktop_launch() {}
15951595

1596+
/// Returns `true` if a display server is available on Linux.
1597+
/// Testable pure function: takes the env-presence booleans directly.
1598+
#[cfg(any(target_os = "linux", test))]
1599+
fn linux_display_server_present(display: bool, wayland_display: bool) -> bool {
1600+
display || wayland_display
1601+
}
1602+
1603+
/// Pre-CEF display-server check for Linux (Sentry OPENHUMAN-TAURI-K1).
1604+
///
1605+
/// CEF/Chromium requires X11 (`DISPLAY`) or Wayland (`WAYLAND_DISPLAY`) to
1606+
/// initialise. Without either, `cef_initialize` returns 0 and the vendored
1607+
/// `tauri-runtime-cef` asserts `result == 1` → panic `left: 0, right: 1`.
1608+
/// This is fatal and silent on WSL2 without WSLg and on any headless Linux box.
1609+
/// Detect it here and exit with a clear message before `CefRuntime::init` runs.
1610+
#[cfg(target_os = "linux")]
1611+
fn check_linux_display_server() {
1612+
if linux_display_server_present(
1613+
has_non_empty_env("DISPLAY"),
1614+
has_non_empty_env("WAYLAND_DISPLAY"),
1615+
) {
1616+
log::debug!(
1617+
"[cef-preflight] Linux display server present: DISPLAY={:?} WAYLAND_DISPLAY={:?}",
1618+
std::env::var("DISPLAY").ok(),
1619+
std::env::var("WAYLAND_DISPLAY").ok()
1620+
);
1621+
return;
1622+
}
1623+
let msg = "[openhuman] no display server found (DISPLAY and WAYLAND_DISPLAY are both unset).\n\
1624+
OpenHuman requires an X11 or Wayland display to run.\n\
1625+
On WSL2: install WSLg or configure X11 forwarding from Windows.\n\
1626+
Set DISPLAY (e.g. export DISPLAY=:0) or WAYLAND_DISPLAY before launching.";
1627+
log::error!(
1628+
"[cef-preflight] Linux display server missing — CEF cannot initialize \
1629+
(OPENHUMAN-TAURI-K1): DISPLAY and WAYLAND_DISPLAY both unset"
1630+
);
1631+
eprintln!("\n{msg}\n");
1632+
std::process::exit(1);
1633+
}
1634+
1635+
#[cfg(not(target_os = "linux"))]
1636+
fn check_linux_display_server() {}
1637+
15961638
type CefCommandLineArg = (&'static str, Option<&'static str>);
15971639

1640+
/// Returns `true` when the process is running as root (UID 0) on Linux.
1641+
/// Testable pure function; takes the uid directly.
1642+
#[cfg(any(target_os = "linux", test))]
1643+
fn linux_is_root_uid(uid: u32) -> bool {
1644+
uid == 0
1645+
}
1646+
15981647
fn append_platform_cef_gpu_workarounds(args: &mut Vec<CefCommandLineArg>, os: &str, arch: &str) {
15991648
// Issue #1697: on Arch/Manjaro-family Linux systems, the AppImage can
16001649
// abort during CEF GPU process startup when EGL context creation fails
@@ -1620,6 +1669,28 @@ fn append_platform_cef_gpu_workarounds(args: &mut Vec<CefCommandLineArg>, os: &s
16201669
"[cef-startup] Intel macOS detected: adding --disable-gpu-compositing (issue #1012)"
16211670
);
16221671
}
1672+
1673+
// Sentry OPENHUMAN-TAURI-K1: `cef::initialize` returns 0 when running as
1674+
// root (uid 0) on Linux unless `--no-sandbox` is passed as a command-line
1675+
// argument. The `no_sandbox: 1` field in `cef::Settings` disables the
1676+
// sub-process sandbox but does NOT satisfy Chromium's separate root-user
1677+
// check in the browser process — that check requires the CLI flag.
1678+
//
1679+
// This hits CI / coder-bot / Docker environments (e.g.
1680+
// `/root/.hermes/profiles/coder-bot/home`) that run as root inside a
1681+
// container. Without the flag, `cef_initialize` returns 0 and the vendored
1682+
// runtime assertion fires (`left: 0, right: 1`).
1683+
#[cfg(target_os = "linux")]
1684+
{
1685+
let uid = nix::unistd::getuid().as_raw();
1686+
if os == "linux" && linux_is_root_uid(uid) {
1687+
args.push(("--no-sandbox", None));
1688+
log::info!(
1689+
"[cef-startup] running as root (uid=0) on Linux: adding --no-sandbox \
1690+
(OPENHUMAN-TAURI-K1)"
1691+
);
1692+
}
1693+
}
16231694
}
16241695

16251696
pub fn run() {
@@ -1812,6 +1883,9 @@ pub fn run() {
18121883
}
18131884

18141885
warn_if_wsl_x11_desktop_launch();
1886+
// Exit before CEF if no display server is available — prevents the
1887+
// `assert_eq!(cef_initialize(…), 1)` panic (OPENHUMAN-TAURI-K1).
1888+
check_linux_display_server();
18151889

18161890
// The vendored tauri-cef dev-server proxy builds a reqwest 0.13 client
18171891
// (see vendor/tauri-cef/crates/tauri/src/protocol/tauri.rs) which calls
@@ -1899,7 +1973,12 @@ pub fn run() {
18991973
#[cfg(target_os = "macos")]
19001974
process_recovery::reap_stale_openhuman_processes();
19011975

1902-
#[cfg(target_os = "macos")]
1976+
// CEF cache-lock preflight: if another OpenHuman instance holds the CEF
1977+
// user-data-dir SingletonLock, `cef_initialize` returns 0 and the vendored
1978+
// runtime panics (`left: 0, right: 1`). Catch the collision here and exit
1979+
// cleanly. Stale locks (PID dead) are removed so crashed processes don't
1980+
// block subsequent launches. macOS: issue #864. Linux: OPENHUMAN-TAURI-K1.
1981+
#[cfg(any(target_os = "macos", target_os = "linux"))]
19031982
if let Err(e) = cef_preflight::check_default_cache() {
19041983
eprintln!("\n[openhuman] {e}\n");
19051984
std::process::exit(1);
@@ -3214,6 +3293,41 @@ mod tests {
32143293
assert!(!should_warn_for_wsl_x11_desktop(true, true, false, true));
32153294
}
32163295

3296+
// -------------------------------------------------------------------------
3297+
// Linux display-server pre-flight (Sentry OPENHUMAN-TAURI-K1)
3298+
// -------------------------------------------------------------------------
3299+
3300+
#[test]
3301+
fn linux_display_present_with_x11() {
3302+
assert!(linux_display_server_present(true, false));
3303+
}
3304+
3305+
#[test]
3306+
fn linux_display_present_with_wayland() {
3307+
assert!(linux_display_server_present(false, true));
3308+
}
3309+
3310+
#[test]
3311+
fn linux_display_present_with_both() {
3312+
assert!(linux_display_server_present(true, true));
3313+
}
3314+
3315+
#[test]
3316+
fn linux_display_absent_without_either() {
3317+
assert!(!linux_display_server_present(false, false));
3318+
}
3319+
3320+
#[test]
3321+
fn linux_root_uid_detected() {
3322+
assert!(linux_is_root_uid(0));
3323+
}
3324+
3325+
#[test]
3326+
fn linux_non_root_uid_not_detected() {
3327+
assert!(!linux_is_root_uid(1000));
3328+
assert!(!linux_is_root_uid(1));
3329+
}
3330+
32173331
// -------------------------------------------------------------------------
32183332
// Platform constants (issue #1012 Sentry tagging)
32193333
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)