From 4a2b15cb82c1307aa52cd83630c6afed329a717e Mon Sep 17 00:00:00 2001 From: houseme Date: Tue, 8 Sep 2026 18:53:43 +0800 Subject: [PATCH] fix(heal): harden MRF replay boundaries (#7483) Reject journal records with unknown version-presence flags even when their CRC is valid, so rollback/future payloads cannot be accepted as known records. Gate committed checkpoint cleanup by the writer owner captured from the replay source, preserving retained manifests from other owners inside the same sequence window. Co-authored-by: zhi22915 --- crates/heal/src/heal/mrf_queue.rs | 52 +++++++++++++++----- crates/heal/src/heal/mrf_queue/snapshot.rs | 56 ++++++++++++++++++++-- 2 files changed, 90 insertions(+), 18 deletions(-) diff --git a/crates/heal/src/heal/mrf_queue.rs b/crates/heal/src/heal/mrf_queue.rs index 48ae146ce..77d52720b 100644 --- a/crates/heal/src/heal/mrf_queue.rs +++ b/crates/heal/src/heal/mrf_queue.rs @@ -294,7 +294,11 @@ fn decode_one(data: &[u8]) -> Option<(MrfIntent, usize)> { }; let attempts = data[3]; let enqueued_at_ms = u64::from_le_bytes(data[4..12].try_into().ok()?); - let has_version = data[12] != 0; + let has_version = match data[12] { + 0 => false, + 1 => true, + _ => return None, + }; let mut cursor = MRF_RECORD_FIXED_HEAD; let version_id = if has_version { if data.len() < cursor + 16 { @@ -718,7 +722,7 @@ fn replay_must_retain_journal( #[derive(Clone, Copy)] enum ReplayCleanup { Legacy, - Committed { sequence: u64 }, + Committed { owner: Uuid, sequence: u64 }, } struct ReplaySource { @@ -731,6 +735,7 @@ async fn read_replay_source(max_bytes: usize) -> Result, sn return Ok(Some(ReplaySource { data: committed.payload().to_vec(), cleanup: ReplayCleanup::Committed { + owner: committed.owner(), sequence: committed.sequence(), }, })); @@ -754,18 +759,20 @@ async fn read_replay_source(max_bytes: usize) -> Result, sn async fn delete_replay_source(cleanup: ReplayCleanup, max_bytes: usize) -> bool { let committed_deleted = match cleanup { ReplayCleanup::Legacy => true, - ReplayCleanup::Committed { sequence } => match snapshot::delete_committed_snapshots_through(sequence, max_bytes).await { - Ok(deleted) => deleted, - Err(err) => { - tracing::warn!( - target: "rustfs::heal::mrf", - error = %err, - sequence, - "MRF committed replay checkpoint cleanup failed" - ); - false + ReplayCleanup::Committed { owner, sequence } => { + match snapshot::delete_committed_snapshots_through(owner, sequence, max_bytes).await { + Ok(deleted) => deleted, + Err(err) => { + tracing::warn!( + target: "rustfs::heal::mrf", + error = %err, + sequence, + "MRF committed replay checkpoint cleanup failed" + ); + false + } } - }, + } }; committed_deleted && delete_journals().await } @@ -1344,6 +1351,25 @@ mod tests { assert_eq!(truncated, corrupt.len()); } + #[test] + fn journal_rejects_unknown_version_presence_flag_even_with_valid_crc() { + let mut versioned = intent("rollback-bucket", "object", 0); + versioned.version_id = Some([9; 16]); + let mut buf = Vec::new(); + assert!(encode_intent(&versioned, &mut buf)); + + buf[12] = 2; + let crc_offset = buf.len() - 4; + let mut hasher = crc_fast::Digest::new(crc_fast::CrcAlgorithm::Crc32IsoHdlc); + hasher.update(&buf[..crc_offset]); + let checksum = u32::try_from(hasher.finalize()).expect("CRC32 fits"); + buf[crc_offset..].copy_from_slice(&checksum.to_le_bytes()); + + let (decoded, truncated) = decode_journal(&buf); + assert!(decoded.is_empty(), "unknown boolean encodings are not rollback-compatible payloads"); + assert_eq!(truncated, buf.len()); + } + #[test] fn heal_request_mapping_follows_priority_matrix() { let decode = build_heal_request(&intent("b", "o", 0)); diff --git a/crates/heal/src/heal/mrf_queue/snapshot.rs b/crates/heal/src/heal/mrf_queue/snapshot.rs index 0921c043f..d4cafac10 100644 --- a/crates/heal/src/heal/mrf_queue/snapshot.rs +++ b/crates/heal/src/heal/mrf_queue/snapshot.rs @@ -555,20 +555,25 @@ pub async fn inspect_local_committed_snapshot(max_bytes: usize) -> Result