CVE-2026-16353
Overview
Background
- `ToJSValue`
- a family of overloaded WebIDL binding helpers in
mozilla::domthat convert native C++ values into a JavaScriptJS::Valuefor theJSContext. - `JS::Value`
- SpiderMonkey’s NaN-boxed tagged union where doubles,
int32, pointers, and other types share 64 bits, so non-double payloads are encoded inside otherwise-unused NaN bit patterns. - Canonical NaN
- the single blessed double NaN bit pattern that
JS::Valuerequires for stored doubles, reserving all other NaN encodings for boxed non-double tags such as GC pointers. - `JS_NumberValue`
- a SpiderMonkey API that builds a numeric
JS::Valuefrom adoublewhile canonicalizing any NaN input before storing it.
Root Cause Analysis
The vulnerable path was the float overload of ToJSValue in dom/bindings/ToJSValue.h, which stored the incoming float directly with aValue.setNumber(aArgument). setNumber writes the widened double into the JS::Value without canonicalizing NaN, violating the core NaN-boxing invariant that every stored double must use the canonical NaN bit pattern. A float NaN (or a non-canonical NaN produced by widening) therefore landed in a JS::Value whose bit pattern overlaps the encoding space reserved for boxed pointers and other tagged types, so subsequent consumers could misinterpret the value as an invalid pointer rather than a number.
The fix replaces the call with aValue.set(JS_NumberValue(double(aArgument))), which explicitly canonicalizes any NaN before the value is stored, restoring the invariant. Because canonicalization now happens for every float conversion, the resulting JS::Value can never carry a rogue NaN payload that aliases a pointer tag.
float result into a NaN-boxed JS::Value via setNumber without NaN canonicalization; the fix routes the value through JS_NumberValue, which canonicalizes the NaN so the double can never be confused with a boxed pointer or other tagged type.Attack Path
- Reach the float binding
Script invokes a WebIDL API whose return value or output is a native
float, driving thefloatoverload ofToJSValue. - Deliver a non-canonical NaN
The attacker arranges the
floatto be (or become) a NaN whose wideneddoublebit pattern is not the canonical NaN. - Store the poisoned value
setNumberwrites the non-canonical NaN into theJS::Valuewithout canonicalization, leaving a bit pattern that overlaps the boxed-pointer encoding space. - Trigger misinterpretation
Downstream engine code inspects the
JS::Valuetag and treats the NaN payload as an invalid pointer instead of a double. - Exploit the invalid pointer The mis-tagged value leads to dereference of an attacker-influenced pointer, the memory-safety fault described in the advisory.
Impact Assessment
float through ToJSValue and the ability to make that float a non-canonical NaN. No special privileges beyond running JavaScript in a page are implied by the diff.Files Changed
dom/bindings/ToJSValue.h
Audit Directions
- Uncanonicalized numeric storesAudit other
ToJSValueoverloads and binding code forsetNumber/setDoublecalls on floating-point inputs that bypassJS_NumberValueNaN canonicalization. - Float-to-double widening into `JS::Value`Flag any path that widens a
floatto adoubleand writes it into a NaN-boxedJS::Valuewithout canonicalizing, since widening can yield non-canonical NaNs. - NaN-boxing invariant sitesReview consumers that branch on a
JS::Valuetag to ensure a rogue NaN payload cannot be dispatched as a pointer or other non-double type.
Patch
diff --git a/dom/bindings/ToJSValue.h b/dom/bindings/ToJSValue.h
index 6bf17bb77e4..2444cf11018 100644
--- a/dom/bindings/ToJSValue.h
+++ b/dom/bindings/ToJSValue.h
@@ -99,13 +99,13 @@ inline bool ToJSValue(JSContext* aCx, uint64_t aArgument,
return true;
}
-// accept floating point types
+// Accept floating point types.
inline bool ToJSValue(JSContext* aCx, float aArgument,
JS::MutableHandle<JS::Value> aValue) {
// Make sure we're called in a compartment
MOZ_ASSERT(JS::CurrentGlobalOrNull(aCx));
- aValue.setNumber(aArgument);
+ aValue.set(JS_NumberValue(double(aArgument)));
return true;
}