Bug 181182 - [JSC] NumberPrototype::extractRadixFromArgs incorrectly cast double to int32_t
Summary: [JSC] NumberPrototype::extractRadixFromArgs incorrectly cast double to int32_t
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: JavaScriptCore (show other bugs)
Version: WebKit Nightly Build
Hardware: Unspecified Unspecified
: P2 Normal
Assignee: Caio Lima
URL:
Keywords: InRadar
Depends on:
Blocks:
 
Reported: 2017-12-28 11:05 PST by Caio Lima
Modified: 2018-01-20 04:53 PST (History)
12 users (show)

See Also:


Attachments
Patch (8.22 KB, patch)
2018-01-03 07:23 PST, Caio Lima
no flags Details | Formatted Diff | Diff
Patch (8.39 KB, patch)
2018-01-03 08:03 PST, Caio Lima
no flags Details | Formatted Diff | Diff
Patch (9.36 KB, patch)
2018-01-10 14:16 PST, Caio Lima
no flags Details | Formatted Diff | Diff
Archive of layout-test-results from ews102 for mac-sierra (2.46 MB, application/zip)
2018-01-10 17:20 PST, EWS Watchlist
no flags Details
Patch (13.57 KB, patch)
2018-01-11 16:04 PST, Caio Lima
no flags Details | Formatted Diff | Diff
Patch (13.75 KB, patch)
2018-01-12 16:48 PST, Caio Lima
no flags Details | Formatted Diff | Diff
Patch (11.95 KB, patch)
2018-01-16 15:54 PST, Caio Lima
no flags Details | Formatted Diff | Diff
Patch (11.93 KB, patch)
2018-01-16 15:58 PST, Caio Lima
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Caio Lima 2017-12-28 11:05:29 PST
current implementation is returning ```static_cast<int32_t>(radixValue.toInteger(exec))``` when radixValue isn't int32 or undefined. It can generate undesired behavior when ```radixValue.toInteger``` returns a value that is not in ```int32_t``` range.
Comment 1 Caio Lima 2018-01-03 07:23:49 PST
Created attachment 330384 [details]
Patch
Comment 2 Caio Lima 2018-01-03 08:03:00 PST
Created attachment 330388 [details]
Patch
Comment 3 Darin Adler 2018-01-07 23:14:26 PST
Comment on attachment 330388 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=330388&action=review

> Source/JavaScriptCore/runtime/BigIntPrototype.cpp:114
> +        if (tempRadixValue >= minSafeInteger() && tempRadixValue <= maxSafeInteger())
> +            radix = static_cast<int64_t>(tempRadixValue);
> +        else
> +            return throwVMError(state, scope, createRangeError(state, ASCIILiteral("toString() radix argument must be between 2 and 36")));

Why minSafeInteger and maxSafeInteger? Why not just compare with 2 and 36?

Also, this gets rid of the reason for radix to be int64_t, so please change it to int32_t and get rid of the static_cast below.

> Source/JavaScriptCore/runtime/NumberPrototype.cpp:596
> +        double tempRadixValue = radixValue.toInteger(state);
> +        RETURN_IF_EXCEPTION(scope, encodedJSValue());
> +        if (tempRadixValue >= minSafeInteger() && tempRadixValue <= maxSafeInteger())
> +            radix = static_cast<int64_t>(tempRadixValue);
> +        else
> +            return throwVMError(state, scope, createRangeError(state, ASCIILiteral("toString() radix argument must be between 2 and 36")));

Same comment as above. But also, it’s not good to have repeated code. Please refactor this in a future patch so we can share all the identical code. It is not good to have two copies.
Comment 4 Caio Lima 2018-01-10 14:07:19 PST
Comment on attachment 330388 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=330388&action=review

Thank you for the review

>> Source/JavaScriptCore/runtime/BigIntPrototype.cpp:114
>> +            return throwVMError(state, scope, createRangeError(state, ASCIILiteral("toString() radix argument must be between 2 and 36")));
> 
> Why minSafeInteger and maxSafeInteger? Why not just compare with 2 and 36?
> 
> Also, this gets rid of the reason for radix to be int64_t, so please change it to int32_t and get rid of the static_cast below.

