fix(webdav): make PATCH's concurrency guard a real compare-and-swap
The app-level ETag re-check before the write still left a gap between the check and the actual UPDATE for a concurrent writer to land in. Push the check into the write path itself: swap_blob_hash now takes an expected_hash and only applies the SET under the same FOR UPDATE row lock it already held, closing the race instead of just narrowing it. Adds ErrorKind::PreconditionFailed (412) for the CAS-miss path; PUT/WOPI/chunked-upload keep blind-overwrite semantics by passing None
This commit is contained in:
@@ -1972,6 +1972,7 @@ async fn handle_put(
|
||||
&content_type,
|
||||
None,
|
||||
user.id,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -2303,25 +2304,15 @@ async fn handle_patch(
|
||||
));
|
||||
}
|
||||
|
||||
// ── Optimistic-concurrency re-check ───────────────────────────────
|
||||
// `file.etag` was snapshotted before the (potentially slow) splice +
|
||||
// CAS-ingest above. Re-verify nothing else wrote to this file in the
|
||||
// meantime, narrowing the window in which two concurrent PATCHes to
|
||||
// disjoint ranges — each individually passing its own If-Match check
|
||||
// against the same stale snapshot — could otherwise silently clobber
|
||||
// each other on the blind-overwrite write path below.
|
||||
if let Ok(current) = file_retrieval_service
|
||||
.get_file_by_path(&path, drive_id)
|
||||
.await
|
||||
&& current.etag != file.etag
|
||||
{
|
||||
upload_ingest::discard_ingested(&state.core.dedup_service, &ingested).await;
|
||||
return Err(AppError::precondition_failed(
|
||||
"File was modified concurrently — retry the PATCH",
|
||||
));
|
||||
}
|
||||
|
||||
// ── Atomic store ──────────────────────────────────────────────────
|
||||
// ── Atomic store, compare-and-swap on the pre-splice content hash ──
|
||||
// `file.content_hash` was snapshotted before the (potentially slow)
|
||||
// splice + CAS-ingest above. Passing it as `expected_hash` makes the
|
||||
// write itself a compare-and-swap: the repository checks and applies
|
||||
// under the same row lock, so nothing else can write to this file
|
||||
// between the check and the write. This is what actually closes the
|
||||
// race two concurrent PATCHes to disjoint ranges could otherwise hit
|
||||
// — each individually passing its own If-Match check against the
|
||||
// same stale snapshot, then blindly overwriting each other.
|
||||
let new_size = ingested.size;
|
||||
let content_type = ingested.content_type.clone();
|
||||
let result = file_upload_service
|
||||
@@ -2332,6 +2323,7 @@ async fn handle_patch(
|
||||
&content_type,
|
||||
None,
|
||||
user.id,
|
||||
Some(&file.content_hash),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user