<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE bugzilla SYSTEM "https://bugs.webkit.org/page.cgi?id=bugzilla.dtd">

<bugzilla version="5.0.4.1"
          urlbase="https://bugs.webkit.org/"
          
          maintainer="admin@webkit.org"
>

    <bug>
          <bug_id>312685</bug_id>
          
          <creation_ts>2026-04-18 10:23:32 -0700</creation_ts>
          <short_desc>Set Spread in DFG/FTL Missing Per-Instance Prototype Check</short_desc>
          <delta_ts>2026-05-11 14:55:11 -0700</delta_ts>
          <reporter_accessible>1</reporter_accessible>
          <cclist_accessible>1</cclist_accessible>
          <classification_id>1</classification_id>
          <classification>Unclassified</classification>
          <product>WebKit</product>
          <component>JavaScriptCore</component>
          <version>WebKit Local Build</version>
          <rep_platform>PC</rep_platform>
          <op_sys>Linux</op_sys>
          <bug_status>RESOLVED</bug_status>
          <resolution>FIXED</resolution>
          
          
          <bug_file_loc></bug_file_loc>
          <status_whiteboard></status_whiteboard>
          <keywords>InRadar</keywords>
          <priority>P2</priority>
          <bug_severity>Minor</bug_severity>
          <target_milestone>---</target_milestone>
          
          
          <everconfirmed>1</everconfirmed>
          <reporter>parkjuny</reporter>
          <assigned_to name="Nobody">webkit-unassigned</assigned_to>
          <cc>bfulgham</cc>
    
    <cc>syg</cc>
    
    <cc>webkit-bug-importer</cc>
          

      

      

      

          <comment_sort_order>oldest_to_newest</comment_sort_order>  
          <long_desc isprivate="0" >
    <commentid>2201867</commentid>
    <comment_count>0</comment_count>
      <attachid>479201</attachid>
    <who name="">parkjuny</who>
    <bug_when>2026-04-18 10:23:32 -0700</bug_when>
    <thetext>Created attachment 479201
poc.js

## Summary

The DFG/FTL JIT compilers produce incorrect output for `[...set]` when the operand&apos;s prototype has been replaced (e.g., `Object.setPrototypeOf(set, {})`). The interpreter correctly throws `TypeError` because `Symbol.iterator` is no longer reachable; the JIT silently reads internal Set storage and returns its values.

**While I&apos;m inclined to think that this is non-security bug, I thought it would be safer to report this as security. Feel free to demote this as a bug.**

## Bug

### Summary

The DFG fixup phase (`DFGFixupPhase.cpp:1993-1996`) enables the `SetObjectUse` fast path on two global conditions only: the `SetIteratorProtocolWatchpoint` is valid and `HavingABadTime` is valid. Neither watchpoint fires when `Object.setPrototypeOf` is called on an individual instance, so the JIT fast path is taken for sets that no longer have a reachable `Symbol.iterator`. The per-instance check — `JSSet::isIteratorProtocolFastAndNonObservable()` — exists and correctly handles this, but is never invoked anywhere in the DFG/FTL Set spread path, nor in the `operationSpreadSet` slow-path fallback.

### Detail

**DFG fixup condition** (`DFGFixupPhase.cpp:1993-1996`):

```cpp
else if (node-&gt;child1()-&gt;shouldSpeculateSetObject()
    &amp;&amp; m_graph.isWatchingSetIteratorProtocolWatchpoint(node-&gt;child1().node())
    &amp;&amp; m_graph.isWatchingHavingABadTimeWatchpoint(node-&gt;child1().node()))
    fixEdge&lt;SetObjectUse&gt;(node-&gt;child1());
```

`SetIteratorProtocolWatchpoint` only fires when `Set.prototype[Symbol.iterator]` or `SetIterator.prototype.next` are modified globally — it is not invalidated by per-instance prototype changes. Compare the array spread path just above (lines 1988–1991), which has an additional `isWatchingArrayPrototypeChainIsSaneWatchpoint()` guard; the Set path has no equivalent.

