Bug 211205

Summary: We can't cast toLength result to unsigned
Product: WebKit Reporter: Saam Barati <saam>
Component: JavaScriptCoreAssignee: Saam Barati <saam>
Status: RESOLVED FIXED    
Severity: Normal CC: ashvayka, benjamin, fpizlo, ggaren, gskachkov, guijemont, joepeck, keith_miller, mark.lam, msaboff, rmorisset, ross.kirsling, ticaiolima, tzagallo, webkit-bug-importer, ysuzuki
Priority: P2 Keywords: InRadar
Version: WebKit Nightly Build   
Hardware: Unspecified   
OS: Unspecified   
See Also: https://bugs.webkit.org/show_bug.cgi?id=211298
https://bugs.webkit.org/show_bug.cgi?id=211313
https://bugs.webkit.org/show_bug.cgi?id=211279
https://bugs.webkit.org/show_bug.cgi?id=216121
Attachments:
Description Flags
WIP
none
WIP
none
patch
ysuzuki: review+
patch for landing
none
patch for landing none

Description Saam Barati 2020-04-29 13:58:02 PDT
This is UB in C++, and does different things depending on what architecture you're on.

For example, on x86, this will give us a length of zero, which violates the spec:

double x = static_cast<double>(UINT_MAX) + 1;
unsigned length = x;

On arm64, this will give us UINT_MAX, which is also wrong.
Comment 1 Radar WebKit Bug Importer 2020-04-29 17:20:41 PDT
<rdar://problem/62625562>
Comment 2 Saam Barati 2020-04-29 19:40:40 PDT
Created attachment 398023 [details]
WIP
Comment 3 Saam Barati 2020-04-29 20:05:08 PDT
Created attachment 398027 [details]
WIP

This is pretty annoying, but I think I'm close to done.

This is also making us more spec compliant. We'll also need to start throwing errors when producing length > maxSafeInteger.
Comment 4 Alexey Shvayka 2020-04-30 15:23:50 PDT
*** Bug 163417 has been marked as a duplicate of this bug. ***
Comment 5 Saam Barati 2020-04-30 16:38:22 PDT
Created attachment 398122 [details]
patch
Comment 6 Yusuke Suzuki 2020-04-30 16:47:18 PDT
Comment on attachment 398122 [details]
patch

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

r=me. Please ensure that performance is neutral.

> Source/JavaScriptCore/runtime/ArrayPrototype.cpp:930
> +        if (LIKELY(length + n <= std::numeric_limits<uint32_t>::max()))

Since maximum-array-index is not UINT32_MAX (MAX_ARRAY_INDEX is UINT32_MAX - 1), should we check `length + n <= MAX_ARRAY_INDEX` instead?

> Source/JavaScriptCore/runtime/JSObject.cpp:1963
> +    if (LIKELY(propertyName <= std::numeric_limits<unsigned>::max()))

Ditto.

> Source/JavaScriptCore/runtime/JSObject.h:214
> +        if (LIKELY(propertyName <= std::numeric_limits<uint32_t>::max()))

Ditto.

> Source/JavaScriptCore/runtime/JSObject.h:262
> +        if (LIKELY(propertyName <= std::numeric_limits<uint32_t>::max()))

Ditto.

> Source/JavaScriptCore/runtime/JSObject.h:312
> +        if (LIKELY(i <= std::numeric_limits<uint32_t>::max()))

Ditto.

> Source/JavaScriptCore/runtime/JSObject.h:380
> +        if (LIKELY(i <= std::numeric_limits<uint32_t>::max()))

Ditto.

> Source/JavaScriptCore/runtime/JSObjectInlines.h:154
> +    if (LIKELY(propertyName <= std::numeric_limits<uint32_t>::max()))

Ditto.

> Source/JavaScriptCore/runtime/JSObjectInlines.h:558
> +    if (LIKELY(propertyName <= std::numeric_limits<uint32_t>::max()))

Ditto.

> Source/JavaScriptCore/runtime/JSObjectInlines.h:566
> +    if (LIKELY(propertyName <= std::numeric_limits<uint32_t>::max()))

