mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
feat(shell): refuse NUL bytes in command/cwd before spawn (#5529 mode 3)
A NUL byte cannot cross the exec boundary: Command panics on it, killing the worker instead of failing the call. require_no_nul rejects command and cwd/working_dir with the byte offset and a re-send hint. Background-job observe/resume half of mode 3 already exists via the tasks tool (list/read/cancel).
This commit is contained in:
1 parent
6533d44df9
commit
b0866943b4
2 files changed
+61
-1
No files matched your search
@@ -3872,6 +3872,17 @@ fn attach_shell_owner_metadata(metadata: &mut serde_json::Value, context: &ToolC
|
||||
metadata["owner_agent_name"] = json!(owner.agent_name);
|
||||
}
|
||||
|
||||
/// NUL bytes cannot cross the `exec` boundary: `Command` panics on them.
|
||||
/// Refuse with the byte offset before anything spawns (#5529).
|
||||
fn require_no_nul<'a>(value: &'a str, field: &str) -> Result<&'a str, ToolError> {
|
||||
if let Some(offset) = value.find('\0') {
|
||||
return Err(ToolError::invalid_input(format!(
|
||||
"Shell {field} contains a NUL byte at byte offset {offset}; it cannot cross the exec boundary. Remove it (usually a truncated heredoc or binary paste) and re-send."
|
||||
)));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn enforce_readonly_github_network_policy(
|
||||
command: &str,
|
||||
context: &ToolContext,
|
||||
@@ -5109,7 +5120,7 @@ impl ToolSpec for BashTool {
|
||||
)));
|
||||
}
|
||||
}
|
||||
let command = required_str(&input, "command")?;
|
||||
let command = require_no_nul(required_str(&input, "command")?, "command")?;
|
||||
match context.shell_policy {
|
||||
ShellPolicy::None => {
|
||||
return Ok(ToolResult::error(
|
||||
@@ -5278,6 +5289,7 @@ impl ToolSpec for BashTool {
|
||||
value
|
||||
.as_str()
|
||||
.ok_or_else(|| type_mismatch(name, value, "a string"))
|
||||
.and_then(|dir| require_no_nul(dir, name))
|
||||
})
|
||||
.transpose()?
|
||||
{
|
||||
|
||||
@@ -3885,6 +3885,54 @@ async fn unknown_bash_action_is_refused_instead_of_running_the_command() {
|
||||
assert!(!marker.exists(), "the command must not have run");
|
||||
}
|
||||
|
||||
/// A NUL byte cannot cross the `exec` boundary: `Command` panics on it.
|
||||
/// Refuse with the byte offset before anything spawns (#5529).
|
||||
#[tokio::test]
|
||||
async fn nul_byte_in_shell_command_is_refused_before_spawn() {
|
||||
let workspace = tempdir().expect("workspace");
|
||||
let context = ToolContext::new(workspace.path().to_path_buf());
|
||||
let marker = workspace.path().join("should-not-exist");
|
||||
|
||||
let error = BashTool::new("Bash")
|
||||
.execute(
|
||||
json!({
|
||||
"command": format!("echo hi\0; touch {}", marker.display()),
|
||||
}),
|
||||
&context,
|
||||
)
|
||||
.await
|
||||
.expect_err("NUL byte must be refused");
|
||||
|
||||
let message = error.to_string();
|
||||
assert!(message.contains("NUL byte"), "{message}");
|
||||
assert!(message.contains("byte offset 7"), "{message}");
|
||||
assert!(!marker.exists(), "the command must not have run");
|
||||
}
|
||||
|
||||
/// `cwd` crosses the same boundary via `current_dir`, so the guard covers it
|
||||
/// too (#5529).
|
||||
#[tokio::test]
|
||||
async fn nul_byte_in_shell_cwd_is_refused_before_spawn() {
|
||||
let workspace = tempdir().expect("workspace");
|
||||
let context = ToolContext::new(workspace.path().to_path_buf());
|
||||
|
||||
let error = BashTool::new("Bash")
|
||||
.execute(
|
||||
json!({
|
||||
"command": "echo hi",
|
||||
"cwd": "sub\0dir",
|
||||
}),
|
||||
&context,
|
||||
)
|
||||
.await
|
||||
.expect_err("NUL byte must be refused");
|
||||
|
||||
let message = error.to_string();
|
||||
assert!(message.contains("NUL byte"), "{message}");
|
||||
assert!(message.contains("cwd"), "{message}");
|
||||
assert!(message.contains("byte offset 3"), "{message}");
|
||||
}
|
||||
|
||||
/// The same hole one type down. `and_then(as_str).unwrap_or("run")` read a
|
||||
/// non-string `action` as absent and fell through to the branch that executes
|
||||
/// arbitrary code, so `Bash{action: 3, command: "…"}` ran the command. `File`,
|
||||
|
||||
Reference in new issue
Block a user