diff --git a/drivers/cube/cubelinux_store.rs b/drivers/cube/cubelinux_store.rs index a930ab37f..a19b6b7cc 100644 --- a/drivers/cube/cubelinux_store.rs +++ b/drivers/cube/cubelinux_store.rs @@ -938,7 +938,7 @@ fn resolve_layout(b: &[u8]) -> Result { /// One record on its way into the merged store. Fixed size, so a `KVVec` of these sorts /// in place; values live in a pool beside them and are referenced by offset. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Copy, PartialEq, Eq)] struct Entry { space: [u8; 32], key: [u8; 24], @@ -952,6 +952,38 @@ struct Entry { deleted: bool, } +/// Order by `(space, key, seq, …)` — **`seq` before `flags`**, which is not the order the fields are +/// declared in and cannot be left to a derive. +/// +/// A fold keeps, for each coordinate, the *last* entry in this order: that is how "the later write +/// wins" is implemented. With `flags` compared first, "later" is not what the order means: a `del` +/// carries no class (0) and a **sealed** record carries `0x0800`, so the removal sorted *before* the +/// record it removed and the record won — a deleted record came back from the fold, with its old +/// value, on a store whose writes are sealed. Found on the box on 2026-09-25 by `put` → `del` → +/// `cube-fold` → the record returning; the guest gate missed it because unsealed writes share one +/// flag value, which is why the gate now deletes a *classed* record. +/// +/// `flags` stays in the order, after `seq`: the sort also has to be total and deterministic, and two +/// entries can share a coordinate and a sequence only if they came from the same source. +impl Ord for Entry { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.space + .cmp(&other.space) + .then_with(|| self.key.cmp(&other.key)) + .then_with(|| self.seq.cmp(&other.seq)) + .then_with(|| self.flags.cmp(&other.flags)) + .then_with(|| self.value_off.cmp(&other.value_off)) + .then_with(|| self.value_len.cmp(&other.value_len)) + .then_with(|| self.deleted.cmp(&other.deleted)) + } +} + +impl PartialOrd for Entry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + /// The store as a sorted set of records, which is what a checkpoint writes and what the /// digest is taken over. struct Merged {