context: quote program, args, cwd and env in ssh, schroot and unshare drivers
All three non-local drivers assembled remote/chroot command strings by
raw concatenation: ssh pushed args verbatim (TODO: escape), schroot
interpolated env values raw so DEB_BUILD_OPTIONS='parallel=4 nocheck'
made sh treat 'nocheck' as the command, and unshare wrapped args in
unescaped double quotes letting quotes break out and $/backticks
expand. Add a shared POSIX shell_quote helper and use it for every
component interpolated into a shell string, including ssh copy_path
(which used Rust's {:?}, not shell quoting) and schroot write_file
(which also switches echo -ne to printf %s so backslash sequences in
content are no longer interpreted).
This commit is contained in:
+76
-11
@@ -1,4 +1,5 @@
|
||||
use super::api::{Context, ContextCommand, ContextDriver, LineSink};
|
||||
use super::shell::shell_quote;
|
||||
use log::debug;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
@@ -484,21 +485,85 @@ impl UnshareDriver {
|
||||
|
||||
// Build the bash command: set up /dev/pts and run the program
|
||||
// /proc should already be bind-mounted from the host before entering the namespace
|
||||
let program_args = args
|
||||
.iter()
|
||||
.map(|a| format!("\"{a}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
cmd.arg("--")
|
||||
.arg("bash")
|
||||
.arg("-c")
|
||||
.arg(format!(
|
||||
"mkdir -p /dev/pts; mount -t devpts devpts /dev/pts 2>/dev/null || true; touch /dev/ptmx; mount --bind /dev/pts/ptmx /dev/ptmx 2>/dev/null || true; {} {}",
|
||||
program,
|
||||
program_args
|
||||
));
|
||||
.arg(build_namespace_script(program, args));
|
||||
|
||||
cmd
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the shell script executed by `bash -c` inside the user namespace:
|
||||
/// bring up `/dev/pts`, then run `program` with `args`.
|
||||
///
|
||||
/// The script is parsed by bash, so the program and every argument are
|
||||
/// POSIX-shell-quoted (see [`shell_quote`]): quotes, `$`, backticks or
|
||||
/// whitespace inside them can neither split the command into different
|
||||
/// words nor trigger expansion. (Previously arguments were wrapped in
|
||||
/// unescaped double quotes, so a `"` in an argument broke out and
|
||||
/// `$`/backticks still expanded.)
|
||||
fn build_namespace_script(program: &str, args: &[String]) -> String {
|
||||
let mut script = String::from(
|
||||
"mkdir -p /dev/pts; mount -t devpts devpts /dev/pts 2>/dev/null || true; touch /dev/ptmx; mount --bind /dev/pts/ptmx /dev/ptmx 2>/dev/null || true; ",
|
||||
);
|
||||
script.push_str(&shell_quote(program));
|
||||
for arg in args {
|
||||
script.push(' ');
|
||||
script.push_str(&shell_quote(arg));
|
||||
}
|
||||
script
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::build_namespace_script;
|
||||
|
||||
fn tail_after_devpts_setup(script: &str) -> &str {
|
||||
script
|
||||
.rsplit_once("|| true; ")
|
||||
.map(|(_, rest)| rest)
|
||||
.unwrap()
|
||||
.trim_end()
|
||||
}
|
||||
|
||||
/// Program and arguments must each be a single, quoted word at the end
|
||||
/// of the `/dev/pts` setup script.
|
||||
#[test]
|
||||
fn script_quotes_program_and_args() {
|
||||
let script = build_namespace_script(
|
||||
"make",
|
||||
&[
|
||||
"install".to_string(),
|
||||
"a b".to_string(),
|
||||
"PREFIX=/opt/my app".to_string(),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
tail_after_devpts_setup(&script),
|
||||
"'make' 'install' 'a b' 'PREFIX=/opt/my app'"
|
||||
);
|
||||
}
|
||||
|
||||
/// An argument containing a double quote must not break out of the
|
||||
/// script (arguments used to be wrapped in unescaped `"`), and `$`/
|
||||
/// backticks must stay literal for bash.
|
||||
#[test]
|
||||
fn script_neutralizes_quotes_and_expansions() {
|
||||
let script = build_namespace_script(
|
||||
"echo",
|
||||
&["$(touch /tmp/pwned) `id` \"; rm -rf /\"".to_string()],
|
||||
);
|
||||
assert_eq!(
|
||||
tail_after_devpts_setup(&script),
|
||||
"'echo' '$(touch /tmp/pwned) `id` \"; rm -rf /\"'"
|
||||
);
|
||||
}
|
||||
|
||||
/// Empty arguments must survive as one empty word (`''`), not vanish.
|
||||
#[test]
|
||||
fn script_preserves_empty_args() {
|
||||
let script = build_namespace_script("prog", &[String::new(), "x".to_string()]);
|
||||
assert_eq!(tail_after_devpts_setup(&script), "'prog' '' 'x'");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user