**`operationSpreadSet` has no per-instance check** (`DFGOperations.cpp:4843-4853`):

```cpp
JSC_DEFINE_JIT_OPERATION(operationSpreadSet, JSCell*, (JSGlobalObject* globalObject, JSCell* cell))
{
    ...
    ASSERT(jsDynamicCast&lt;JSSet*&gt;(cell));
    JSSet* set = jsCast&lt;JSSet*&gt;(cell);
    OPERATION_RETURN(scope, JSCellButterfly::createFromSet(globalObject, set));
}
```

The array counterpart `operationSpreadFastArray` (`DFGOperations.cpp:4856-4867`) has:

```cpp
    ASSERT(array-&gt;isIteratorProtocolFastAndNonObservable()); // absent in operationSpreadSet
```

The missing function (`JSSetInlines.h:37-56`) correctly validates the instance:

```cpp
ALWAYS_INLINE bool JSSet::isIteratorProtocolFastAndNonObservable()
{
    JSGlobalObject* globalObject = this-&gt;realm();
    if (!globalObject-&gt;isSetPrototypeIteratorProtocolFastAndNonObservable())
        return false;
    Structure* structure = this-&gt;structure();
    if (structure == globalObject-&gt;setStructure())
        return true;
    if (getPrototypeDirect() != globalObject-&gt;jsSetPrototype())
        return false;  // catches Object.setPrototypeOf(set, {}) / null
    if (getDirectOffset(vm, vm.propertyNames-&gt;iteratorSymbol) != invalidOffset)
        return false;  // catches set[Symbol.iterator] = customFn
    return true;
}
```

The same fast-path inline code appears in the FTL at `FTLLowerDFGToB3.cpp:10312`.

### Trigger Conditions

1. A function containing `[...set]` is compiled to DFG or FTL
2. `SetIteratorProtocolWatchpoint` is valid (global `Set.prototype` is unmodified)
3. A `JSSet` instance has its prototype replaced via `Object.setPrototypeOf` or owns a `Symbol.iterator` property
4. The modified instance is passed to the compiled function

## Version

### Reproduced Version

- `main` branch latest commit (2026/04/19): `a4390137a403`

## Reproduction Case

### Release Build

```bash
WebKitBuild/JSCOnly/Release/bin/jsc poc.js
```

```
interp: TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function
jit: [1,2,3]
```

Debug build produces identical output with no ASSERT fired, confirming `operationSpreadSet` lacks the invariant check present in `operationSpreadFastArray`.

### PoC Code

```js
function spreadSet(s) { return [...s]; }

// Baseline: interpreter must throw TypeError (no Symbol.iterator in prototype chain)
let cold = new Set([1, 2, 3]);
Object.setPrototypeOf(cold, {});
try { [...cold]; print(&quot;interp: no error&quot;); } catch (e) { print(&quot;interp: &quot; + e); }

// Warm up spreadSet to DFG/FTL
for (let i = 0; i &lt; 200000; i++) spreadSet(new Set([1, 2, 3]));

// JIT: same input — must throw TypeError, but returns values instead
let hot = new Set([1, 2, 3]);
Object.setPrototypeOf(hot, {});
try { let r = spreadSet(hot); print(&quot;jit: &quot; + JSON.stringify(r)); }
catch (e) { print(&quot;jit: &quot; + e); }
```

## Suggested Patch

Verified: applying this patch fixes the bug (both lines print `TypeError`); reverting restores it.

