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

time: better handling of overflow in time.Time #71979

Open
neild opened this issue Feb 26, 2025 · 12 comments
Open

time: better handling of overflow in time.Time #71979

neild opened this issue Feb 26, 2025 · 12 comments
Labels
LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.

Comments

@neild
Copy link
Contributor

neild commented Feb 26, 2025

The time.Time type has a large, but finite range. The time package does not, so far as I can determine, document what this range is.

A Time can represent instants billions of years in the past or future. So far as I can tell, we've taken a general attitude that instants outside the probable lifespan of the human species are out of scope for the package and can be ignored.

For example, time.Unix converts a Unix timestamp (seconds & nanoseconds since the Unix epoch) to a Time. It documents the handling of out-of-range values only by saying:

Not all sec values have a corresponding time value. One such value is 1<<63-1 (the largest int64 value).

Functions such as time.AddDate do not document their behavior on overflow.

Most programs will never encounter out-of-range dates. (If Go proves to be unexpectedly durable on geologic time scales, we can leave the problem of increasing the size of the time.Time seconds counter to future generations.) However, a program handling adversarial inputs (such as a very large Unix timestamp) may be induced to overflow a time.Time, with surprising and undocumented results.

We could harden the time package a bit more against overflow. Some specific functions that we might harden:

  • time.Unix could either saturate or return the zero time when provided with an out-of-range value.
  • time.Add and time.AddDate could saturate rather than wrapping on overflow.
  • time.Round could either truncate or saturate on overflow.

There might be others I'm missing.

Alternatively, we could leave the implementation alone and document what validation users should perform to adequately defend against adversarial inputs to time.Unix.

Edit: Thanks to @kuprumxyz for bringing potential security issues in time overflow to our attention.

@gabyhelp gabyhelp added the LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool label Feb 26, 2025
@seankhliao
Copy link
Member

In #63844 it was noted that restricting to ±32767 years wasn't really acceptable

@neild
Copy link
Contributor Author

neild commented Feb 26, 2025

If it isn't clear, this issue isn't about changing the range of supported instants. It's about what we do when some time package operation goes outside that range.

@seankhliao
Copy link
Member

I think it does point to needing to support a range outside the probable lifespan of the human species...

there's #64514 for underflow, #56909 and #20678 for overflow, perhaps we should merge these?

@seankhliao seankhliao added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Feb 26, 2025
@dsnet
Copy link
Member

dsnet commented Feb 27, 2025

Personally, I would expect to overflow to use saturation arithmetic. It's what I've observed for time.Time.Sub and I guess I had mistakenly assumed was true for all other arithmetic methods. I've relied on the implicit saturation handling of Sub quite a bit.

Do you have any suggestions for time.Duration where you use the native Go + and - operation, which will overflow according to typical two's complement arithmentic? Should there be a Add method to add with saturation?

@neild
Copy link
Contributor Author

neild commented Feb 27, 2025

