util/deephash: fix map hashing when key & element have the same type

Regression from 09afb8e35b, in which the
same reflect.Value scratch value was being used as the map iterator
copy destination.

Also: make nil and empty maps hash differently, add test.

Fixes #4871

Co-authored-by: Josh Bleecher Snyder <josharian@gmail.com>
Change-Id: I67f42524bc81f694c1b7259d6682200125ea4a66
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2022-06-16 13:21:32 -07:00 committed by Brad Fitzpatrick
parent 22c544bca7
commit 757ecf7e80
2 changed files with 52 additions and 10 deletions

View File

@ -98,10 +98,14 @@ func (s Sum) String() string {
} }
var ( var (
once sync.Once seedOnce sync.Once
seed uint64 seed uint64
) )
func initSeed() {
seed = uint64(time.Now().UnixNano())
}
func (h *hasher) sum() (s Sum) { func (h *hasher) sum() (s Sum) {
h.bw.Flush() h.bw.Flush()
// Sum into scratch & copy out, as hash.Hash is an interface // 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) h := hasherPool.Get().(*hasher)
defer hasherPool.Put(h) defer hasherPool.Put(h)
h.reset() h.reset()
once.Do(func() { seedOnce.Do(initSeed)
seed = uint64(time.Now().UnixNano())
})
h.hashUint64(seed) h.hashUint64(seed)
h.hashValue(reflect.ValueOf(v), false) h.hashValue(reflect.ValueOf(v), false)
return h.sum() return h.sum()
@ -425,9 +427,9 @@ func (h *hasher) hashValueWithType(v reflect.Value, ti *typeInfo, forceCycleChec
} }
type mapHasher struct { type mapHasher struct {
h hasher h hasher
val valueCache // re-usable values for map iteration valKey, valElem valueCache // re-usable values for map iteration
iter reflect.MapIter // re-usable map iterator iter reflect.MapIter // re-usable map iterator
} }
var mapHasherPool = &sync.Pool{ var mapHasherPool = &sync.Pool{
@ -461,8 +463,12 @@ func (h *hasher) hashMap(v reflect.Value, ti *typeInfo, checkCycles bool) {
defer iter.Reset(reflect.Value{}) // avoid pinning v from mh.iter when we return defer iter.Reset(reflect.Value{}) // avoid pinning v from mh.iter when we return
var sum Sum var sum Sum
k := mh.val.get(v.Type().Key()) if v.IsNil() {
e := mh.val.get(v.Type().Elem()) 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 mh.h.visitStack = h.visitStack // always use the parent's visit stack to avoid cycles
for iter.Next() { for iter.Next() {
k.SetIterKey(iter) k.SetIterKey(iter)

View File

@ -11,9 +11,11 @@
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"math" "math"
"math/rand"
"reflect" "reflect"
"runtime" "runtime"
"testing" "testing"
"testing/quick"
"time" "time"
"unsafe" "unsafe"
@ -144,6 +146,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 { func getVal() []any {
return []any{ return []any{
&wgcfg.Config{ &wgcfg.Config{