From f38f001bad18f011c37e6167e052027dedbf51da Mon Sep 17 00:00:00 2001 From: Raj Singh Date: Fri, 4 Sep 2026 00:01:32 +0000 Subject: [PATCH 1/2] api/s3: don't panic when final multipart version has no blocks CompleteMultipartUpload builds the final version and then reads `final_version.blocks.items()[0]` unconditionally. When the assembled version ends up with an empty block list the index panics and the process dies before it can answer the request: panicked at src/api/s3/multipart.rs:479:3: index out of bounds: the len is 0 but the index is 0 Use `.first()` and return an internal error instead, matching how the rest of this file handles missing data. The empty-block case is an internal consistency condition rather than a malformed request, so it maps to an internal error rather than a 4xx. The same panic was reported earlier in #1403, at multipart.rs:483 on v2.2.0. That report was closed without a fix after the reporter moved off consistency_mode = "dangerous". Closes #1521 --- src/api/s3/multipart.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/api/s3/multipart.rs b/src/api/s3/multipart.rs index 16c8b6aa..1f493a24 100644 --- a/src/api/s3/multipart.rs +++ b/src/api/s3/multipart.rs @@ -476,7 +476,13 @@ pub async fn handle_complete_multipart_upload( size: total_size, etag: etag.clone(), }, - final_version.blocks.items()[0].1.hash, + final_version + .blocks + .items() + .first() + .ok_or_internal_error("Multipart completion produced a final version with no blocks")? + .1 + .hash, )); let final_object = Object::new(*bucket_id, key.clone(), vec![object_version]); From c54077af73cd7546277d1e6d7edb27e0b5e023f1 Mon Sep 17 00:00:00 2001 From: Raj Singh Date: Fri, 4 Sep 2026 03:19:07 +0000 Subject: [PATCH 2/2] api/s3: don't panic when reading a version with no blocks body_from_blocks_range indexed all_blocks[0] to size a capacity hint. A version with zero blocks made that index panic and kill the process on a plain GET, the read-side twin of the CompleteMultipartUpload panic at multipart.rs:479. Fall back to the existing 1024 floor when the slice is empty; with a non-empty slice the hint is unchanged. --- src/api/s3/get.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/api/s3/get.rs b/src/api/s3/get.rs index 02500bf4..bb6fbd3d 100644 --- a/src/api/s3/get.rs +++ b/src/api/s3/get.rs @@ -698,9 +698,14 @@ fn body_from_blocks_range( // range, as well as their "true offset", which is their actual offset in the complete // file (whereas block.offset designates the offset of the block WITHIN THE PART // block.part_number, which is not the same in the case of a multipart upload) + // A version with no blocks yields no data, so the capacity hint must not index + // into an empty slice. + let capacity_block_size = all_blocks + .first() + .map_or(1024, |(_, b)| std::cmp::max(b.size, 1024)); let mut blocks: Vec<(VersionBlock, u64)> = Vec::with_capacity(std::cmp::min( all_blocks.len(), - 4 + ((end - begin) / std::cmp::max(all_blocks[0].1.size, 1024)) as usize, + 4 + ((end - begin) / capacity_block_size) as usize, )); let mut block_offset: u64 = 0; for (_, b) in all_blocks.iter() {