Skip to content

cutoff() wraps to a future timestamp when info.seconds exceeds freeQuotaSeconds, bypassing the free-quota cutoff #94

Description

@bibonix

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:

  1. Let a free client accumulate more than 900 seconds of usage.
  2. The Hub sets info.seconds to a value greater than 900.
  3. On the next heartbeat reply, cutoff() computes a wrapped uint64.
  4. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions