App version: node (internal server component)
Platform: all
In internal/server/phone.go, the cutoff() method computes the cutoff timestamp like this when there is no active lease:
return uint64(p.lastFlushAt.Load() + int64(freeQuotaSeconds) - info.seconds)
freeQuotaSeconds is 900 (15 minutes). info.seconds is int64 and holds the cumulative seconds of usage fetched from the Hub. When info.seconds exceeds 900, the subtraction yields a negative int64. Casting a negative int64 to uint64 wraps to a very large value (close to math.MaxUint64). The result is that cutoff() returns a timestamp approximately 292 billion years in the future, so blocked() returns false and the free-quota enforcement never triggers.
Steps to reproduce:
- Let a free client accumulate more than 900 seconds of usage.
- The Hub sets info.seconds to a value greater than 900.
- On the next heartbeat reply, cutoff() computes a wrapped uint64.
- The client receives a Pong with Cutoff far in the future and continues sending traffic indefinitely.
Expected: cutoff() should clamp the result to the current time (or a past timestamp) when info.seconds >= freeQuotaSeconds, not wrap to a future value.
Minimal fix: guard against underflow before the cast.
remaining := int64(freeQuotaSeconds) - info.seconds
if remaining < 0 {
remaining = 0
}
return uint64(p.lastFlushAt.Load() + remaining)
App version: node (internal server component)
Platform: all
In internal/server/phone.go, the cutoff() method computes the cutoff timestamp like this when there is no active lease:
freeQuotaSeconds is 900 (15 minutes). info.seconds is int64 and holds the cumulative seconds of usage fetched from the Hub. When info.seconds exceeds 900, the subtraction yields a negative int64. Casting a negative int64 to uint64 wraps to a very large value (close to math.MaxUint64). The result is that cutoff() returns a timestamp approximately 292 billion years in the future, so blocked() returns false and the free-quota enforcement never triggers.
Steps to reproduce:
Expected: cutoff() should clamp the result to the current time (or a past timestamp) when info.seconds >= freeQuotaSeconds, not wrap to a future value.
Minimal fix: guard against underflow before the cast.