Integer/Time parsing overflow (input validation for time parsing)
Description
This commit adds explicit overflow checks in time parsing to prevent int64 overflow when computing nanoseconds for relative times. Specifically, ParseTimeAt now computes nsec := currentTimestamp + int64(d) and if nsec < 0 it returns an error indicating the value is outside the allowed time range, instead of allowing wraparound. Additionally, relative duration parsing in ParseDuration is bounded by min/max valid values and returns an error when the parsed duration is outside those limits. These changes tighten input validation for time parsing, reducing the risk of negative or out-of-range timestamps being produced from crafted inputs, which could otherwise lead to incorrect time calculations, crashes, or logic errors in time-dependent components.
Proof of Concept
Proof-of-concept (Go):
- Scenario 1: Trigger overflow with an extremely large numeric input for a time value (MaxInt64+1) that would overflow when added to the current timestamp. This demonstrates the path that previously could wrap to an invalid or unintended time.
Code (example):
package main
import (
"fmt"
tutil "github.com/VictoriaMetrics/VictoriaMetrics/lib/timeutil"
)
func main() {
// Choose an arbitrary current timestamp (in nanoseconds)
now := int64(1<<60) // a large value to illustrate overflow risk
// This string encodes a value that, when parsed by the relative-time logic,
// would be combined with 'now' and could overflow a signed 64-bit int.
// In the real parser, this would typically come from user input (e.g., API param).
input := "9223372036854775808" // MaxInt64+1 as decimal
if v, err := tutil.ParseTimeAt(input, now); err != nil {
fmt.Println("expected error (overflow):", err)
} else {
fmt.Println("parsed time:", v, "(UTC)")
}
}
Notes:
- A successful exploit would craft an input that, when parsed as a relative time, causes currentTimestamp + int64(d) to overflow or become negative. With the fix, such inputs now return an error instead of producing an out-of-range or wrapped timestamp.
- This PoC relies on the library path used by VictoriaMetrics (lib/timeutil). Adapt import path as needed for your build environment.
- Scenario 2 (edge-case before fix): Use a value that resolves to a timestamp before 1970 (e.g., a string like "-9223372036.854" or similar large negative relative value). In older behavior this could produce a negative timestamp without proper validation; the fix guards against this by returning an error when nsec < 0.
Commit Details
Author: andriibeee
Date: 2026-05-22 10:17 UTC
Message:
lib/timeutil: handle int64 overflow in relative duration parsing (#10883)
Fix `int64` overflow in `ParseTimeAt()` in `lib/timeutil/time.go` when
converting time expressed as string into nanoseconds.
Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10880.
---------
Signed-off-by: Andrii Beskomornyi <andriibeee@gmail.com>
Co-authored-by: Artem Fetishev <rtm@victoriametrics.com>
Triage Assessment
Vulnerability Type: Input Validation / Time parsing overflow
Confidence: MEDIUM
Reasoning:
The changes add explicit overflow checks when parsing times and durations, preventing potential negative nanosecond results and out-of-range timestamps. This tightens input validation during time parsing, which can mitigate edge-case exploits or crashes caused by overflow, a form of input handling vulnerability.
Verification Assessment
Vulnerability Type: Integer/Time parsing overflow (input validation for time parsing)
Confidence: MEDIUM
Affected Versions: <= 1.139.0 (prior to this fix)
Code Diff
diff --git a/app/vmctl/vmctlutil/time_test.go b/app/vmctl/vmctlutil/time_test.go
index fa2b996377333..52cefeb18b679 100644
--- a/app/vmctl/vmctlutil/time_test.go
+++ b/app/vmctl/vmctlutil/time_test.go
@@ -20,6 +20,9 @@ func TestGetTime_Failure(t *testing.T) {
// negative time
f("-292273086-05-16T16:47:06Z")
+
+ // relative duration that resolves to a timestamp before 1970
+ f("-9223372036.855")
}
func TestGetTime_Success(t *testing.T) {
@@ -77,9 +80,6 @@ func TestGetTime_Success(t *testing.T) {
// float timestamp representation",
f("1562529662.324", time.Date(2019, 7, 7, 20, 01, 02, 324e6, time.UTC))
- // negative timestamp
- f("-9223372036.855", time.Date(1970, 01, 01, 00, 00, 00, 00, time.UTC))
-
// big timestamp
f("1223372036855", time.Date(2008, 10, 7, 9, 33, 56, 855e6, time.UTC))
diff --git a/lib/httputil/time_test.go b/lib/httputil/time_test.go
index 1b536d39dccbd..e037ff4f0cbbb 100644
--- a/lib/httputil/time_test.go
+++ b/lib/httputil/time_test.go
@@ -51,8 +51,6 @@ func TestGetTimeSuccess(t *testing.T) {
f("-292273086-05-16T16:47:06Z", minTimeMsecs)
f("292277025-08-18T07:12:54.999999999Z", maxTimeMsecs)
f("1562529662.324", 1562529662324)
- f("-9223372036.854", minTimeMsecs)
- f("-9223372036.855", minTimeMsecs)
f("1223372036.855", 1223372036855)
}
@@ -85,4 +83,8 @@ func TestGetTimeError(t *testing.T) {
f("292277025-08-18T07:12:54.999999998Z")
f("123md")
f("-12.3md")
+
+ // relative duration that resolves to a timestamp before 1970
+ f("-9223372036.854")
+ f("-9223372036.855")
}
diff --git a/lib/timeutil/duration.go b/lib/timeutil/duration.go
index c60133cb6985a..24949481d7cdd 100644
--- a/lib/timeutil/duration.go
+++ b/lib/timeutil/duration.go
@@ -1,16 +1,25 @@
package timeutil
import (
+ "fmt"
"time"
"github.com/VictoriaMetrics/metricsql"
)
+var (
+ minDuration = time.Duration(minValidMilli * time.Millisecond)
+ maxDuration = time.Duration(maxValidMilli * time.Millisecond)
+)
+
// ParseDuration parses duration string in Prometheus format
func ParseDuration(s string) (time.Duration, error) {
ms, err := metricsql.DurationValue(s, 0)
if err != nil {
return 0, err
}
+ if ms < minValidMilli || maxValidMilli < ms {
+ return 0, fmt.Errorf("duration %q must be in the range [%v, %v]", s, minDuration, maxDuration)
+ }
return time.Duration(ms) * time.Millisecond, nil
}
diff --git a/lib/timeutil/duration_test.go b/lib/timeutil/duration_test.go
index 030df32429953..27cfbbe0cd0cf 100644
--- a/lib/timeutil/duration_test.go
+++ b/lib/timeutil/duration_test.go
@@ -1,6 +1,7 @@
package timeutil
import (
+ "fmt"
"testing"
"time"
)
@@ -27,3 +28,126 @@ func TestParseDuration(t *testing.T) {
f("-1m30s", -(time.Minute + time.Second*30))
f("1d-4h", time.Hour*20)
}
+
+func TestParseDurationLimits(t *testing.T) {
+ f := func(s string, want time.Duration) {
+ t.Helper()
+ got, err := ParseDuration(s)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if got != want {
+ t.Fatalf("unexpected result: got %v, want %v", got, want)
+ }
+ }
+
+ var s string
+ var want time.Duration
+
+ s = fmt.Sprintf("%dms", int64(minValidMilli))
+ f(s, minDuration)
+ s = fmt.Sprintf("%dms", int64(maxValidMilli))
+ f(s, maxDuration)
+
+ s = fmt.Sprintf("%ds", int64(minValidSecond))
+ want = minValidSecond * time.Second
+ f(s, want)
+ s = fmt.Sprintf("%ds", int64(maxValidSecond))
+ want = maxValidSecond * time.Second
+ f(s, want)
+
+ // When no unit is specified, seconds are assumed.
+ s = fmt.Sprintf("%d", int64(minValidSecond))
+ want = minValidSecond * time.Second
+ f(s, want)
+ s = fmt.Sprintf("%d", int64(maxValidSecond))
+ want = maxValidSecond * time.Second
+ f(s, want)
+
+ minValidMinute := int64(minValidSecond) / 60
+ maxValidMinute := int64(maxValidSecond) / 60
+ s = fmt.Sprintf("%dm", minValidMinute)
+ want = time.Duration(minValidMinute) * time.Minute
+ f(s, want)
+ s = fmt.Sprintf("%dm", maxValidMinute)
+ want = time.Duration(maxValidMinute) * time.Minute
+ f(s, want)
+
+ minValidHour := minValidMinute / 60
+ maxValidHour := maxValidMinute / 60
+ s = fmt.Sprintf("%dh", minValidHour)
+ want = time.Duration(minValidHour) * time.Hour
+ f(s, want)
+ s = fmt.Sprintf("%dh", maxValidHour)
+ want = time.Duration(maxValidHour) * time.Hour
+ f(s, want)
+
+ minValidDay := minValidHour / 24
+ maxValidDay := maxValidHour / 24
+ s = fmt.Sprintf("%dd", minValidDay)
+ want = time.Duration(minValidDay) * 24 * time.Hour
+ f(s, want)
+ s = fmt.Sprintf("%dd", maxValidDay)
+ want = time.Duration(maxValidDay) * 24 * time.Hour
+ f(s, want)
+
+ minValidWeek := minValidDay / 7
+ maxValidWeek := maxValidDay / 7
+ s = fmt.Sprintf("%dw", minValidWeek)
+ want = time.Duration(minValidWeek) * 7 * 24 * time.Hour
+ f(s, want)
+ s = fmt.Sprintf("%dw", maxValidWeek)
+ want = time.Duration(maxValidWeek) * 7 * 24 * time.Hour
+ f(s, want)
+
+ minValidYear := minValidDay / 365
+ maxValidYear := maxValidDay / 365
+ s = fmt.Sprintf("%dy", minValidYear)
+ want = time.Duration(minValidYear) * 365 * 24 * time.Hour
+ f(s, want)
+ s = fmt.Sprintf("%dy", maxValidYear)
+ want = time.Duration(maxValidYear) * 365 * 24 * time.Hour
+ f(s, want)
+}
+
+func TestParseDurationOutsideLimits(t *testing.T) {
+ f := func(s string) {
+ t.Helper()
+ got, err := ParseDuration(s)
+ gotDuration := time.Duration(got) * time.Millisecond
+ if err == nil {
+ t.Fatalf("ParseDuration(%s) unexpected result: got %d (%s), want error", s, got, gotDuration)
+ }
+ }
+
+ f(fmt.Sprintf("%dms", int64(minValidMilli)-1))
+ f(fmt.Sprintf("%dms", int64(maxValidMilli)+1))
+
+ f(fmt.Sprintf("%ds", int64(minValidSecond)-1))
+ f(fmt.Sprintf("%ds", int64(maxValidSecond)+1))
+
+ minValidMinute := int64(minValidSecond)/60 - 1
+ f(fmt.Sprintf("%dm", minValidMinute))
+ maxValidMinute := int64(maxValidSecond)/60 + 1
+ f(fmt.Sprintf("%dm", maxValidMinute))
+
+ minValidHour := minValidMinute/60 - 1
+ f(fmt.Sprintf("%dh", minValidHour))
+ maxValidHour := maxValidMinute/60 + 2
+ f(fmt.Sprintf("%dh", maxValidHour))
+
+ minValidDay := minValidHour/24 - 1
+ f(fmt.Sprintf("%dd", minValidDay))
+ maxValidDay := maxValidHour/24 + 1
+ f(fmt.Sprintf("%dd", maxValidDay))
+
+ minValidWeek := minValidDay/7 - 1
+ f(fmt.Sprintf("%dw", minValidWeek))
+ maxValidWeek := maxValidDay/7 + 1
+ f(fmt.Sprintf("%dw", maxValidWeek))
+
+ minValidYear := minValidDay/365 - 1
+ f(fmt.Sprintf("%dy", minValidYear))
+ maxValidYear := maxValidDay/365 + 1
+ f(fmt.Sprintf("%dy", maxValidYear))
+}
diff --git a/lib/timeutil/time.go b/lib/timeutil/time.go
index 7dc205ebc0fea..b84c46c571367 100644
--- a/lib/timeutil/time.go
+++ b/lib/timeutil/time.go
@@ -74,7 +74,11 @@ func ParseTimeAt(s string, currentTimestamp int64) (int64, error) {
if d > 0 {
d = -d
}
- return currentTimestamp + int64(d), nil
+ nsec := currentTimestamp + int64(d)
+ if nsec < 0 {
+ return 0, fmt.Errorf("time %s (%v) must be in the range [%v, %v]", sOrig, time.Unix(0, nsec).UTC(), minTime, maxTime)
+ }
+ return nsec, nil
}
if len(s) == 4 {
// Parse YYYY
diff --git a/lib/timeutil/time_test.go b/lib/timeutil/time_test.go
index ad2f9ef8cc6df..0714e168e0398 100644
--- a/lib/timeutil/time_test.go
+++ b/lib/timeutil/time_test.go
@@ -1,6 +1,8 @@
package timeutil
import (
+ "fmt"
+ "math"
"strings"
"testing"
"time"
@@ -204,10 +206,11 @@ func TestParseTimeAtSuccess(t *testing.T) {
}
func TestParseTimeAtLimits(t *testing.T) {
+ now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
+
f := func(s string, wantTime time.Time) {
t.Helper()
- currentTimestamp := time.Now().UnixNano()
- got, err := ParseTimeAt(s, currentTimestamp)
+ got, err := ParseTimeAt(s, now.UnixNano())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -226,6 +229,14 @@ func TestParseTimeAtLimits(t *testing.T) {
}
east := location(t, "Etc/GMT-14") // UTC+14:00
west := location(t, "Etc/GMT+12") // UTC-12:00
+ var s string
+
+ // min timestamp
+ f("0", time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC))
+ s = fmt.Sprintf("-%d", now.Unix())
+ f(s, time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC))
+ s = fmt.Sprintf("now-%d", now.Unix())
+ f(s, time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC))
// min year
f("1970Z", time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC))
@@ -286,13 +297,39 @@ func TestParseTimeAtLimits(t *testing.T) {
f("2262-04-11T23:47:16Z", time.Date(2262, 4, 11, 23, 47, 16, 0, time.UTC))
f("2262-04-12T13:47:16+14:00", time.Date(2262, 4, 12, 13, 47, 16, 0, east))
f("2262-04-11T11:47:16-12:00", time.Date(2262, 4, 11, 11, 47, 16, 0, west))
+
+ // max timestamp
+ s = fmt.Sprintf("%d", int64(maxValidSecond))
+ f(s, time.Date(2262, 4, 11, 23, 47, 16, 0, time.UTC))
+ s = fmt.Sprintf("%d", int64(maxValidMilli))
+ f(s, time.Date(2262, 4, 11, 23, 47, 16, 854_000_000, time.UTC))
+ s = fmt.Sprintf("%d", int64(maxValidMicro))
+ f(s, time.Date(2262, 4, 11, 23, 47, 16, 854_775_000, time.UTC))
+ s = fmt.Sprintf("%d", int64(math.MaxInt64))
+ f(s, time.Date(2262, 4, 11, 23, 47, 16, 854_775_807, time.UTC))
+
+ // timestamps beyond max valid second are still valid but are treated as
+ // milliseconds.
+ s = fmt.Sprintf("%d", int64(maxValidSecond)+1)
+ f(s, time.Date(1970, 4, 17, 18, 2, 52, 37_000_000, time.UTC))
+
+ // timestamps beyond max valid millisecond are still valid but are treated
+ // as microseconds.
+ s = fmt.Sprintf("%d", int64(maxValidMilli)+1)
+ f(s, time.Date(1970, 4, 17, 18, 2, 52, 36_855_000, time.UTC))
+
+ // timestamps beyond max valid microsecond are still valid but are treated
+ // as nanoseconds.
+ s = fmt.Sprintf("%d", int64(maxValidMicro)+1)
+ f(s, time.Date(1970, 4, 17, 18, 2, 52, 36_854_776, time.UTC))
}
func TestParseTimeAtOutsideLimits(t *testing.T) {
+ now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
+
f := func(s string) {
t.Helper()
- currentTimestamp := time.Now().UnixNano()
- got, err := ParseTimeAt(s, currentTimestamp)
+ got, err := ParseTimeAt(s, now.UnixNano())
if err == nil {
t.Fatalf("expected error but got %d", got)
}
@@ -301,6 +338,10 @@ func TestParseTimeAtOutsideLimits(t *testing.T) {
}
}
+ // min timestamp
+ f(fmt.Sprintf("-%d", now.Unix()+1))
+ f(fmt.Sprintf("now-%d", now.Unix()+1))
+
// min year
f("1969Z")
f("1970+14:00")
@@ -362,6 +403,24 @@ func TestParseTimeAtOutsideLimits(t *testing.T) {
f("2262-04-11T11:47:17-12:00")
}
+func TestParseTimeAtOutsideLimits_Nanos(t *testing.T) {
+ now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
+
+ f := func(s string) {
+ t.Helper()
+ got, err := ParseTimeAt(s, now.UnixNano())
+ if err == nil {
+ t.Fatalf("expected error but got %d", got)
+ }
+ if !strings.Contains(err.Error(), "cannot parse numeric timestamp") {
+ t.Fatalf("expected error: %v", err)
+ }
+ }
+
+ // max unix nano
+ f(fmt.Sprintf("%d", uint64(math.MaxInt64+1)))
+}
+
func TestParseTimeMsecFailure(t *testing.T) {
f := func(s string) {
t.Helper()