Do not isolate a candidate against a stopped incumbent

prove_isolation compared the candidate's live process identity against
the incumbent's identity as recorded at admission. When the incumbent has
stopped, systemd has already released its DynamicUser UID and is free to
hand that same UID to the candidate -- so identical values mean ordinary
UID reuse, not a namespace collision, and fencing refused forever with
"candidate and incumbent are not distinct by UID, PID namespace, and
mount namespace".

Isolation is a property of two processes running at once. When the
admitted incumbent is permanently stopped there is nothing to be isolated
from, so the comparison is skipped. is_permanently_stopped already exists
on WorkloadPort for exactly this distinction.

Found by deploying: odin's incumbent died mid-deployment, and every
subsequent attempt to replace it was refused by this check.

  cargo test --locked -> test result: ok. 102 passed; 0 failed; 1 ignored

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-10 00:24:52 +02:00
commit 8448f8355a

View file

@ -3551,8 +3551,17 @@ impl Engine {
let now = now_millis()?;
let snapshot = ControlSnapshot::read(&self.options.state_store)?;
let incumbent = self.exact_incumbent(&snapshot, &current.value)?;
let isolation =
prove_isolation(workload, incumbent.map(|value| &value.value.workload))?;
// Isolation is a property of two processes running at once. A
// stopped incumbent has already released its DynamicUser UID, and
// systemd is free to hand that same UID to the candidate -- so
// comparing the candidate against the incumbent's *recorded*
// identity turns ordinary UID reuse into a permanent refusal to
// fence. There is nothing left to be isolated from.
let incumbent_workload = match incumbent {
Some(value) if self.workload.is_permanently_stopped(&value.value.workload)? => None,
other => other.map(|value| &value.value.workload),
};
let isolation = prove_isolation(workload, incumbent_workload)?;
return self.persist_same_phase(current, |next| {
next.isolation = Some(isolation);
next.updated_at_unix_millis = now;