Skip to content

Commit f13abb7

Browse files
committed
fix: add cross-process file lock to prevent config.yaml corruption
Concurrent CLI invocations race on config.yaml through the non-atomic read-modify-write in New() (viper.WriteConfig + addDocCommentsToYAML). Wrap the entire critical section in an exclusive flock via gofrs/flock.
1 parent c41a0e0 commit f13abb7

4 files changed

Lines changed: 14 additions & 4 deletions

File tree

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/charmbracelet/huh v0.7.0
1111
github.com/charmbracelet/lipgloss v1.1.0
1212
github.com/go-viper/mapstructure/v2 v2.4.0
13+
github.com/gofrs/flock v0.13.0
1314
github.com/google/uuid v1.6.0
1415
github.com/mattn/go-colorable v0.1.14
1516
github.com/mattn/go-runewidth v0.0.19
@@ -22,7 +23,7 @@ require (
2223
github.com/stretchr/testify v1.11.1
2324
github.com/tidwall/gjson v1.18.0
2425
go.uber.org/mock v0.6.0
25-
golang.org/x/sys v0.36.0
26+
golang.org/x/sys v0.37.0
2627
golang.org/x/term v0.35.0
2728
gopkg.in/yaml.v3 v3.0.1
2829
modernc.org/sqlite v1.39.0

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
6262
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
6363
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
6464
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
65+
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
66+
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
6567
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
6668
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
6769
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
@@ -170,8 +172,8 @@ golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBc
170172
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
171173
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
172174
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
173-
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
174-
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
175+
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
176+
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
175177
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
176178
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
177179
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=

internal/config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"github.com/go-viper/mapstructure/v2"
11+
"github.com/gofrs/flock"
1112
"github.com/spf13/pflag"
1213
"github.com/spf13/viper"
1314

@@ -68,6 +69,12 @@ func New(dataDir string) (*Config, cenclierrors.CencliError) {
6869

6970
configPath := filepath.Join(dataDir, "config.yaml")
7071

72+
fileLock := flock.New(configPath + ".lock")
73+
if err := fileLock.Lock(); err != nil {
74+
return nil, newInvalidConfigError(fmt.Errorf("failed to acquire config lock: %w", err).Error())
75+
}
76+
defer func() { _ = fileLock.Unlock() }()
77+
7178
if err := viper.ReadInConfig(); err != nil {
7279
var configFileNotFoundError viper.ConfigFileNotFoundError
7380
if !errors.As(err, &configFileNotFoundError) {

internal/config/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ func TestConfigWriteErrors(t *testing.T) {
346346
_, err = New(tempDir)
347347
var invalidConfigErr InvalidConfigError
348348
assert.ErrorAs(t, err, &invalidConfigErr)
349-
assert.Contains(t, err.Error(), "failed to write config file")
349+
assert.Contains(t, err.Error(), "permission denied")
350350
})
351351
}
352352

0 commit comments

Comments
 (0)