mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 16:28:15 +00:00
fix(extract): treat tar mtime=0 as unset to fix unreadable entries (#4948)
An extracted entry whose tar header mtime is 0 was stored with mod_time = UNIX_EPOCH, which xl.meta encodes as 0 nanos (= no mod_time). On read-back the version failed valid() and parsing fell into the legacy rmp_serde fallback, so every read of the object returned 500 (invalid type: integer 0, expected an OffsetDateTime). Treat mtime 0 as unset (tar convention) and fall back to the upload time. Also fix two test-side issues uncovered behind the 500: the pax fixture must use a ustar header for its XHeader entry, and the SSE-S3 extract tests must provision RUSTFS_SSE_S3_MASTER_KEY. Re-admit the 19 quarantined tests to the e2e-full merge gate. Fixes #4842
This commit is contained in:
@@ -286,8 +286,6 @@ path = "junit.xml"
|
||||
# ci-profile quarantine (docs/testing/README.md): every entry MUST cite one
|
||||
# OPEN issue, and the fixing PR MUST delete the exclusion. The passing
|
||||
# negative-path siblings of each family stay in as regression guards.
|
||||
# * rustfs#4842 — extract/snowball expand pipeline 500s (mtime=0
|
||||
# OffsetDateTime deserialization + same-path failures).
|
||||
# * rustfs#4843 — over-limit archive entry paths hard-reject the whole
|
||||
# archive even under ignore-errors semantics.
|
||||
# * rustfs#4844 — anonymous POST-object with SSE-S3 / bucket-default SSE
|
||||
@@ -303,8 +301,6 @@ default-filter = """
|
||||
& !test(/^protocols::/)
|
||||
& !test(/^(admin_timeout_regression_test|cluster_concurrency_test|cluster_multidrive_pool_test|heal_erasure_disk_rebuild_test|namespace_lock_quorum_test|object_lambda_test|stale_multipart_cleanup_cluster_test)::/)
|
||||
& !test(/^replication_extension_test::/)
|
||||
& !test(/^multipart_auth_test::test_signed_put_object_extract_(accepts_compat_header|expands_tar_entries_with_prefix_headers|expands_tar_gz_archive|expands_tbz2_archive|expands_tgz_archive|expands_txz_archive|expands_tzst_archive|normalizes_prefix_header_value|preserves_directory_markers_by_default|preserves_object_lock_legal_hold|preserves_object_lock_retention|preserves_pax_metadata_and_version_id|preserves_request_metadata_on_extracted_objects|preserves_sse_c|preserves_sse_s3_and_redirect|preserves_storage_class|uses_bucket_default_sse_s3)$/)
|
||||
& !test(/^snowball_auto_extract_test::tests::snowball_auto_extract_(prefers_exact_minio_prefix_over_suffix_fallback|supports_minio_prefix_and_directory_markers)$/)
|
||||
& !test(/^multipart_auth_test::test_signed_put_object_extract_skips_invalid_entry_when_ignore_errors_enabled$/)
|
||||
& !test(/^snowball_auto_extract_test::tests::snowball_auto_extract_(ignores_invalid_entries_when_requested|supports_standard_headers_with_combined_extract_options)$/)
|
||||
& !test(/^multipart_auth_test::test_anonymous_post_object_(accepts_sse_s3|rejects_sse_s3_missing_from_policy_conditions|uses_bucket_default_sse_kms|uses_bucket_default_sse_s3)$/)
|
||||
|
||||
@@ -105,7 +105,11 @@ async fn make_tar_with_pax_entry(path: &str, data: &[u8], mtime: Option<u64>, pa
|
||||
pax_payload.extend(build_pax_record(key, value));
|
||||
}
|
||||
|
||||
let mut pax_header = tokio_tar::Header::new_gnu();
|
||||
// Pax extension entries must carry a POSIX ustar header — this is what real
|
||||
// tar writers emit, and the server-side reader rejects an XHeader typeflag on
|
||||
// GNU-format headers ("extension typeflag is not permitted on an unrecognized
|
||||
// header").
|
||||
let mut pax_header = tokio_tar::Header::new_ustar();
|
||||
pax_header.set_entry_type(tokio_tar::EntryType::XHeader);
|
||||
pax_header.set_size(pax_payload.len() as u64);
|
||||
pax_header.set_mode(0o644);
|
||||
@@ -5041,7 +5045,9 @@ async fn test_signed_put_object_extract_preserves_sse_s3_and_redirect() -> Resul
|
||||
init_logging();
|
||||
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
env.start_rustfs_server(vec![]).await?;
|
||||
let sse_master_key = base64::engine::general_purpose::STANDARD.encode([0x42u8; 32]);
|
||||
env.start_rustfs_server_with_env(vec![], &[("RUSTFS_SSE_S3_MASTER_KEY", sse_master_key.as_str())])
|
||||
.await?;
|
||||
|
||||
let bucket = "signed-extract-sse-s3-redirect";
|
||||
let archive_key = "encrypted-metadata.tar";
|
||||
@@ -5318,7 +5324,9 @@ async fn test_signed_put_object_extract_uses_bucket_default_sse_s3() -> Result<(
|
||||
init_logging();
|
||||
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
env.start_rustfs_server(vec![]).await?;
|
||||
let sse_master_key = base64::engine::general_purpose::STANDARD.encode([0x42u8; 32]);
|
||||
env.start_rustfs_server_with_env(vec![], &[("RUSTFS_SSE_S3_MASTER_KEY", sse_master_key.as_str())])
|
||||
.await?;
|
||||
|
||||
let bucket = "signed-extract-default-sse-s3";
|
||||
let archive_key = "default-encryption.tar";
|
||||
|
||||
@@ -6872,10 +6872,14 @@ impl DefaultObjectUsecase {
|
||||
}
|
||||
let mut size =
|
||||
i64::try_from(entry_size).map_err(|_| s3_error!(InvalidArgument, "Archive entry size does not fit into i64"))?;
|
||||
// mtime 0 means "unset" in tar headers, and xl.meta cannot represent an
|
||||
// epoch mod_time anyway (0 nanos decodes as no-mod_time, making the version
|
||||
// unreadable — rustfs#4842), so fall back to the upload time instead.
|
||||
let archive_entry_mod_time = f
|
||||
.header()
|
||||
.mtime()
|
||||
.ok()
|
||||
.filter(|&modified_at_secs| modified_at_secs > 0)
|
||||
.and_then(|modified_at_secs| OffsetDateTime::from_unix_timestamp(modified_at_secs as i64).ok());
|
||||
let mut metadata = HashMap::new();
|
||||
let has_explicit_object_lock_retention = object_lock_mode.is_some() || object_lock_retain_until_date.is_some();
|
||||
|
||||
Reference in New Issue
Block a user