diff --git a/util/deephash/deephash.go b/util/deephash/deephash.go index 955f83e55..d32aa0984 100644 --- a/util/deephash/deephash.go +++ b/util/deephash/deephash.go @@ -98,10 +98,14 @@ func (s Sum) String() string { } var ( - once sync.Once - seed uint64 + seedOnce sync.Once + seed uint64 ) +func initSeed() { + seed = uint64(time.Now().UnixNano()) +} + func (h *hasher) sum() (s Sum) { h.bw.Flush() // Sum into scratch & copy out, as hash.Hash is an interface @@ -121,9 +125,7 @@ func Hash(v any) (s Sum) { h := hasherPool.Get().(*hasher) defer hasherPool.Put(h) h.reset() - once.Do(func() { - seed = uint64(time.Now().UnixNano()) - }) + seedOnce.Do(initSeed) h.hashUint64(seed) h.hashValue(reflect.ValueOf(v)) return h.sum() @@ -298,9 +300,9 @@ func (h *hasher) hashValue(v reflect.Value) { } type mapHasher struct { - h hasher - val valueCache // re-usable values for map iteration - iter reflect.MapIter // re-usable map iterator + h hasher + valKey, valElem valueCache // re-usable values for map iteration + iter reflect.MapIter // re-usable map iterator } var mapHasherPool = &sync.Pool{ @@ -334,8 +336,12 @@ func (h *hasher) hashMap(v reflect.Value) { defer iter.Reset(reflect.Value{}) // avoid pinning v from mh.iter when we return var sum Sum - k := mh.val.get(v.Type().Key()) - e := mh.val.get(v.Type().Elem()) + if v.IsNil() { + sum.sum[0] = 1 // something non-zero + } + + k := mh.valKey.get(v.Type().Key()) + e := mh.valElem.get(v.Type().Elem()) mh.h.visitStack = h.visitStack // always use the parent's visit stack to avoid cycles for iter.Next() { k.SetIterKey(iter) diff --git a/util/deephash/deephash_test.go b/util/deephash/deephash_test.go index 3650beade..56d7e1c41 100644 --- a/util/deephash/deephash_test.go +++ b/util/deephash/deephash_test.go @@ -11,9 +11,11 @@ "crypto/sha256" "fmt" "math" + "math/rand" "reflect" "runtime" "testing" + "testing/quick" "go4.org/mem" "inet.af/netaddr" @@ -141,6 +143,40 @@ func TestIssue4868(t *testing.T) { } } +func TestIssue4871(t *testing.T) { + m1 := map[string]string{"": "", "x": "foo"} + m2 := map[string]string{} + if h1, h2 := Hash(m1), Hash(m2); h1 == h2 { + t.Errorf("bogus: h1=%x, h2=%x", h1, h2) + } +} + +func TestNilVsEmptymap(t *testing.T) { + m1 := map[string]string(nil) + m2 := map[string]string{} + if h1, h2 := Hash(m1), Hash(m2); h1 == h2 { + t.Errorf("bogus: h1=%x, h2=%x", h1, h2) + } +} + +func TestMapFraming(t *testing.T) { + m1 := map[string]string{"foo": "", "fo": "o"} + m2 := map[string]string{} + if h1, h2 := Hash(m1), Hash(m2); h1 == h2 { + t.Errorf("bogus: h1=%x, h2=%x", h1, h2) + } +} + +func TestQuick(t *testing.T) { + initSeed() + err := quick.Check(func(v, w map[string]string) bool { + return (Hash(v) == Hash(w)) == reflect.DeepEqual(v, w) + }, &quick.Config{MaxCount: 1000, Rand: rand.New(rand.NewSource(int64(seed)))}) + if err != nil { + t.Fatalf("seed=%v, err=%v", seed, err) + } +} + func getVal() []any { return []any{ &wgcfg.Config{