fix(s3): return InvalidRange when CopySourceRange exceeds source object size (#2029)

This commit is contained in:
安正超
2026-03-01 17:46:31 +08:00
committed by GitHub
parent 8aecc7267b
commit e5e1010c31
3 changed files with 58 additions and 3 deletions
+53 -1
View File
@@ -38,7 +38,7 @@ use rustfs_ecstore::compress::is_compressible;
use rustfs_ecstore::error::{StorageError, is_err_object_not_found, is_err_version_not_found};
use rustfs_ecstore::new_object_layer_fn;
use rustfs_ecstore::set_disk::{MAX_PARTS_COUNT, is_valid_storage_class};
use rustfs_ecstore::store_api::{CompletePart, MultipartUploadResult, ObjectIO, ObjectOptions, PutObjReader};
use rustfs_ecstore::store_api::{CompletePart, HTTPRangeSpec, MultipartUploadResult, ObjectIO, ObjectOptions, PutObjReader};
use rustfs_ecstore::store_api::{MultipartOperations, ObjectOperations};
use rustfs_filemeta::{ReplicationStatusType, ReplicationType};
use rustfs_rio::{CompressReader, HashReader, Reader, WarpReader};
@@ -58,6 +58,18 @@ use tokio::sync::RwLock;
use tokio_util::io::StreamReader;
use tracing::{info, instrument, warn};
/// Returns InvalidRange error if CopySourceRange end exceeds the source object size.
/// Used by execute_upload_part_copy to reject out-of-bounds ranges per S3 spec.
fn validate_copy_source_range_not_exceeds(range_spec: &HTTPRangeSpec, object_size: i64) -> S3Result<()> {
if range_spec.end >= object_size {
return Err(S3Error::with_message(
S3ErrorCode::InvalidRange,
"The requested range is not satisfiable".to_string(),
));
}
Ok(())
}
#[derive(Clone, Default)]
pub struct DefaultMultipartUsecase {
context: Option<Arc<AppContext>>,
@@ -973,6 +985,8 @@ impl DefaultMultipartUsecase {
_ => src_info.size,
};
validate_copy_source_range_not_exceeds(range_spec, validation_size)?;
range_spec
.get_offset_length(validation_size)
.map_err(|e| S3Error::with_message(S3ErrorCode::InvalidRange, e.to_string()))?
@@ -1217,6 +1231,44 @@ mod tests {
assert_eq!(err.code(), &S3ErrorCode::InternalError);
}
#[test]
fn test_validate_copy_source_range_not_exceeds_returns_invalid_range_when_range_exceeds() {
use super::validate_copy_source_range_not_exceeds;
let range_exceeds = HTTPRangeSpec {
is_suffix_length: false,
start: 0,
end: 21,
};
let err = validate_copy_source_range_not_exceeds(&range_exceeds, 5).unwrap_err();
assert_eq!(err.code(), &S3ErrorCode::InvalidRange);
assert!(err.to_string().contains("not satisfiable"));
}
#[test]
fn test_validate_copy_source_range_not_exceeds_ok_when_range_valid() {
use super::validate_copy_source_range_not_exceeds;
let range_valid = HTTPRangeSpec {
is_suffix_length: false,
start: 0,
end: 4,
};
assert!(validate_copy_source_range_not_exceeds(&range_valid, 5).is_ok());
}
#[test]
fn test_validate_copy_source_range_not_exceeds_ok_for_suffix_range() {
use super::validate_copy_source_range_not_exceeds;
let range_suffix = HTTPRangeSpec {
is_suffix_length: true,
start: -5,
end: -1,
};
assert!(validate_copy_source_range_not_exceeds(&range_suffix, 5).is_ok());
}
#[tokio::test]
async fn execute_upload_part_rejects_missing_body() {
let input = UploadPartInput::builder()
+5 -1
View File
@@ -34,8 +34,9 @@
# - CORS: Bucket-level CORS configuration, preflight, origin matching
# - HTTP: 100-continue, multipart abort, tagging in POST Object
# - DeleteObject: Proper NoSuchBucket for deleted buckets
# - Multipart Copy: InvalidRange when CopySourceRange exceeds source size
#
# Total: 393 tests
# Total: 394 tests
test_basic_key_count
test_bucket_create_naming_bad_short_one
@@ -486,3 +487,6 @@ test_100_continue
test_abort_multipart_upload_not_found
test_post_object_tags_authenticated_request
test_object_delete_key_bucket_gone
# Multipart copy range validation
test_multipart_copy_invalid_range
-1
View File
@@ -224,7 +224,6 @@ test_list_buckets_paginated
test_list_multipart_upload
test_list_multipart_upload_owner
test_multipart_checksum_sha256
test_multipart_copy_invalid_range
test_multipart_copy_multiple_sizes
test_multipart_copy_versioned
test_multipart_get_part