pub const STDERR_DIAGNOSTIC_DEADLINE: Duration;Expand description
Total wall-clock ALL diagnostic writes in ONE PROCESS may spend waiting for a reader that is not draining. Hermit forks, so read the fork note below before treating this as the whole exit path’s budget.
⚠️ WITHOUT A CEILING THIS LOOP NEVER ENDS, AND IT RUNS WHILE HERMIT IS ALREADY
FAILING. EAGAIN -> poll(POLLOUT, 1s) -> retry is correct for a reader that
is SLOW and unbounded for a reader that is STOPPED. Measured 2026-08-26 on a
4096-byte pipe filled to 3900 with O_NONBLOCK set, where nothing ever reads:
before RetryingStderr (eprintln!) EXITED rc=101 immediately
RetryingStderr, no ceiling STILL RUNNING after 25s, no exitThe first is wrong loudly; the second does not return at all, on the path that reports why hermit is stopping. A supervisor that hangs while reporting an error is harder to diagnose than one that dies reporting it badly.
⚠️ THIS IS A TOTAL FOR THE WHOLE PROCESS, NOT A BUDGET PER write() CALL, AND
THE DIFFERENCE IS THE ENTIRE GUARANTEE. An earlier version started the clock
inside fn write, so every call got the full allowance. Nothing writes to
stderr exactly once on the exit path:
hermit-cli/src/bin/hermit/main.rs the failure class, the head of the
error chain, then ONE PER CAUSE in
`for cause in chain` -- 2 + chain length
hermit-cli/src/bin/hermit/tracing.rs .with_writer(|| RetryingStderr) is the
writer for the WHOLE subscriber: one
write per log event
detcore/src/tool_global.rs a multi-part `write!`, which `write_fmt`
splits into several `write` callsWith a per-call budget the process-level cost was N times this number, N is
bounded by the error chain rather than by anything here, and every caller’s
let _ = swallowed the overrun silently. A --log info run measured at 138
lines would have been minutes, not seconds.
⚠️ THE VALUE IS DERIVED FROM THE BOUND THAT ENCLOSES IT, NOT CHOSEN. Diagnostics
on the exit path run inside RUN_TIMEOUT_UNWIND_GRACE (10s,
hermit-cli/src/lib.rs): the window between a --timeout expiring and the
SIGALRM fallback calling _exit. Overrun there does not merely delay the
report, it loses it – the fallback fires mid-sentence and additionally emits
HERMIT_RUN_TIMEOUT_FALLBACK, which is supposed to mean the teardown wedged.
So the arithmetic is:
⚠️ IT USED TO APPLY ONCE PER PROCESS, BECAUSE HERMIT FORKS. Container::run
forks the container init, which got its own COPY of the process-wide clock and
spent its own full deadline; both write diagnostics to the same fd 2 on the way
out. Measured 2026-08-26 against a stopped reader on a real guest, which is the
case where both processes report:
deadline 5000ms -> 10.03s observed two processes, one deadline each
deadline 2500ms -> 5.02s observed the same two, halved
deadline 2500ms -> 2.52s observed missing guest: only ONE process reports⚠️ AND THE MULTIPLIER WAS NOT ALWAYS TWO, WHICH IS WHY IT IS NO LONGER A
CONSTANT. hermit run --verify runs the guest twice and forks once per run, so
it is the outer hermit plus TWO container inits – three writers, and siblings
rather than a chain. From the call structure, so it needs no sampling:
run.rs fn verify() Run1 -> self.run_verify(..) Run2 -> self.run_verify(..)
fn run_verify() -> with_container(..) forks a container initAt three, 3 x 2500ms x 2 = 15s against a 10s grace: the stated split does not
hold. Correcting the constant to three would force the deadline to ~1666ms and
leave a hardcoded count for the next differently-forking path to falsify.
So the ORIGIN is shared across the invocation instead (see
STDERR_ORIGIN_ADDR): every hermit process measures from the same start, the
invocation spends ONE deadline however many processes write, and the count
stops being an input. The arithmetic no longer contains it:
RUN_TIMEOUT_UNWIND_GRACE 10s the enclosing bound
diagnostics may have half of it 5s leaving 5s for the unwind
this deadline, per INVOCATION 2500ms comfortably inside the 5sHalf is a split, not a measurement, and is stated as one. What is ALSO measured is
that the previous arrangement could not fit: at 5s per CALL, the same real-guest
fixture took 15.04s – three blocked writes – against a 10s grace, so the inner
bound exceeded its outer bound before any unwind work happened at all.
hermit-cli asserts this relationship in a test, so raising either side without
the other fails by name.
On expiry the write returns WouldBlock, writeln! gives up, the caller’s
let _ = drops that line, and hermit exits — the pre-existing contract for a
diagnostic that cannot be delivered.