mirror of
https://github.com/vhaudiquet/ecrou.git
synced 2026-11-12 22:22:44 +00:00
fix(vm): a single quoted payload argument runs through the image's shell
'ecrou run alpine -- "apk add x && y"' reached the guest as one argv element and execvp'd a program named after the whole line. One argument containing whitespace is a quoted shell line: hand it to the image's shell as 'sh -c'. Argv vectors stay literal, including ones that begin with sh -c.
This commit is contained in:
1 parent
2e91db9acc
commit
a27879dbe7
3 files changed
+81
-7
No files matched your search
@@ -144,7 +144,11 @@ hostname and workdir ride `krun_set_exec`'s config natively (the init
|
||||
blob applies both); the script's own hostname and cd lines stay as
|
||||
fallbacks. There is no exec (the init must outlive the payload to
|
||||
report it) and no share-less special case: the same contract covers a
|
||||
bare shell run. Ending the machine is not the script's job either:
|
||||
bare shell run. The payload itself is an argv vector taken
|
||||
literally; the one rewrite is a single argument containing whitespace,
|
||||
which is a quoted shell line ("apk add x && y") and runs through the
|
||||
image's shell as `sh -c`. Ending the machine is not the script's job
|
||||
either:
|
||||
when the script exits, the init blob reports the code and the VMM
|
||||
powers the machine off (the libkrunfw kernel has no sysrq).
|
||||
|
||||
@@ -223,12 +227,13 @@ pub struct RunOptions {
|
||||
pub workdir: String, // guest cwd; empty = /
|
||||
pub env: Vec<(String, String)>, // composed envp; host env never inherited
|
||||
pub hostname: String, // empty = generated ecrou-vm-XXXXXX
|
||||
pub quiet: bool,
|
||||
pub log_level: LogLevel, // Quiet | Normal | Verbose
|
||||
}
|
||||
pub fn run(options: &RunOptions, progress: &dyn Progress) -> Result<i32>
|
||||
```
|
||||
|
||||
stdio is inherited; the return value is the guest exit code. A run
|
||||
Guest output reaches real stdout through the streamed console; the
|
||||
return value is the guest exit code. A run
|
||||
without working KVM is `Error::KvmUnavailable` with the remediation
|
||||
text. Everything else is `Error::Other` over the anyhow chain, ecr's
|
||||
shape. The v1 API has no captured-output variant and no long-lived
|
||||
|
||||
@@ -29,7 +29,9 @@ pub enum Command {
|
||||
pub struct RunArgs {
|
||||
/// Distribution name or OCI image reference
|
||||
pub image: String,
|
||||
/// Command to run in the machine; the image's default shell when omitted
|
||||
/// Command to run in the machine; the image's default shell when
|
||||
/// omitted. An argument vector after `--` is taken literally; a
|
||||
/// single quoted line runs through the image's shell.
|
||||
#[arg(last = true)]
|
||||
pub command: Vec<String>,
|
||||
/// Directory to share into the machine as HOST:GUEST[:ro] (repeatable)
|
||||
|
||||
+70
-3
@@ -362,10 +362,19 @@ fn collect_shares(
|
||||
|
||||
/// The guest payload: the caller's argv, or the rootfs's default shell.
|
||||
fn payload_command(options: &RunOptions, rootfs_dir: &Path) -> Vec<String> {
|
||||
if !options.command.is_empty() {
|
||||
return options.command.clone();
|
||||
let shell = utils::detect_shell(rootfs_dir).to_string();
|
||||
match options.command.as_slice() {
|
||||
// No command: the image's shell.
|
||||
[] => vec![shell],
|
||||
// One quoted shell line: `ecrou run alpine -- "apk add x && y"`.
|
||||
// The whole line would otherwise be execvp'd as one program
|
||||
// name; the consumer clearly asked for a shell line.
|
||||
[line] if line.contains(char::is_whitespace) => {
|
||||
vec![shell, "-c".to_string(), line.clone()]
|
||||
}
|
||||
// An argv vector, taken literally.
|
||||
cmd => cmd.to_vec(),
|
||||
}
|
||||
vec![utils::detect_shell(rootfs_dir).to_string()]
|
||||
}
|
||||
|
||||
/// The guest environment, composed from scratch: a usable base, the
|
||||
@@ -447,6 +456,64 @@ fn parse_memory_mib(s: &str) -> anyhow::Result<u32> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A scratch with just a shell, for payload_command tests.
|
||||
fn shell_rootfs() -> tempfile::TempDir {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir_all(dir.path().join("bin")).unwrap();
|
||||
std::fs::write(dir.path().join("bin/sh"), "").unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payload_defaults_to_the_image_shell() {
|
||||
let rootfs = shell_rootfs();
|
||||
let options = RunOptions::default();
|
||||
assert_eq!(
|
||||
payload_command(&options, rootfs.path()),
|
||||
vec!["/bin/sh".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payload_argv_is_taken_literally() {
|
||||
let rootfs = shell_rootfs();
|
||||
let options = RunOptions {
|
||||
command: vec!["uname".to_string(), "-a".to_string()],
|
||||
..RunOptions::default()
|
||||
};
|
||||
assert_eq!(
|
||||
payload_command(&options, rootfs.path()),
|
||||
vec!["uname".to_string(), "-a".to_string()]
|
||||
);
|
||||
// Even when it looks like a shell invocation: the consumer
|
||||
// asked for argv.
|
||||
let options = RunOptions {
|
||||
command: vec!["sh".to_string(), "-c".to_string(), "exit 7".to_string()],
|
||||
..RunOptions::default()
|
||||
};
|
||||
assert_eq!(
|
||||
payload_command(&options, rootfs.path()),
|
||||
vec!["sh".to_string(), "-c".to_string(), "exit 7".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payload_single_quoted_line_runs_through_the_shell() {
|
||||
let rootfs = shell_rootfs();
|
||||
let options = RunOptions {
|
||||
command: vec!["apk add fastfetch && fastfetch".to_string()],
|
||||
..RunOptions::default()
|
||||
};
|
||||
assert_eq!(
|
||||
payload_command(&options, rootfs.path()),
|
||||
vec![
|
||||
"/bin/sh".to_string(),
|
||||
"-c".to_string(),
|
||||
"apk add fastfetch && fastfetch".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memory_parses_to_whole_mib() {
|
||||
assert_eq!(parse_memory_mib("1G").unwrap(), 1024);
|
||||
|
||||
Reference in new issue
Block a user