From 0913686b12845eefc69a2f4ed4ee6a8161e25742 Mon Sep 17 00:00:00 2001 From: cxymds Date: Thu, 24 Sep 2026 11:16:02 +0800 Subject: [PATCH] fix(heal): detect stale delete-marker metadata (#8102) --- crates/ecstore/src/set_disk/mod.rs | 35 +++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/ecstore/src/set_disk/mod.rs b/crates/ecstore/src/set_disk/mod.rs index 0a0225654..e6059f6cd 100644 --- a/crates/ecstore/src/set_disk/mod.rs +++ b/crates/ecstore/src/set_disk/mod.rs @@ -6821,7 +6821,12 @@ pub fn should_heal_object_on_disk( return (false, false, err.clone()); } - if !meta.equals(latest_meta) { + // `FileInfo::equals` intentionally compares the erasure payload shape and + // modification time, but it does not compare the selected version or the + // delete-marker bit. Heal uses this decision for versioned metadata, so a + // stale historical version that is still marked latest must be treated as + // outdated even when those storage-level fields happen to match. + if !meta.equals(latest_meta) || !heal_metadata_identity_matches(meta, latest_meta) { debug!( event = EVENT_SET_DISK_HEAL, component = LOG_COMPONENT_ECSTORE, @@ -6844,6 +6849,11 @@ pub fn should_heal_object_on_disk( (false, false, None) } +fn heal_metadata_identity_matches(meta: &FileInfo, latest_meta: &FileInfo) -> bool { + meta.deleted == latest_meta.deleted + && meta.version_id.filter(|version| !version.is_nil()) == latest_meta.version_id.filter(|version| !version.is_nil()) +} + /// Probe every drive of the set at once. Each live probe is bounded by the /// drive `disk_info` timeout, and the admin peer probe budget only covers one /// such timeout; a sequential walk over several stalled drives after a power @@ -11288,6 +11298,29 @@ mod tests { assert_eq!(reason, Some(DiskError::FileCorrupt)); } + #[test] + fn stale_delete_marker_identity_requires_metadata_heal() { + let historical_version = Uuid::new_v4(); + let marker_version = Uuid::new_v4(); + let mut stale = FileInfo { + version_id: Some(historical_version), + ..FileInfo::default() + }; + let mut latest = stale.clone(); + stale.deleted = false; + latest.deleted = true; + latest.version_id = Some(marker_version); + + // The generic equality helper intentionally considers these records + // equal when their erasure shape and mod-time match. Heal must still + // repair the version identity and delete-marker state. + assert!(stale.equals(&latest)); + let (should_heal, metadata, reason) = should_heal_object_on_disk(&None, &[], &stale, &latest); + assert!(should_heal); + assert!(metadata); + assert_eq!(reason, Some(DiskError::OutdatedXLMeta)); + } + #[test] fn metadata_io_failures_never_authorize_heal_overwrite() { let meta = FileInfo::default();