```diff
diff --git a/Source/JavaScriptCore/dfg/DFGOperations.cpp b/Source/JavaScriptCore/dfg/DFGOperations.cpp
--- a/Source/JavaScriptCore/dfg/DFGOperations.cpp
+++ b/Source/JavaScriptCore/dfg/DFGOperations.cpp
@@ -4850,6 +4850,19 @@ JSC_DEFINE_JIT_OPERATION(operationSpreadSet, JSCell*, (JSGlobalObject* globalObj
     ASSERT(jsDynamicCast&lt;JSSet*&gt;(cell));
     JSSet* set = jsCast&lt;JSSet*&gt;(cell);
 
+    if (!set-&gt;isIteratorProtocolFastAndNonObservable()) {
+        JSFunction* iterationFunction = globalObject-&gt;iteratorProtocolFunction();
+        auto callData = JSC::getCallData(iterationFunction);
+        ASSERT(callData.type != CallData::Type::None);
+        MarkedArgumentBuffer arguments;
+        arguments.append(JSValue(set));
+        ASSERT(!arguments.hasOverflowed());
+        JSValue arrayResult = call(globalObject, iterationFunction, callData, jsNull(), arguments);
+        OPERATION_RETURN_IF_EXCEPTION(scope, nullptr);
+        JSArray* array = jsCast&lt;JSArray*&gt;(arrayResult);
+        OPERATION_RETURN(scope, JSCellButterfly::createFromArray(globalObject, vm, array));
+    }
+
     OPERATION_RETURN(scope, JSCellButterfly::createFromSet(globalObject, set));
 }
 
diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
--- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
+++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp
@@ -9392,6 +9392,14 @@ void SpeculativeJIT::compileSpread(Node* node)
 
         using Helper = JSSet::Helper;
 
+        // Guard: Object.setPrototypeOf and own-property additions both cause structure
+        // transitions, so a mismatched StructureID means the iterator protocol may have
+        // changed on this instance. Route such sets to the slow path.
+        JSGlobalObject* globalObject = m_graph.globalObjectFor(node-&gt;origin.semantic);
+        load32(Address(argument, JSCell::structureIDOffset()), scratch2GPR);
+        slowPath.append(branch32(NotEqual, scratch2GPR,
+            TrustedImm32(m_graph.registerStructure(globalObject-&gt;setStructure())-&gt;id().bits())));
+
         // Load Set storage pointer.
         loadPtr(Address(argument, JSSet::offsetOfStorage()), scratch1GPR);
         slowPath.append(branchTestPtr(Zero, scratch1GPR));
 
diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
--- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
+++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
@@ -10312,6 +10312,7 @@ IGNORE_CLANG_WARNINGS_END
         } else if (m_node-&gt;child1().useKind() == SetObjectUse) {
             using Helper = JSSet::Helper;
 
+            LBasicBlock structureCheck = m_out.newBlock();
             LBasicBlock obsoleteCheck = m_out.newBlock();
             LBasicBlock deletedCheck = m_out.newBlock();
             LBasicBlock sizeCheck = m_out.newBlock();
@@ -10320,12 +10321,20 @@ IGNORE_CLANG_WARNINGS_END
             LBasicBlock slowPath = m_out.newBlock();
             LBasicBlock continuation = m_out.newBlock();
 
+            // Guard: route prototype-mutated or own-Symbol.iterator Sets to slowPath.
+            LValue structureID = m_out.load32(argument, m_heaps.JSCell_structureID);
+            m_out.branch(m_out.notEqual(structureID,
+                weakStructureID(m_graph.registerStructure(globalObject-&gt;setStructure()))),
+                rarely(slowPath), usually(structureCheck));
+
+            LBasicBlock lastNext = m_out.appendTo(structureCheck, obsoleteCheck);
+
             // Load Set storage pointer.
             LValue storage = m_out.loadPtr(argument, m_heaps.JSSet_storage);
             m_out.branch(m_out.isNull(storage), rarely(slowPath), usually(obsoleteCheck));
 
             // Check storage is not obsolete (slot 0 must be Int32).
-            LBasicBlock lastNext = m_out.appendTo(obsoleteCheck, deletedCheck);
+            m_out.appendTo(obsoleteCheck, deletedCheck);
             LValue storageButterfly = toButterfly(storage);
```

### Credit Information

