Files
dingtalk-workspace-cli/internal/upgrade/skill_publication_identity_other.go
T
玉澜 27aca3ccc3 fix(skills): close no-replace fallback TOCTOU and surface uncertain publications
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).
2026-08-19 17:42:20 +08:00

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)
}