Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prevent overflow on counter adding #1474

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions prometheus/counter.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,22 @@ func (c *counter) Add(v float64) {
}

ival := uint64(v)
if float64(ival) == v {
atomic.AddUint64(&c.valInt, ival)
return
}

// if the v is an unsigned integer
// and the precise doesn't lose during uint cast and cast it back to float
for v == float64(ival) {
Copy link
Author

@xieyuschen xieyuschen Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one thing to note here is that the casting here varies in different platform. for example, in linux, the v won't be equal with float64(uint64(v)). but in macos m1, it equals. It means the checking here is needed and cannot be skipped

The verification code is:

func TestVerifyRounding(t *testing.T) {
	var f float64 = 1<<64 - 1
	if f == float64(uint64(f)) {
		t.Fatal("values are the same after casting")
	} else {
		t.Fatal("values are different")
	}
}

LInux output could be found in the pipeline here.
image

and the macos outputs:
image

oldUint := atomic.LoadUint64(&c.valInt)
leftNum := math.MaxUint64 - oldUint

if leftNum < ival {
// overflow happens, we need to handle as a float number
break
}
if atomic.CompareAndSwapUint64(&c.valInt, oldUint, oldUint+ival) {
return
}

}
for {
oldBits := atomic.LoadUint64(&c.valBits)
newBits := math.Float64bits(math.Float64frombits(oldBits) + v)
Expand Down
44 changes: 44 additions & 0 deletions prometheus/counter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,50 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)

func TestCounterAddExcess(t *testing.T) {
now := time.Now()
counter := NewCounter(CounterOpts{
Name: "test",
Help: "test help",
ConstLabels: Labels{"a": "1", "b": "2"},
now: func() time.Time { return now },
}).(*counter)

counter.Add(1 << 63)
if expected, got := uint64(1<<63), counter.valInt; expected != got {
t.Errorf("Expected %d, got %d.", expected, got)
}

counter.Add(1 << 63)
if expected, got := uint64(0), counter.valInt; expected == got {
t.Errorf("oops! overflow")
}

m := &dto.Metric{}
counter.Write(m)

expected := &dto.Metric{
Label: []*dto.LabelPair{
{Name: proto.String("a"), Value: proto.String("1")},
{Name: proto.String("b"), Value: proto.String("2")},
},
Counter: &dto.Counter{
Value: proto.Float64(1 << 64),
CreatedTimestamp: timestamppb.New(now),
},
}

if !proto.Equal(expected, m) {
t.Errorf("failed because the internal overflow")
}

// verify the overflow case
expected.Counter.Value = proto.Float64(0)
if proto.Equal(expected, m) {
t.Errorf("verify the overflow case")
}
}

func TestCounterAdd(t *testing.T) {
now := time.Now()

Expand Down