mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-24 13:16:28 +00:00
fix(multipart): enforce complete part number limit (#6528)
This commit is contained in:
@@ -87,7 +87,7 @@ async fn test_large_xml_body_rejection() -> Result<(), Box<dyn Error + Send + Sy
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Excessive multipart parts must be rejected.
|
/// Multipart completion must reject part numbers above the 10,000-part limit.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_excessive_multipart_parts() -> Result<(), Box<dyn Error + Send + Sync>> {
|
async fn test_excessive_multipart_parts() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
init_logging();
|
init_logging();
|
||||||
@@ -107,11 +107,24 @@ async fn test_excessive_multipart_parts() -> Result<(), Box<dyn Error + Send + S
|
|||||||
|
|
||||||
let upload_id = create_result.upload_id().expect("upload_id should be present").to_string();
|
let upload_id = create_result.upload_id().expect("upload_id should be present").to_string();
|
||||||
|
|
||||||
// Try to complete with too many parts (should be rejected).
|
// Upload one real control part so a generic missing-part rejection cannot
|
||||||
let mut parts = Vec::new();
|
// masquerade as enforcement of the 10,000-part boundary.
|
||||||
for i in 1..=10001 {
|
let uploaded = client
|
||||||
parts.push(CompletedPart::builder().part_number(i).e_tag(format!("etag-{i}")).build());
|
.upload_part()
|
||||||
}
|
.bucket(&bucket_name)
|
||||||
|
.key("test-large")
|
||||||
|
.upload_id(&upload_id)
|
||||||
|
.part_number(1)
|
||||||
|
.body(ByteStream::from_static(b"valid-control-part"))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
let parts = vec![
|
||||||
|
CompletedPart::builder()
|
||||||
|
.part_number(1)
|
||||||
|
.e_tag(uploaded.e_tag().expect("uploaded part should return an ETag"))
|
||||||
|
.build(),
|
||||||
|
CompletedPart::builder().part_number(10001).e_tag("out-of-range").build(),
|
||||||
|
];
|
||||||
|
|
||||||
let result = client
|
let result = client
|
||||||
.complete_multipart_upload()
|
.complete_multipart_upload()
|
||||||
@@ -132,7 +145,16 @@ async fn test_excessive_multipart_parts() -> Result<(), Box<dyn Error + Send + S
|
|||||||
.await;
|
.await;
|
||||||
let _ = client.delete_bucket().bucket(&bucket_name).send().await;
|
let _ = client.delete_bucket().bucket(&bucket_name).send().await;
|
||||||
|
|
||||||
assert!(result.is_err(), "Server should reject excessive multipart parts");
|
let error = result.expect_err("server should reject a completion part number above 10000");
|
||||||
|
let service_error = error
|
||||||
|
.as_service_error()
|
||||||
|
.expect("part-limit rejection must be an S3 service error");
|
||||||
|
assert_eq!(service_error.code(), Some("InvalidPart"), "unexpected part-limit error: {error:?}");
|
||||||
|
assert_eq!(
|
||||||
|
service_error.message(),
|
||||||
|
Some("Part number 10001 must be between 1 and 10000"),
|
||||||
|
"completion must fail at the part-number boundary, not a later missing-part check"
|
||||||
|
);
|
||||||
|
|
||||||
env.stop_server();
|
env.stop_server();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ use super::storage_api::multipart_usecase::bucket::{
|
|||||||
use super::storage_api::multipart_usecase::compression::{is_disk_compressible, is_multipart_disk_compression_enabled};
|
use super::storage_api::multipart_usecase::compression::{is_disk_compressible, is_multipart_disk_compression_enabled};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use super::storage_api::multipart_usecase::contract::http::HTTPPreconditions;
|
use super::storage_api::multipart_usecase::contract::http::HTTPPreconditions;
|
||||||
use super::storage_api::multipart_usecase::contract::multipart::{CompletePart, MultipartOperations as _, MultipartUploadResult};
|
use super::storage_api::multipart_usecase::contract::multipart::{
|
||||||
|
CompletePart, MAX_MULTIPART_PART_NUMBER, MultipartOperations as _, MultipartUploadResult,
|
||||||
|
};
|
||||||
use super::storage_api::multipart_usecase::contract::object::{ObjectIO as _, ObjectOperations as _};
|
use super::storage_api::multipart_usecase::contract::object::{ObjectIO as _, ObjectOperations as _};
|
||||||
use super::storage_api::multipart_usecase::contract::range::HTTPRangeSpec;
|
use super::storage_api::multipart_usecase::contract::range::HTTPRangeSpec;
|
||||||
use super::storage_api::multipart_usecase::data_usage::{
|
use super::storage_api::multipart_usecase::data_usage::{
|
||||||
@@ -153,6 +155,16 @@ fn validate_copy_source_range_not_exceeds(range_spec: &HTTPRangeSpec, object_siz
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn validate_complete_multipart_parts(parts: &[CompletePart]) -> S3Result<()> {
|
fn validate_complete_multipart_parts(parts: &[CompletePart]) -> S3Result<()> {
|
||||||
|
if let Some(part) = parts
|
||||||
|
.iter()
|
||||||
|
.find(|part| !(1..=MAX_MULTIPART_PART_NUMBER as usize).contains(&part.part_num))
|
||||||
|
{
|
||||||
|
return Err(S3Error::with_message(
|
||||||
|
S3ErrorCode::InvalidPart,
|
||||||
|
format!("Part number {} must be between 1 and {MAX_MULTIPART_PART_NUMBER}", part.part_num),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if parts.windows(2).any(|window| window[0].part_num >= window[1].part_num) {
|
if parts.windows(2).any(|window| window[0].part_num >= window[1].part_num) {
|
||||||
return Err(s3_error!(InvalidPartOrder, "Part numbers must be strictly increasing"));
|
return Err(s3_error!(InvalidPartOrder, "Part numbers must be strictly increasing"));
|
||||||
}
|
}
|
||||||
@@ -2504,6 +2516,26 @@ mod tests {
|
|||||||
assert_eq!(err.code(), &S3ErrorCode::InvalidPartOrder);
|
assert_eq!(err.code(), &S3ErrorCode::InvalidPartOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_complete_multipart_parts_enforces_part_number_range() {
|
||||||
|
validate_complete_multipart_parts(&[CompletePart {
|
||||||
|
part_num: MAX_MULTIPART_PART_NUMBER as usize,
|
||||||
|
..Default::default()
|
||||||
|
}])
|
||||||
|
.expect("part number 10000 must remain valid");
|
||||||
|
|
||||||
|
for part_num in [0, MAX_MULTIPART_PART_NUMBER as usize + 1] {
|
||||||
|
let err = validate_complete_multipart_parts(&[CompletePart {
|
||||||
|
part_num,
|
||||||
|
..Default::default()
|
||||||
|
}])
|
||||||
|
.expect_err("out-of-range complete part number must be rejected");
|
||||||
|
assert_eq!(err.code(), &S3ErrorCode::InvalidPart);
|
||||||
|
let expected = format!("Part number {part_num} must be between 1 and {MAX_MULTIPART_PART_NUMBER}");
|
||||||
|
assert_eq!(err.message(), Some(expected.as_str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normalize_complete_multipart_parts_keeps_last_duplicate_part() {
|
fn normalize_complete_multipart_parts_keeps_last_duplicate_part() {
|
||||||
let input = vec![
|
let input = vec![
|
||||||
|
|||||||
@@ -1181,6 +1181,7 @@ pub(crate) mod multipart_usecase {
|
|||||||
|
|
||||||
pub(crate) mod multipart {
|
pub(crate) mod multipart {
|
||||||
pub(crate) use super::super::super::storage_contracts::{CompletePart, MultipartOperations, MultipartUploadResult};
|
pub(crate) use super::super::super::storage_contracts::{CompletePart, MultipartOperations, MultipartUploadResult};
|
||||||
|
pub(crate) use crate::storage::storage_api::s3_api_consumer::multipart::contract::multipart::MAX_MULTIPART_PART_NUMBER;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) mod object {
|
pub(crate) mod object {
|
||||||
|
|||||||
Reference in New Issue
Block a user