Skip to content

fix(mcp): replace TOCTOU race in Session.Initialize with sync.Once - #6

Merged
hackwither merged 1 commit into
hackwither:mainfrom
hannanmax:fix/init-race
Sep 6, 2026
Merged

hackwither merged 1 commit into
hackwither:mainfrom
hannanmax:fix/init-race

Conversation

@hannanmax

@hannanmax hannanmax commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Problem

Session.Initialize used a check-unlock-call-lock pattern for memoisation:

s.mu.Lock()
if s.initDone { /* return cached */ }
s.mu.Unlock()
// gap: another goroutine can enter here
res, raw, err := s.handshake(ctx)
s.mu.Lock()
s.initDone = true
// ...

Two goroutines that both observe initDone == false before the unlock can both enter handshake() concurrently. The second call's result overwrites the first's, and notifications/initialized can be sent twice — which violates the MCP spec and may confuse servers that track notification state.

Fix

Replace initDone bool with sync.Once. The Do block runs exactly once regardless of how many goroutines call Initialize simultaneously:

s.initOnce.Do(func() {
    res, raw, err := s.handshake(ctx)
    s.mu.Lock()
    s.initResult, s.initRaw, s.initErr = res, raw, err
    s.mu.Unlock()
    if err == nil {
        _ = s.notify(ctx, "notifications/initialized", nil)
    }
})
s.mu.Lock()
res, raw, err := s.initResult, s.initRaw, s.initErr
s.mu.Unlock()
return res, raw, err

Observable behaviour for sequential callers is identical. sync is already imported.

The previous memoisation used check-unlock-call-lock, which left a
window where two goroutines could both observe initDone==false,
release the lock, and enter handshake() concurrently. The second
call's result would overwrite the first's, and notifications/initialized
could be sent twice.

Replace initDone bool with sync.Once: the Do block runs exactly once
regardless of concurrency, eliminating the race without changing the
observable behaviour for sequential callers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants