Skip to content

Commit 9b7a59e

Browse files
ErmakovDmitriyDmitrii Ermakov
and
Dmitrii Ermakov
authored
Fix a potential race: StaticOIDCClientsStore concurrent access (#45)
This PR introduces an RWMutex to protect StaticOIDCClientsStore access. I read the StaticOIDCClientsStore implementation and think that a race condition is possible under some circumstances, in particular when a new OIDCClientConfig is added (just after the auth agent startup) and at the same time HAProxy causes the auth agent to lookup for another OIDCClientConfig for another domain. I tried to write a test case (in the commit) to check how it behaved and before I added the sync.RWMutex, I managed to get, sometimes (once every 5-8 test runs) an error: ``` fatal error: concurrent map read and map write goroutine 9 [running]: github.com/criteo/haproxy-spoe-auth/internal/auth.(*StaticOIDCClientsStore).GetClient(...) /home/demonihin/projects/haproxy-spoe-auth/internal/auth/oidc_clients_store.go:38 github.com/criteo/haproxy-spoe-auth/internal/auth.TestStaticOIDCClientsStoreRace.func2() /home/demonihin/projects/haproxy-spoe-auth/internal/auth/oidc_clients_store_test.go:38 +0x78 created by github.com/criteo/haproxy-spoe-auth/internal/auth.TestStaticOIDCClientsStoreRace in goroutine 6 /home/demonihin/projects/haproxy-spoe-auth/internal/auth/oidc_clients_store_test.go:36 +0x114 goroutine 1 [runnable]: internal/poll.(*FD).Write(0x400014bc18, {0x222380, 0x400000e348, 0x100004000103450}) /opt/go/src/internal/poll/fd_unix.go:366 os.(*File).write(...) /opt/go/src/os/file_posix.go:46 os.(*File).Write(0x4000052030, {0x40000108b8?, 0x5, 0xf202c?}) /opt/go/src/os/file.go:183 +0x5c fmt.Fprint({0x293048, 0x4000052030}, {0x400014bdb8, 0x2, 0x2}) /opt/go/src/fmt/print.go:263 +0x74 fmt.Print(...) /opt/go/src/fmt/print.go:272 testing.(*M).Run(0x4000104dc0) /opt/go/src/testing/testing.go:1961 +0x8bc main.main() _testmain.go:59 +0x1a8 goroutine 7 [runnable]: strings.Clone(...) /opt/go/src/strings/clone.go:25 github.com/criteo/haproxy-spoe-auth/internal/auth.(*StaticOIDCClientsStore).AddClient(0x400006e2e0, {0x400004f790, 0xe}, {0x22d5fe, 0x9}, {0x22eab3, 0xd}, {0x4000200340, 0x1f}) /home/demonihin/projects/haproxy-spoe-auth/internal/auth/oidc_clients_store.go:49 +0xac github.com/criteo/haproxy-spoe-auth/internal/auth.TestStaticOIDCClientsStoreRace.func1() /home/demonihin/projects/haproxy-spoe-auth/internal/auth/oidc_clients_store_test.go:26 +0xa4 created by github.com/criteo/haproxy-spoe-auth/internal/auth.TestStaticOIDCClientsStoreRace in goroutine 6 /home/demonihin/projects/haproxy-spoe-auth/internal/auth/oidc_clients_store_test.go:17 +0xec exit status 2 FAIL github.com/criteo/haproxy-spoe-auth/internal/auth 0.040s ``` Co-authored-by: Dmitrii Ermakov <dmitrii.ermakov@maxiv.lu.se>
1 parent 00a3fc7 commit 9b7a59e

File tree

2 files changed

+64
-3
lines changed

2 files changed

+64
-3
lines changed

internal/auth/oidc_clients_store.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package auth
22

33
import (
44
"strings"
5+
"sync"
56
)
67

78
type OIDCClientConfig struct {
@@ -18,6 +19,8 @@ type OIDCClientsStore interface {
1819

1920
type StaticOIDCClientsStore struct {
2021
clients map[string]OIDCClientConfig
22+
23+
mtx sync.RWMutex
2124
}
2225

2326
func NewStaticOIDCClientStore(config map[string]OIDCClientConfig) *StaticOIDCClientsStore {
@@ -29,18 +32,24 @@ func NewEmptyStaticOIDCClientStore() *StaticOIDCClientsStore {
2932
}
3033

3134
func (ocf *StaticOIDCClientsStore) GetClient(domain string) (*OIDCClientConfig, error) {
35+
ocf.mtx.RLock()
36+
defer ocf.mtx.RUnlock()
37+
3238
if config, ok := ocf.clients[domain]; ok {
3339
return &config, nil
3440
}
3541
return nil, ErrOIDCClientConfigNotFound
3642
}
3743

3844
func (ocf *StaticOIDCClientsStore) AddClient(domain string, clientid string, clientsecret string, redirecturl string) {
45+
ocf.mtx.Lock()
46+
defer ocf.mtx.Unlock()
47+
3948
if _, ok := ocf.clients[domain]; !ok {
40-
ocf.clients[strings.Clone(domain)] = OIDCClientConfig {
41-
ClientID: strings.Clone(clientid),
49+
ocf.clients[strings.Clone(domain)] = OIDCClientConfig{
50+
ClientID: strings.Clone(clientid),
4251
ClientSecret: strings.Clone(clientsecret),
43-
RedirectURL: strings.Clone(redirecturl),
52+
RedirectURL: strings.Clone(redirecturl),
4453
}
4554
}
4655
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package auth
2+
3+
import (
4+
"strconv"
5+
"sync"
6+
"testing"
7+
)
8+
9+
func TestStaticOIDCClientsStoreRace(t *testing.T) {
10+
var wg = &sync.WaitGroup{}
11+
var expectedValue OIDCClientConfig
12+
var store = NewEmptyStaticOIDCClientStore()
13+
const steps = 10000
14+
15+
// One Goroutine changes the store state while 2 other try to read from it.
16+
wg.Add(1)
17+
go func() {
18+
for i := 0; i < steps; i++ {
19+
// Something comes with requests for a new and a valid domain,
20+
// so it is being added to the store.
21+
expectedValue.ClientID = "client-id"
22+
expectedValue.ClientSecret = "client-secret"
23+
expectedValue.RedirectURL = "https://" + strconv.Itoa(i) + ".example.com/redirect"
24+
domain := strconv.Itoa(i) + ".example.com"
25+
26+
store.AddClient(domain, expectedValue.ClientID, expectedValue.ClientSecret, expectedValue.RedirectURL)
27+
}
28+
29+
wg.Done()
30+
}()
31+
32+
// Read and compare.
33+
var found bool
34+
for i := 0; i < 2; i++ {
35+
wg.Add(1)
36+
go func() {
37+
for i := 0; i < steps; i++ {
38+
_, err := store.GetClient("100000.example.com")
39+
if err == nil {
40+
found = true
41+
break
42+
}
43+
}
44+
45+
wg.Done()
46+
}()
47+
}
48+
49+
if found {
50+
t.Fatal("Received a value while should get ErrOIDCClientConfigNotFound")
51+
}
52+
}

0 commit comments

Comments
 (0)