-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Prevent Time::Span overflow #5306
Conversation
src/time/span.cr
Outdated
) | ||
end | ||
|
||
def initialize(*, seconds : Int, nanoseconds : Int) | ||
# check for possible overflow | ||
# seconds could be too big | ||
if Int64::MIN > seconds || seconds > Int64::MAX |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe unless Int64::MIN <= seconds <= Int64::MAX
is easier to understand?
src/time/span.cr
Outdated
# check for possible overflow | ||
# seconds could be too big | ||
if Int64::MIN > seconds || seconds > Int64::MAX | ||
raise ArgumentError.new "Time::Span too big or too small" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about raising a runtime error... it would be safer to enforce value range through type safety. Meaning, a BigInt would need to be cast to Int64 by the user.
src/time/span.cr
Outdated
seconds: to_i.to_i64 * number, | ||
nanoseconds: nanoseconds.to_i64 * number, | ||
) | ||
(total_nanoseconds * number).nanoseconds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method should still return a Time::Span
, not nanoseconds as Int.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does return Time::Span
, because (Float64 * Number)
results in a Float64
and invoking Float#nanoseconds
method returns a Time::Span
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, my bad.
src/time/span.cr
Outdated
seconds: seconds, | ||
nanoseconds: nanoseconds, | ||
) | ||
(total_nanoseconds / number).nanoseconds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
This is more related to #3103 that other issue. If implicit safe / explicit unsafe if chosen, then there would nothing to be done here. And if the oposite (explicit safe / implicit unsafe), then a |
@bcardiff Actually there are two things that overflow/wraparound here.
I would like to avoid handmade overflow guard conditions as much as possible. I see crystal currently does not have a built-in overflow detection, so I would leave But I would like to fix the second issue. So trying to create an invalid For example, doing Anyway, I will fix |
src/time/span.cr
Outdated
# seconds could be too big | ||
unless Int64::MIN <= seconds <= Int64::MAX | ||
raise ArgumentError.new "Time::Span too big or too small" | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does a negative Time::Span make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, why wouldn't it? If you subtract an earlier instance from a later one, the difference is a negative duration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it does. Otherwise some operations are a bit hard to do.
@petoem thanks for the interest of the safeness in the numbers realm. It think the second item.
Is just another case of implicit overflow. Don't you think? Otherwise whatever fix you accomplish for Time::Span would be required to be applied in other places. I use to_* methods to 1) increase beforehand the precision of the arithmetic to be done, and to 2) ensure the final type to store in var/ivar. I agree that for this second case a safer thing to do is to raise if the receiver is wider than the final range. |
Perhaps, something to think about is to make |
I have removed the overflow detection I added on arithmetic operations, because I think that llvm provided ones are better and faster. Following changes/improvement are still in this PR:
|
@RX14 This is a great idea! Maybe the other way around is better (easier to understand), to make |
I'd rather have the "default" be safer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too sure about all this overflow handling, i'd rather implement it at the language level, since it's painful and error-prone to implement it manually.
LLVM has the intrinsics, and it does two's complement overflow always by default (it's not undefined like C) and so it should be failry easy to implement. We just need to work out what to do.
src/time/span.cr
Outdated
) | ||
end | ||
|
||
def initialize(*, seconds : Int, nanoseconds : Int) | ||
def initialize(*, seconds : Int8 | Int16 | Int32 | Int64 | UInt8 | UInt16 | UInt32, nanoseconds : Int = 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please just leave this as Int
and place the unless
/raise
block from the Int
restricted method in this method. It's cleaner, and it'll get easilly optimized out when the compiler can prove it's in range (i.e. the right datatype) anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, it's cleaner.
# Normalize nanoseconds in the range 0...1_000_000_000 | ||
seconds += nanoseconds.tdiv(NANOSECONDS_PER_SECOND) | ||
# check for possible overflow seconds could become too big | ||
sec = nanoseconds.tdiv(NANOSECONDS_PER_SECOND) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that sec
itself could be > Int64::MAX
here. That overflow case isn't handled (i think?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be, but then nanoseconds
would need to be really big. tdiv
returns the result value with the type of nanoseconds
, so sec
will have the same possible range as nanoseconds
.
But because sec
will be smaller than nanoseconds
it fits and doesn't overflow.
src/time/span.cr
Outdated
@@ -262,6 +278,7 @@ struct Time::Span | |||
Time.now - self | |||
end | |||
|
|||
# Returns the result of subtracting `self` and *other*. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we leave doc changes to another PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure.
src/time/span.cr
Outdated
Span.new( | ||
seconds: -to_i, | ||
nanoseconds: -nanoseconds, | ||
) | ||
end | ||
|
||
# Returns the result of adding `self` and *other*. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
src/time/span.cr
Outdated
@@ -286,20 +303,27 @@ struct Time::Span | |||
) | |||
end | |||
|
|||
# Returns self. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
src/time/span.cr
Outdated
@@ -435,6 +464,7 @@ struct Int | |||
|
|||
# Returns a `Time::Span` of `self` milliseconds. | |||
def milliseconds : Time::Span | |||
# this might overflow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment isn't really helpful
Can't this PR just be adding multiply and divide for timespan? It's too many things at once: docs, overflow, new methods. |
because of `seconds += sec` it could become e.g. BigInt
Curious if anything still happening with this PR? Would be quite useful for me to get this in :) |
@Groogy The thing is, that this PR does not prevent Something like the Maybe I should rename this PR to something else. 🤔 |
Yes, let's have a fix for the method arguments now in a separate PR 👍 |
@straight-shoota #5272 fixed in PR #5563 . |
Let's give this PR a bit more ❤️ |
@sdogruyol the initial issue #5272 was fixed, now only the overflow part remains.
Whether it should be implicit or explicit safe and how it should look, needs more discussion. |
@petoem I am just looking through some PRs. Are you still working on this? |
I am closing this, because #5272 is fixed and int overflow is out of this PRs scope. |
While fixing multiply and divide by numbers, I noticed that there are a lot of possible overflows.
The biggest problem is that a lot of
.to_i64
etc. are used and the general type restrictionInt
orNumber
for methods.eg. This makes it possible to pass in a
BigInt
, which could overflow if.to_i64
is invoked on it.Related to #5272 and possibly others.
This is a work-in-progress PR.