diff --git a/crates/ecstore/src/object_api/mod.rs b/crates/ecstore/src/object_api/mod.rs index 30a605962..12b12ff23 100644 --- a/crates/ecstore/src/object_api/mod.rs +++ b/crates/ecstore/src/object_api/mod.rs @@ -53,7 +53,7 @@ pub const BLOCK_SIZE_V2: usize = 1024 * 1024; // 1M pub(crate) const ENCRYPTED_PART_LAYOUT_CANDIDATE_SUFFIX: &str = "encrypted-part-layout-quorum-candidate-v1"; pub(crate) const ENCRYPTED_PART_LAYOUT_QUORUM_SUFFIX: &str = "encrypted-part-layout-quorum-v1"; pub(crate) const ENV_RUSTFS_ENCRYPTED_RANGE_SEEK: &str = "RUSTFS_ENCRYPTED_RANGE_SEEK"; -pub(crate) const DEFAULT_RUSTFS_ENCRYPTED_RANGE_SEEK: bool = false; +pub(crate) const DEFAULT_RUSTFS_ENCRYPTED_RANGE_SEEK: bool = true; pub(crate) fn has_encrypted_part_layout_marker(metadata: &HashMap, suffix: &str, expected: &str) -> bool { let mut value = None; @@ -70,7 +70,11 @@ pub(crate) fn has_encrypted_part_layout_marker(metadata: &HashMap bool { - // RUSTFS_COMPAT_TODO(backlog-1316): mixed-version MPUs need opt-in. Remove after all servers use candidate markers and uploadId locks. + // On by default (backlog-1316 Phase A): every misfit direction falls back to the + // conservative full read — MPUs created without a candidate marker, completions + // that cannot revalidate the candidate against the data_dir under the uploadId + // lock, and reads whose quorum marker disagrees with the current data_dir all + // serve the full-object path. RUSTFS_ENCRYPTED_RANGE_SEEK=false is the kill switch. #[cfg(test)] { rustfs_utils::get_env_bool(ENV_RUSTFS_ENCRYPTED_RANGE_SEEK, DEFAULT_RUSTFS_ENCRYPTED_RANGE_SEEK) diff --git a/crates/ecstore/src/object_api/readers.rs b/crates/ecstore/src/object_api/readers.rs index 6f43a340c..4d2838c6d 100644 --- a/crates/ecstore/src/object_api/readers.rs +++ b/crates/ecstore/src/object_api/readers.rs @@ -3329,11 +3329,14 @@ mod tests { } #[tokio::test] - async fn test_legacy_range_seek_defaults_disabled() { - async_with_vars([(ENV_RUSTFS_ENCRYPTED_RANGE_SEEK, None::<&str>)], async { - let key_bytes = [0x7D; 32]; - let fixture = build_legacy_ssec_multipart_fixture(key_bytes, &[20_000, 9_000, 5_000]).await; + async fn test_legacy_range_seek_defaults_enabled() { + let key_bytes = [0x7D; 32]; + // The unset-env default must behave exactly like the explicit opt-in: + // an eligible multipart object seeks to the covering part boundary + // instead of reading the whole ciphertext. + let default_plan = async_with_vars([(ENV_RUSTFS_ENCRYPTED_RANGE_SEEK, None::<&str>)], async { + let fixture = build_legacy_ssec_multipart_fixture(key_bytes, &[20_000, 9_000, 5_000]).await; let plan = ReadPlan::build( Some(range(33_900, 33_999)), &fixture.object_info, @@ -3341,12 +3344,31 @@ mod tests { &ssec_headers_from_key(key_bytes), ) .await - .expect("default-disabled read plan should build"); - - assert_eq!(plan.storage_offset, 0); - assert_eq!(plan.storage_length, fixture.ciphertext.len() as i64); + .expect("default read plan should build"); + (plan.storage_offset, plan.storage_length, fixture.ciphertext.len()) }) .await; + + let opt_in_plan = async_with_vars([(ENV_RUSTFS_ENCRYPTED_RANGE_SEEK, Some("true"))], async { + let fixture = build_legacy_ssec_multipart_fixture(key_bytes, &[20_000, 9_000, 5_000]).await; + let plan = ReadPlan::build( + Some(range(33_900, 33_999)), + &fixture.object_info, + &ObjectOptions::default(), + &ssec_headers_from_key(key_bytes), + ) + .await + .expect("opt-in read plan should build"); + (plan.storage_offset, plan.storage_length) + }) + .await; + + assert_eq!((default_plan.0, default_plan.1), opt_in_plan, "unset env must match explicit opt-in"); + assert_ne!(default_plan.0, 0, "an eligible range read must seek past the leading parts"); + assert!( + (default_plan.1 as usize) < default_plan.2, + "an eligible range read must not span the whole ciphertext" + ); } #[tokio::test]