fix(correlation): prevent race condition in session manager

Fixed potential use-after-free in Update() when gcRound deletes
a session between GetOrCreate() and acquiring the session lock.

Changes:
- Add 'deleted' flag to SessionState
- Mark sessions as deleted before removing from map in gcRound
- Check deleted flag in Update and recreate session if needed

This ensures updates to deleted sessions create a new session
instead of modifying a freed/dangling reference.

Race detector verified: go test ./internal/correlation/... -race

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-04-19 14:34:30 +02:00
parent 3353b3ae82
commit 678aa48a12
2 changed files with 19 additions and 1 deletions

View File

@ -84,10 +84,21 @@ func (m *Manager) GetOrCreate(key SessionKey) *SessionState {
// Update applique la fonction fn sur la session identifiée par key, // Update applique la fonction fn sur la session identifiée par key,
// en créant la session si elle n'existe pas encore. // en créant la session si elle n'existe pas encore.
// Thread-safe : vérifie le flag deleted pour éviter les use-after-free.
func (m *Manager) Update(key SessionKey, fn func(*SessionState)) { func (m *Manager) Update(key SessionKey, fn func(*SessionState)) {
s := m.GetOrCreate(key) s := m.GetOrCreate(key)
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
// Vérifier si la session a été marquée comme supprimée par gcRound
if s.deleted {
// La session a été supprimée, recréer une nouvelle session
s = m.GetOrCreate(key)
s.mu.Lock()
defer s.mu.Unlock()
}
fn(s) fn(s)
s.LastActivity = time.Now() s.LastActivity = time.Now()
} }
@ -128,6 +139,12 @@ func (m *Manager) gcRound(slowlorisThreshold time.Duration) {
if expired || slowloris { if expired || slowloris {
toDelete = append(toDelete, key) toDelete = append(toDelete, key)
// Marquer la session comme supprimée AVANT de l'envoyer à ReadyCh
// pour éviter les race conditions avec Update()
s.mu.Lock()
s.deleted = true
s.mu.Unlock()
// Envoyer sans bloquer (drop si le canal est plein) // Envoyer sans bloquer (drop si le canal est plein)
select { select {
case m.ReadyCh <- s: case m.ReadyCh <- s:

View File

@ -88,7 +88,8 @@ type SessionState struct {
FirstSeen time.Time // horodatage de création de la session FirstSeen time.Time // horodatage de création de la session
LastActivity time.Time // horodatage de la dernière activité LastActivity time.Time // horodatage de la dernière activité
mu sync.Mutex // protection des modifications concurrentes deleted bool // true si la session a été supprimée du map par gcRound
mu sync.Mutex // protection des modifications concurrentes
} }
// IsExpired indique si la session n'a reçu aucune activité depuis timeout. // IsExpired indique si la session n'a reçu aucune activité depuis timeout.