New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
hash/maphash, runtime: Fallback versions of the hash function are likely vulnerable to hash-flooding DoS #66841
Comments
CC @golang/runtime @randall77 |
There are a bunch of discussions in #9365 about this. TL;DR we're not really providing a hash-flooding DoS guarantee, but we should probably mitigate easy seed-independent collisions (like this one is). I also said:
Which I haven't done. Maybe we should do that. |
Maybe I did do that? And then it was undone by https://go-review.googlesource.com/c/go/+/279612 |
I did, it was https://go-review.googlesource.com/c/go/+/2344 |
Change https://go.dev/cl/579115 mentions this issue: |
Good finding! |
@randall77 Thank you for the quick CL, but I'm wondering if this is a proper fix to this issue (I'm assuming it's OK to ping, do tell me otherwise). package main
import (
"encoding/binary"
"math/rand"
"testing"
)
const (
width = 1 << 10
)
var (
sink bool
)
func BenchmarkMap(b *testing.B) {
tab := make(map[string]bool, width)
var keys [width]string
for n := range width {
var buf [48 + 16]byte
// Must use this random 8 bytes on both buf[24:] and buf[40:].
random := rand.Uint64()
binary.LittleEndian.PutUint64(buf[16:], 0x8ebc6af09c88c6e3) // constant m3.
binary.LittleEndian.PutUint64(buf[24:], random)
binary.LittleEndian.PutUint64(buf[32:], 0x589965cc75374cc3) // constant m4.
binary.LittleEndian.PutUint64(buf[40:], random)
key := string(buf[:])
keys[n] = key
tab[key] = true
}
b.ResetTimer()
b.ReportAllocs()
for n := range b.N {
sink = sink != tab[keys[n%width]]
}
} (Note: I didn't realize there was a seed1 := seed
seed2 := seed
for ; l > 48; l -= 48 {
seed = mix(r8(p)^m2, r8(add(p, 8))^seed)
seed1 = mix(r8(add(p, 16))^m3, r8(add(p, 24))^seed1)
seed2 = mix(r8(add(p, 32))^m4, r8(add(p, 40))^seed2)
p = add(p, 48)
}
seed ^= seed1 ^ seed2 Since it's doing |
FYI it is based on uint64_t see1=seed, see2=seed;
do{
seed=_wymix(_wyr8(p)^secret[1],_wyr8(p+8)^seed);
see1=_wymix(_wyr8(p+16)^secret[2],_wyr8(p+24)^see1);
see2=_wymix(_wyr8(p+32)^secret[3],_wyr8(p+40)^see2);
p+=48; i-=48;
}while(_likely_(i>=48));
seed^=see1^see2; Maybe we should replace hard-wired secret(primes) XORed process-wide seed? seed1 := seed
seed2 := seed
for ; l > 48; l -= 48 {
seed = mix(r8(p)^m2^hashkey[1], r8(add(p, 8))^seed)
seed1 = mix(r8(add(p, 16))^m3^hashkey[2], r8(add(p, 24))^seed1)
seed2 = mix(r8(add(p, 32))^m4^hashkey[3], r8(add(p, 40))^seed2)
p = add(p, 48)
}
seed ^= seed1 ^ seed2 |
Yeah, the long-key case can still get key-independent collisions. |
Go version
go version go1.22.2 linux/amd64
Output of
go env
in your module/workspace:What did you do?
Consider these examples:
What did you see happen?
If you run the first program with
go run -tags purego .
, you will see all the values hashed into 0.If you run the second program like
go test -bench . -count 10
, it will report ~2000 ns/op, while it will report ~18 ns/op on other cases.What did you expect to see?
While machines without AES or
hash/maphash
with purego tag are not so common, I think it's important to prevent issues like this.The cause is that it relies on simple multiplications, which means we can use the XORed constant as inputs to cancel the effect, essentially making one of the operand 0, which in turn makes the whole product 0. I don't really have good fixes to make without losing much speed.
The text was updated successfully, but these errors were encountered: