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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user