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)
|
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> {
|
fn decode_status(status: libc::c_int, exit_file_host: &Path) -> Result<i32> {
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
let code = if let Some(sig) = std::process::ExitStatus::from_raw(status).signal() {
|
let status = std::process::ExitStatus::from_raw(status);
|
||||||
128 + sig
|
if let Some(sig) = status.signal() {
|
||||||
} else {
|
// The init catches the payload's signals and reports them as
|
||||||
std::process::ExitStatus::from_raw(status)
|
// codes, so a signal on the machine child means the VMM or the
|
||||||
.code()
|
// engine died: the payload never reported.
|
||||||
.unwrap_or(125)
|
return Err(Error::Other(anyhow!(
|
||||||
};
|
"the machine process died from signal {sig}; this is an \
|
||||||
if code == 125 {
|
engine or hypervisor failure, not the payload's exit code"
|
||||||
// Boot failure: the init blob never reported a code. The
|
)));
|
||||||
// control file is the diagnostic when it ran partway.
|
}
|
||||||
|
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(s) = std::fs::read_to_string(exit_file_host) {
|
||||||
if let Ok(c) = s.trim().parse::<i32>() {
|
if let Ok(c) = s.trim().parse::<i32>() {
|
||||||
return Ok(c);
|
return Ok(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Err(Error::Other(anyhow!(
|
let cause = if code == 125 {
|
||||||
"the machine failed to boot; run with --quiet off and check the \
|
"the machine failed to boot"
|
||||||
console output above"
|
} 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