From cf5bed822721f54f802b637739801f0d189271b4 Mon Sep 17 00:00:00 2001 From: Faye Amacker <33205765+fxamacker@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:05:27 -0500 Subject: [PATCH] sync: fix deadlock in Map.Range callback Currently, calling any sync.Map method from inside the sync.Map.Range callback f deadlocks. Moreover, Go's sync.Map explicitly permits the Range callback to call other methods on the map ("Range does not block other methods on the receiver; even f itself may call any method on m"). This commit prevents the deadlock by changing sync.Map.Range to: - copy the map's keys under the lock - release the lock - iterate over the key snapshot A snapshot satisfies Go's sync.Map.Range contract, which only requires that no key is visited more than once and may reflect any mapping from any point during the call. Using a snapshot keeps the implementation simple, in line with this file's stated scope ("no more efficient than a map with a lock"). Also added TestMapRangeAndDelete regression test, which deletes map entries from inside the map's Range callback. --- src/sync/map.go | 28 ++++++++++++++++++++-------- src/sync/map_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/sync/map.go b/src/sync/map.go index b26f2dc3b..e5d6be794 100644 --- a/src/sync/map.go +++ b/src/sync/map.go @@ -1,6 +1,8 @@ package sync -import "internal/task" +import ( + "internal/task" +) // This file implements just enough of sync.Map to get packages to compile. It // is no more efficient than a map with a lock. @@ -56,17 +58,27 @@ func (m *Map) Store(key, value interface{}) { m.m[key] = value } +// Range calls f for each key and value in the map. If f returns false, the iteration stops. func (m *Map) Range(f func(key, value interface{}) bool) { + // Iterate over a key snapshot instead of holding the lock across the callback, + // to prevent deadlock when a Map method is called inside f. + // + // Using a key snapshot in Map.Range is sufficient because Go specifies that: + // - Range only requires that no key is visited more than once, and + // - Range may reflect any mapping from any point during the Range call. + m.lock.Lock() - defer m.lock.Unlock() - - if m.m == nil { - return + keys := make([]interface{}, 0, len(m.m)) + for k := range m.m { + keys = append(keys, k) } + m.lock.Unlock() - for k, v := range m.m { - if !f(k, v) { - break + for _, k := range keys { + if v, ok := m.Load(k); ok { + if !f(k, v) { + break + } } } } diff --git a/src/sync/map_test.go b/src/sync/map_test.go index a41faa40f..5ae88789f 100644 --- a/src/sync/map_test.go +++ b/src/sync/map_test.go @@ -36,3 +36,31 @@ func TestMapSwap(t *testing.T) { t.Errorf("Load after Swap returned %v, %v, want foo, true", v, ok) } } + +func TestMapRangeAndDelete(t *testing.T) { + var sm sync.Map + sm.Store(0, "0") + sm.Store(1, "1") + sm.Store(2, "2") + + sm.Range(func(k, v any) bool { + keyAsInt, ok := k.(int) + if !ok { + return true + } + if keyAsInt%2 == 0 { + sm.Delete(keyAsInt) + } + return true + }) + + if v, ok := sm.Load(0); ok { + t.Errorf("Load(0) after Delete returned %v, %v, want nil, false", v, ok) + } + if v, ok := sm.Load(1); !ok || v.(string) != "1" { + t.Errorf("Load(1) after Delete returned %v, %v, want \"1\", true", v, ok) + } + if v, ok := sm.Load(2); ok { + t.Errorf("Load(2) after Delete returned %v, %v, want nil, false", v, ok) + } +}