mirror of
https://github.com/vhaudiquet/ecrou.git
synced 2026-11-12 22:22:44 +00:00
feat(guest): the guest init script
Generated into the scratch rootfs before boot: mounts each virtiofs share (ro through the mount option), runs the payload as a child so the init survives to write its exit code to the control file, then powers off through sysrq, which works without a poweroff binary in the rootfs. Shell quoting keeps payload argv and share paths free of interpolation.
This commit is contained in:
1 parent
18d81c2e7d
commit
cc9151eb56
2 files changed
+139
No files matched your search
@@ -0,0 +1,138 @@
|
|||||||
|
//! The guest init: a script provisioned into the scratch rootfs before
|
||||||
|
//! boot. It mounts the virtiofs shares, runs the payload, writes the
|
||||||
|
//! payload's exit code to the control file the host reads back, and
|
||||||
|
//! powers the machine off. As PID 1 it is deliberately the only thing
|
||||||
|
//! that runs; there is no guest agent.
|
||||||
|
|
||||||
|
/// A share as the init sees it: a virtiofs tag to mount at a guest path.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct GuestShare {
|
||||||
|
pub tag: String,
|
||||||
|
pub target: String,
|
||||||
|
pub read_only: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The control file, at the guest root: the init writes the payload's
|
||||||
|
/// exit code here, and `vm::run` reads it back host-side from the same
|
||||||
|
/// scratch directory.
|
||||||
|
pub const EXIT_FILE: &str = "/.ecrou-exit";
|
||||||
|
|
||||||
|
/// Shell-quote a string for safe interpolation into the init script.
|
||||||
|
/// Single quotes wrap everything; an embedded single quote closes,
|
||||||
|
/// escapes, and reopens.
|
||||||
|
pub fn sh_quote(s: &str) -> String {
|
||||||
|
format!("'{}'", s.replace('\'', r"'\''"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the guest init. The payload runs as a child (never `exec`):
|
||||||
|
/// the init must outlive it to write the exit code and power the
|
||||||
|
/// machine off.
|
||||||
|
pub fn init_script(shares: &[GuestShare], payload: &[String], workdir: &str) -> String {
|
||||||
|
let mut script = String::from(
|
||||||
|
"#!/bin/sh\n\
|
||||||
|
# ecrou guest init: mount the shares, run the payload, report its\n\
|
||||||
|
# exit code, power off. Generated; do not edit.\n\
|
||||||
|
set -u\n\n\
|
||||||
|
mount_share() {\n\
|
||||||
|
\x20 tag=\"$1\"; target=\"$2\"; ro=\"$3\"\n\
|
||||||
|
\x20 mkdir -p \"$target\"\n\
|
||||||
|
\x20 if [ \"$ro\" = \"ro\" ]; then\n\
|
||||||
|
\x20 mount -t virtiofs -o ro \"$tag\" \"$target\"\n\
|
||||||
|
\x20 else\n\
|
||||||
|
\x20 mount -t virtiofs \"$tag\" \"$target\"\n\
|
||||||
|
\x20 fi\n\
|
||||||
|
}\n\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
for share in shares {
|
||||||
|
let ro = if share.read_only { "ro" } else { "rw" };
|
||||||
|
script.push_str(&format!(
|
||||||
|
"mount_share {} {} {}\n",
|
||||||
|
sh_quote(&share.tag),
|
||||||
|
sh_quote(&share.target),
|
||||||
|
sh_quote(ro),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
script.push_str(&format!(
|
||||||
|
"\ncd {} || exit 126\n\n",
|
||||||
|
sh_quote(if workdir.is_empty() { "/" } else { workdir })
|
||||||
|
));
|
||||||
|
|
||||||
|
for arg in payload {
|
||||||
|
script.push_str(&sh_quote(arg));
|
||||||
|
script.push(' ');
|
||||||
|
}
|
||||||
|
script.push_str("\n\ncode=$?\n");
|
||||||
|
script.push_str(&format!("echo \"$code\" > {}\n", sh_quote(EXIT_FILE)));
|
||||||
|
// sysrq poweroff: works without a poweroff binary in the rootfs. If
|
||||||
|
// sysrq is disabled the machine lingers and the consumer's kill
|
||||||
|
// (T4) is the backstop.
|
||||||
|
script.push_str("echo o > /proc/sysrq-trigger\nexit \"$code\"\n");
|
||||||
|
script
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quotes_shell_metacharacters() {
|
||||||
|
assert_eq!(sh_quote("plain"), "'plain'");
|
||||||
|
assert_eq!(sh_quote("it's"), r"'it'\''s'");
|
||||||
|
assert_eq!(sh_quote("$HOME `id` && rm"), "'$HOME `id` && rm'");
|
||||||
|
assert_eq!(sh_quote("new\nline"), "'new\nline'");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mounts_each_share_with_its_ro_flag() {
|
||||||
|
let shares = vec![
|
||||||
|
GuestShare {
|
||||||
|
tag: "ws0".into(),
|
||||||
|
target: "/workspace".into(),
|
||||||
|
read_only: false,
|
||||||
|
},
|
||||||
|
GuestShare {
|
||||||
|
tag: "sk1".into(),
|
||||||
|
target: "/mnt/skills".into(),
|
||||||
|
read_only: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let script = init_script(&shares, &["true".to_string()], "/");
|
||||||
|
assert!(script.contains("mount_share 'ws0' '/workspace' 'rw'"));
|
||||||
|
assert!(script.contains("mount_share 'sk1' '/mnt/skills' 'ro'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn payload_runs_unquoted_free_of_interpolation() {
|
||||||
|
let payload = vec![
|
||||||
|
"/bin/bash".to_string(),
|
||||||
|
"-c".to_string(),
|
||||||
|
"echo $HOME; rm -rf '/'".to_string(),
|
||||||
|
];
|
||||||
|
let script = init_script(&[], &payload, "/");
|
||||||
|
assert!(script.contains(r"'/bin/bash' '-c' 'echo $HOME; rm -rf '\''/'\''' "));
|
||||||
|
// The payload line must appear after the cd and before the code
|
||||||
|
// capture, so its exit code is what gets written.
|
||||||
|
let cd = script.find("cd '/").unwrap();
|
||||||
|
let payload_at = script.find("'/bin/bash'").unwrap();
|
||||||
|
let capture = script.find("code=$?").unwrap();
|
||||||
|
assert!(cd < payload_at && payload_at < capture);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn writes_exit_code_and_powers_off() {
|
||||||
|
let script = init_script(&[], &["true".to_string()], "/work");
|
||||||
|
assert!(script.contains(&format!("echo \"$code\" > '{}'", EXIT_FILE)));
|
||||||
|
assert!(script.contains("echo o > /proc/sysrq-trigger"));
|
||||||
|
assert!(script.contains("exit \"$code\""));
|
||||||
|
// No exec of the payload: the init must survive it to report.
|
||||||
|
assert!(!script.contains("exec "));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_workdir_falls_back_to_root() {
|
||||||
|
let script = init_script(&[], &["true".to_string()], "");
|
||||||
|
assert!(script.contains("cd '/'"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ pub mod distro;
|
|||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod extract;
|
pub mod extract;
|
||||||
|
pub mod guest;
|
||||||
pub mod krun;
|
pub mod krun;
|
||||||
pub mod progress;
|
pub mod progress;
|
||||||
pub mod rootfs;
|
pub mod rootfs;
|
||||||
|
|||||||
Reference in new issue
Block a user