app: the sender map's rebuild is serialised (audit W3)
Security audit 2026-10-17, finding W3 (low), stage 1. Resync read the bindings and wrote the map with no lock (the selector table has tableMu); two application changes at once could write in the order opposite to their reads and leave the map stale - an application whose binding was missing refused with reject_sender_login_mismatch until the next change. Now a mutex on the Service holds the read, the write and the reload together. Test: TestResyncRunsOneAtATime (eight at once, never two inside the rebuild). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,10 @@ package app
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mixeme/selfpost/internal/postfix"
|
||||
"github.com/mixeme/selfpost/internal/store"
|
||||
@@ -505,3 +508,44 @@ func TestSaveSettingsIsAllOrNothing(t *testing.T) {
|
||||
t.Errorf("unknown application = %v, want ErrApplicationNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
// overlapMaps counts rebuilds that run at once.
|
||||
type overlapMaps struct {
|
||||
inside, most atomic.Int32
|
||||
}
|
||||
|
||||
func (o *overlapMaps) RebuildSenderLoginMaps([]postfix.Binding) error {
|
||||
n := o.inside.Add(1)
|
||||
for {
|
||||
m := o.most.Load()
|
||||
if n <= m || o.most.CompareAndSwap(m, n) {
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
o.inside.Add(-1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resync's read of the bindings and its write of the map are one step: two
|
||||
// at once would write in the order opposite to their reads and leave the
|
||||
// older set in the map (security audit 2026-10-17, W3).
|
||||
func TestResyncRunsOneAtATime(t *testing.T) {
|
||||
svc, _, _, _ := newServiceHarness(t)
|
||||
maps := &overlapMaps{}
|
||||
svc.pf = maps
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := svc.Resync(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
if got := maps.most.Load(); got != 1 {
|
||||
t.Errorf("%d rebuilds ran at once, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user