perf(ecstore): enable encrypted range part-seek by default (#6598)

Range GETs on encrypted objects read the whole ciphertext from offset 0
and discarded the decrypted prefix, because the part-boundary seek
shipped behind RUSTFS_ENCRYPTED_RANGE_SEEK defaulted to false
(backlog#1316 Phase A).

Flip the default to true. Safety rests on the marker chain: MPUs created
without a candidate layout marker never become seek-eligible,
CompleteMultipartUpload promotes the candidate to the quorum marker only
after revalidating it against the object's data_dir under the uploadId
write lock, and reads seek only when the quorum marker matches the
current data_dir. Single-part, compressed and markerless objects keep
the full-read path; RUSTFS_ENCRYPTED_RANGE_SEEK=false remains the kill
switch.

The stale default-off regression test becomes
test_legacy_range_seek_defaults_enabled: the unset-env default must
match the explicit opt-in plan, seek past the leading parts, and not
span the whole ciphertext.
This commit is contained in:
唐小鸭
2026-08-26 09:35:48 +08:00
committed by GitHub
parent a65b306fb0
commit f51b06f0ae
2 changed files with 36 additions and 10 deletions
+6 -2
View File
@@ -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<String, String>, suffix: &str, expected: &str) -> bool {
let mut value = None;
@@ -70,7 +70,11 @@ pub(crate) fn has_encrypted_part_layout_marker(metadata: &HashMap<String, String
}
pub(crate) fn legacy_encrypted_range_seek_enabled() -> 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)
+30 -8
View File
@@ -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]