fix(s3): align snowball member semantics (#6944)

* fix(s3): harden Snowball extract error boundaries

* fix(s3): close Snowball extract compatibility gaps

* fix(s3): verify Snowball request body completion

* test(s3): reject forged Snowball streaming signatures

* build(deps): pin Snowball archive parser limits

* fix(s3): preserve Snowball trailer and member errors

* docs(architecture): register Snowball tar fork cleanup

* refactor(s3): route Snowball errors through object boundary

* ci(deps): allow pinned tokio-tar source

* fix: align Snowball archive codec detection

* fix(s3): harden Snowball codec compatibility

* fix(s3): preserve Snowball codec compatibility

* test(zip): align yield wake assertion with Tokio

* fix(rio): preserve legacy large-block reads

* fix(zip): accept blank tar numeric fields

* fix(s3): align Snowball member import semantics

* fix(s3): authorize PAX legal-hold conditions

* refactor(s3): preserve Snowball error boundary

* fix(iam): support legal-hold policy conditions
This commit is contained in:
cxymds
2026-08-31 23:18:28 +08:00
committed by GitHub
parent 0d1e40ee73
commit ec0a65703a
10 changed files with 1446 additions and 227 deletions
File diff suppressed because it is too large Load Diff
+4
View File
@@ -182,6 +182,10 @@ fn object_s3_error(code: S3ErrorCode, message: impl Into<std::borrow::Cow<'stati
S3Error::with_message(code, message)
}
fn object_s3_error_default(code: S3ErrorCode) -> S3Error {
S3Error::new(code)
}
mod copy;
mod delete;
mod extract;
+3 -5
View File
@@ -958,11 +958,9 @@ impl DefaultObjectUsecase {
return Err(s3_error!(InvalidStorageClass));
}
// An authorized inbound replication PUT must store the replica verbatim.
// A snowball-extracted member object keeps `x-amz-meta-snowball-auto-extract`
// in its user metadata, and the replication client replays stored metadata
// as headers re-dispatching that PUT into the extract path would try to
// untar the member's own bytes (failing replication for any non-archive
// member) instead of writing the replica.
// Legacy snowball-extracted members may still carry the auto-extract
// metadata, which replication replays as a header. Do not interpret that
// historical user metadata as a request to untar the member again.
let inbound_replication_put = replication_request_authorized(&req)
&& get_header(&req.headers, SUFFIX_SOURCE_REPLICATION_REQUEST).as_deref() == Some("true");
if max_content_length.is_some() && is_put_object_extract_requested(&req.headers) {
+3 -1
View File
@@ -1097,7 +1097,9 @@ pub(crate) mod s3_api {
}
pub(crate) mod tagging {
pub(crate) use crate::storage::storage_api::s3_api_consumer::tagging::resolve_copy_object_tags;
pub(crate) use crate::storage::storage_api::s3_api_consumer::tagging::{
parse_copy_object_tags, resolve_copy_object_tags,
};
}
}
+29 -1
View File
@@ -815,7 +815,15 @@ pub fn get_condition_values_with_query_and_client_info(
/// `key`, either because the server already derived that key from verified state or
/// because it is a well-known identity/context key that only the server may populate.
fn is_reserved_condition_key(key: &str, server_derived: &HashMap<String, Vec<String>>) -> bool {
server_derived.contains_key(key) || is_server_derived_condition_key(key)
server_derived.contains_key(key)
|| [
AMZ_OBJECT_LOCK_MODE_LOWER,
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER,
AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
]
.iter()
.any(|header| key.eq_ignore_ascii_case(header.trim_start_matches("x-amz-")))
|| is_server_derived_condition_key(key)
}
/// Get request authentication type
@@ -1670,17 +1678,37 @@ mod tests {
let cred = create_test_credentials();
let mut headers = HeaderMap::new();
headers.insert(AMZ_OBJECT_LOCK_MODE_LOWER, HeaderValue::from_static("GOVERNANCE"));
headers.insert(AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, HeaderValue::from_static("OFF"));
headers.insert(AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER, HeaderValue::from_static("2024-12-31T23:59:59Z"));
headers.insert("object-lock-mode", HeaderValue::from_static("COMPLIANCE"));
headers.insert("object-lock-legal-hold", HeaderValue::from_static("ON"));
headers.insert("object-lock-retain-until-date", HeaderValue::from_static("2099-12-31T23:59:59Z"));
let conditions = get_condition_values(&headers, &cred, None, None, None);
assert_eq!(conditions.get("object-lock-mode"), Some(&vec!["GOVERNANCE".to_string()]));
assert_eq!(conditions.get("object-lock-legal-hold"), Some(&vec!["OFF".to_string()]));
assert_eq!(
conditions.get("object-lock-retain-until-date"),
Some(&vec!["2024-12-31T23:59:59Z".to_string()])
);
}
#[test]
fn object_lock_condition_aliases_cannot_spoof_canonical_headers() {
let cred = create_test_credentials();
let mut headers = HeaderMap::new();
headers.insert("object-lock-mode", HeaderValue::from_static("COMPLIANCE"));
headers.insert("object-lock-legal-hold", HeaderValue::from_static("ON"));
headers.insert("object-lock-retain-until-date", HeaderValue::from_static("2099-12-31T23:59:59Z"));
let conditions = get_condition_values(&headers, &cred, None, None, None);
assert_eq!(conditions.get("object-lock-mode"), None);
assert_eq!(conditions.get("object-lock-legal-hold"), None);
assert_eq!(conditions.get("object-lock-retain-until-date"), None);
}
#[test]
fn test_get_condition_values_with_grant_headers() {
let cred = create_test_credentials();
+1 -1
View File
@@ -350,7 +350,7 @@ pub(crate) mod s3_api_consumer {
}
pub(crate) mod tagging {
pub(crate) use super::super::super::s3_api::tagging::resolve_copy_object_tags;
pub(crate) use super::super::super::s3_api::tagging::{parse_copy_object_tags, resolve_copy_object_tags};
}
}