From 10cd66fe6a5b484a4dc569326a228c1c5484a5d4 Mon Sep 17 00:00:00 2001 From: mptyl Date: Tue, 18 Aug 2026 15:15:51 +0200 Subject: [PATCH] fix(windows): lock claim validation boundary --- tools/tht/internal/safeio/claim_unix.go | 4 ++++ tools/tht/internal/safeio/claim_windows.go | 11 +++++------ tools/tht/internal/safeio/files.go | 6 ++++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/tools/tht/internal/safeio/claim_unix.go b/tools/tht/internal/safeio/claim_unix.go index b6832bf8..638b5706 100644 --- a/tools/tht/internal/safeio/claim_unix.go +++ b/tools/tht/internal/safeio/claim_unix.go @@ -10,6 +10,10 @@ import ( "golang.org/x/sys/unix" ) +func lockCanonicalPrivateClaimOperation() func() { + return func() {} +} + func claimCanonicalPrivateRegular(source, claim string) (bool, error) { directory, sourceName, err := openCanonicalParentDirectory(source) if err != nil { diff --git a/tools/tht/internal/safeio/claim_windows.go b/tools/tht/internal/safeio/claim_windows.go index 7893341b..e98c4cd5 100644 --- a/tools/tht/internal/safeio/claim_windows.go +++ b/tools/tht/internal/safeio/claim_windows.go @@ -14,6 +14,11 @@ import ( // another's bounded external-contention retry while its validated parent handle is retained. var windowsPrivateClaimOperationMu sync.Mutex +func lockCanonicalPrivateClaimOperation() func() { + windowsPrivateClaimOperationMu.Lock() + return windowsPrivateClaimOperationMu.Unlock +} + type windowsPrivateRegular struct { parents *windowsParentHandles handle windows.Handle @@ -54,8 +59,6 @@ func openWindowsPrivateRegular(path string, links uint32) (*windowsPrivateRegula } func claimCanonicalPrivateRegular(source, claim string) (claimed bool, resultErr error) { - windowsPrivateClaimOperationMu.Lock() - defer windowsPrivateClaimOperationMu.Unlock() directory, sourceName, claimName, err := openWindowsPrivateClaimDirectory(source, claim) if err != nil { return false, ErrUnsafeFile @@ -70,8 +73,6 @@ func claimCanonicalPrivateRegular(source, claim string) (claimed bool, resultErr } func readCanonicalPrivateClaim(source, claim string, maximum int64) (contents []byte, found bool, resultErr error) { - windowsPrivateClaimOperationMu.Lock() - defer windowsPrivateClaimOperationMu.Unlock() directory, sourceName, claimName, err := openWindowsPrivateClaimDirectory(source, claim) if err != nil { return nil, false, ErrUnsafeFile @@ -104,8 +105,6 @@ func openWindowsPrivateClaimDirectory(source, claim string) (PrivateDirectoryHan } func removeCanonicalPrivateClaim(source, claim string) (removed bool, resultErr error) { - windowsPrivateClaimOperationMu.Lock() - defer windowsPrivateClaimOperationMu.Unlock() directory, sourceName, claimName, err := openWindowsPrivateClaimDirectory(source, claim) if err != nil { return false, ErrUnsafeFile diff --git a/tools/tht/internal/safeio/files.go b/tools/tht/internal/safeio/files.go index 0ea06497..7bda7aee 100644 --- a/tools/tht/internal/safeio/files.go +++ b/tools/tht/internal/safeio/files.go @@ -354,6 +354,8 @@ func RemoveCanonicalPrivateRegular(path string) error { // ClaimCanonicalPrivateRegular atomically creates a second, explicit private hard link to one // existing record. It is used only for digest-named, single-use OIDC state claims. func ClaimCanonicalPrivateRegular(source, claim string) (bool, error) { + unlock := lockCanonicalPrivateClaimOperation() + defer unlock() if err := validateClaimPaths(source, claim); err != nil { return false, ErrUnsafeFile } @@ -363,6 +365,8 @@ func ClaimCanonicalPrivateRegular(source, claim string) (bool, error) { // ReadCanonicalPrivateClaim reads a verified two-link source/claim pair. found=false means the // state has already been consumed or a winning process is between its two removal steps. func ReadCanonicalPrivateClaim(source, claim string, maximum int64) ([]byte, bool, error) { + unlock := lockCanonicalPrivateClaimOperation() + defer unlock() if maximum < 0 || maximum == int64(^uint64(0)>>1) || validateClaimPaths(source, claim) != nil { return nil, false, ErrUnsafeFile } @@ -371,6 +375,8 @@ func ReadCanonicalPrivateClaim(source, claim string, maximum int64) ([]byte, boo // RemoveCanonicalPrivateClaim removes exactly a verified two-link source/claim pair. func RemoveCanonicalPrivateClaim(source, claim string) (bool, error) { + unlock := lockCanonicalPrivateClaimOperation() + defer unlock() if err := validateClaimPaths(source, claim); err != nil { return false, ErrUnsafeFile }