mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-18 18:46:17 +00:00
fix(filemeta): validate part array lengths in into_fileinfo (#4382)
MetaObject::into_fileinfo indexed part_sizes[i]/part_actual_sizes[i] by part_numbers.len() without checking the arrays are the same length, unlike the adjacent part_etags/part_indices which are length-guarded. decode_from pushes the three arrays independently and the xl.meta CRC only covers bytes, so a CRC-valid but internally inconsistent xl.meta (foreign writer / MinIO interop) triggers an out-of-bounds panic on the GET/HEAD/LIST decode path. Guard the three arrays for equal length and return Err(FileCorrupt) so a divergent shard is skipped and quorum uses the other disks, instead of panicking the request task. Cascade into_fileinfo to Result across its callers, and fix io_primitives early-return to derive the version id from the merged header and fall into the per-disk loop (single-disk survival + heal). The 2118 merge-first path is left as a documented follow-up. Refs backlog#900 (filemeta-01).
This commit is contained in:
@@ -2242,31 +2242,41 @@ impl SetDisks {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let finfo = match meta.into_fileinfo(bucket, object, "", true, incl_free_vers, true) {
|
||||
Ok(res) => res,
|
||||
Err(err) => {
|
||||
for item in errs.iter_mut() {
|
||||
if item.is_none() {
|
||||
*item = Some(err.clone().into());
|
||||
// Determine the winning version id. When the merged representative decodes to
|
||||
// a valid FileInfo, use its version id. When it is undecodable (Err from
|
||||
// corrupt part arrays) OR decodes but is not valid (e.g. a shallow merged
|
||||
// representative missing erasure detail), do NOT poison every disk: derive the
|
||||
// winning vid from the intact version header and fall into the per-disk loop
|
||||
// below, so healthy disks still populate `meta_file_infos` to satisfy
|
||||
// read_quorum while corrupt disks fail `into_fileinfo` and are flagged
|
||||
// `FileCorrupt` for heal. If every disk is corrupt, they all fail in the loop,
|
||||
// leaving no valid FileInfo so the caller's read_quorum fails cleanly instead
|
||||
// of panicking or returning half-corrupt data. Only when there is no non-free
|
||||
// version header at all is there genuinely nothing to read.
|
||||
//
|
||||
// `into_fileinfo` with an empty version_id selects the first non-free version
|
||||
// (see FileMeta::into_fileinfo); replicate that selection from the header here.
|
||||
let vid = match meta.into_fileinfo(bucket, object, "", true, incl_free_vers, true) {
|
||||
Ok(finfo) if finfo.is_valid() => finfo.version_id.unwrap_or(Uuid::nil()),
|
||||
_ => match meta
|
||||
.versions
|
||||
.iter()
|
||||
.find(|v| !v.header.free_version())
|
||||
.and_then(|v| v.header.version_id)
|
||||
{
|
||||
Some(id) => id,
|
||||
None => {
|
||||
for item in errs.iter_mut() {
|
||||
if item.is_none() {
|
||||
*item = Some(DiskError::FileCorrupt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (meta_file_infos, errs);
|
||||
}
|
||||
return (meta_file_infos, errs);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if !finfo.is_valid() {
|
||||
for item in errs.iter_mut() {
|
||||
if item.is_none() {
|
||||
*item = Some(DiskError::FileCorrupt);
|
||||
}
|
||||
}
|
||||
|
||||
return (meta_file_infos, errs);
|
||||
}
|
||||
|
||||
let vid = finfo.version_id.unwrap_or(Uuid::nil());
|
||||
|
||||
for (idx, meta_op) in metadata_array.iter().enumerate() {
|
||||
if let Some(meta) = meta_op {
|
||||
match meta.into_fileinfo(bucket, object, vid.to_string().as_str(), read_data, incl_free_vers, true) {
|
||||
@@ -3701,4 +3711,81 @@ mod tests {
|
||||
let key = ReadRepairHealCacheKey::new("bucket", &object, None, 3, 4);
|
||||
release_read_repair_heal_reservation(&key).await;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// backlog#900: pick_latest_quorum_files_info must survive a single
|
||||
// corrupt-part disk (even in the merged representative slot) by deriving
|
||||
// the vid from the header and falling into the per-disk loop, flagging the
|
||||
// corrupt disk for heal instead of poisoning the whole read.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
use rustfs_filemeta::{ChecksumAlgo, ErasureAlgo, FileMeta, FileMetaVersion, MetaObject, RawFileInfo, VersionType};
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn raw_object_version(vid: Uuid, part_sizes: Vec<usize>) -> RawFileInfo {
|
||||
let mut fm = FileMeta::new();
|
||||
fm.add_version_filemata(FileMetaVersion {
|
||||
version_type: VersionType::Object,
|
||||
object: Some(MetaObject {
|
||||
version_id: Some(vid),
|
||||
erasure_algorithm: ErasureAlgo::ReedSolomon,
|
||||
erasure_m: 2,
|
||||
erasure_n: 1,
|
||||
erasure_index: 1,
|
||||
erasure_dist: vec![1, 2, 3],
|
||||
erasure_block_size: 1 << 20,
|
||||
bitrot_checksum_algo: ChecksumAlgo::HighwayHash,
|
||||
part_numbers: vec![1, 2],
|
||||
part_sizes,
|
||||
part_actual_sizes: vec![10, 20],
|
||||
mod_time: Some(OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
RawFileInfo {
|
||||
buf: fm.marshal_msg().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pick_latest_quorum_masks_single_corrupt_disk_in_representative_slot() {
|
||||
let vid = Uuid::new_v4();
|
||||
// Deterministic: the corrupt disk is fixed at index 0 (representative slot).
|
||||
let fileinfos = vec![
|
||||
Some(raw_object_version(vid, vec![10])),
|
||||
Some(raw_object_version(vid, vec![10, 20])),
|
||||
Some(raw_object_version(vid, vec![10, 20])),
|
||||
];
|
||||
let errs = vec![None, None, None];
|
||||
|
||||
let (infos, out_errs) = SetDisks::pick_latest_quorum_files_info(fileinfos, errs, "bucket", "obj", false, false).await;
|
||||
|
||||
// The corrupt representative disk is flagged for heal.
|
||||
assert_eq!(out_errs[0], Some(DiskError::FileCorrupt), "corrupt representative disk must be flagged");
|
||||
// Good disks still produce valid FileInfo, satisfying read_quorum (3.div_ceil(2)=2).
|
||||
let good = infos.iter().filter(|fi| fi.is_valid()).count();
|
||||
assert!(good >= 2, "quorum of good disks must survive corrupt representative, got {good}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pick_latest_quorum_all_corrupt_fails_clean_without_panic() {
|
||||
let vid = Uuid::new_v4();
|
||||
let fileinfos = vec![
|
||||
Some(raw_object_version(vid, vec![10])),
|
||||
Some(raw_object_version(vid, vec![10])),
|
||||
Some(raw_object_version(vid, vec![10])),
|
||||
];
|
||||
let errs = vec![None, None, None];
|
||||
|
||||
let (infos, out_errs) = SetDisks::pick_latest_quorum_files_info(fileinfos, errs, "bucket", "obj", false, false).await;
|
||||
|
||||
assert!(
|
||||
out_errs.iter().all(|e| e == &Some(DiskError::FileCorrupt)),
|
||||
"all disks must be flagged corrupt"
|
||||
);
|
||||
assert!(infos.iter().all(|fi| !fi.is_valid()), "no half-corrupt FileInfo may be returned");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user