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
|
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
|
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
|
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
|
when the script exits, the init blob reports the code and the VMM
|
||||||
powers the machine off (the libkrunfw kernel has no sysrq).
|
powers the machine off (the libkrunfw kernel has no sysrq).
|
||||||
|
|
||||||
@@ -223,12 +227,13 @@ pub struct RunOptions {
|
|||||||
pub workdir: String, // guest cwd; empty = /
|
pub workdir: String, // guest cwd; empty = /
|
||||||
pub env: Vec<(String, String)>, // composed envp; host env never inherited
|
pub env: Vec<(String, String)>, // composed envp; host env never inherited
|
||||||
pub hostname: String, // empty = generated ecrou-vm-XXXXXX
|
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>
|
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
|
without working KVM is `Error::KvmUnavailable` with the remediation
|
||||||
text. Everything else is `Error::Other` over the anyhow chain, ecr's
|
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
|
shape. The v1 API has no captured-output variant and no long-lived
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ pub enum Command {
|
|||||||
pub struct RunArgs {
|
pub struct RunArgs {
|
||||||
/// Distribution name or OCI image reference
|
/// Distribution name or OCI image reference
|
||||||
pub image: String,
|
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)]
|
#[arg(last = true)]
|
||||||
pub command: Vec<String>,
|
pub command: Vec<String>,
|
||||||
/// Directory to share into the machine as HOST:GUEST[:ro] (repeatable)
|
/// 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.
|
/// The guest payload: the caller's argv, or the rootfs's default shell.
|
||||||
fn payload_command(options: &RunOptions, rootfs_dir: &Path) -> Vec<String> {
|
fn payload_command(options: &RunOptions, rootfs_dir: &Path) -> Vec<String> {
|
||||||
if !options.command.is_empty() {
|
let shell = utils::detect_shell(rootfs_dir).to_string();
|
||||||
return options.command.clone();
|
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
|
/// 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 {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn memory_parses_to_whole_mib() {
|
fn memory_parses_to_whole_mib() {
|
||||||
assert_eq!(parse_memory_mib("1G").unwrap(), 1024);
|
assert_eq!(parse_memory_mib("1G").unwrap(), 1024);
|
||||||
|
|||||||
Reference in new issue
Block a user