mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-08 14:23:13 +00:00
fix: prevent object lock retention race (#2634)
This commit is contained in:
+23
-35
@@ -38,7 +38,7 @@ use rustfs_ecstore::{
|
||||
},
|
||||
error::{StorageError, is_err_bucket_not_found, is_err_object_not_found, is_err_version_not_found},
|
||||
new_object_layer_fn,
|
||||
store_api::{BucketOperations, BucketOptions, ObjectOperations, ObjectOptions},
|
||||
store_api::{BucketOperations, BucketOptions, ObjectLockRetentionOptions, ObjectOperations, ObjectOptions},
|
||||
};
|
||||
use rustfs_s3_common::{S3Operation, record_s3_op};
|
||||
use rustfs_targets::EventName;
|
||||
@@ -1284,44 +1284,27 @@ impl S3 for FS {
|
||||
.as_ref()
|
||||
.and_then(|r| r.retain_until_date.as_ref())
|
||||
.map(|d| OffsetDateTime::from(d.clone()));
|
||||
let new_mode = retention.as_ref().and_then(|r| r.mode.as_ref()).map(|mode| mode.as_str());
|
||||
let new_mode = retention
|
||||
.as_ref()
|
||||
.and_then(|r| r.mode.as_ref())
|
||||
.map(|mode| mode.as_str().to_string());
|
||||
|
||||
// TODO(security): Known TOCTOU race condition (fix in future PR).
|
||||
//
|
||||
// There is a time-of-check-time-of-use (TOCTOU) window between the retention
|
||||
// check below (using get_object_info + check_retention_for_modification) and
|
||||
// the actual update performed later in put_object_metadata.
|
||||
//
|
||||
// In theory:
|
||||
// * Thread A reads retention mode = GOVERNANCE and checks the bypass header.
|
||||
// * Thread B updates retention to COMPLIANCE mode.
|
||||
// * Thread A then proceeds to modify retention, still assuming GOVERNANCE,
|
||||
// and effectively bypasses what is now COMPLIANCE mode.
|
||||
//
|
||||
// This would violate the S3 spec, which states that COMPLIANCE-mode retention
|
||||
// cannot be modified even with a bypass header.
|
||||
//
|
||||
// Possible fixes (to be implemented in a future change):
|
||||
// 1. Pass the expected retention mode down to the storage layer and verify
|
||||
// it has not changed immediately before the update.
|
||||
// 2. Use optimistic concurrency (e.g., version/etag) so that the update
|
||||
// fails if the object changed between check and update.
|
||||
// 3. Perform the retention check inside the same lock/transaction scope as
|
||||
// the metadata update within the storage layer.
|
||||
//
|
||||
// Current mitigation: the storage layer provides a fast_lock_manager, which
|
||||
// offers some protection, but it does not fully eliminate this race.
|
||||
let bypass_governance = has_bypass_governance_header(&req.headers);
|
||||
// Keep the early check for existing response behavior; put_object_metadata
|
||||
// repeats the same check after taking the metadata write lock.
|
||||
let check_opts: ObjectOptions = get_opts(&bucket, &key, version_id.clone(), None, &req.headers)
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
|
||||
if let Ok(existing_obj_info) = store.get_object_info(&bucket, &key, &check_opts).await {
|
||||
let bypass_governance = has_bypass_governance_header(&req.headers);
|
||||
if let Some(block_reason) =
|
||||
check_retention_for_modification(&existing_obj_info.user_defined, new_mode, new_retain_until, bypass_governance)
|
||||
{
|
||||
return Err(S3Error::with_message(S3ErrorCode::AccessDenied, block_reason.error_message()));
|
||||
}
|
||||
if let Ok(existing_obj_info) = store.get_object_info(&bucket, &key, &check_opts).await
|
||||
&& let Some(block_reason) = check_retention_for_modification(
|
||||
&existing_obj_info.user_defined,
|
||||
new_mode.as_deref(),
|
||||
new_retain_until,
|
||||
bypass_governance,
|
||||
)
|
||||
{
|
||||
return Err(S3Error::with_message(S3ErrorCode::AccessDenied, block_reason.error_message()));
|
||||
}
|
||||
|
||||
let eval_metadata = parse_object_lock_retention(retention)?;
|
||||
@@ -1330,10 +1313,15 @@ impl S3 for FS {
|
||||
.await
|
||||
.map_err(ApiError::from)?;
|
||||
opts.eval_metadata = Some(eval_metadata);
|
||||
opts.object_lock_retention = Some(ObjectLockRetentionOptions {
|
||||
mode: new_mode,
|
||||
retain_until: new_retain_until,
|
||||
bypass_governance,
|
||||
});
|
||||
|
||||
let object_info = store.put_object_metadata(&bucket, &key, &opts).await.map_err(|e| {
|
||||
error!("put_object_metadata failed, {}", e.to_string());
|
||||
s3_error!(InternalError, "{}", e.to_string())
|
||||
S3Error::from(ApiError::from(e))
|
||||
})?;
|
||||
|
||||
let output = PutObjectRetentionOutput {
|
||||
|
||||
Reference in New Issue
Block a user