Skip to content

TT-5486: Fix session cache access rights race#8320

Merged
buger merged 5 commits into
masterfrom
TT-5486-fix-auth-load-v2
Jun 17, 2026
Merged

TT-5486: Fix session cache access rights race#8320
buger merged 5 commits into
masterfrom
TT-5486-fix-auth-load-v2

Conversation

@andrei-tyk

@andrei-tyk andrei-tyk commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

This fixes the gateway panic seen under high authentication load:

fatal error: concurrent map iteration and map write
fatal error: concurrent map clone and map write

The fix stores cloned SessionState values in the local session cache, so cached entries do not share AccessRights map internals with request-local sessions that policy application mutates.

Changes:

  • Store session.Clone() in SessionCache.Set(...) on both session-store and auth-store cache write paths.
  • Add a regression test proving the cached session does not share AccessRights with the request-local session.
  • Add an opt-in child-process reproducer that demonstrates the original Go runtime fatal without breaking normal CI.

Root Cause

SessionState is a struct, but several of its fields are reference types. In particular, AccessRights is a map.

Before this PR, the gateway could cache a SessionState value before policy application finished:

t.Gw.SessionCache.Set(cacheKey, session, cache.DefaultExpiration)

That stores a struct copy, but not a deep copy of the maps. As a result, the cached session and the request-local session could share the same AccessRights map.

The request-local session is then mutated by policy application and MCP normalization. At the same time, another request can load the cached value and clone it. That means one goroutine can iterate/clone AccessRights while another goroutine writes to the same underlying map, which Go treats as a fatal runtime error.

Request And Cache Lifecycle

For each authenticated request, the gateway looks up a session from either:

  • the local in-memory session cache,
  • the global/session backing store,
  • or the auth store fallback path.

The session is cloned into a request-local SessionState. That local copy can then be safely modified for the duration of that request by policy application, endpoint normalization, key hash setup, Touch(), and other request-scoped work.

The important boundary is ownership:

  • The cache may hold shared data reused by future requests.
  • The active request should mutate only its own detached copy.
  • The cached value must not share map internals with an in-flight request.

This PR keeps that model by cloning before storing in the cache.

Why The Local Cache Is Still Needed

The local session cache avoids repeatedly hitting the backing session/auth stores on every request. That matters on the hot authentication path, especially under high request concurrency.

The cache is still valid here; the bug was not the existence of the cache. The bug was that the cached value and request-local value were accidentally sharing mutable map internals.

Stale Data Considerations

There is always some existing stale-data behavior with a local cache: cached session fields can remain in memory until cache expiry or explicit invalidation.

This PR does not increase that stale-data window. It preserves the existing cache-hit behavior: cache hits still clone the cached session and apply policies during request validation.

That means policy changes are still evaluated on the request-local copy, while the cached raw session remains isolated from request mutations.

Why This Fix Is Narrow

An alternative approach is to add locking inside SessionState, but that is a larger ownership change and is risky because SessionState is copied by value in many places.

Putting a sync.RWMutex directly inside SessionState makes Clone() and other value copies vulnerable to copying a live lock. That can introduce deadlocks and go vet -copylocks failures unless the entire type ownership model is redesigned.

For this bug, we do not need to make all SessionState map access globally synchronized. We only need to ensure that the local cache never stores maps that are still owned by an active request.

Reproduction

The opt-in test intentionally recreates the old shallow-copy behavior:

  1. Create a session with a large AccessRights map.
  2. Make a shallow cached copy, matching the old SessionCache.Set(cacheKey, session, ...) behavior.
  3. Clone the cached session concurrently.
  4. Apply policies to the request-local session concurrently.
  5. Assert that the child process hits the Go runtime concurrent-map fatal.

It is skipped by default and only runs when explicitly requested:

TYK_RUN_SESSION_CACHE_RACE_REPRO=1 GOCACHE=/private/tmp/tyk-go-cache go test ./gateway -run '^TestOnDemandReproduceSessionCacheAccessRightsConcurrentMapCrash$' -count=1 -v

Observed output includes:

fatal error: concurrent map clone and map write
github.com/TykTechnologies/tyk/user.SessionState.Clone
github.com/TykTechnologies/tyk/internal/policy.(*Service).Apply

Validation

Normal focused regression path:

GOCACHE=/private/tmp/tyk-go-cache go test ./gateway -run '^(TestCheckSessionAndIdentityForValidKey_CachesSessionWithoutSharingAccessRights|TestOnDemandReproduceSessionCacheAccessRightsConcurrentMapCrash)$' -count=1 -v

Result:

  • TestCheckSessionAndIdentityForValidKey_CachesSessionWithoutSharingAccessRights: passed.
  • TestOnDemandReproduceSessionCacheAccessRightsConcurrentMapCrash: skipped by default.

