Skip to content

hash/maphash, runtime: Fallback versions of the hash function are likely vulnerable to hash-flooding DoS #66841

Closed
@sugawarayuuta

Description

@sugawarayuuta

Go version

go version go1.22.2 linux/amd64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/home/ys/.cache/go-build'
GOENV='/home/ys/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/ys/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/ys/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/snap/go/10585'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/snap/go/10585/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.22.2'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/home/ys/dos/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2137305734=/tmp/go-build -gno-record-gcc-switches'

What did you do?

Consider these examples:

package main

import (
	"encoding/binary"
	"fmt"
	"hash/maphash"
	"math/rand"
)

func main() {
	// Note: only works on -tags purego.
	seed := maphash.MakeSeed()
	for n := 0; n < 10; n++ {
		var buf [16]byte
		binary.LittleEndian.PutUint32(buf[:], 0xe7037ed1)      // Upper bits of constant m2.
		binary.LittleEndian.PutUint32(buf[4:], rand.Uint32())  // Arbitrary bits.
		binary.LittleEndian.PutUint32(buf[8:], 0xa0b428db)     // Lower bits of constant m2.
		binary.LittleEndian.PutUint32(buf[12:], rand.Uint32()) // Arbitrary bits.
		fmt.Printf("%x\n", buf[:])
		fmt.Printf("%016x\n", maphash.Bytes(seed, buf[:]))
	}
}
package main

import (
	"encoding/binary"
	"math/rand"
	"testing"

	_ "unsafe" // Only for the linkname.
)

const (
	width = 1 << 10
)

var (
	sink bool
)

var (
	//go:linkname useAeshash runtime.useAeshash
	useAeshash bool
)

func BenchmarkMap(b *testing.B) {
	useAeshash = false
	tab := make(map[string]bool, width)
	var keys [width]string
	for n := range width {
		var buf [16]byte
		binary.LittleEndian.PutUint64(buf[:], 0xe7037ed1a0b428db)
		binary.LittleEndian.PutUint64(buf[8:], rand.Uint64())
		key := string(buf[:])
		keys[n] = key
		tab[key] = true
	}
	b.ResetTimer()
	b.ReportAllocs()
	for n := range b.N {
		sink = sink != tab[keys[n%width]]
	}
}

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    FrozenDueToAgeNeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.compiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions