Compare commits

...

1 Commits

Author SHA1 Message Date
Claude 6f19f855f1 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
2026-01-04 02:23:40 +00:00
3 changed files with 81 additions and 2 deletions
+11 -1
View File
@@ -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
View File
@@ -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.
+63
View File
@@ -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)
})
}
}