I'm worried about double precision, however, I'm going to follow your comment.
Comment 5 Caio Lima 2018-01-10 14:16:53 PST
Created attachment 330962 [details]
Patch
Comment 6 Caio Lima 2018-01-10 14:21:11 PST
(In reply to Caio Lima from comment #5)
> Created attachment 330962 [details]
> Patch

Changed Patch based on last Darin's comment. Since I changed a lot the code last Patch, I'm asking for a new Review.
Comment 7 EWS Watchlist 2018-01-10 17:20:01 PST
Comment on attachment 330962 [details]
Patch

Attachment 330962 [details] did not pass mac-ews (mac):
Output: http://webkit-queues.webkit.org/results/6026998

New failing tests:
webgl/1.0.2/conformance/uniforms/uniform-default-values.html
Comment 8 EWS Watchlist 2018-01-10 17:20:03 PST
Created attachment 330992 [details]
Archive of layout-test-results from ews102 for mac-sierra

The attached test failures were seen while running run-webkit-tests on the mac-ews.
Bot: ews102  Port: mac-sierra  Platform: Mac OS X 10.12.6
Comment 9 Darin Adler 2018-01-10 21:06:22 PST
Comment on attachment 330962 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=330962&action=review

Please add a test case for when the valueOf function throws an exception, making sure that the proper exception is propagated, not a different one. I believe this patch breaks that code path and causes it to throw a RangeError instead of propagating the existing exception, so we should make sure we have that tested.

> Source/JavaScriptCore/runtime/BigIntPrototype.cpp:102
> +    std::optional<int32_t> radix = extractRadix(state, state->argument(0));

We need a RETURN_IF_EXCEPTION here. Otherwise, we will call throwVMError and create a new exception rather than propagating the one that already happened.

Also, I suggest using auto instead of writing out std::optional<int32_t>.

> Source/JavaScriptCore/runtime/BigIntPrototype.cpp:104
> +    if (!radix || *radix < 2 || *radix > 36)

It’s not good  that half of the radix checking is inside extractRadix, and the other half is out here. We should put the range checking entirely inside the function.

> Source/JavaScriptCore/runtime/JSCJSValueInlines.h:1130
> +inline std::optional<int32_t> extractRadix(ExecState* state, JSValue radixValue)

I suggest taking an ExecState& instead of an ExecState*.

This is the wrong place for this function. JSCJSValue.h and its inlines header, JSCJSValueInlines.h are just for the JSValue class itself. We should not put other arbitrary parts of the runtime in here. I suggest putting this into NumberPrototype.h.

Also not entirely sure this need to be marked inline. If we are so performance sensitive that we need inlining then the extra throw scopes and extra RETURN_IF_EXCEPTION caused by factoring this way might be too much.

> Source/JavaScriptCore/runtime/JSCJSValueInlines.h:1133
> +        return radixValue.asInt32();

I suggest checking that the number is in the 2-36 range and returning nullopt otherwise.

> Source/JavaScriptCore/runtime/JSCJSValueInlines.h:1139
> +    double tempRadixValue = radixValue.toInteger(state);

I don’t know why we are using "temp" in this local variable name. I would have called this radixDouble.

> Source/JavaScriptCore/runtime/JSCJSValueInlines.h:1140
> +    RETURN_IF_EXCEPTION(scope, encodedJSValue());

The return value here needs to be a std::optional<int32_t>, so it should be std::nullopt, not encodedJSValue().

> Source/JavaScriptCore/runtime/JSCJSValueInlines.h:1141
> +    if (tempRadixValue >= 2 && tempRadixValue <= 32)

This should be 36, not 32.

> Source/JavaScriptCore/runtime/NumberPrototype.cpp:584
> +    std::optional<int32_t> radix = extractRadix(state, state->argument(0));

We need a RETURN_IF_EXCEPTION here. Otherwise, we will call throwVMError and create a new exception rather than propagating the one that already happened.

Also, I suggest using auto instead of writing out std::optional<int32_t>.

> Source/JavaScriptCore/runtime/NumberPrototype.cpp:587
> +    if (!radix || *radix < 2 || *radix > 36)
> +        return throwVMError(state, scope, createRangeError(state, ASCIILiteral("toString() radix argument must be between 2 and 36")));

Since this code is repeated at both call sites, I suggest changing extractRadix so that it throws this error rather than having the caller check and throw it. And then I would also rename it to extractToStringRadixArgument.
Comment 10 Darin Adler 2018-01-10 21:07:16 PST
Comment on attachment 330388 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=330388&action=review

>>> Source/JavaScriptCore/runtime/BigIntPrototype.cpp:114
>>> +            return throwVMError(state, scope, createRangeError(state, ASCIILiteral("toString() radix argument must be between 2 and 36")));
>> 
>> Why minSafeInteger and maxSafeInteger? Why not just compare with 2 and 36?
>> 
>> Also, this gets rid of the reason for radix to be int64_t, so please change it to int32_t and get rid of the static_cast below.
> 
> I'm worried about double precision, however, I'm going to follow your comment.

What is your worry? You should translate your worry into a test case so you can stop worrying.
Comment 11 Saam Barati 2018-01-10 21:11:05 PST
Comment on attachment 330962 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=330962&action=review

>> Source/JavaScriptCore/runtime/JSCJSValueInlines.h:1130
>> +inline std::optional<int32_t> extractRadix(ExecState* state, JSValue radixValue)
> 
> I suggest taking an ExecState& instead of an ExecState*.
> 
> This is the wrong place for this function. JSCJSValue.h and its inlines header, JSCJSValueInlines.h are just for the JSValue class itself. We should not put other arbitrary parts of the runtime in here. I suggest putting this into NumberPrototype.h.
> 
> Also not entirely sure this need to be marked inline. If we are so performance sensitive that we need inlining then the extra throw scopes and extra RETURN_IF_EXCEPTION caused by factoring this way might be too much.

I'm not making a value judgement of ExecState* vs ExecState&, but because we use ExecState* almost unilaterally throughout JSC, I'd vote for ExecState* just for consistency.
Comment 12 Caio Lima 2018-01-11 16:04:54 PST
Created attachment 331132 [details]
Patch
Comment 13 Darin Adler 2018-01-11 19:58:15 PST
Comment on attachment 331132 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=331132&action=review

> Source/JavaScriptCore/runtime/NumberPrototype.cpp:628
> +        return throwVMError(state, scope, createRangeError(state, ASCIILiteral("toString() radix argument must be between 2 and 36")));

Using "else" we could share the two calls to throwVMError and createRangeError.
Comment 14 Darin Adler 2018-01-11 20:04:39 PST
Comment on attachment 331132 [details]
Patch

Could still add some test cases for other values such as the two infinities, NaN, booleans, strings, null, and negative zero. Not really needed, I guess.
Comment 15 Caio Lima 2018-01-12 16:48:08 PST
(In reply to Darin Adler from comment #13)
> Comment on attachment 331132 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=331132&action=review
> 
> > Source/JavaScriptCore/runtime/NumberPrototype.cpp:628
> > +        return throwVMError(state, scope, createRangeError(state, ASCIILiteral("toString() radix argument must be between 2 and 36")));
> 
> Using "else" we could share the two calls to throwVMError and
> createRangeError.

Thanks for the Review!. I applied suggested changes and also added suggested test cases.
Comment 16 Caio Lima 2018-01-12 16:48:47 PST
Created attachment 331254 [details]
Patch
Comment 17 WebKit Commit Bot 2018-01-13 07:16:27 PST
Comment on attachment 331254 [details]
Patch

Clearing flags on attachment: 331254

Committed r226937: <https://trac.webkit.org/changeset/226937>
Comment 18 WebKit Commit Bot 2018-01-13 07:16:29 PST
All reviewed patches have been landed.  Closing bug.
Comment 19 Radar WebKit Bug Importer 2018-01-13 07:17:29 PST
<rdar://problem/36496249>
Comment 20 Darin Adler 2018-01-13 12:14:12 PST
One more idea for a last bit of refinement that I think is still worth it even though this is already landed: There is no need for the helper function to return an EncodedJSValue in the case of an exception. When an exception is present the return value of the host call is ignored; it does not matter what is returned so we don’t need to provide a value to the host functions. So we could simplify and get rid of the need for Variant in one of the two following ways:

Plan A:

    1a) Change the return type of extractToStringRadixArgument to be std::optional<int32_t> instead of Variant<int32_t, EncodedJSValue>.
    2a) Use throwRangeError instead of throwVMError. No need to separately call createRangeError and throw it, that's what the throwRangeError convenience function is for. No need to use throwVMRangeError since that's only a convenience to be used in a function where the return value is EncodedJSValue, and extractToStringRadixArgument no longer falls into that category.
    3a) Return std::nullopt after the call to throwRangeError, ignoring the return value of throwRangeError.
    4a) At the two call sites, return EncodedJSValue() when the radix is not present, since this is the conventional thing to return when letting an exception propagate. Ignored by the caller.

    auto radix = extractToStringRadixArgument(state, state->argument(0));
    if (!radix)
        return EncodedJSValue();

