hts_rename_over() falls back to unlink-then-rename because Windows' rename() refuses an existing target. Between the unlink and the retry there is no copy of the destination left, so any retry failure loses it.
The ordinary Windows shape that hits this: a third party holds src open without FILE_SHARE_DELETE (virus scanner, indexer, backup agent). _wrename fails with ERROR_SHARING_VIOLATION, which _dosmaperr maps to EACCES; src exists, so the gate added in #784 lets the fallback through, the unlink succeeds, and the retry fails the same way. The caller gets HTS_FALSE back with dst already gone.
Two smaller variants of the same window: src removed by an external actor between the existence check and the retry, and a caller whose src and dst sit in different directories where only dst's parent is writable. No current call site can produce the second one — all four build src as dst plus a suffix — but the header documents no such restriction, so a future caller inherits it.
The fix is to rename dst aside rather than unlink it, and drop the aside copy only once the move has succeeded. #784 narrowed the failure classes that reach the unlink at all and documented what survives; closing the window itself needs the aside.
Found reviewing #784. Three of the four destinations are user data: a mirrored file, a finished .wacz, and a rewritten page nothing will re-fetch.
hts_rename_over()falls back to unlink-then-rename because Windows'rename()refuses an existing target. Between the unlink and the retry there is no copy of the destination left, so any retry failure loses it.The ordinary Windows shape that hits this: a third party holds
srcopen withoutFILE_SHARE_DELETE(virus scanner, indexer, backup agent)._wrenamefails withERROR_SHARING_VIOLATION, which_dosmaperrmaps toEACCES;srcexists, so the gate added in #784 lets the fallback through, the unlink succeeds, and the retry fails the same way. The caller getsHTS_FALSEback withdstalready gone.Two smaller variants of the same window:
srcremoved by an external actor between the existence check and the retry, and a caller whosesrcanddstsit in different directories where onlydst's parent is writable. No current call site can produce the second one — all four buildsrcasdstplus a suffix — but the header documents no such restriction, so a future caller inherits it.The fix is to rename
dstaside rather than unlink it, and drop the aside copy only once the move has succeeded. #784 narrowed the failure classes that reach the unlink at all and documented what survives; closing the window itself needs the aside.Found reviewing #784. Three of the four destinations are user data: a mirrored file, a finished
.wacz, and a rewritten page nothing will re-fetch.