Skip to content

x/time/rate: Limiter depends on implementation-defined large float to int conversion #71154

@tomcoupland

Description

@tomcoupland

Go version

go version go1.22.1 darwin/arm64

Output of go env in your module/workspace:

GO111MODULE='auto'
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/tomcoupland/Library/Caches/go-build'
GOENV='/Users/tomcoupland/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/tomcoupland/pkg/mod'
GONOPROXY='github.com/monzo'
GONOSUMDB='github.com/monzo'
GOOS='darwin'
GOPATH='/Users/tomcoupland'
GOPRIVATE='github.com/monzo'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/tomcoupland/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/tomcoupland/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.23.1'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/tomcoupland/Library/Application Support/go/telemetry'
GCCGO='gccgo'
GOARM64='v8.0'
AR='ar'
CC='clang'
CXX='clang++'
CGO_ENABLED='1'
GOMOD='/Users/tomcoupland/src/github.com/monzo/wearedev/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 -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/ll/3h483n6j4jbd64x_tjbgwzd40000gn/T/go-build4023913863=/tmp/go-build -gno-record-gcc-switches -fno-common'

What did you do?

fmt.Printf("GOARCH: %s\n", runtime.GOARCH)
limiter := rate.NewLimiter(1e-10, 1)

if limiter.Allow() {
  fmt.Println("allow")
} else {
  fmt.Println("not allow")
}
if limiter.Allow() {
  fmt.Println("allow")
} else {
  fmt.Println("not allow")
}

Playground

There is a conversion from float64 to int64 in durationFromTokens, when the final time.Duration is created:

func (limit Limit) durationFromTokens(tokens float64) time.Duration {
	if limit <= 0 {
		return InfDuration
	}
	seconds := tokens / float64(limit)
	return time.Duration(float64(time.Second) * seconds)
}

When limiters are configured with small values (1e-10) this overflows and allows request through that should be blocked. amd64 does not saturate int64's when converting, where as arm64 does. Therefore this code is safe on arm64, but not amd64.

Results of running:

GOARCH = "amd64", see "allow", "allow"
GOARCH = 'arm64', see "allow", "not allow"

This leads to the second "allow" on amd64. I'm preparing a PR with some overflow protection.

What did you see happen?

When GOARCH = "amd64", see "allow", "allow"
Whe GOARCH = 'arm64', see "allow", "not allow"

What did you expect to see?

Regardless of GOARCH: "allow", "not allow"

The first attempt should succeed, due to the burst of 1, but the second should be denied.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.FrozenDueToAgeNeedsFixThe path to resolution is known, but the work has not been done.help wanted

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions