From bed72670a3fed702b8c4bd627b69fc23a85515da Mon Sep 17 00:00:00 2001 From: Taksh Date: Wed, 22 Jul 2026 14:15:39 +0300 Subject: [PATCH] sanitize appimage env for child processes Signed-off-by: Taksh --- desktop/src-tauri/src/appimage_env.rs | 129 ++++++++++++++++++ .../src/commands/project_git_exec.rs | 3 + desktop/src-tauri/src/lib.rs | 1 + .../src-tauri/src/managed_agents/runtime.rs | 3 + 4 files changed, 136 insertions(+) create mode 100644 desktop/src-tauri/src/appimage_env.rs diff --git a/desktop/src-tauri/src/appimage_env.rs b/desktop/src-tauri/src/appimage_env.rs new file mode 100644 index 0000000000..1a8211906f --- /dev/null +++ b/desktop/src-tauri/src/appimage_env.rs @@ -0,0 +1,129 @@ +//! Sanitize AppImage-bundled environment for child processes. +//! +//! When Buzz Desktop runs from an AppImage, AppRun sets `LD_LIBRARY_PATH`, +//! `PYTHONHOME`, and related vars to the mount dir. System tools spawned from +//! Buzz (git, curl, python3) then link against older bundled libs and crash. +//! Restore the host environment for those children. + +use std::ffi::OsString; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Env keys AppRun commonly points into `$APPDIR`. Unset them on children when +/// they still reference the mount (unless an `ORIGINAL_*` restore applies). +const APPDIR_SCOPED_KEYS: &[&str] = &[ + "PYTHONHOME", + "PYTHONPATH", + "PERLLIB", + "GTK_PATH", + "QT_PLUGIN_PATH", + "GST_PLUGIN_SYSTEM_PATH", + "GST_PLUGIN_SYSTEM_PATH_1_0", +]; + +/// Apply a host-safe environment to `cmd` when running under an AppImage. +/// +/// No-op when `APPDIR` is unset (DMG / native installs). Prefer `ORIGINAL_*` +/// values saved by AppRun when present; otherwise strip `$APPDIR` entries from +/// path-like variables. +pub(crate) fn sanitize_appimage_env_for_child(cmd: &mut Command) { + let Some(appdir) = std::env::var_os("APPDIR").map(PathBuf::from) else { + return; + }; + + apply_path_like(cmd, "LD_LIBRARY_PATH", "ORIGINAL_LD_LIBRARY_PATH", &appdir); + apply_path_like(cmd, "PATH", "ORIGINAL_PATH", &appdir); + + for key in APPDIR_SCOPED_KEYS { + let original = format!("ORIGINAL_{key}"); + if let Some(restored) = std::env::var_os(&original) { + if restored.is_empty() { + cmd.env_remove(key); + } else { + cmd.env(key, restored); + } + continue; + } + if let Ok(value) = std::env::var(key) { + if value_references_appdir(&value, &appdir) { + cmd.env_remove(key); + } + } + } +} + +fn apply_path_like(cmd: &mut Command, key: &str, original_key: &str, appdir: &Path) { + if let Some(restored) = std::env::var_os(original_key) { + if restored.is_empty() { + cmd.env_remove(key); + } else { + cmd.env(key, restored); + } + return; + } + let Ok(current) = std::env::var(key) else { + return; + }; + let cleaned = filter_appdir_entries(¤t, appdir); + if cleaned.is_empty() { + cmd.env_remove(key); + } else { + cmd.env(key, cleaned); + } +} + +fn filter_appdir_entries(value: &str, appdir: &Path) -> OsString { + let kept: Vec = std::env::split_paths(value) + .filter(|entry| !is_under_or_equal(entry, appdir)) + .collect(); + std::env::join_paths(kept).unwrap_or_default() +} + +fn value_references_appdir(value: &str, appdir: &Path) -> bool { + let appdir_str = appdir.to_string_lossy(); + value.contains(appdir_str.as_ref()) + || std::env::split_paths(value).any(|entry| is_under_or_equal(&entry, appdir)) +} + +fn is_under_or_equal(path: &Path, root: &Path) -> bool { + if path == root { + return true; + } + path.starts_with(root) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn filter_drops_appdir_entries_keeps_system() { + let appdir = PathBuf::from("/tmp/.mount_Buzz_abc/usr"); + let input = std::env::join_paths([ + PathBuf::from("/tmp/.mount_Buzz_abc/usr/lib"), + PathBuf::from("/usr/lib"), + PathBuf::from("/tmp/.mount_Buzz_abc/usr/bin"), + PathBuf::from("/bin"), + ]) + .expect("join"); + let cleaned = filter_appdir_entries(&input.to_string_lossy(), &appdir); + let cleaned = cleaned.to_string_lossy().into_owned(); + assert!(cleaned.contains("/usr/lib"), "{cleaned}"); + assert!(cleaned.contains("/bin"), "{cleaned}"); + assert!( + !cleaned.contains(".mount_Buzz"), + "appdir entries must be stripped: {cleaned}" + ); + } + + #[test] + fn value_references_detects_appdir_substring() { + let appdir = PathBuf::from("/tmp/.mount_Buzz_x"); + assert!(value_references_appdir( + "/tmp/.mount_Buzz_x/usr/lib/python3", + &appdir + )); + assert!(!value_references_appdir("/usr/lib/python3", &appdir)); + } +} diff --git a/desktop/src-tauri/src/commands/project_git_exec.rs b/desktop/src-tauri/src/commands/project_git_exec.rs index 3f16c0fba9..cf3b59ef86 100644 --- a/desktop/src-tauri/src/commands/project_git_exec.rs +++ b/desktop/src-tauri/src/commands/project_git_exec.rs @@ -77,6 +77,9 @@ pub(crate) fn run_git( LOCAL_GIT_TIMEOUT }; configure_git_auth(&mut command, auth, needs_credentials); + // AppImage AppRun leaks bundled lib paths into children; strip them so + // system git/curl resolve host libraries (#2315). + crate::appimage_env::sanitize_appimage_env_for_child(&mut command); command.stdin(Stdio::null()); command.stdout(Stdio::piped()); command.stderr(Stdio::piped()); diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index c22de0205f..31d48c8fee 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ #![recursion_limit = "256"] // Deep Tauri command futures exceed the default layout query depth. mod app_state; +mod appimage_env; mod archive; mod builderlab; mod commands; diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 77b3a241e0..3b0f1560d0 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -1582,6 +1582,9 @@ pub fn spawn_agent_child( command.stdin(std::process::Stdio::null()); command.stdout(std::process::Stdio::from(stdout)); command.stderr(std::process::Stdio::from(stderr)); + // Drop AppImage-bundled lib paths before applying the managed PATH so + // harness children (git/curl/node) link against the host (#2315). + crate::appimage_env::sanitize_appimage_env_for_child(&mut command); if let Some(ref path) = augmented_path { command.env("PATH", path); }