← All issues

[3] DFG MapIterationEntryKey Type Confusion

Severity: Critical | Component: JSC DFG JIT — DFGNodeType.h | 3f6f783

Rated Critical because the observable effect is a DFG type confusion where object keys are misrepresented with integer semantics, and the included test case demonstrates a near-complete exploitation sequence — GC-assisted object aliasing via heap spray — projected with confidence 0.88 from the single-token root cause and the PoC-grade regression test.

MapIterationEntryKey returns arbitrary JSValues (map keys can be any type), so it should be declared with NodeResultJS to match MapIterationEntryValue.

Source/JavaScriptCore/dfg/DFGNodeType.h

macro(MapIterationEntry, NodeResultJS) \
- macro(MapIterationEntryKey, NodeResultInt32) \
+ macro(MapIterationEntryKey, NodeResultJS) \
macro(MapIterationEntryValue, NodeResultJS) \

JSTests/stress/map-forEach.js

+ function inlinee(value, key) {
+ object.key = key;
+ }
+
+ for (let i = 0; i < 200; i++) {
+ map.forEach(inlinee);
+ }
+
+ for (let i = 0; i < 100; i++) {
+ startScan();
+ (new Map([[{0: 1.1}, 1]])).forEach(inlinee);
+ const spray = [];
+ for (let j = 0; j < 1000; j++) {
+ spray.push({0: 2.2});
+ }
+ if (object.key[0] === 2.2) {
+ throw new Error("bad");
+ }
+ }

The fix is a single-token change in the FOR_EACH_DFG_OP macro table in DFGNodeType.h: MapIterationEntryKey is changed from NodeResultInt32 to NodeResultJS. The companion regression test demonstrates the bug by using Map.forEach with object keys, triggering DFG compilation via a hot loop, then heap-spraying to verify that the type confusion produces observable object aliasing.

JSC's DFG JIT uses a node type table (FOR_EACH_DFG_OP macro) where each IR node declares its result type via flags like NodeResultInt32, NodeResultJS, NodeResultDouble. These flags drive prediction propagation, register allocation, representation selection (boxed vs. unboxed), and write barrier generation throughout the DFG pipeline. NodeResultInt32 tells the compiler the node always produces an unboxed 32-bit integer, which affects how downstream nodes consume the value — the tag is stripped, and the raw 32-bit payload is operated on directly.

JSC uses NaN-boxing on 64-bit platforms to encode JSValues: object pointers, doubles, and integers all share a 64-bit representation distinguished by tag bits. When the DFG compiles code under int32 speculation, it strips the tag and operates on the raw 32-bit payload, re-tagging when a boxed JSValue is needed downstream.

JavaScript Maps can have keys of any type — objects, strings, numbers, symbols. The Map.prototype.forEach method iterates over all entries, passing (value, key, map) to the callback. When the DFG inlines such a callback, it emits MapIterationEntryKey and MapIterationEntryValue nodes to extract the key and value from each entry. MapIterationEntryValue was already correctly declared as NodeResultJS.

The root cause is a single wrong flag in the DFG node type table. MapIterationEntryKey was declared with NodeResultInt32, indicating that its result is always an unboxed 32-bit integer. In reality, Map keys are arbitrary JSValues. When the DFG compiler processes a Map.forEach callback and encounters the iteration key, it uses the NodeResultInt32 declaration to drive prediction propagation and code generation. For a key that is actually an object pointer (a 64-bit tagged JSValue in NaN-boxing), the DFG generates code that treats the result with integer representation semantics. This misrepresentation causes the object pointer to be handled with wrong type assumptions through the DFG pipeline — the value is likely truncated, mis-tagged, or lacks proper GC write barriers. If the GC does not properly trace the reference (because the wrong representation hides the pointer from the collector), the original key object could be collected while a stale pointer remains, leading to a use-after-free.

The asymmetry between MapIterationEntryKey (Int32) and MapIterationEntryValue (JS) strongly suggests a copy-paste error or an assumption that map keys are integer indices rather than arbitrary values. The FOR_EACH_DFG_OP table is the single source of truth for node result types across the entire DFG pipeline, making any error in this table automatically propagate through prediction, speculation, register allocation, and write barrier generation.

🔒

Detailed vulnerability analysis & security impact assessment

Subscribe to read more

🔒

Pattern-based audit directions for variant discovery

Subscribe to read more