Consider canonicalizing NaNs by default in JS::Value setDouble/setNumber
Categories
(Core :: JavaScript Engine, task, P3)
Tracking
()
People
(Reporter: jandem, Assigned: jandem)
References
(Blocks 1 open bug)
Details
(Keywords: sec-want, Whiteboard: [adv-main153-])
Attachments
(8 external files)
LLMs have recently found a number of NaN-canonicalization issues in browser code.
It's time to consider doing one of:
- Canonicalize doubles stored in
JS::Valueby default. We could add an Unsafe/Unchecked version that doesn't canonicalize. - Change the
isDouble()assertion insetDouble()to a release assertion.
Also note that JS_NumberValue canonicalizes, but JS::NumberValue doesn't; this distinction is not obvious at all.
Maybe we could start by measuring the performance overhead on some micro-benchmarks and Speedometer3/JetStream3.
Comment 1•4 months ago
|
||
How much does this overlap with Bug 932767?
Updated•4 months ago
|
| Assignee | ||
Comment 2•4 months ago
|
||
(In reply to Matthew Gaudet (he/him) [:mgaudet] from comment #1)
How much does this overlap with Bug 932767?
Mostly unrelated. Bug 932767 is improving NaN canonicalization for typed array loads with a JIT optimization pass and this bug is about the JS::Value API.
Comment 3•4 months ago
|
||
There's also a lot of calls to JS::NumberValue which could potentially be a problem. For instance, I found this one in Canvas WebGL. It looks to me like that's just a double that comes in from the graphics driver or something and it won't get canonicalized if it is NaN AFAICT.
| Assignee | ||
Comment 4•4 months ago
|
||
I ran some micro-benchmarks for non-JIT-inlined Math.sqrt and Math.atan. NaN-canonicalization is pretty cheap on my Intel machine, but only if the NaN-check is inlined and optimized by Clang. std::isnan isn't inlined by Clang due to our pinned glibc so I changed JS::CanonicalizeNaN to use d != d instead.
I think we should do this, but I want to wait until bug 2050657 is fixed to avoid drawing attention to this with try pushes.
The plan would be roughly:
- Canonicalize by default for
JS::Value. - Make sure
setNumberdoesn't canonicalize foruint32_tetc inputs. - Remove now-redundant NaN-canonicalizations that flow into these functions. Remove the confusing
JS_NumberValuethat already canonicalizes today. - Measure benchmark performance. Consider adding accessors that don't canonicalize and use those inside SpiderMonkey where it matters.
| Assignee | ||
Updated•4 months ago
|
Comment 5•4 months ago
|
||
(In reply to Jan de Mooij [:jandem] from comment #4)
NaN-canonicalization is pretty cheap on my Intel machine, [...]
That matches my observations when doing µ-benchmarks for the changes in bug 932767: NaN-canonicalization is cheap in most cases because of the branch predictor. But when I tested inputs which were alternating between non-NaN and NaN inputs to thwart branch prediction, omitting NaN-canonicalization through bug 932767 clearly did pay off. (And in addition to that, omitting NaN-canonicalization also leads to fewer instructions, which helps code density.)
FTR: Using d != d will probably require disabling the "NaNExprChecker.h" clang-plugin.
| Assignee | ||
Comment 6•4 months ago
|
||
(In reply to André Bargull [:anba] from comment #5)
That matches my observations when doing µ-benchmarks for the changes in bug 932767: NaN-canonicalization is cheap in most cases because of the branch predictor. But when I tested inputs which were alternating between non-NaN and NaN inputs to thwart branch prediction, omitting NaN-canonicalization through bug 932767 clearly did pay off. (And in addition to that, omitting NaN-canonicalization also leads to fewer instructions, which helps code density.)
Optimizing canonicalization in the JIT and perf-sensitive C++ code (the interpreter, Math builtins?) makes a lot of sense. The current JS::Value default behavior is a footgun though, especially for browser/IPC code where engineers are less familiar with JS::Value internals. Non-canonical NaNs are very rare in these cases so we don't really see these bugs in crash reports, but if an attacker can trigger it it's an instant type confusion.
This will need perf numbers for at least sp3 and js3, but with an escape hatch to skip canonicalization where performance really matters, I think it's a good trade-off. For code density it helps that the generated WebIDL bindings already use JS_NumberValue for floating point types so there should be no change there.
Comment 7•4 months ago
|
||
I fully agree with that. My comment was just aimed at supporting your observation that NaN canonicalization is cheap, because I've observed the same when testing for bug 932767.
Related: If JS::NumberValue canonicalizes by default and WebIDL bindings are changed to use JS::NumberValue instead of JS_NumberValue, it could actually improve performance for int64_t/uint64_t values, which are currently first converted to double and then passed to JS_NumberValue, which calls mozilla::NumberIsInt32 to detect Int32 representable values. JS::NumberValue performs this Int32-valued check with simple branches avoiding to call mozilla::NumberIsInt32.
| Assignee | ||
Comment 8•4 months ago
|
||
(In reply to André Bargull [:anba] from comment #7)
Related: If
JS::NumberValuecanonicalizes by default and WebIDL bindings are changed to useJS::NumberValueinstead ofJS_NumberValue, it could actually improve performance forint64_t/uint64_tvalues, which are currently first converted todoubleand then passed toJS_NumberValue, which callsmozilla::NumberIsInt32to detect Int32 representable values.JS::NumberValueperforms this Int32-valued check with simple branches avoiding to callmozilla::NumberIsInt32.
Ah yes. A workaround for the std::isnan issue could also improve DOM bindings performance a bit on Linux. (Assuming this affects browser builds the same as shell builds; I haven't checked.)
(In reply to André Bargull [:anba] from comment #5)
FTR: Using
d != dwill probably require disabling the "NaNExprChecker.h" clang-plugin.
Good point. Another option is __builtin_isnan. (Maybe it's time to remove NaNExprChecker, I think it was added for buggy MSVC versions back then, e.g. bug 977538...)
| Assignee | ||
Comment 9•4 months ago
|
||
This avoids a much slower out-of-line call for std::isnan on Linux.
| Assignee | ||
Comment 10•4 months ago
|
||
Clang is smart enough to optimize away the NaN check for setNumber(uint32_t) and
setNumber(uint64_t).
JS::CanonicalizedDoubleValue now forwards to JS::DoubleValue. We should remove it,
but this can happen in a follow-up bug. Similarly, JS_NumberValue just calls
JS::NumberValue for now.
| Assignee | ||
Comment 11•4 months ago
|
||
| Assignee | ||
Comment 12•4 months ago
|
||
These behave like setDouble and setNumber before part 2.
| Assignee | ||
Comment 13•4 months ago
|
||
I've put up patches for this, but I haven't done any Try pushes yet (including perf comparisons) because I'm waiting for bug 2050657 and bug 2050990 to land first since they involves sandbox issues.
Comment 15•4 months ago
|
||
Comment 16•4 months ago
|
||
https://hg.mozilla.org/mozilla-central/rev/4d6dd15febb8
https://hg.mozilla.org/mozilla-central/rev/4097f5d4b08b
https://hg.mozilla.org/mozilla-central/rev/97a0e5cf651f
https://hg.mozilla.org/mozilla-central/rev/88c178c93adf
| Assignee | ||
Comment 17•4 months ago
|
||
We should uplift this to ESR153 to avoid a footgun when people uplift patches (safe on 154+ but vulnerable on ESR153).
| Assignee | ||
Comment 18•4 months ago
|
||
This avoids a much slower out-of-line call for std::isnan on Linux.
Original Revision: https://phabricator.services.mozilla.com/D309383
Updated•4 months ago
|
| Assignee | ||
Comment 19•4 months ago
|
||
Clang is smart enough to optimize away the NaN check for setNumber(uint32_t) and
setNumber(uint64_t).
JS::CanonicalizedDoubleValue now forwards to JS::DoubleValue. We should remove it,
but this can happen in a follow-up bug. Similarly, JS_NumberValue just calls
JS::NumberValue for now.
Original Revision: https://phabricator.services.mozilla.com/D309384
Updated•4 months ago
|
| Assignee | ||
Comment 20•4 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D309385
Updated•4 months ago
|
Comment 21•4 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Security bugs, especially a risk for patches that will be uplifted to ESR153
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: medium
- Explanation of risk level: Low-Medium. Multiple patches but they're pretty small and code is exercised by lots of tests.
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 22•4 months ago
|
||
These behave like setDouble and setNumber before part 2.
Original Revision: https://phabricator.services.mozilla.com/D309386
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Comment 23•4 months ago
|
||
| uplift | ||
Updated•4 months ago
|
Updated•4 months ago
|
Updated•3 months ago
|
Updated•1 month ago
|
Description
•