time.Duration is an int64, and the fact that it's an int64 is user-visible, so the overflow behavior there is at least predictable. time.Time, in contrast, doesn't define what its valid range is and has no documented way to detect or handle overflow. (What happens if you call time.Unix with an out-of-range value? What values are out-of-range? We don't say. Maybe the answer is "don't create times more than a billion years away from the present", but if so, we should actually document that.)

Maybe there should be a saturating Duration.Add, but I think that's out of scope for this issue.

@neild
Copy link
Contributor Author

neild commented Feb 27, 2025

Apologies for failing to provide proper credit: Thanks to @kuprumxyz for bringing the security issues in time overflow to our attention.

@kuprumxyz
Copy link

Hi everyone here, and thanks to @neild for giving the credit! To clarify: I am a security researcher, and encountered this issue while auditing a blockchain project; so I've made a responsible vulnerability disclosure via [email protected]. I will describe the details of what I've encountered, and will offer my perspective, as a security researcher.

In the blockchain project I am auditing I have encountered the following fragment (abstracted):

// timestamp is received from the transaction input (untrusted)
timeout := time.Unix(int64(timestamp), 0)
if timeout.Before(blockTime) {
    return nil, "timeout is less than the current block timestamp"
}

if timeout.After(blockTime.Add(MaxTimeoutDelta)) {
    return nil, "timeout exceeds the maximum expected value"
}

In the above, some transaction is received by the system, which contains the timestamp. A transaction can be freely submitted by anyone, so timestamp can't be trusted: it can be any value that fits into int64. The two checks below are used to validate the passed timestamp: it should be in a certain interval.

During my testing I've found some values for timestamp which behave contrary to expectations. I've created the below test, which I submitted to [email protected]:

package time_test

import (
"fmt"
"testing"
"time"
)

func TestTime(t *testing.T) {
fmt.Println(" ===== Testing Timestamps =====")

var timestamp int64

// This timestamp is after Now: OK
timestamp = 9223371974719179007
timeout := time.Unix(timestamp, 0)

fmt.Println("Timeout            : ", timeout)
fmt.Println("Now                : ", time.Now().Round(0))
fmt.Println("Timeout after now? : ", timeout.After(time.Now().Round(0)))

// This timestamp is before Now: WHAT ???
timestamp = 9223371974719179008
timeout = time.Unix(timestamp, 0)

fmt.Println("Timeout            : ", timeout)
fmt.Println("Now                : ", time.Now().Round(0))
fmt.Println("Timeout after now? : ", timeout.After(time.Now().Round(0)))
}

Running go test gave me me:

 ===== Testing Timestamps =====
Timeout            :  292277024627-12-06 16:30:07 +0100 CET
Now                :  2025-02-17 13:53:00.642838989 +0100 CET
Timeout after now? :  true
Timeout            :  292277024627-12-06 16:30:08 +0100 CET
Now                :  2025-02-17 13:53:00.642844197 +0100 CET
Timeout after now? :  false
PASS

The crux of the problem: for a large enough timestamp, there is a phase transition: up to timestamp 9223371974719179007, time.Unix(timestamp, 0) reports that it's after time.Now(), but we add 1 second, and suddenly it reports that we are before time.Now().

In the audited project it came very close to an exploit: the only factor was that there was not only After, but also a Before check, so if one flipped from false to true, then the other one flipped from true to false: only this double-check prevented an exploit. But there are projects that do only a single check (either Before or After), and such projects would be susceptible to an exploit.

Now, here is a security perspective wrt. the Golang's Time module. Functions, such as time.Unix and others, have a certain API: they accept e.g. int64. Such API is a contract: a user has the right to expect that whatever int64 is passed, the function behaves as expected. The actual behavior violates this contract, in that it silently constructs time objects which which behave unexpectedly (when comparing to others).

As an engineer, I fully understand that there are no systems with such lifespans (but otoh, who knows?). But, as a security researcher, here is the important point:

A system should be able to handle adversarial inputs, and its functions should not violate their contracts. Any such violation can be exploited.

How to fix that? Ideally, I would like to see all functions strictly adhering to their contracts. If a function accepts int64 -- it should construct proper Time objects from those timestamps. If that's not possible due to some constraints, then, at least:

  • Make Time constructors saturating. Not returning a 0 time, but saturating is much better, because returning 0 would be again counter to all expectations.
  • Update the documentation, clearly stating the maximum value which timestamp should have, saying that above that it will saturate to that value. Better make such paragraphs in bold, because this is still counter to expectations (but to a much lesser degree).

I hope this is helpful, and looking forward to a more secure Golang, for everyone's benefit!

@dsnet
Copy link
Member

dsnet commented Feb 28, 2025

Hi @kuprumxyz, thank you for your research into this bug.

This issue is of interest to the json/v2 project (#71497) since it aims to support directly unmarshaling a JSON number containing a Unix timestamp into a time.Time. The implementation does make sure that we don't overflow an int64, but it then passes the int64 along to time.Unix assuming that the time package doesn't overflow. I am effectively that user who is assuming that "whatever int64 is passed, the function behaves as expected".

The problem is that time.Time internally uses an epoch of 0001-01-01 and Unix timestamps are stored relative to that epoch. The boundary at which you observed overflow behavior is exactly 62135596800 seconds from math.MaxInt64, which is the value of the unixToInternal constant in the time package, or the number of seconds between 1970-01-01 and 0001-01-01.

I don't think we can trivial change the internal epoch of time.Time as that would change the meaning of the zero time.Time value, but I wonder if we can use an extra bit in time.Time.wall to indicate that overflow has occurred.

@neild
Copy link
Contributor Author

neild commented Feb 28, 2025

I should note that @kuprumxyz's example above is exhibiting extra confusing behavior because it's triggering #56909: time.Time.Year contains an overflow bug which causes values very far in the past to incorrectly report a positive (large) year, so you can have a time which formats as being in the future but compares as being in the past. This is plainly a bug, and we should fix it.

@rsc
Copy link
Contributor

rsc commented Mar 3, 2025

The Unix overflow issue is not quite #56909 since that one is about the Date constructor. I think we probably shouldn't fix any of those until we decide about saturation, because saturating would be an alternative and arguably better fix.

@neild
Copy link
Contributor Author

neild commented Mar 3, 2025

Is there any good reason not to do saturation on any operation that can wrap the timestamp? We don't define our behavior on over/underflow (so hopefully no backwards compatibility concerns), and saturating seems strictly better than wrapping.

That does leave the question of what, if anything, the time.Unix documentation should say about the acceptable input range. Perhaps we could conservatively document the Unix seconds [-(1<<62), 1<<62) as being guaranteed to have corresponding Time values?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests

6 participants