Reporter credit: Junyoung Park(@candymate) of KAIST Hacking Lab</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2201868</commentid>
    <comment_count>1</comment_count>
    <who name="Radar WebKit Bug Importer">webkit-bug-importer</who>
    <bug_when>2026-04-18 10:23:38 -0700</bug_when>
    <thetext>&lt;rdar://problem/175083041&gt;</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2207405</commentid>
    <comment_count>2</comment_count>
    <who name="Shu-yu Guo">syg</who>
    <bug_when>2026-05-04 16:19:05 -0700</bug_when>
    <thetext>This is a correctness bug, not a security one.</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2207407</commentid>
    <comment_count>3</comment_count>
    <who name="Kai Tamkun">k_tamkun</who>
    <bug_when>2026-05-04 16:20:17 -0700</bug_when>
    <thetext>Pull request: https://github.com/WebKit/WebKit/pull/64218</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2209842</commentid>
    <comment_count>4</comment_count>
    <who name="Kai Tamkun">k_tamkun</who>
    <bug_when>2026-05-11 14:12:34 -0700</bug_when>
    <thetext>Pull request: https://github.com/apple/WebKit/pull/5236</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2209859</commentid>
    <comment_count>5</comment_count>
    <who name="EWS">ews-feeder</who>
    <bug_when>2026-05-11 14:55:09 -0700</bug_when>
    <thetext>Committed 313031@main (3876c27e9c01): &lt;https://commits.webkit.org/313031@main&gt;

Reviewed commits have been landed. Closing PR #64218 and removing active labels.</thetext>
  </long_desc>
      
          <attachment
              isobsolete="0"
              ispatch="0"
              isprivate="0"
          >
            <attachid>479201</attachid>
            <date>2026-04-18 10:23:32 -0700</date>
            <delta_ts>2026-04-18 10:23:32 -0700</delta_ts>
            <desc>poc.js</desc>
            <filename>poc.js</filename>
            <type>text/javascript</type>
            <size>609</size>
            <attacher>parkjuny</attacher>
            
              <data encoding="base64">ZnVuY3Rpb24gc3ByZWFkU2V0KHMpIHsgcmV0dXJuIFsuLi5zXTsgfQoKLy8gQmFzZWxpbmU6IGlu
dGVycHJldGVyIG11c3QgdGhyb3cgVHlwZUVycm9yIChubyBTeW1ib2wuaXRlcmF0b3IgaW4gcHJv
dG90eXBlIGNoYWluKQpsZXQgY29sZCA9IG5ldyBTZXQoWzEsIDIsIDNdKTsKT2JqZWN0LnNldFBy
b3RvdHlwZU9mKGNvbGQsIHt9KTsKdHJ5IHsgWy4uLmNvbGRdOyBwcmludCgiaW50ZXJwOiBubyBl
cnJvciIpOyB9IGNhdGNoIChlKSB7IHByaW50KCJpbnRlcnA6ICIgKyBlKTsgfQoKLy8gV2FybSB1
cCBzcHJlYWRTZXQgdG8gREZHL0ZUTApmb3IgKGxldCBpID0gMDsgaSA8IDIwMDAwMDsgaSsrKSBz
cHJlYWRTZXQobmV3IFNldChbMSwgMiwgM10pKTsKCi8vIEpJVDogc2FtZSBpbnB1dCDigJQgbXVz
dCB0aHJvdyBUeXBlRXJyb3IsIGJ1dCByZXR1cm5zIHZhbHVlcyBpbnN0ZWFkCmxldCBob3QgPSBu
ZXcgU2V0KFsxLCAyLCAzXSk7Ck9iamVjdC5zZXRQcm90b3R5cGVPZihob3QsIHt9KTsKdHJ5IHsg
bGV0IHIgPSBzcHJlYWRTZXQoaG90KTsgcHJpbnQoImppdDogIiArIEpTT04uc3RyaW5naWZ5KHIp
KTsgfQpjYXRjaCAoZSkgeyBwcmludCgiaml0OiAiICsgZSk7IH0K
</data>

          </attachment>
      

    </bug>

</bugzilla>