Ditto.
Comment 7 Alexey Shvayka 2020-04-30 16:51:14 PDT
(In reply to Saam Barati from comment #5)
> Created attachment 398122 [details]
> patch

I wonder if `i > MAX_ARRAY_INDEX` check in JSObject::deletePropertyByIndex() is useful , considering `unsigned i`?

Would you please unskip test/built-ins/Array/* files in JSTests/test/config.yaml? They should be passing (and fast) with the patch.
Comment 8 Saam Barati 2020-04-30 16:55:42 PDT
Comment on attachment 398122 [details]
patch

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

>> Source/JavaScriptCore/runtime/ArrayPrototype.cpp:930
>> +        if (LIKELY(length + n <= std::numeric_limits<uint32_t>::max()))
> 
> Since maximum-array-index is not UINT32_MAX (MAX_ARRAY_INDEX is UINT32_MAX - 1), should we check `length + n <= MAX_ARRAY_INDEX` instead?

Yeah I think you’re right. Maybe I should add an isIndex() specialized on uint64_t parameter
Comment 9 Mark Lam 2020-04-30 17:09:19 PDT
Comment on attachment 398122 [details]
patch

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

> Source/JavaScriptCore/ChangeLog:9
> +        toLength, according to the spec, returns an 53-bit integer. In our

/an/a/

> Source/JavaScriptCore/ChangeLog:17
> +        - Casting to unsigned from double is undefined behavior when the integer

/integer/integer is/

> Source/JavaScriptCore/ChangeLog:25
> +        This patch addresses each bad use the undefined behavior, and by doing so,

/bad use the/bad use of the/

> Source/JavaScriptCore/runtime/ArrayPrototype.cpp:385
> +        if (count + length > std::numeric_limits<uint32_t>::max()) {

Do we have a guarantee that count + length can never overflow?  I think in practice, this will never be, but can you add a debug ASSERT here just in case?  Either that or use CheckedInt32 and be sure.

> Source/JavaScriptCore/runtime/ArrayPrototype.cpp:926
> +    if (UNLIKELY(length + argCount > static_cast<uint64_t>(maxSafeInteger())))

Do we need an overflow check for length + argCount here?  Or do we have a guarantee that both length and argCount are less than UINT_MAX?  If so, can you add debug ASSERT on those?

> Source/JavaScriptCore/runtime/ArrayPrototype.cpp:1257
> +        if (UNLIKELY(length + nrArgs > static_cast<uint64_t>(maxSafeInteger())))

Ditto: overflow check?
Comment 10 Alexey Shvayka 2020-04-30 17:19:56 PDT
Comment on attachment 398122 [details]
patch

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

If we want to support all edge cases of the spec, we should vet all OOM throws.

> Source/JavaScriptCore/runtime/ArrayPrototype.cpp:1112
> +            throwOutOfMemoryError(globalObject, scope);

We should throw a RangeError here.

> Source/JavaScriptCore/runtime/JSONObject.cpp:482
> +                throwOutOfMemoryError(globalObject, scope);

If there are indexed getters between 0 and `length`, we should invoke them.

> Source/JavaScriptCore/runtime/JSONObject.cpp:657
> +                    throwOutOfMemoryError(m_globalObject, scope);

Ditto.
Comment 11 Saam Barati 2020-04-30 19:41:57 PDT
Comment on attachment 398122 [details]
patch

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

>> Source/JavaScriptCore/runtime/ArrayPrototype.cpp:385
>> +        if (count + length > std::numeric_limits<uint32_t>::max()) {
> 
> Do we have a guarantee that count + length can never overflow?  I think in practice, this will never be, but can you add a debug ASSERT here just in case?  Either that or use CheckedInt32 and be sure.

That's correct. Because these are expected to be 53-bit integers. I can assert in the prologue of this function.

>> Source/JavaScriptCore/runtime/ArrayPrototype.cpp:926
>> +    if (UNLIKELY(length + argCount > static_cast<uint64_t>(maxSafeInteger())))
> 
> Do we need an overflow check for length + argCount here?  Or do we have a guarantee that both length and argCount are less than UINT_MAX?  If so, can you add debug ASSERT on those?

same reason. This is a 53-bit int since it came from toLength.

>> Source/JavaScriptCore/runtime/ArrayPrototype.cpp:1112
>> +            throwOutOfMemoryError(globalObject, scope);
> 
> We should throw a RangeError here.

Why? The spec essentially allows for engines to throw OOM whenever it feels like it. Why is RangeError better?

>> Source/JavaScriptCore/runtime/JSONObject.cpp:482
>> +                throwOutOfMemoryError(globalObject, scope);
> 
> If there are indexed getters between 0 and `length`, we should invoke them.

Same comment. My understand is spec allows us to OOM whenever we feel like it. This feels like a reasonable thing to do here.

>> Source/JavaScriptCore/runtime/JSONObject.cpp:657
>> +                    throwOutOfMemoryError(m_globalObject, scope);
> 
> Ditto.

ditto
Comment 12 Saam Barati 2020-04-30 19:47:04 PDT
(In reply to Alexey Shvayka from comment #7)
> (In reply to Saam Barati from comment #5)
> > Created attachment 398122 [details]
> > patch
> 
> I wonder if `i > MAX_ARRAY_INDEX` check in JSObject::deletePropertyByIndex()
> is useful , considering `unsigned i`?
> 
> Would you please unskip test/built-ins/Array/* files in
> JSTests/test/config.yaml? They should be passing (and fast) with the patch.

What tests do you mean here?
Comment 13 Saam Barati 2020-04-30 23:30:49 PDT
Created attachment 398164 [details]
patch for landing
Comment 14 Saam Barati 2020-04-30 23:42:39 PDT
A quick smoke test shows this is neutral on JS2 on the cli
Comment 15 Saam Barati 2020-04-30 23:46:47 PDT
Created attachment 398165 [details]
patch for landing
Comment 16 EWS 2020-05-01 01:45:24 PDT
Committed r260990: <https://trac.webkit.org/changeset/260990>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 398165 [details].
Comment 18 Alexey Shvayka 2020-08-30 13:29:11 PDT
*** Bug 164456 has been marked as a duplicate of this bug. ***
Comment 19 Alexey Shvayka 2020-08-30 13:35:15 PDT
*** Bug 177326 has been marked as a duplicate of this bug. ***