carlos / platform Public

metrics: fix CPUFrac uint64 wrap when Busy regresses under advancing Total

CPUFrac only guarded cur.Total <= prev.Total, not cur.Busy < prev.Busy.
With Total still advancing but Busy alone going backwards, the unsigned
subtraction cur.Busy-prev.Busy wrapped into a huge garbage fraction
instead of nil, contradicting the package's own "fail toward nil"
contract. Adds the missing guard and a regression test pinning the
exact values (Busy:500,Total:1000 -> Busy:100,Total:1100) that produced
the garbage reading.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 514f50712a6a75e5c208a8c4a8bc2fdf76e237c1 parent f288ec6
2 files changed, +17 −5
  • internal/metrics/parse.go +9 −5
  • internal/metrics/parse_test.go +8 −0
diff --git a/internal/metrics/parse.go b/internal/metrics/parse.go
index f53fcee73..0900ab587 100644
--- a/internal/metrics/parse.go
+++ b/internal/metrics/parse.go
@@ -56,12 +56,16 @@ func ParseProcStat(text string) *CPUCounters {
}
// CPUFrac turns two /proc/stat readings into a fraction of CPU busy over the
-// interval between them. Both readings must be non-nil and cur.Total must
-// exceed prev.Total — counters that went backwards mean the box rebooted or
-// the reading is otherwise invalid, and reporting a fraction from that would
-// be a guess dressed up as a measurement, so it's nil instead.
+// interval between them. Both readings must be non-nil, cur.Total must
+// exceed prev.Total, and cur.Busy must not be behind prev.Busy — counters
+// that went backwards mean the box rebooted or the reading is otherwise
+// invalid, and reporting a fraction from that would be a guess dressed up as
+// a measurement, so it's nil instead. The Busy check matters on its own:
+// Total can still advance while Busy alone regresses, and since both are
+// unsigned, cur.Busy-prev.Busy would silently wrap into a huge garbage
+// value rather than go negative.
func CPUFrac(prev, cur *CPUCounters) *float64 {
- if prev == nil || cur == nil || cur.Total <= prev.Total {
+ if prev == nil || cur == nil || cur.Total <= prev.Total || cur.Busy < prev.Busy {
return nil
}
frac := float64(cur.Busy-prev.Busy) / float64(cur.Total-prev.Total)
diff --git a/internal/metrics/parse_test.go b/internal/metrics/parse_test.go
index 726caf91e..c9bcbbff1 100644
--- a/internal/metrics/parse_test.go
+++ b/internal/metrics/parse_test.go
@@ -34,6 +34,14 @@ func TestCPUFrac(t *testing.T) {
if CPUFrac(cur, prev) != nil { // counters went backwards (reboot) — no guess
t.Fatal("backwards counters must be nil")
}
+
+ // Total advances but Busy alone regresses: uint64 subtraction of
+ // cur.Busy-prev.Busy would wrap into a huge garbage value instead of
+ // going negative, so this must be caught explicitly, not just inferred
+ // from the Total check.
+ if got := CPUFrac(&CPUCounters{Busy: 500, Total: 1000}, &CPUCounters{Busy: 100, Total: 1100}); got != nil {
+ t.Fatalf("Busy regressing while Total advances must be nil, got %v", *got)
+ }
}
func TestParseLoadAvg(t *testing.T) {