From 547c678eed544f0d46f2a068ea23b7b366539fe0 Mon Sep 17 00:00:00 2001 From: cxymds Date: Tue, 28 Jul 2026 17:35:53 +0800 Subject: [PATCH] fix(filemeta): reject positive size without parts (#5354) * fix(filemeta): reject positive size without parts * test(ecstore): keep optimized read fixture valid * test(ecstore): keep listing fixtures valid --- crates/ecstore/src/disk/local.rs | 6 +++ crates/ecstore/src/set_disk/read.rs | 65 ++++++++++++++++++++++++ crates/ecstore/src/store/list_objects.rs | 14 ++++- crates/filemeta/src/fileinfo.rs | 23 +++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) diff --git a/crates/ecstore/src/disk/local.rs b/crates/ecstore/src/disk/local.rs index 37ae245b2..c24e38385 100644 --- a/crates/ecstore/src/disk/local.rs +++ b/crates/ecstore/src/disk/local.rs @@ -8500,6 +8500,12 @@ mod test { file_info.data_dir = data_dir; file_info.data = data; file_info.size = size; + file_info.parts = vec![ObjectPartInfo { + number: 1, + size: usize::try_from(size).expect("test object size should fit usize"), + actual_size: size, + ..Default::default() + }]; file_info.mod_time = Some(OffsetDateTime::now_utc()); file_info } diff --git a/crates/ecstore/src/set_disk/read.rs b/crates/ecstore/src/set_disk/read.rs index eff386832..64369b5d5 100644 --- a/crates/ecstore/src/set_disk/read.rs +++ b/crates/ecstore/src/set_disk/read.rs @@ -2019,6 +2019,33 @@ mod metadata_cache_tests { fi } + #[tokio::test] + async fn get_object_with_fileinfo_rejects_positive_size_without_parts() { + let mut output = Vec::new(); + let err = SetDisks::get_object_with_fileinfo( + "bucket", + "object", + 0, + 1, + &mut output, + valid_test_fileinfo("object"), + Vec::new(), + &[], + 0, + 0, + false, + false, + GET_OBJECT_PATH_SET_DISK, + "plain", + "small", + ) + .await + .expect_err("positive-size metadata without parts must fail without panicking"); + + assert_eq!(err, Error::FileCorrupt); + assert!(output.is_empty()); + } + #[tokio::test] async fn get_object_with_fileinfo_rejects_invalid_ranges_before_reader_setup() { let bucket = "bucket"; @@ -2092,6 +2119,12 @@ mod metadata_cache_tests { let mut invalid_erasure = valid_test_fileinfo(object); invalid_erasure.erasure.block_size = 0; + invalid_erasure.parts.push(ObjectPartInfo { + number: 1, + size: 1, + actual_size: 1, + ..Default::default() + }); let err = SetDisks::get_object_with_fileinfo( bucket, object, @@ -2120,6 +2153,37 @@ mod metadata_cache_tests { assert!(output.is_empty()); } + #[tokio::test] + async fn get_object_with_fileinfo_accepts_zero_size_without_parts() { + let bucket = "bucket"; + let object = "empty"; + let mut fi = valid_test_fileinfo(object); + fi.size = 0; + + let mut output = Vec::new(); + SetDisks::get_object_with_fileinfo( + bucket, + object, + 0, + 0, + &mut output, + fi, + Vec::new(), + &[], + 0, + 0, + false, + false, + GET_OBJECT_PATH_SET_DISK, + "plain", + "empty", + ) + .await + .expect("zero-byte object without parts must remain readable"); + + assert!(output.is_empty()); + } + #[tokio::test] async fn get_object_with_fileinfo_fails_closed_without_read_quorum() { let bucket = "bucket"; @@ -2166,6 +2230,7 @@ mod metadata_cache_tests { let object = "object"; let (_dir, disk) = new_read_version_test_disk(bucket).await; let mut fi = valid_test_fileinfo(object); + fi.size = 0; fi.mod_time = Some(OffsetDateTime::now_utc()); disk.write_metadata(bucket, bucket, object, fi.clone()) .await diff --git a/crates/ecstore/src/store/list_objects.rs b/crates/ecstore/src/store/list_objects.rs index 758b6279c..bdfab4fe8 100644 --- a/crates/ecstore/src/store/list_objects.rs +++ b/crates/ecstore/src/store/list_objects.rs @@ -6696,7 +6696,7 @@ mod test { use crate::object_api::ObjectInfo; use rustfs_filemeta::{ FileInfo, FileMeta, FileMetaVersion, MetaCacheEntries, MetaCacheEntriesSorted, MetaCacheEntry, MetaDeleteMarker, - VersionType, + ObjectPartInfo, VersionType, }; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; @@ -6899,6 +6899,12 @@ mod test { fi.volume = "bucket".to_owned(); fi.name = "object".to_owned(); fi.size = 1; + fi.parts = vec![ObjectPartInfo { + number: 1, + size: 1, + actual_size: 1, + ..Default::default() + }]; fi.fresh = true; fi.erasure.index = 1; fi.mod_time = Some(time::OffsetDateTime::from_unix_timestamp(1_705_312_300).expect("valid timestamp")); @@ -6954,6 +6960,12 @@ mod test { fi.version_id = Some(Uuid::from_u128(version_idx)); fi.versioned = true; fi.size = 1; + fi.parts = vec![ObjectPartInfo { + number: 1, + size: 1, + actual_size: 1, + ..Default::default() + }]; fi.mod_time = Some(*mod_time); fi.metadata = metadata; diff --git a/crates/filemeta/src/fileinfo.rs b/crates/filemeta/src/fileinfo.rs index 98a39c8a9..2f1714e01 100644 --- a/crates/filemeta/src/fileinfo.rs +++ b/crates/filemeta/src/fileinfo.rs @@ -415,6 +415,10 @@ impl FileInfo { } fn validate_collection_contents(&self, layout: Option<&ValidatedErasureLayout>) -> Result<()> { + if layout.is_some() && self.size > 0 && self.parts.is_empty() { + return Err(Error::FileCorrupt); + } + let mut part_numbers = [0u64; FILEINFO_PART_BITMAP_WORDS]; for part in &self.parts { if let Some(layout) = layout { @@ -692,6 +696,10 @@ impl FileInfo { // to_part_offset gets the part index where offset is located, returns part index and offset pub fn to_part_offset(&self, offset: usize) -> Result<(usize, usize)> { + if self.size > 0 && self.parts.is_empty() { + return Err(Error::FileCorrupt); + } + if offset == 0 { return Ok((0, 0)); } @@ -1185,6 +1193,21 @@ mod tests { assert_eq!(layout.expect("strict layout must be present").data_blocks, 16); } + #[test] + fn validate_require_erasure_rejects_positive_size_without_parts() { + let mut fi = FileInfo::new("bucket/object", 4, 2); + fi.erasure.index = 1; + fi.size = 1; + + assert_eq!(fi.validate_for_metadata_read(), Err(Error::FileCorrupt)); + assert_eq!(fi.to_part_offset(0), Err(Error::FileCorrupt)); + + fi.size = 0; + fi.validate_for_metadata_read() + .expect("zero-byte object metadata may omit parts"); + assert_eq!(fi.to_part_offset(0), Ok((0, 0))); + } + #[test] fn validate_for_erasure_write_only_relaxes_pending_shard_index() { let mut fi = validation_test_fileinfo();