diff --git a/crates/e2e_test/src/presigned_negative_test.rs b/crates/e2e_test/src/presigned_negative_test.rs index b755be190..04874e0c7 100644 --- a/crates/e2e_test/src/presigned_negative_test.rs +++ b/crates/e2e_test/src/presigned_negative_test.rs @@ -87,7 +87,9 @@ fn valid_config() -> PresigningConfig { } /// Flip bytes inside the `X-Amz-Signature=` query value without changing its -/// length, producing a structurally valid but incorrect signature. +/// length, producing a structurally valid but incorrect signature. Every hex +/// digit is replaced by its complement (15 - v), which has no fixed point, so +/// the tamper changes the value no matter which digits the signature contains. fn tamper_signature(uri: &str) -> String { let marker = "X-Amz-Signature="; let idx = uri.find(marker).expect("presigned uri must carry X-Amz-Signature") + marker.len(); @@ -96,10 +98,9 @@ fn tamper_signature(uri: &str) -> String { let (sig, tail) = rest.split_at(end); let tampered: String = sig .chars() - .map(|c| match c { - '0' => 'f', - 'a' => '0', - other => other, + .map(|c| { + let v = c.to_digit(16).expect("X-Amz-Signature value must be hex"); + char::from_digit(15 - v, 16).expect("complement of a hex digit is a hex digit") }) .collect(); assert_ne!(sig, tampered, "tamper must actually change the signature hex"); diff --git a/crates/ecstore/src/config/com.rs b/crates/ecstore/src/config/com.rs index 1e40c003d..6039b0368 100644 --- a/crates/ecstore/src/config/com.rs +++ b/crates/ecstore/src/config/com.rs @@ -5220,7 +5220,7 @@ mod tests { #[tokio::test] async fn server_config_snapshot_serializes_read_modify_write_transactions() { let baseline = encode_server_config_blob(&Config::new(), None).expect("baseline config should encode"); - let store = Arc::new(RecoveryMockStore::new(RecoveryReadState::Blob(baseline), None)); + let store = Arc::new(RecoveryMockStore::new(RecoveryReadState::Blob(baseline.clone()), None)); let first = read_server_config_snapshot(store.clone()) .await .expect("first config snapshot"); @@ -5234,7 +5234,11 @@ mod tests { .await .expect("second transaction should acquire after the first snapshot is dropped") .expect("second config snapshot"); - assert!(configs_semantically_equal(&second.config, &Config::new())); + // Compare raw bytes against the baseline blob rather than a fresh + // Config::new(): the process-global DEFAULT_KVS can be registered by a + // sibling test mid-run, which would make a Config::new() evaluated here + // diverge from the baseline encoded above. + assert_eq!(second.raw.as_deref(), Some(baseline.as_slice())); } #[tokio::test] diff --git a/rustfs/src/app/object/get.rs b/rustfs/src/app/object/get.rs index ca84b6345..809425b82 100644 --- a/rustfs/src/app/object/get.rs +++ b/rustfs/src/app/object/get.rs @@ -7242,6 +7242,12 @@ mod tests { let mut staged_targets = Vec::with_capacity(pool_disk_paths[target_pool].len()); for (source_disk, target_disk) in pool_disk_paths[source_pool].iter().zip(&pool_disk_paths[target_pool]) { let source_dir = source_disk.join(&bucket).join(object); + // The same write-quorum minority gap tolerated above (#6701) can + // leave a lagging source-pool disk without the object; skip it and + // stage the replicas that exist — the reader tolerates the gap. + if !source_dir.join("xl.meta").is_file() { + continue; + } let target_dir = target_disk.join(&bucket).join(object); let staging_dir = temp_dir.path().join(format!("resume-relocate-{}", Uuid::new_v4())); std::fs::create_dir_all(&staging_dir).expect("create relocated target staging directory"); @@ -7258,15 +7264,18 @@ mod tests { } } std::fs::copy(source_dir.join("xl.meta"), staging_dir.join("xl.meta")).expect("stage relocated object metadata"); - staged_targets.push((staging_dir, target_dir)); + staged_targets.push((staging_dir, target_dir, source_dir.join("xl.meta"))); } + assert!( + staged_targets.len() > pool_disk_paths[source_pool].len() / 2, + "a write-quorum majority of the source pool's disks must hold the object to stage the relocation" + ); let (version_dirs, deleted) = delete_object_part_shards(&pool_disk_paths[source_pool], &bucket, object, &[2, 3]); assert!(version_dirs > 0, "the source pool must have at least one version data directory"); assert_eq!(deleted, version_dirs * 2); - for ((staging_dir, target_dir), source_disk) in staged_targets.into_iter().zip(&pool_disk_paths[source_pool]) { + for (staging_dir, target_dir, source_meta) in staged_targets { std::fs::rename(staging_dir, target_dir).expect("publish relocated target object"); - let source_meta = source_disk.join(&bucket).join(object).join("xl.meta"); std::fs::remove_file(source_meta).expect("remove relocated source object metadata"); } store