Skip to content

x/time: Reserve on zero rate limiter allows infinite reservations (because of divide by zero) #47221

@mpl

Description

@mpl

What version of Go are you using (go version)?

$ go version
go version go1.16 darwin/amd64

Does this issue reproduce with the latest release?

Yes (with go version go1.16.6 darwin/amd64)

What operating system and processor architecture are you using (go env)?

go env Output
$ go env

GOARCH="amd64"
GOOS="darwin"

What did you do?

Consider these two, that only differ in their argument to NewLimiter:

func TestReserveAtZero(t *testing.T) {
	atZero := NewLimiter(0, 1)
	for i := 0; i < 1000; i++ {
		res := atZero.Reserve()
		if !res.OK() {
			println(fmt.Sprintf("Reservation %d denied, as expected", i))
			return
		}
		if delay := res.Delay(); delay != 0 {
			println(fmt.Sprintf("Reservation %d delay not zero (%v), as expected", i, delay))
			return
		}
	}
	t.Fatal("zero rate limiter allows unlimited reservations")
}
func TestReserveAtAlmostZero(t *testing.T) {
	almostAtZero := NewLimiter(0.00001, 1)
	for i := 0; i < 1000; i++ {
		res := almostAtZero.Reserve()
		if !res.OK() {
			println(fmt.Sprintf("Reservation denied, as expected, at %d", i))
			return
		}
		if delay := res.Delay(); delay != 0 {
			println(fmt.Sprintf("Reservation %d delay not zero (%v), as expected", i, delay))
			return
		}
	}
	t.Fatal("almost zero rate limiter allows unlimited reservations")
}
% go test -run TestReserveAtAlmostZero
Reservation 1 delay not zero (27h46m39.999999087s), as expected
PASS
ok  	golang.org/x/time/rate	0.128s
% go test -run TestReserveAtZero
--- FAIL: TestReserveAtZero (0.00s)
    rate_test.go:34: zero rate limiter allows unlimited reservations
FAIL
exit status 1
FAIL	golang.org/x/time/rate	0.262s

The behaviour at zero happens afaict because in:

// durationFromTokens is a unit conversion function from the number of tokens to the duration
// of time it takes to accumulate them at a rate of limit tokens per second.
func (limit Limit) durationFromTokens(tokens float64) time.Duration {
	seconds := tokens / float64(limit)
	return time.Nanosecond * time.Duration(1e9*seconds)
}

there is no safeguard for a division by a limit at zero, which means seconds is then Inf. Then it gets "worse" because the final operation is an overflow, so the returned number is a huge negative (-9223372036854775808).

Then in reserveN, we have:

	if tokens < 0 {
		waitDuration = lim.limit.durationFromTokens(-tokens)
	}

	// Decide result
	ok := n <= lim.burst && waitDuration <= maxFutureReserve

which results in the reservation being ok, because of the huge negative.

And in addition, in DelayFrom, we have:

	delay := r.timeToAct.Sub(now)
	if delay < 0 {
		return 0
	}

which explains why we always get a zero delay instead of an increasingly large delay when we pile on the reservations on the Limiter.

So in summary, I think we have

  1. a problem with the behaviour of Reserve, which imho should always return non ok reservation if it is on a Limiter that was initialized with a zero Limit
  2. a bug that in the implementation of durationFromTokens (or before in the flow), which allows a division by zero, which at least partially explains the above problem with Reserve (and Delay).

What did you expect to see?

I expected Reserve to return a non-ok reservation on a Limiter initialized with a zero Limit. And if not, I would at least expect the resulting reservation to not have a delay of zero.

What did you see instead?

Reserve returns an ok reservation, with a zero delay.

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.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions