# Use a real Windows lock for the file token store

`0eb86b0`→[main](/content/gh/entireio/git-sync/commits/main/index.html)·

Soph·4w ago·2 files·+39 added/-14 removed

The initial Windows shim made the package compile but its lock was a no-op, so
two concurrent file-store writers could lose an update or collide on the shared
".tmp" file. Use LockFileEx/UnlockFileEx (golang.org/x/sys/windows, already a
dependency) on the ".lock" file for the same blocking, interprocess advisory
exclusion the Unix path gets from flock, which serializes writeFileToken's
read-modify-write. Promotes golang.org/x/sys to a direct dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## Sessions

aef01c5e047dView transcript

## Changes

2

- Mgo.mod+1/-1

- internal/auth

- Mtokenstore_lock_windows.go+38/-13

```
7 unmodified lines

8
9
10
11
12
13
14
15 unmodified lines

30
31
32
32
33
34

7 unmodified lines

github.com/spf13/cobra v1.10.2
	github.com/stretchr/testify v1.11.1
	github.com/zalando/go-keyring v0.2.8
	golang.org/x/sys v0.44.0
)

require (
15 unmodified lines

golang.org/x/crypto v0.51.0 // indirect
	golang.org/x/net v0.54.0 // indirect
	golang.org/x/sync v0.20.0 // indirect
	golang.org/x/sys v0.44.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)
```

Mgo.mod+1/-1

```
4 unmodified lines

5
6
7
8
9
10
11
10
11
12
13
14
15
16
17
18
19
20
12
13
14
15
16
17
22
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
27
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

4 unmodified lines

import (
	"fmt"
	"os"

"golang.org/x/sys/windows"
)

// Windows has no flock(2). The file token store's writes are atomic
// (temp file + rename), which keeps a single write safe on its own; the lock
// only guards against a lost update between concurrent read-modify-write
// processes, which is rare for a credential store. Rather than pull in a
// Windows-specific locking dependency, open (and create) the lock file so the
// call still succeeds and behaves like a no-op advisory lock.
//
// flockShared / flockExclusive mirror the Unix signatures so callers compile
// unchanged across platforms.
func flockShared(path string) (func(), error)    { return flockOpen(path + ".lock") }
func flockExclusive(path string) (func(), error) { return flockOpen(path + ".lock") }
// Windows has no flock(2); use LockFileEx on a dedicated ".lock" file for the
// same advisory, interprocess mutual exclusion the Unix path gets from flock.
// writeFileToken relies on this to serialize its read-modify-write (and the
// shared temp-file write that precedes the rename), so a no-op would let
// concurrent logins/refreshes lose an update.

func flockOpen(lockPath string) (func(), error) {
// flockShared acquires a shared (read) lock on path+".lock".
func flockShared(path string) (func(), error) {
	return flockOpen(path + ".lock", 0)
}

// flockExclusive acquires an exclusive (write) lock on path+".lock".
func flockExclusive(path string) (func(), error) {
	return flockOpen(path + ".lock", windows.LOCKFILE_EXCLUSIVE_LOCK)
}

func flockOpen(lockPath string, flags uint32) (func(), error) {
	f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return nil, fmt.Errorf("open lock file: %w", err)
	}
	return func() { _ = f.Close() }, nil
	// Lock the entire file range, blocking until the lock is available
	// (no LOCKFILE_FAIL_IMMEDIATELY), matching flock's blocking semantics.
	//
	// os.OpenFile yields a synchronous handle (Go does not pass
	// FILE_FLAG_OVERLAPPED), so LockFileEx blocks until the lock is granted and
	// never returns ERROR_IO_PENDING — that pending/GetOverlappedResult path
	// only applies to handles opened for asynchronous I/O. Treating any error
	// as failure is therefore correct here; this matches the long-standing
	// github.com/gofrs/flock implementation.
	if err := windows.LockFileEx(windows.Handle(f.Fd()), flags, 0, maxUint32, maxUint32, new(windows.Overlapped)); err != nil {
		f.Close()
		return nil, fmt.Errorf("acquire file lock: %w", err)
	}
	return func() {
		//nolint:errcheck // unlock errors on close are not actionable
		windows.UnlockFileEx(windows.Handle(f.Fd()), 0, maxUint32, maxUint32, new(windows.Overlapped))
		f.Close()
	}, nil
}

const maxUint32 = ^uint32(0)
```
