From 38136cb0c00ef6fcdb20ddbd8178a899227f23ca Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Mar 2025 12:07:53 -0500 Subject: [PATCH] Add sandboxed_shell --- Cargo.lock | 23 ++-- crates/scripting_tool/Cargo.toml | 1 + crates/scripting_tool/src/sandboxed_shell.rs | 104 +++++++++++++++++++ crates/scripting_tool/src/scripting_tool.rs | 1 + 4 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 crates/scripting_tool/src/sandboxed_shell.rs diff --git a/Cargo.lock b/Cargo.lock index 9004719ae8..9d395b116b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2300,7 +2300,7 @@ dependencies = [ "cap-primitives", "cap-std", "io-lifetimes", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2328,7 +2328,7 @@ dependencies = [ "ipnet", "maybe-owned", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", "winx", ] @@ -4402,7 +4402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5064,7 +5064,7 @@ checksum = "5e2e6123af26f0f2c51cc66869137080199406754903cc926a7690401ce09cb4" dependencies = [ "io-lifetimes", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6709,7 +6709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" dependencies = [ "io-lifetimes", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -10734,7 +10734,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -11656,7 +11656,7 @@ dependencies = [ "libc", "linux-raw-sys", "once_cell", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -11927,6 +11927,7 @@ dependencies = [ "serde", "serde_json", "settings", + "shlex", "util", ] @@ -13426,7 +13427,7 @@ dependencies = [ "fd-lock", "io-lifetimes", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", "winx", ] @@ -13566,7 +13567,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -15912,7 +15913,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -16377,7 +16378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ "bitflags 2.8.0", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/crates/scripting_tool/Cargo.toml b/crates/scripting_tool/Cargo.toml index ab80d96fe0..fcb342be4e 100644 --- a/crates/scripting_tool/Cargo.toml +++ b/crates/scripting_tool/Cargo.toml @@ -16,6 +16,7 @@ doctest = false anyhow.workspace = true assistant_tool.workspace = true collections.workspace = true +shlex.workspace = true futures.workspace = true gpui.workspace = true mlua.workspace = true diff --git a/crates/scripting_tool/src/sandboxed_shell.rs b/crates/scripting_tool/src/sandboxed_shell.rs new file mode 100644 index 0000000000..5da0362a48 --- /dev/null +++ b/crates/scripting_tool/src/sandboxed_shell.rs @@ -0,0 +1,104 @@ +/// Models will commonly generate POSIX shell one-liner commands which +/// they run via io.popen() in Lua. Instead of giving those shell command +/// strings to the operating system - which is a security risk, and +/// which can eaisly fail on Windows, since Windows doesn't do POSIX - we +/// parse the shell command ourselves and translate it into a sequence of +/// commands in our normal sandbox. Essentially, this is an extremely +/// minimalstic shell which Lua popen() commands can execute in. +use mlua::{Error, Result}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct ShellCmd { + command: String, + args: Vec, + stdout_redirect: Option, + stderr_redirect: Option, +} + +impl ShellCmd { + /// Parse a shell string, which we assume the model will generate in POSIX format. + /// Note that since we are turning this into our own representation, this should + /// work seamlessly on Windows too, even though Windows has a different shell syntax. + /// + /// If there are multiple commands piped into one another, this returns them all. + pub fn parse_shell_str(string: &str) -> Result> { + // For now, we don't support any of these shell features. We can add support them + // in the future, though. + if string.contains('$') + || string.contains('`') + || string.contains('(') + || string.contains(')') + || string.contains(';') + || string.contains('&') + || string.contains('{') + || string.contains('}') + { + return Err(Error::RuntimeError( + "Complex shell features (pipes, subshells, variables, etc.) are not available in this shell." + .to_string(), + )); + } + + // Use shlex to split the command line into tokens + let tokens = shlex::split(string) + .ok_or_else(|| Error::RuntimeError("Failed to parse shell command".to_string()))?; + + // The first token is the command + let mut tokens_iter = tokens.into_iter(); + let Some(command) = tokens_iter.next() else { + return Err(Error::RuntimeError("Missing popen command".to_string())); + }; + + let mut args = Vec::new(); + let mut stdout_redirect = None; + let mut stderr_redirect = None; + + // Process the remaining tokens + while let Some(token) = tokens_iter.next() { + match token.as_str() { + ">" | "1>" => { + // stdout redirection + let target = tokens_iter.next().ok_or_else(|| { + Error::RuntimeError("Missing redirection target".to_string()) + })?; + stdout_redirect = Some(target); + } + "2>" => { + // stderr redirection + let target = tokens_iter.next().ok_or_else(|| { + Error::RuntimeError("Missing redirection target".to_string()) + })?; + stderr_redirect = Some(target); + } + "&>" | ">&" => { + // both stdout and stderr redirection + let target = tokens_iter.next().ok_or_else(|| { + Error::RuntimeError("Missing redirection target".to_string()) + })?; + stdout_redirect = Some(target.clone()); + stderr_redirect = Some(target); + } + _ if token.starts_with(">") + || token.starts_with("2>") + || token.starts_with("&>") => + { + // Handle cases like ">file" without a space + return Err(Error::RuntimeError( + "Redirections must have a space between operator and target".to_string(), + )); + } + _ => { + // Regular argument + args.push(token); + } + } + } + + Ok(ShellCmd { + command, + args, + stdout_redirect, + stderr_redirect, + }) + } +} diff --git a/crates/scripting_tool/src/scripting_tool.rs b/crates/scripting_tool/src/scripting_tool.rs index 0880c42a0c..3cd286098a 100644 --- a/crates/scripting_tool/src/scripting_tool.rs +++ b/crates/scripting_tool/src/scripting_tool.rs @@ -1,3 +1,4 @@ +mod sandboxed_shell; mod session; use project::Project;