The mkdir->rename->remove->rename directory fallback had a TOCTOU window between the second remove and the second rename: a concurrent writer creating an entry at the destination was silently clobbered. The fallback now claims the destination once with mkdir and never unlinks it: the fast-path rename publishes over the claim (Linux), and platforms that refuse directory renames (macOS, Windows) move the staged children into the claim through atomic no-clobber primitives (mkdir/os.Link/os.Symlink), consuming the emptied source shell on success. renameSkillPathNoReplace now returns the mkdir-claim identity captured by the child-move path. PublishSkillPathNoReplace uses it as a three-state ownership witness: the atomic/fast paths keep the staged-inode proof, the child-move path proves dest is still the mkdir claim, and a mismatch reports the new ErrSkillPathPublicationUncertain sentinel with the destination retained. The witness is real on POSIX now: darwin and linux report the dev:ino file identity instead of the empty no-op. Upstream consumers honor the sentinel: the mono/multi upgrade copy fallbacks no longer retry over an uncertain destination (the retry would displace the concurrent writer's object), and skill setup reports the retained destination instead of claiming a rollback. Rewrites the fallback tests that pinned the removed remove-and-retry flow and adds regression coverage: concurrent claim entries abort with the destination retained, wholesale replacement after child-move reports the uncertain sentinel, staged-set transactions pass the sentinel through, and both copy fallbacks short-circuit (ablation-verified).
28 lines
729 B
Go
28 lines
729 B
Go
//go:build !linux && !darwin && !windows
|
|
|
|
package upgrade
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
func skillPathFileIncarnationImpl(info os.FileInfo) string {
|
|
return fmt.Sprintf("%d:%d:%s", info.ModTime().UnixNano(), info.Size(), info.Mode())
|
|
}
|
|
|
|
func skillPathSameFileIdentityImpl(left, right os.FileInfo) bool {
|
|
return os.SameFile(left, right)
|
|
}
|
|
|
|
// skillPathFileIdentityImpl reports no identity on platforms whose stat
|
|
// layout we do not decode; the child-move claim witness degrades gracefully
|
|
// to the publication fingerprint backstop.
|
|
func skillPathFileIdentityImpl(_ string) string {
|
|
return ""
|
|
}
|
|
|
|
func skillPathIdentityProven(staged, published os.FileInfo, _, _ string) bool {
|
|
return skillPathSameFileIdentity(staged, published)
|
|
}
|