Skip to content

Commit 33d13b6

Browse files
Taranum Wasucursoragent
authored andcommitted
fix sync: avoid spurious mhash pulls and encode race
Stop blind pulls to unpublished 32=sv prefixes on inline FULL mhash mismatch (spec §5.6: merge inline first). Clone SvMap before encoding outside the mutex to avoid concurrent map mutation panics. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8540360 commit 33d13b6

4 files changed

Lines changed: 42 additions & 9 deletions

File tree

std/sync/svs.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,8 +493,11 @@ func (s *SvSync) sendSyncInterestWith(dataWire enc.Wire) {
493493
func (s *SvSync) encodeSyncData(reason syncSendReason, sender enc.Name) enc.Wire {
494494
s.mutex.Lock()
495495
s.enterSteadyState()
496-
stateSnap := s.state
497-
mtimeSnap := s.mtime
496+
stateSnap := cloneSvMap(s.state)
497+
mtimeSnap := make(map[string]time.Time, len(s.mtime))
498+
for k, v := range s.mtime {
499+
mtimeSnap[k] = v
500+
}
498501
repair, propagation := s.partialTargets()
499502
s.mutex.Unlock()
500503

std/sync/svs_map.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ func NewSvMap[V any](size int) SvMap[V] {
2929
return make(SvMap[V], size)
3030
}
3131

32+
// cloneSvMap returns a shallow copy safe for use without holding SvSync.mutex.
33+
func cloneSvMap[V any](m SvMap[V]) SvMap[V] {
34+
out := NewSvMap[V](len(m))
35+
for hash, vals := range m {
36+
out[hash] = slices.Clone(vals)
37+
}
38+
return out
39+
}
40+
3241
// Get seq entry for a bootstrap time.
3342
func (m SvMap[V]) Get(hash string, boot uint64) (value V) {
3443
entry := SvMapVal[V]{boot, value}

std/sync/svs_pull.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,11 @@ func (s *SvSync) handleMhashMismatch(args svSyncRecvSvArgs, recvSv SvMap[uint64]
105105
return
106106
}
107107

108-
ref := args.svsDataRef
109-
if len(ref) == 0 {
110-
ref = trustPrefix
111-
}
112-
if len(ref) > 0 {
113-
log.Debug(s, "sync recovery path", "action", "pull", "ref", ref)
114-
go s.pullFullVector(ref, trustPrefix)
108+
// Inline FULL is already merged in onReceiveStateVector (spec §5.6 step 2).
109+
// Pull only when the sender provided a retrievable SvsDataRef (announce-only sync).
110+
if len(args.svsDataRef) == 0 {
111+
return
115112
}
113+
log.Debug(s, "sync recovery path", "action", "pull", "ref", args.svsDataRef)
114+
go s.pullFullVector(args.svsDataRef, trustPrefix)
116115
}

std/sync/svs_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,28 @@ func TestSvSyncJoinLargeGroup(t *testing.T) {
159159
require.EqualValues(t, 3, mesh.node("carol").svs.GetSeqNo(mesh.node("alice").producer))
160160
}
161161

162+
func TestHandleMhashMismatchNoBlindPull(t *testing.T) {
163+
tu.SetT(t)
164+
165+
mesh := newTestMesh(t, []string{"alice", "bob"}, nil)
166+
defer mesh.stop()
167+
168+
alice := mesh.node("alice").svs
169+
bobOnly := NewSvMap[uint64](0)
170+
bobOnly.Set(mesh.node("bob").producer.TlvStr(), testMeshBoot, 1)
171+
partialSv := bobOnly.Encode(func(s uint64) uint64 { return s })
172+
173+
// Bob's inline FULL with mismatched mhash; alice is not a strict superset.
174+
alice.onReceiveStateVector(svSyncRecvSvArgs{
175+
sv: partialSv,
176+
vectorType: optional.Some(spec_svs.VectorTypeFull),
177+
mhash: ComputeMhash(bobOnly),
178+
})
179+
180+
// Should not hang or panic from blind pull to unpublished 32=sv prefix.
181+
mesh.wait(100 * time.Millisecond)
182+
}
183+
162184
func TestSvSyncMhashMismatchSenderAnnounce(t *testing.T) {
163185
tu.SetT(t)
164186

0 commit comments

Comments
 (0)