Fix leap second overflow in datetime parsing (#1015)
Normalize leap seconds (second=60) to second=59 before passing to time.Date() to prevent year overflow. When Go's time.Date() receives second=60, it normalizes the time by adding 1 minute, which can cause dates like 9999-12-31 23:59:60 to overflow to year 10000 - outside the valid TOML date range (0000-9999). This fix ensures that timestamps with leap seconds can be successfully round-tripped (parsed and re-serialized) without causing parsing errors. Fixes OSS-Fuzz issue 472183443
This commit is contained in:
@@ -144,13 +144,23 @@ func parseDateTime(b []byte) (time.Time, error) {
|
|||||||
return time.Time{}, unstable.NewParserError(b, "extra bytes at the end of the timezone")
|
return time.Time{}, unstable.NewParserError(b, "extra bytes at the end of the timezone")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize leap seconds (second=60) to second=59 to prevent overflow
|
||||||
|
// when Go's time.Date normalizes the time. This is necessary because
|
||||||
|
// time.Date(9999, 12, 31, 23, 59, 60, 0, UTC) normalizes to year 10000,
|
||||||
|
// which is outside the valid TOML date range (0000-9999).
|
||||||
|
// See: https://github.com/pelletier/go-toml/issues/1015
|
||||||
|
second := dt.Second
|
||||||
|
if second == 60 {
|
||||||
|
second = 59
|
||||||
|
}
|
||||||
|
|
||||||
t := time.Date(
|
t := time.Date(
|
||||||
dt.Year,
|
dt.Year,
|
||||||
time.Month(dt.Month),
|
time.Month(dt.Month),
|
||||||
dt.Day,
|
dt.Day,
|
||||||
dt.Hour,
|
dt.Hour,
|
||||||
dt.Minute,
|
dt.Minute,
|
||||||
dt.Second,
|
second,
|
||||||
dt.Nanosecond,
|
dt.Nanosecond,
|
||||||
zone)
|
zone)
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -94,7 +94,13 @@ type LocalDateTime struct {
|
|||||||
|
|
||||||
// AsTime converts d into a specific time instance in zone.
|
// AsTime converts d into a specific time instance in zone.
|
||||||
func (d LocalDateTime) AsTime(zone *time.Location) time.Time {
|
func (d LocalDateTime) AsTime(zone *time.Location) time.Time {
|
||||||
return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, d.Second, d.Nanosecond, zone)
|
// Normalize leap seconds (second=60) to second=59 to prevent overflow
|
||||||
|
// when Go's time.Date normalizes the time.
|
||||||
|
second := d.Second
|
||||||
|
if second == 60 {
|
||||||
|
second = 59
|
||||||
|
}
|
||||||
|
return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, second, d.Nanosecond, zone)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns RFC 3339 representation of d.
|
// String returns RFC 3339 representation of d.
|
||||||
|
|||||||
@@ -4242,3 +4242,66 @@ func TestIssue995_SliceNonEmpty_UsesLastElement(t *testing.T) {
|
|||||||
t.Fatalf("unexpected values in allowlists: %v", got)
|
t.Fatalf("unexpected values in allowlists: %v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLeapSecondRoundTrip tests that leap seconds (second=60) don't cause
|
||||||
|
// year overflow issues during round-trip marshaling. This reproduces OSS-Fuzz
|
||||||
|
// issue 472183443.
|
||||||
|
func TestLeapSecondRoundTrip(t *testing.T) {
|
||||||
|
// This is the test case from OSS-Fuzz issue #1015
|
||||||
|
input := []byte("s=9999-12-31 23:59:60z")
|
||||||
|
|
||||||
|
var v interface{}
|
||||||
|
err := toml.Unmarshal(input, &v)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Marshal back to TOML
|
||||||
|
encoded, err := toml.Marshal(v)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Unmarshal again - this should not fail with year overflow
|
||||||
|
var v2 interface{}
|
||||||
|
err = toml.Unmarshal(encoded, &v2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLeapSecondVariants tests various leap second edge cases
|
||||||
|
func TestLeapSecondVariants(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "leap second with UTC offset datetime",
|
||||||
|
input: "s=9999-12-31 23:59:60z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "leap second with positive offset",
|
||||||
|
input: "s=9999-12-31 23:59:60+00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "leap second with negative offset",
|
||||||
|
input: "s=9999-12-31 23:59:60-05:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "leap second earlier in year",
|
||||||
|
input: "s=2015-06-30 23:59:60z",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var v interface{}
|
||||||
|
err := toml.Unmarshal([]byte(tc.input), &v)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Marshal back to TOML
|
||||||
|
encoded, err := toml.Marshal(v)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Unmarshal again - this should not fail
|
||||||
|
var v2 interface{}
|
||||||
|
err = toml.Unmarshal(encoded, &v2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user