Skip to content

mapsutil: Data race in (*SyncLockMap[K, V]).triggerCleanupIfNeeded() causes sporadic test failures #704

@dwisiswant0

Description

@dwisiswant0

Description

A data race exists in (*SyncLockMap[K, V]).triggerCleanupIfNeeded() where the lastCleanup field is accessed without proper sync. This causes sporadic test failures when the race detector is enabled.

Root cause

In SyncLockMap[K, V], the triggerCleanupIfNeeded() method performs unsync'ed read-modify-write ops on (*SyncLockMap[K, V]).triggerCleanupIfNeeded():

// triggerCleanupIfNeeded triggers a one-shot cleanup if it hasn't run in the cleanup interval
func (s *SyncLockMap[K, V]) triggerCleanupIfNeeded() {
if s.inactivityDuration <= 0 {
return
}
// Check if cleanup is needed using instance-specific interval
now := time.Now()
if now.Sub(s.lastCleanup) < s.cleanupInterval {
return
}
// Update last cleanup time and trigger async cleanup
s.lastCleanup = now
go s.evictInactiveEntries()
}

L76: Read w/o lock
L81: Write w/o lock

This method is called from Get() after releasing the (*SyncLockMap[K, V]).mu.

Output:

WARNING: DATA RACE
Read at 0x00c00146c4b8 by goroutine 73:
  github.com/projectdiscovery/utils/maps.(*SyncLockMap[...]).triggerCleanupIfNeeded()
      /home/runner/go/pkg/mod/github.com/projectdiscovery/[email protected]/maps/synclock_map.go:76 +0x74
  github.com/projectdiscovery/utils/maps.(*SyncLockMap[...]).Get()
      /home/runner/go/pkg/mod/github.com/projectdiscovery/[email protected]/maps/synclock_map.go:184 +0x27d

Previous write at 0x00c00146c4b8 by goroutine 96:
  github.com/projectdiscovery/utils/maps.(*SyncLockMap[...]).triggerCleanupIfNeeded()
      /home/runner/go/pkg/mod/github.com/projectdiscovery/[email protected]/maps/synclock_map.go:81 +0xd8
  github.com/projectdiscovery/utils/maps.(*SyncLockMap[...]).Get()
      /home/runner/go/pkg/mod/github.com/projectdiscovery/[email protected]/maps/synclock_map.go:184 +0x27d

Ref: https://github.com/projectdiscovery/nuclei/actions/runs/19495312364/job/55796169237

Why is this race sporadic?

The race manifests infrequently because:

  1. Narrow race window: With typical cleanup intervals (12-24hrs), only the first concurrent access triggers a write. Subsequent accesses see the updated lastCleanup and return early, closing the race window within μs.

  2. Non-deterministic: The race detector only catches races that actually occur during execution. Retries may pass due to different timing/scheduling.

Impact

Sporadic CI test failures and potential UB (though typically non-critical due to narrow race window).

Additional Context

time.Time is a 24-byte struct (on 64-bit systems) and cannot be read/written atomically. According to the Go memory model, all concurrent access to shared mutable state must be sync'ed, even if the consequences seem minor.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions