CVE-2026-12299
Overview
Background
- `[Pure]`
- a Gecko WebIDL extended attribute asserting a getter/method is side-effect-free and returns consistent results, letting the JIT cache, hoist, reorder, or eliminate calls to it.
- WebIDL bindings
- generated C++ glue in
dom/webidl/*.webidlthat connects JavaScript-visible DOM APIs to their native implementations and carries JIT optimization hints. - JIT miscompilation
- incorrect machine code produced when the JIT optimizes based on a false invariant, here treating impure DOM accessors as pure.
- `innerText`/`editor` accessors
- DOM members that respectively force layout reflow and lazily construct an
nsIEditor, i.e. they mutate observable state on read.
Root Cause Analysis
Several JavaScript-visible DOM getters and methods across Document, EditContext, Element, HTMLElement, HTMLInputElement, and MozEditableElement were annotated [Pure], telling SpiderMonkey’s JIT they had no side effects and would return equivalent results on repeated calls. This invariant was violated: accessors like innerText/outerText flush layout, editor lazily creates and attaches an nsIEditor instance on first access, and sequence-returning methods such as getAttributeNames(), attachedElements(), and characterBounds() allocate a fresh object each call rather than being idempotent. Trusting the false [Pure] hint, the JIT could legally hoist, deduplicate, reorder, or eliminate these calls, producing code whose behavior diverged from the DOM’s actual semantics.
The fix removes [Pure] from exactly those members whose real implementations perform allocation, lazy construction, or layout side effects, so the JIT must treat each call as an ordinary effectful operation. Because the annotation was the sole source of the unsound assumption, dropping it restores correct sequencing and object identity for the affected APIs.
[Pure], granting the JIT license to optimize away calls whose side effects and per-call object identity actually mattered; removing the annotation forces the JIT to emit a real, ordered call every time.Attack Path
- Reach a JIT-hot path
Attacker-controlled script repeatedly reads a mislabeled member such as
innerText,editor, orgetAttributeNames()until the method is JIT-compiled. - Trigger unsound optimization
The JIT, trusting
[Pure], hoists or coalesces the repeated accesses into a single call or elides one entirely. - Desynchronize state
The elided/reordered call skips a layout flush or lazy
nsIEditorcreation, or reuses one allocation where distinct objects were expected, leaving JS-visible state inconsistent with the DOM’s true state. - Exploit the mismatch Attacker crafts follow-on operations that assume the skipped side effect ran, driving the engine into an incorrect or unsafe state.
Impact Assessment
Files Changed
dom/webidl/Document.webidldom/webidl/EditContext.webidldom/webidl/Element.webidldom/webidl/HTMLElement.webidldom/webidl/HTMLInputElement.webidl
Audit Directions
- Purity annotations vs. implementationFor every
[Pure](and related JIT hint) indom/webidl, confirm the backing C++ neither allocates fresh objects per call, lazily constructs state, nor flushes layout. - Layout-flushing gettersFlag any DOM accessor that reflows or computes layout (
innerText, sizing, grid/flex queries) as inherently impure and never eligible for[Pure]. - Lazy-construction accessorsAudit getters that create-on-first-access (editors, controllers, cached child objects) since their first call has side effects the JIT must not deduplicate or hoist.
Patch
diff --git a/dom/webidl/Document.webidl b/dom/webidl/Document.webidl
index 03284d1f13c..944864e0b28 100644
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -669,7 +669,7 @@ partial interface Document {
* tracking, fingerprinting, cryptomining and so on. This method is for
* testing only.
*/
- [ChromeOnly, Pure]
+ [ChromeOnly]
readonly attribute NodeList blockedNodesByClassifier;
};
diff --git a/dom/webidl/EditContext.webidl b/dom/webidl/EditContext.webidl
index 2f7df1c2c69..8d1c3808bde 100644
--- a/dom/webidl/EditContext.webidl
+++ b/dom/webidl/EditContext.webidl
@@ -25,7 +25,6 @@ interface EditContext : EventTarget {
undefined updateSelectionBounds(DOMRect selectionBounds);
undefined updateCharacterBounds(unsigned long rangeStart, sequence<DOMRect> characterBounds);
- [Pure]
sequence<HTMLElement> attachedElements();
[Pure]
@@ -36,7 +35,6 @@ interface EditContext : EventTarget {
readonly attribute unsigned long selectionEnd;
[Pure]
readonly attribute unsigned long characterBoundsRangeStart;
- [Pure]
sequence<DOMRect> characterBounds();
attribute EventHandler ontextupdate;
diff --git a/dom/webidl/Element.webidl b/dom/webidl/Element.webidl
index 90c4a004958..aceb70aeebb 100644
--- a/dom/webidl/Element.webidl
+++ b/dom/webidl/Element.webidl
@@ -39,7 +39,6 @@ interface Element : Node {
[SameObject]
readonly attribute NamedNodeMap attributes;
- [Pure]
sequence<DOMString> getAttributeNames();
[Pure]
DOMString? getAttribute(DOMString name);
@@ -365,7 +364,7 @@ partial interface Element {
* properties, as well as a property that exposes the flex lines
* in this container.
*/
- [ChromeOnly, Pure]
+ [ChromeOnly]
Flex? getAsFlexContainer();
// Support reporting of Grid properties
@@ -374,13 +373,13 @@ partial interface Element {
* this property returns an object with computed values for grid
* tracks and lines.
*/
- [ChromeOnly, Pure]
+ [ChromeOnly]
sequence<Grid> getGridFragments();
/**
* Returns whether there are any grid fragments on this element.
*/
- [ChromeOnly, Pure]
+ [ChromeOnly]
boolean hasGridFragments();
/**
@@ -388,7 +387,7 @@ partial interface Element {
* that have display:grid or display:inline-grid style and generate
* a frame.
*/
- [ChromeOnly, Pure]
+ [ChromeOnly]
sequence<Element> getElementsWithGrid();
/**
diff --git a/dom/webidl/HTMLElement.webidl b/dom/webidl/HTMLElement.webidl
index 034aa0de194..e5d4fea347e 100644
--- a/dom/webidl/HTMLElement.webidl
+++ b/dom/webidl/HTMLElement.webidl
@@ -25,9 +25,9 @@ interface HTMLElement : Element {
[CEReactions, SetterThrows, Pure]
attribute DOMString dir;
- [CEReactions, GetterThrows, Pure]
+ [CEReactions, GetterThrows]
attribute [LegacyNullToEmptyString] DOMString innerText;
- [CEReactions, GetterThrows, SetterThrows, Pure]
+ [CEReactions, GetterThrows, SetterThrows]
attribute [LegacyNullToEmptyString] DOMString outerText;
// user interaction
diff --git a/dom/webidl/HTMLInputElement.webidl b/dom/webidl/HTMLInputElement.webidl
index be41ed49406..09964a99669 100644
--- a/dom/webidl/HTMLInputElement.webidl
+++ b/dom/webidl/HTMLInputElement.webidl
@@ -59,7 +59,7 @@ interface HTMLInputElement : HTMLElement {
attribute boolean formNoValidate;
[CEReactions, Pure, SetterThrows]
attribute DOMString formTarget;
- [CEReactions, Pure, SetterThrows]
+ [CEReactions, SetterThrows]
attribute unsigned long height;
[Pure]
attribute boolean indeterminate;
@@ -215,7 +215,7 @@ interface mixin MozEditableElement {
// Returns an nsIEditor instance which is associated with the element.
// If the element can be associated with an editor but not yet created,
// this creates new one automatically.
- [Pure, ChromeOnly, BinaryName="editorForBindings"]
+ [ChromeOnly, BinaryName="editorForBindings"]
readonly attribute nsIEditor? editor;
// Returns true if an nsIEditor instance has already been associated with