Fix Flatpak sandbox preventing access to host system commands (#8109)

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Vincenzo Palazzo
2026-04-02 15:53:20 +02:00
committed by GitHub
parent 5a8686bb83
commit c7b5fb9587
@@ -14,6 +14,79 @@ use tokio_stream::{wrappers::SplitStream, StreamExt};
use crate::subprocess::SubprocessExt;
/// Check if the current process is running inside a Flatpak sandbox.
///
/// When inside Flatpak, shell commands must be wrapped with `flatpak-spawn --host`
/// to execute on the host system rather than inside the sandbox.
#[cfg(not(windows))]
fn is_flatpak() -> bool {
std::path::Path::new("/.flatpak-info").exists()
}
#[cfg(not(windows))]
const FLATPAK_HOST_ARGS: [&str; 2] = ["--host", "--watch-bus"];
#[cfg(not(windows))]
fn flatpak_spawn_command() -> tokio::process::Command {
let mut command = tokio::process::Command::new("flatpak-spawn");
command.args(FLATPAK_HOST_ARGS);
command
}
#[cfg(not(windows))]
fn flatpak_spawn_process() -> std::process::Command {
let mut command = std::process::Command::new("flatpak-spawn");
command.args(FLATPAK_HOST_ARGS);
command
}
/// Resolve the preferred Unix shell, respecting GOOSE_SHELL.
///
/// Returns `(shell_path, is_user_configured)` — the boolean is true when
/// `GOOSE_SHELL` was explicitly set, which matters in Flatpak mode: an
/// explicit path is passed through as-is (the user likely intends a host
/// path), whereas auto-detected defaults are reduced to their basename so
/// the host's PATH resolves the correct binary.
///
/// In Flatpak mode without an explicit GOOSE_SHELL, we skip sandbox
/// filesystem checks (e.g. `/bin/bash` existing inside the sandbox tells
/// us nothing about the host) and default to `"bash"` directly.
#[cfg(not(windows))]
fn unix_shell() -> (String, bool) {
match std::env::var("GOOSE_SHELL") {
Ok(shell) => (shell, true),
Err(_) => {
let shell = if is_flatpak() {
// Don't inspect sandbox paths — they don't reflect the host.
// Default to "bash" (ubiquitous on Flatpak-capable Linux hosts).
"bash".to_string()
} else if PathBuf::from("/bin/bash").is_file() {
"/bin/bash".to_string()
} else {
std::env::var("SHELL").unwrap_or_else(|_| "sh".to_string())
};
(shell, false)
}
}
}
/// Return the shell reference to pass to `flatpak-spawn --host`.
///
/// If the user explicitly configured GOOSE_SHELL, honour the full path
/// (it likely refers to a host binary, e.g. a Nix-profile shell).
/// Otherwise strip to basename so the host's default PATH resolves it.
#[cfg(not(windows))]
fn flatpak_shell_arg(shell: &str, is_user_configured: bool) -> &str {
if is_user_configured {
shell
} else {
std::path::Path::new(shell)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("bash")
}
}
const OUTPUT_LIMIT_LINES: usize = 2000;
pub const OUTPUT_LIMIT_BYTES: usize = 50_000;
const OUTPUT_PREVIEW_LINES: usize = 50;
@@ -69,21 +142,31 @@ pub struct ShellOutput {
/// source the user's profile and recover the full PATH.
#[cfg(not(windows))]
fn resolve_login_shell_path() -> Option<String> {
let shell = std::env::var("GOOSE_SHELL").unwrap_or_else(|_| {
if PathBuf::from("/bin/bash").is_file() {
"/bin/bash".to_string()
} else {
std::env::var("SHELL").unwrap_or_else(|_| "sh".to_string())
}
});
let (shell, is_user_configured) = unix_shell();
let mut child = std::process::Command::new(&shell)
.args(["-l", "-i", "-c", "echo $PATH"])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
let mut child = if is_flatpak() {
flatpak_spawn_process()
.args([
flatpak_shell_arg(&shell, is_user_configured),
"-l",
"-i",
"-c",
"echo $PATH",
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?
} else {
std::process::Command::new(&shell)
.args(["-l", "-i", "-c", "echo $PATH"])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?
};
let mut stdout = child.stdout.take()?;
let (tx, rx) = std::sync::mpsc::channel();
@@ -309,14 +392,7 @@ async fn run_command(
working_dir: Option<&std::path::Path>,
login_path: Option<&str>,
) -> Result<ExecutionOutput, String> {
let mut command = build_shell_command(command_line);
if let Some(path) = working_dir {
command.current_dir(path);
}
if let Some(path) = login_path {
command.env("PATH", path);
}
let mut command = build_shell_command(command_line, working_dir, login_path);
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
@@ -401,7 +477,11 @@ async fn run_command(
})
}
fn build_shell_command(command_line: &str) -> tokio::process::Command {
fn build_shell_command(
command_line: &str,
working_dir: Option<&std::path::Path>,
login_path: Option<&str>,
) -> tokio::process::Command {
#[cfg(windows)]
let mut command = {
let shell = std::env::var("GOOSE_SHELL").unwrap_or_else(|_| "cmd".to_string());
@@ -423,21 +503,45 @@ fn build_shell_command(command_line: &str) -> tokio::process::Command {
command.args(["-c", command_line]);
}
}
if let Some(path) = working_dir {
command.current_dir(path);
}
if let Some(path) = login_path {
command.env("PATH", path);
}
command
};
#[cfg(not(windows))]
let mut command = {
let shell = std::env::var("GOOSE_SHELL").unwrap_or_else(|_| {
if PathBuf::from("/bin/bash").is_file() {
"/bin/bash".to_string()
} else {
std::env::var("SHELL").unwrap_or_else(|_| "sh".to_string())
let (shell, is_user_configured) = unix_shell();
if is_flatpak() {
let mut command = flatpak_spawn_command();
if let Some(dir) = working_dir {
command.arg(format!("--directory={}", dir.display()));
}
});
let mut command = tokio::process::Command::new(shell);
command.arg("-c").arg(command_line);
command
if let Some(path) = login_path {
command.arg(format!("--env=PATH={}", path));
}
// If GOOSE_SHELL was explicitly set, honour the full path (likely a host
// binary). Otherwise use basename so the host's PATH resolves it.
command
.arg(flatpak_shell_arg(&shell, is_user_configured))
.arg("-c")
.arg(command_line);
command
} else {
let mut command = tokio::process::Command::new(shell);
command.arg("-c").arg(command_line);
if let Some(path) = working_dir {
command.current_dir(path);
}
if let Some(path) = login_path {
command.env("PATH", path);
}
command
}
};
command.set_no_window();