Plan B:

    1b) Change the return type of extractToStringRadixArgument to be int32_t instead of Variant<int32_t, EncodedJSValue>.
    2b) Use throwRangeError instead of throwVMError. Same rationale as 2a above.
    3b) Return 0 after the call to throwRangeError, ignoring the return value of throwRangeError.
    4b) Pass a ThrowScope& into extractToStringRadixArgument; use it instead of doing a DECLARE_THROW_SCOPE inside the function.
    5b) At the two call sites, use RETURN_IF_EXCEPTION before looking at the return value:

    int32_t radix = extractToStringRadixArgument(state, scope, state->argument(0));
    RETURN_IF_EXCEPTION(scope, encodedJSValue());

We don’t have to do either of these, but I think I like the idea of plan B since it is more like the standard idiom for bindings code.
Comment 21 Ryan Haddad 2018-01-16 11:00:10 PST
The tests added with this change are hitting an assertion failure:
https://build.webkit.org/builders/Apple%20High%20Sierra%20Debug%20JSC%20%28Tests%29/builds/501

ERROR: Unchecked JS exception:
    This scope can throw a JS exception: extractToStringRadixArgument @ ./runtime/NumberPrototype.cpp:622
        (ExceptionScope::m_recursionDepth was 5)
    But the exception was unchecked as of this scope: bigIntProtoFuncToString @ ./runtime/BigIntPrototype.cpp:96
        (ExceptionScope::m_recursionDepth was 4)

