Skip to content

Commit c9b8b36

Browse files
committed
fix Time-of-Check-Time-of-Use bug in rate limiter
1 parent 1d63c1c commit c9b8b36

File tree

2 files changed

+142
-1
lines changed

2 files changed

+142
-1
lines changed

middleware/rate_limiter.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,9 @@ func (store *RateLimiterMemoryStore) Allow(identifier string) (bool, error) {
249249
if now.Sub(store.lastCleanup) > store.expiresIn {
250250
store.cleanupStaleVisitors()
251251
}
252+
allowed := limiter.AllowN(now, 1)
252253
store.mutex.Unlock()
253-
return limiter.AllowN(store.timeNow(), 1), nil
254+
return allowed, nil
254255
}
255256

256257
/*

middleware/rate_limiter_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"net/http/httptest"
1111
"sync"
12+
"sync/atomic"
1213
"testing"
1314
"time"
1415

@@ -457,3 +458,142 @@ func BenchmarkRateLimiterMemoryStore_conc100_10000(b *testing.B) {
457458
var store = NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{Rate: 100, Burst: 200, ExpiresIn: testExpiresIn})
458459
benchmarkStore(store, 100, 10000, b)
459460
}
461+
462+
// TestRateLimiterMemoryStore_TOCTOUFix verifies that the TOCTOU race condition is fixed
463+
// by ensuring timeNow() is only called once per Allow() call
464+
func TestRateLimiterMemoryStore_TOCTOUFix(t *testing.T) {
465+
t.Parallel()
466+
467+
store := NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{
468+
Rate: 1,
469+
Burst: 1,
470+
ExpiresIn: 2 * time.Second,
471+
})
472+
473+
// Track time calls to verify we use the same time value
474+
timeCallCount := 0
475+
baseTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
476+
477+
store.timeNow = func() time.Time {
478+
timeCallCount++
479+
return baseTime
480+
}
481+
482+
// First request - should succeed
483+
allowed, err := store.Allow("127.0.0.1")
484+
assert.NoError(t, err)
485+
assert.True(t, allowed, "First request should be allowed")
486+
487+
// Verify timeNow() was only called once
488+
assert.Equal(t, 1, timeCallCount, "timeNow() should only be called once per Allow()")
489+
}
490+
491+
// TestRateLimiterMemoryStore_ConcurrentAccess verifies rate limiting correctness under concurrent load
492+
func TestRateLimiterMemoryStore_ConcurrentAccess(t *testing.T) {
493+
t.Parallel()
494+
495+
store := NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{
496+
Rate: 10,
497+
Burst: 5,
498+
ExpiresIn: 5 * time.Second,
499+
})
500+
501+
const goroutines = 50
502+
const requestsPerGoroutine = 20
503+
504+
var wg sync.WaitGroup
505+
var allowedCount, deniedCount int32
506+
507+
for i := 0; i < goroutines; i++ {
508+
wg.Add(1)
509+
go func() {
510+
defer wg.Done()
511+
for j := 0; j < requestsPerGoroutine; j++ {
512+
allowed, err := store.Allow("test-user")
513+
assert.NoError(t, err)
514+
if allowed {
515+
atomic.AddInt32(&allowedCount, 1)
516+
} else {
517+
atomic.AddInt32(&deniedCount, 1)
518+
}
519+
time.Sleep(time.Millisecond)
520+
}
521+
}()
522+
}
523+
524+
wg.Wait()
525+
526+
totalRequests := goroutines * requestsPerGoroutine
527+
allowed := int(allowedCount)
528+
denied := int(deniedCount)
529+
530+
assert.Equal(t, totalRequests, allowed+denied, "All requests should be processed")
531+
assert.Greater(t, denied, 0, "Some requests should be denied due to rate limiting")
532+
assert.Greater(t, allowed, 0, "Some requests should be allowed")
533+
}
534+
535+
// TestRateLimiterMemoryStore_RaceDetection verifies no data races with high concurrency
536+
// Run with: go test -race ./middleware -run TestRateLimiterMemoryStore_RaceDetection
537+
func TestRateLimiterMemoryStore_RaceDetection(t *testing.T) {
538+
t.Parallel()
539+
540+
store := NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{
541+
Rate: 100,
542+
Burst: 200,
543+
ExpiresIn: 1 * time.Second,
544+
})
545+
546+
const goroutines = 100
547+
const requestsPerGoroutine = 100
548+
549+
var wg sync.WaitGroup
550+
identifiers := []string{"user1", "user2", "user3", "user4", "user5"}
551+
552+
for i := 0; i < goroutines; i++ {
553+
wg.Add(1)
554+
go func(routineID int) {
555+
defer wg.Done()
556+
for j := 0; j < requestsPerGoroutine; j++ {
557+
identifier := identifiers[routineID%len(identifiers)]
558+
_, err := store.Allow(identifier)
559+
assert.NoError(t, err)
560+
}
561+
}(i)
562+
}
563+
564+
wg.Wait()
565+
}
566+
567+
// TestRateLimiterMemoryStore_TimeOrdering verifies time ordering consistency in rate limiting decisions
568+
func TestRateLimiterMemoryStore_TimeOrdering(t *testing.T) {
569+
t.Parallel()
570+
571+
store := NewRateLimiterMemoryStoreWithConfig(RateLimiterMemoryStoreConfig{
572+
Rate: 1,
573+
Burst: 2,
574+
ExpiresIn: 5 * time.Second,
575+
})
576+
577+
currentTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
578+
store.timeNow = func() time.Time {
579+
return currentTime
580+
}
581+
582+
// First two requests should succeed (burst=2)
583+
allowed1, _ := store.Allow("user1")
584+
assert.True(t, allowed1, "Request 1 should be allowed (burst)")
585+
586+
allowed2, _ := store.Allow("user1")
587+
assert.True(t, allowed2, "Request 2 should be allowed (burst)")
588+
589+
// Third request should be denied
590+
allowed3, _ := store.Allow("user1")
591+
assert.False(t, allowed3, "Request 3 should be denied (burst exhausted)")
592+
593+
// Advance time by 1 second
594+
currentTime = currentTime.Add(1 * time.Second)
595+
596+
// Fourth request should succeed
597+
allowed4, _ := store.Allow("user1")
598+
assert.True(t, allowed4, "Request 4 should be allowed (1 token available)")
599+
}

0 commit comments

Comments
 (0)