mirror of
https://github.com/vhaudiquet/ecrou.git
synced 2026-11-12 22:22:44 +00:00
fix(supervise): report engine failures instead of guest exit codes
Only the guest init can mint a guest code: it reports the payload's code through the control file and the VMM exits the child with it. A signal on the machine child therefore means the VMM or the engine died; mapping it to 128+n masqueraded an OOM-killed VMM as the payload having died by that signal (T3). 125 and 126 keep their two-sided reading: the child bails with 125 on a boot failure and the init refuses with 126 on a failed share or a missing workdir, but a payload may genuinely exit either code — the control file is the arbiter, and a run that never reached the payload is an error naming the cause.
This commit is contained in:
1 parent
0ce450b19d
commit
2e6c84b65b
1 file changed
+99
-15
@@ -221,27 +221,111 @@ pub(crate) fn wait_machine(
|
||||
decode_status(status, exit_file_host)
|
||||
}
|
||||
|
||||
/// Translate the machine child's raw wait status into the result of a
|
||||
/// run. Only the guest init can mint a guest exit code: it reports the
|
||||
/// payload's code through the control file and the VMM exits the child
|
||||
/// with it. Everything else is ecrou's own machinery talking, and is
|
||||
/// an error, never a code — the shell's 128+n convention would
|
||||
/// otherwise masquerade a SIGKILLed or OOM-killed VMM as the payload
|
||||
/// having died by that signal.
|
||||
fn decode_status(status: libc::c_int, exit_file_host: &Path) -> Result<i32> {
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
let code = if let Some(sig) = std::process::ExitStatus::from_raw(status).signal() {
|
||||
128 + sig
|
||||
} else {
|
||||
std::process::ExitStatus::from_raw(status)
|
||||
.code()
|
||||
.unwrap_or(125)
|
||||
};
|
||||
if code == 125 {
|
||||
// Boot failure: the init blob never reported a code. The
|
||||
// control file is the diagnostic when it ran partway.
|
||||
let status = std::process::ExitStatus::from_raw(status);
|
||||
if let Some(sig) = status.signal() {
|
||||
// The init catches the payload's signals and reports them as
|
||||
// codes, so a signal on the machine child means the VMM or the
|
||||
// engine died: the payload never reported.
|
||||
return Err(Error::Other(anyhow!(
|
||||
"the machine process died from signal {sig}; this is an \
|
||||
engine or hypervisor failure, not the payload's exit code"
|
||||
)));
|
||||
}
|
||||
let code = status.code().unwrap_or(125);
|
||||
match code {
|
||||
// Reserved by the machinery itself: 125 is the child's bail
|
||||
// code (boot failure), 126 is the init refusing to run the
|
||||
// payload (a share that would not mount, a missing workdir). A
|
||||
// payload that genuinely exits either code reported through
|
||||
// the control file first — the file is the arbiter.
|
||||
125 | 126 => {
|
||||
if let Ok(s) = std::fs::read_to_string(exit_file_host) {
|
||||
if let Ok(c) = s.trim().parse::<i32>() {
|
||||
return Ok(c);
|
||||
}
|
||||
}
|
||||
return Err(Error::Other(anyhow!(
|
||||
"the machine failed to boot; run with --quiet off and check the \
|
||||
console output above"
|
||||
)));
|
||||
let cause = if code == 125 {
|
||||
"the machine failed to boot"
|
||||
} else {
|
||||
"the guest init aborted before running the payload: a \
|
||||
share failed to mount, or the workdir does not exist"
|
||||
};
|
||||
Err(Error::Other(anyhow!(
|
||||
"{cause}; check the console output above"
|
||||
)))
|
||||
}
|
||||
_ => Ok(code),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The raw waitpid status for a child exited with `code`.
|
||||
fn exited(code: i32) -> libc::c_int {
|
||||
code << 8
|
||||
}
|
||||
|
||||
/// The raw waitpid status for a child killed by `sig`.
|
||||
fn signaled(sig: i32) -> libc::c_int {
|
||||
sig
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_signaled_machine_is_an_engine_failure() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
for raw in [signaled(libc::SIGSEGV), signaled(libc::SIGKILL), signaled(libc::SIGTERM)] {
|
||||
let err = decode_status(raw, &dir.path().join("absent")).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("engine or hypervisor failure"),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_boot_failure_is_an_error_and_a_payload_code_is_not() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let exit_file = dir.path().join(".ecrou-exit");
|
||||
// 125 with no report: the child's bail code.
|
||||
let err = decode_status(exited(125), &exit_file).unwrap_err();
|
||||
assert!(err.to_string().contains("failed to boot"), "{err}");
|
||||
// 126 with no report: the init refused (share, workdir).
|
||||
let err = decode_status(exited(126), &exit_file).unwrap_err();
|
||||
assert!(err.to_string().contains("share failed to mount"), "{err}");
|
||||
// A payload that genuinely exited a reserved code reported
|
||||
// through the control file first.
|
||||
std::fs::write(&exit_file, "7").unwrap();
|
||||
assert_eq!(decode_status(exited(125), &exit_file).unwrap(), 7);
|
||||
std::fs::write(&exit_file, "3").unwrap();
|
||||
assert_eq!(decode_status(exited(126), &exit_file).unwrap(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_ordinary_code_needs_no_control_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
assert_eq!(
|
||||
decode_status(exited(42), &dir.path().join("absent")).unwrap(),
|
||||
42
|
||||
);
|
||||
assert_eq!(
|
||||
decode_status(exited(0), &dir.path().join("absent")).unwrap(),
|
||||
0
|
||||
);
|
||||
// A garbage control file does not turn a normal code into an
|
||||
// error either — the code came through the VMM's own exit.
|
||||
let exit_file = dir.path().join(".ecrou-exit");
|
||||
std::fs::write(&exit_file, "not a number").unwrap();
|
||||
assert_eq!(decode_status(exited(42), &exit_file).unwrap(), 42);
|
||||
}
|
||||
Ok(code)
|
||||
}
|
||||
Reference in new issue
Block a user