Unchecked exception detected at:
    1   0x102bac313 JSC::VM::verifyExceptionCheckNeedIsSatisfied(unsigned int, JSC::ExceptionEventLocation&)
    2   0x102b883fb JSC::ThrowScope::~ThrowScope()
    3   0x102b887d5 JSC::ThrowScope::~ThrowScope()
    4   0x1028cf210 JSC::bigIntProtoFuncToString(JSC::ExecState*)
    5   0x50632d001178
    6   0x1019736c1 llint_entry
    7   0x1019736c1 llint_entry
    8   0x10196b5d2 vmEntryToJavaScript
    9   0x1026cf62e JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*)
    10  0x1026755bd JSC::Interpreter::executeProgram(JSC::SourceCode const&, JSC::ExecState*, JSC::JSObject*)
    11  0x102926ba7 JSC::evaluate(JSC::ExecState*, JSC::SourceCode const&, JSC::JSValue, WTF::NakedPtr<JSC::Exception>&)
    12  0x1018afc9e runWithOptions(GlobalObject*, CommandLine&)
    13  0x10188a9a4 jscmain(int, char**)::$_3::operator()(JSC::VM&, GlobalObject*) const
    14  0x101872f1f int runJSC<jscmain(int, char**)::$_3>(CommandLine, bool, jscmain(int, char**)::$_3 const&)
    15  0x101871a0f jscmain(int, char**)
    16  0x10187196e main
    17  0x7fff68743115 start
    18  0x9

ASSERTION FAILED: !m_needExceptionCheck
./runtime/VM.cpp(1090) : void JSC::VM::verifyExceptionCheckNeedIsSatisfied(unsigned int, JSC::ExceptionEventLocation &)
1   0x102e1f0ad WTFCrash
2   0x102bac469 JSC::VM::verifyExceptionCheckNeedIsSatisfied(unsigned int, JSC::ExceptionEventLocation&)
3   0x102b883fb JSC::ThrowScope::~ThrowScope()
4   0x102b887d5 JSC::ThrowScope::~ThrowScope()
5   0x1028cf210 JSC::bigIntProtoFuncToString(JSC::ExecState*)
6   0x50632d001178
7   0x1019736c1 llint_entry
8   0x1019736c1 llint_entry
9   0x10196b5d2 vmEntryToJavaScript
10  0x1026cf62e JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*)
11  0x1026755bd JSC::Interpreter::executeProgram(JSC::SourceCode const&, JSC::ExecState*, JSC::JSObject*)
12  0x102926ba7 JSC::evaluate(JSC::ExecState*, JSC::SourceCode const&, JSC::JSValue, WTF::NakedPtr<JSC::Exception>&)
13  0x1018afc9e runWithOptions(GlobalObject*, CommandLine&)
14  0x10188a9a4 jscmain(int, char**)::$_3::operator()(JSC::VM&, GlobalObject*) const
15  0x101872f1f int runJSC<jscmain(int, char**)::$_3>(CommandLine, bool, jscmain(int, char**)::$_3 const&)
16  0x101871a0f jscmain(int, char**)
17  0x10187196e main
18  0x7fff68743115 start
19  0x9
Comment 22 Ryan Haddad 2018-01-16 14:35:43 PST
Reverted r226937 for reason:

Tests added with this change are failing due to a missing exception check.

Committed r227004: <https://trac.webkit.org/changeset/227004>
Comment 23 Caio Lima 2018-01-16 15:54:48 PST
Created attachment 331439 [details]
Patch
Comment 24 Caio Lima 2018-01-16 15:58:03 PST
Created attachment 331440 [details]
Patch
Comment 25 Caio Lima 2018-01-19 13:41:40 PST
Ping review.
Comment 26 WebKit Commit Bot 2018-01-20 04:53:56 PST
Comment on attachment 331440 [details]
Patch

Clearing flags on attachment: 331440

Committed r227271: <https://trac.webkit.org/changeset/227271>
Comment 27 WebKit Commit Bot 2018-01-20 04:53:58 PST
All reviewed patches have been landed.  Closing bug.