Opt-in reproducer:

TYK_RUN_SESSION_CACHE_RACE_REPRO=1 GOCACHE=/private/tmp/tyk-go-cache go test ./gateway -run '^TestOnDemandReproduceSessionCacheAccessRightsConcurrentMapCrash$' -count=1 -v

Result:

  • Parent test passed.
  • Child process reproduced the original Go runtime fatal.

Pre-push hook:

  • Passed lint.
  • Passed codegen checks.
  • Passed build.
  • Passed gateway test compile.

Store cloned session states in the local session cache so cached entries do not share AccessRights maps with request-local sessions that policy application mutates.

Add a regression test proving cached sessions remain isolated, plus an opt-in crash reproducer that can be run with TYK_RUN_SESSION_CACHE_RACE_REPRO=1 to demonstrate the old concurrent map failure.
@probelabs

probelabs Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

This PR addresses a critical race condition causing gateway panics (fatal error: concurrent map iteration and map write) under high authentication loads. The root cause was that the local session cache stored a shallow copy of user.SessionState, leading to its AccessRights map being shared between the cache and the active request. This allowed policies to mutate the map while other concurrent requests were reading it, triggering the fatal error.

The fix is to store a deep copy of the session object in the cache by using session.Clone(). This ensures the cached session is completely isolated from the request-local session, preventing concurrent access conflicts. The change is validated with new regression tests that confirm this isolation and a new, on-demand test that reliably reproduces the original crash to prevent regressions.

Files Changed Analysis

The changes are focused and well-tested:

  • gateway/middleware.go: The core fix is applied in two locations, changing t.Gw.SessionCache.Set(cacheKey, session, ...) to t.Gw.SessionCache.Set(cacheKey, session.Clone(), ...). This ensures a deep copy is cached, resolving the race condition.
  • gateway/middleware_test.go: Adds two new tests (TestCheckSessionAndIdentityForValidKey_CachesSessionWithoutSharingAccessRights and TestCheckSessionAndIdentityForValidKey_AuthStorePath_CachesClonedSession) to verify that mutations to a request-local session do not affect the cached version.
  • gateway/session_cache_race_repro_test.go: This new file contains a dedicated, opt-in test that reproduces the original race condition, providing robust validation that the fix is effective.

Architecture & Impact Assessment

  • What this PR accomplishes: It eliminates a significant stability issue by fixing a race condition, making the gateway more resilient under high concurrent authentication traffic.
  • Key technical changes: The core change is enforcing data isolation between the session cache and active request processing by switching from caching a shallow copy to a deep copy of the SessionState.
  • Affected system components: The change primarily affects the Gateway's Authentication Middleware and its Local Session Cache. The impact is a critical improvement in the reliability of the session management system.

Data Flow: Before vs. After

The diagrams below illustrate how the fix isolates the cached data from live request data.

Before (Shared Map Reference):

graph TD
    A[Request] --> B{CheckSessionAndIdentityForValidKey};
    B --> C[Load SessionState];
    C --|Shallow Copy|--> D[Request-Local Session];
    C --|Shallow Copy|--> E[Local Session Cache];
    D --|Mutates|--> F(Shared AccessRights Map);
    E --|Another request reads or clones|--> F;
Loading

After (Isolated Map via Clone):

graph TD
    A[Request] --> B{CheckSessionAndIdentityForValidKey};
    B --> C[Load SessionState];
    C --> C_Clone(Clone SessionState);
    C -- Original object --> D[Request-Local Session];
    C_Clone -- Cloned object --> E[Local Session Cache];
    D -- Mutates --> F(Original AccessRights Map);
    E -- Another request reads --> G(Isolated Cached AccessRights Map);
Loading

Scope Discovery & Context Expansion

  • The modification is within CheckSessionAndIdentityForValidKey, a critical function in the request authentication flow. While the code change is minimal, its effect is substantial for any API deployment that relies on key-based authentication with the local session cache enabled.
  • The fix's correctness depends on user.SessionState.Clone() performing a proper deep copy of reference-type fields, especially the AccessRights map. The PR description confirms this is the case, ensuring the required data isolation.
  • This change is specific to the in-memory SessionCache. It does not alter the behavior of the global session manager (e.g., Redis), which already ensures data isolation through its serialization/deserialization process.
  • The inclusion of a dedicated crash reproducer is a best practice for complex concurrency bugs, offering a reliable method to verify the fix and guard against future regressions.

References

  • gateway/middleware.go:665
  • gateway/middleware.go:697
  • gateway/middleware_test.go
  • gateway/session_cache_race_repro_test.go
Metadata
  • Review Effort: 3 / 5
  • Primary Label: bug

Powered by Visor from Probelabs

Last updated: 2026-06-17T12:11:50.846Z | Triggered by: pr_updated | Commit: 9726025

