feat: enhance safety filters for shell commands by normalizing ANSI-C quoting
This commit is contained in:
+30
-6
@@ -15,10 +15,17 @@ use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::io::BufRead;
|
||||
|
||||
/// Maximum number of output lines buffered in memory per background job.
|
||||
/// Beyond this limit, old output is dropped to prevent OOM (CWE-770).
|
||||
/// 10_000 lines at ~100 bytes each ≈ 1 MiB per job, sufficient for most
|
||||
/// command output. The stderr drain thread also uses the same limit.
|
||||
const MAX_OUTPUT_LINES: usize = 10_000;
|
||||
|
||||
/// Handle to a bash command running in a detached background thread.
|
||||
///
|
||||
/// Why: output is streamed over an mpsc channel rather than buffered
|
||||
/// Why: output is streamed over a bounded mpsc channel rather than buffered
|
||||
/// synchronously, so the TUI can poll for new lines without blocking.
|
||||
/// The bounded channel prevents OOM from fast producers (e.g. `yes`).
|
||||
pub struct BashJob {
|
||||
pub id: String,
|
||||
pub child_pid: u32,
|
||||
@@ -43,9 +50,10 @@ pub struct BashJob {
|
||||
/// output channel.
|
||||
pub fn spawn_bash_job(command: String) -> BashJob {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let (output_tx, output_rx) = mpsc::channel::<String>();
|
||||
let (output_tx, output_rx) = mpsc::sync_channel::<String>(MAX_OUTPUT_LINES);
|
||||
let (pid_tx, pid_rx) = mpsc::channel::<u32>();
|
||||
let cmd = command.clone();
|
||||
let id_for_log = id.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut child = match Command::new("sh")
|
||||
@@ -57,8 +65,8 @@ pub fn spawn_bash_job(command: String) -> BashJob {
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
let _ = output_tx.send(format!("__error:{}", e));
|
||||
let _ = output_tx.send("__exit:-1".to_string());
|
||||
let _ = output_tx.try_send(format!("__error:{}", e));
|
||||
let _ = output_tx.try_send("__exit:-1".to_string());
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -70,24 +78,40 @@ pub fn spawn_bash_job(command: String) -> BashJob {
|
||||
// the child produces more than ~64 KB of stderr after closing
|
||||
// stdout (the pipe buffer fills and the child blocks on write,
|
||||
// while the parent thread waits for the child to exit).
|
||||
let stderr_tx = output_tx.clone();
|
||||
let _stderr_drain = child.stderr.take().map(|stderr| {
|
||||
std::thread::spawn(move || {
|
||||
let reader = std::io::BufReader::new(stderr);
|
||||
// stderr is intentionally discarded to prevent output-line
|
||||
// quota pressure from error diagnostics.
|
||||
for _line in reader.lines().map_while(Result::ok) {
|
||||
// Discard stderr lines to prevent pipe buffer deadlock.
|
||||
}
|
||||
drop(stderr_tx);
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
let reader = std::io::BufReader::new(stdout);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
let _ = output_tx.send(line);
|
||||
// Use try_send so if the channel buffer is full (producer
|
||||
// faster than consumer), old lines are silently dropped
|
||||
// rather than growing memory without bound.
|
||||
if output_tx.try_send(line).is_err() {
|
||||
// Buffer full — consumer is not draining fast enough.
|
||||
// Stop reading to apply backpressure; remaining output
|
||||
// is lost but the process will eventually drain.
|
||||
tracing::debug!(
|
||||
"[bgbash:{}] output buffer full ({} lines), discarding remaining output",
|
||||
id_for_log, MAX_OUTPUT_LINES,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let status = child.wait();
|
||||
let code = status.ok().and_then(|s| s.code());
|
||||
let _ = output_tx.send(format!("__exit:{}", code.unwrap_or(-1)));
|
||||
let _ = output_tx.try_send(format!("__exit:{}", code.unwrap_or(-1)));
|
||||
});
|
||||
|
||||
let child_pid = pid_rx.recv().unwrap_or(0);
|
||||
|
||||
Reference in New Issue
Block a user