💡 TIP: You can chat with Visor using /visor ask <your question>

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

🎯 Recommended Merge Targets

Based on JIRA ticket TT-5486: panic: fatal error: concurrent map iteration and map write

Fix Version: Tyk 5.8.15

Required:

  • release-5.8 - Minor version branch for 5.8.x patches - required for creating Tyk 5.8.15
  • master - Main development branch - ensures fix is in all future releases

Fix Version: Tyk 5.13.1

Required:

  • release-5.13 - Minor version branch for 5.13.x patches - required for creating Tyk 5.13.1
  • master - Main development branch - ensures fix is in all future releases

Fix Version: Tyk 5.14.0

⚠️ Warning: Expected release branches not found in repository

Required:

  • master - No matching release branches found. Fix will be included in future releases.

📋 Workflow

  1. Merge this PR to master first

  2. Cherry-pick to release branches by commenting on the merged PR:

    • /release to release-5.8
    • /release to release-5.13
  3. Automated backport - The bot will automatically create backport PRs to the specified release branches

@probelabs

probelabs Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

✅ Security Check Passed

No security issues found – changes LGTM.

✅ Architecture Check Passed

No architecture issues found – changes LGTM.

Performance Issues (1)

Severity Location Issue
🟡 Warning gateway/middleware.go:665
The `session.Clone()` operation is performed synchronously on the request path. While this change is crucial for fixing a race condition, the deep copy performed by `Clone()` can introduce latency, especially for sessions with a large number of access rights. The cost of this operation is proportional to the size of the maps within the `SessionState` object. This could become a performance bottleneck under high load for tokens with many policies.
💡 SuggestionTo avoid adding latency to the request path, consider executing the caching operation in a goroutine, similar to the pattern used for the auth store path on line 697 (`go t.Gw.SessionCache.Set(...)`). This would make the performance behavior consistent across both session retrieval paths by offloading the cloning and caching work from the main request-response cycle.

✅ Security Check Passed

No security issues found – changes LGTM.

\n\n

✅ Architecture Check Passed

No architecture issues found – changes LGTM.

\n\n \n\n

Quality Issues (1)

Severity Location Issue
🟡 Warning gateway/middleware_test.go:150
The test uses `time.Sleep` to wait for a background goroutine to cache a session. This can cause test flakiness, as the fixed duration does not guarantee the goroutine has completed, especially on slow or heavily loaded systems.
💡 SuggestionReplace `time.Sleep` with a more reliable synchronization method. A polling mechanism, such as `require.Eventually` from the testify suite, would be a robust alternative. It would repeatedly check for the cached item until it's found or a timeout is exceeded, making the test more resilient to timing variations.

Powered by Visor from Probelabs

Last updated: 2026-06-17T12:10:21.005Z | Triggered by: pr_updated | Commit: 9726025

💡 TIP: You can chat with Visor using /visor ask <your question>

@andrei-tyk
andrei-tyk marked this pull request as ready for review June 15, 2026 20:50
@imogenkraak
imogenkraak force-pushed the TT-5486-fix-auth-load-v2 branch from bec76d9 to 5ac2519 Compare June 17, 2026 12:01
@sentinelone-cnapp-eu1

Copy link
Copy Markdown

SentinelOne CNS Hardcoded Secret Detector
✅ Congratulations, your code is safe

SentinelOne CNS is a cloud-agnostic, agentless CSPM & CWPP solution that continuously detects and prevents vulnerabilities that have the highest probability of being exploited in Azure, AWS, Google Cloud, and Kubernetes.

@github-actions

Copy link
Copy Markdown
Contributor

🚨 Jira Linter Failed

Commit: 9726025
Failed at: 2026-06-17 12:09:28 UTC

The Jira linter failed to validate your PR. Please check the error details below:

🔍 Click to view error details
failed to get Jira issue: failed to fetch Jira issue TT-5486: Issue does not exist or you do not have permission to see it.: request failed. Please analyze the request body for more details. Status code: 404

Next Steps

  • Ensure your branch name contains a valid Jira ticket ID (e.g., ABC-123)
  • Verify your PR title matches the branch's Jira ticket ID
  • Check that the Jira ticket exists and is accessible

This comment will be automatically deleted once the linter passes.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
50.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@buger
buger merged commit a3adb93 into master Jun 17, 2026
53 of 56 checks passed
@buger
buger deleted the TT-5486-fix-auth-load-v2 branch June 17, 2026 13:25
@imogenkraak

Copy link
Copy Markdown
Contributor

/release to release-5.8

@imogenkraak

Copy link
Copy Markdown
Contributor

/release to release-5.13

@probelabs

probelabs Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

✅ Cherry-pick successful. A PR was created: #8329

@probelabs

probelabs Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

✅ Cherry-pick successful. A PR was created: #8330

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.

3 participants