<?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>312672</bug_id>
          
          <creation_ts>2026-04-18 02:27:28 -0700</creation_ts>
          <short_desc>Array ToPrimitive Fast Path Ignores Object.prototype.valueOf Override</short_desc>
          <delta_ts>2026-05-11 14:14:03 -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 Nightly 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>Normal</bug_severity>
          <target_milestone>---</target_milestone>
          
          
          <everconfirmed>1</everconfirmed>
          <reporter>parkjuny</reporter>
          <assigned_to name="Nobody">webkit-unassigned</assigned_to>
          <cc>webkit-bug-importer</cc>
          

      

      

      

          <comment_sort_order>oldest_to_newest</comment_sort_order>  
          <long_desc isprivate="0" >
    <commentid>2201827</commentid>
    <comment_count>0</comment_count>
      <attachid>479192</attachid>
    <who name="">parkjuny</who>
    <bug_when>2026-04-18 02:27:28 -0700</bug_when>
    <thetext>Created attachment 479192
poc.js

## Summary

`JSObject::toPrimitive` has a fast path for arrays that calls `fastToString()` directly, bypassing `OrdinaryToPrimitive`. The guard `isToPrimitiveFastAndNonObservable()` checks four watchpoints, none of which detect reassignment of `Object.prototype.valueOf`. The bug is a runtime C++ fast-path issue, uniform across all JIT tiers. The discrepancy is between arrays with original structure (fast path taken, wrong result) and arrays with a modified prototype (fast path skipped, correct result).

## Bug

### Summary

When `isToPrimitiveFastAndNonObservable()` returns true, `JSObject::toPrimitive` (JSObject.cpp:2601–2604) calls `fastToString()` and returns early, ignoring `preferredType`. Replacing `Object.prototype.valueOf` with a function that returns a primitive does not cause any JSC structure transition (it is a value-slot update on a pre-existing writable property), so none of the four watchpoints fire. The fast path then incorrectly returns a join string instead of the `valueOf` result.

### Detail

**Fast path (JSObject.cpp:2596–2604):**

```cpp
JSValue JSObject::toPrimitive(JSGlobalObject* globalObject, PreferredPrimitiveType preferredType) const
{
    VM&amp; vm = globalObject-&gt;vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (isJSArray(this)) {
        auto* array = jsCast&lt;JSArray*&gt;(const_cast&lt;JSObject*&gt;(this));
        if (array-&gt;isToPrimitiveFastAndNonObservable()) [[likely]]
            RELEASE_AND_RETURN(scope, array-&gt;fastToString(globalObject));
    }
    // falls through to ordinaryToPrimitive
}
```

**Guard function (JSArray.cpp:1999–2013):**

```cpp
bool JSArray::isToPrimitiveFastAndNonObservable()
{
    JSGlobalObject* globalObject = this-&gt;realm();
    if (!globalObject-&gt;arrayPrototypeChainIsSane()) [[unlikely]]
        return false;
    if (!globalObject-&gt;arrayToStringWatchpointSet().isStillValid()) [[unlikely]]
        return false;
    if (!globalObject-&gt;arraySymbolToPrimitiveWatchpointSet().isStillValid()) [[unlikely]]
        return false;
    if (!globalObject-&gt;arrayJoinWatchpointSet().isStillValid()) [[unlikely]]
        return false;

    Structure* structure = this-&gt;structure();
    return globalObject-&gt;isOriginalArrayStructure(structure);
}
```

Why `Object.prototype.valueOf = function() { return 42; }` is not detected:

1. **`arrayPrototypeChainIsSane`** — `ObjectAdaptiveStructureWatchpoint` installed via `ObjectPropertyCondition::absenceOfIndexedProperties` (JSGlobalObject.cpp:3296). Fires only on structure transitions (property additions/deletions). Reassigning an existing writable property&apos;s value is a value-slot update with no structure transition.
2. **`arrayToStringWatchpointSet`** — Watches `Array.prototype.toString` only (JSGlobalObject.cpp:2245).
3. **`arraySymbolToPrimitiveWatchpointSet`** — Watches for absence of `Symbol.toPrimitive` on `Array.prototype` / `Object.prototype` (JSGlobalObject.cpp:2275–2276). Unrelated to `valueOf`.
4. **`arrayJoinWatchpointSet`** — Watches `Array.prototype.join` only (JSGlobalObject.cpp:2244).

JSC has `m_stringValueOfWatchpointSet` for `String.prototype.valueOf` (JSGlobalObject.cpp:2251), but no equivalent for `Object.prototype.valueOf`.

### Trigger Conditions

1. `Object.prototype.valueOf` is overridden to return a primitive.
2. Array has original structure (unmodified prototype chain).
3. An operation invokes ToPrimitive on the array (e.g., `+`, `==`, `&lt;`, `-`).

## Version

### Reproduced Version

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

## Reproduction Case

### Release Build

```bash
jsc poc.js
```

Result:

```
1,2,3
42
```

`arr` (fast path) produces `&quot;1,2,3&quot;` instead of `&quot;42&quot;`; `slowArr` (slow path) correctly returns `&quot;42&quot;`.

### PoC Code

```js
Object.prototype.valueOf = function() { return 42; };

// arr: original structure → isToPrimitiveFastAndNonObservable() = true → fast path
var arr = [1, 2, 3];
// slowArr: modified prototype → fast path skipped → ordinaryToPrimitive (correct)
var slowArr = [1, 2, 3];
slowArr.__proto__ = Object.create(Array.prototype);

print(&quot;&quot; + arr);     // 1,2,3  (bug: fast path ignores valueOf, should be &quot;42&quot;)
print(&quot;&quot; + slowArr); // 42     (correct: slow path calls valueOf first)
```

## Suggested Patch

Add a watchpoint for `Object.prototype.valueOf`, mirroring `m_stringValueOfWatchpointSet`, and check it in `isToPrimitiveFastAndNonObservable()`.

```diff
diff --git a/Source/JavaScriptCore/runtime/JSArray.cpp b/Source/JavaScriptCore/runtime/JSArray.cpp
index 3bb3ce58fdf6..90f0a615d787 100644
--- a/Source/JavaScriptCore/runtime/JSArray.cpp
+++ b/Source/JavaScriptCore/runtime/JSArray.cpp
@@ -2007,6 +2007,8 @@ bool JSArray::isToPrimitiveFastAndNonObservable()
         return false;
     if (!globalObject-&gt;arrayJoinWatchpointSet().isStillValid()) [[unlikely]]
         return false;
+    if (!globalObject-&gt;objectPrototypeValueOfWatchpointSet().isStillValid()) [[unlikely]]
+        return false;
 
     Structure* structure = this-&gt;structure();
     return globalObject-&gt;isOriginalArrayStructure(structure);
diff --git a/Source/JavaScriptCore/runtime/JSGlobalObject.cpp b/Source/JavaScriptCore/runtime/JSGlobalObject.cpp
index 160a40e9f15f..59c57c9d3888 100644
--- a/Source/JavaScriptCore/runtime/JSGlobalObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSGlobalObject.cpp
@@ -2249,6 +2249,7 @@ capitalName ## Constructor* lowerName ## Constructor = featureFlag ? capitalName
     installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_stringPrototype.get(), vm.propertyNames-&gt;iteratorSymbol), m_stringIteratorProtocolWatchpointSet);
     installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_stringPrototype.get(), vm.propertyNames-&gt;toString), m_stringToStringWatchpointSet);
     installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_stringPrototype.get(), vm.propertyNames-&gt;valueOf), m_stringValueOfWatchpointSet);
+    installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_objectPrototype.get(), vm.propertyNames-&gt;valueOf), m_objectPrototypeValueOfWatchpointSet);
     installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_regExpPrototype.get(), vm.propertyNames-&gt;exec), m_regExpPrimordialPropertiesWatchpointSet);
     installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_regExpPrototype.get(), vm.propertyNames-&gt;flags), m_regExpPrimordialPropertiesWatchpointSet);
     installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_regExpPrototype.get(), vm.propertyNames-&gt;dotAll), m_regExpPrimordialPropertiesWatchpointSet);
diff --git a/Source/JavaScriptCore/runtime/JSGlobalObject.h b/Source/JavaScriptCore/runtime/JSGlobalObject.h
index 87ad406bd9a0..77c5dbba3848 100644
--- a/Source/JavaScriptCore/runtime/JSGlobalObject.h
+++ b/Source/JavaScriptCore/runtime/JSGlobalObject.h
@@ -530,6 +530,7 @@ public:
     InlineWatchpointSet m_numberToStringWatchpointSet { IsWatched };
     InlineWatchpointSet m_stringToStringWatchpointSet { IsWatched };
     InlineWatchpointSet m_stringValueOfWatchpointSet { IsWatched };
+    InlineWatchpointSet m_objectPrototypeValueOfWatchpointSet { IsWatched };
     InlineWatchpointSet m_structureCacheClearedWatchpointSet { IsWatched };
     InlineWatchpointSet m_arrayBufferSpeciesWatchpointSet { ClearWatchpoint };
     InlineWatchpointSet m_sharedArrayBufferSpeciesWatchpointSet { ClearWatchpoint };
@@ -584,6 +585,7 @@ public:
     InlineWatchpointSet&amp; arraySymbolToPrimitiveWatchpointSet() LIFETIME_BOUND { return m_arraySymbolToPrimitiveWatchpointSet; }
     InlineWatchpointSet&amp; stringToStringWatchpointSet() LIFETIME_BOUND { return m_stringToStringWatchpointSet; }
     InlineWatchpointSet&amp; stringValueOfWatchpointSet() LIFETIME_BOUND { return m_stringValueOfWatchpointSet; }
+    InlineWatchpointSet&amp; objectPrototypeValueOfWatchpointSet() LIFETIME_BOUND { return m_objectPrototypeValueOfWatchpointSet; }
     InlineWatchpointSet&amp; regExpPrimordialPropertiesWatchpointSet() LIFETIME_BOUND { return m_regExpPrimordialPropertiesWatchpointSet; }
     InlineWatchpointSet&amp; mapSetWatchpointSet() LIFETIME_BOUND { return m_mapSetWatchpointSet; }
     InlineWatchpointSet&amp; setAddWatchpointSet() LIFETIME_BOUND { return m_setAddWatchpointSet; }
```

### Credit Information

Reporter credit: Junyoung Park (@candymate) of KAIST Hacking Lab</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2202033</commentid>
    <comment_count>1</comment_count>
    <who name="Radar WebKit Bug Importer">webkit-bug-importer</who>
    <bug_when>2026-04-19 13:22:15 -0700</bug_when>
    <thetext>&lt;rdar://problem/175122250&gt;</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2204144</commentid>
    <comment_count>2</comment_count>
    <who name="Kai Tamkun">k_tamkun</who>
    <bug_when>2026-04-24 08:23:25 -0700</bug_when>
    <thetext>Pull request: https://github.com/WebKit/WebKit/pull/63524</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2209843</commentid>
    <comment_count>3</comment_count>
    <who name="EWS">ews-feeder</who>
    <bug_when>2026-05-11 14:14:01 -0700</bug_when>
    <thetext>Committed 313028@main (0c9e1d804a0f): &lt;https://commits.webkit.org/313028@main&gt;

Reviewed commits have been landed. Closing PR #63524 and removing active labels.</thetext>
  </long_desc>
      
          <attachment
              isobsolete="0"
              ispatch="0"
              isprivate="0"
          >
            <attachid>479192</attachid>
            <date>2026-04-18 02:27:28 -0700</date>
            <delta_ts>2026-04-18 02:27:28 -0700</delta_ts>
            <desc>poc.js</desc>
            <filename>poc.js</filename>
            <type>text/javascript</type>
            <size>481</size>
            <attacher>parkjuny</attacher>
            
              <data encoding="base64">T2JqZWN0LnByb3RvdHlwZS52YWx1ZU9mID0gZnVuY3Rpb24oKSB7IHJldHVybiA0MjsgfTsKCi8v
IGFycjogb3JpZ2luYWwgc3RydWN0dXJlIOKGkiBpc1RvUHJpbWl0aXZlRmFzdEFuZE5vbk9ic2Vy
dmFibGUoKSA9IHRydWUg4oaSIGZhc3QgcGF0aAp2YXIgYXJyID0gWzEsIDIsIDNdOwovLyBzbG93
QXJyOiBtb2RpZmllZCBwcm90b3R5cGUg4oaSIGZhc3QgcGF0aCBza2lwcGVkIOKGkiBvcmRpbmFy
eVRvUHJpbWl0aXZlIChjb3JyZWN0KQp2YXIgc2xvd0FyciA9IFsxLCAyLCAzXTsKc2xvd0Fyci5f
X3Byb3RvX18gPSBPYmplY3QuY3JlYXRlKEFycmF5LnByb3RvdHlwZSk7CgpwcmludCgiIiArIGFy
cik7ICAgICAvLyAxLDIsMyAgKGJ1ZzogZmFzdCBwYXRoIGlnbm9yZXMgdmFsdWVPZiwgc2hvdWxk
IGJlICI0MiIpCnByaW50KCIiICsgc2xvd0Fycik7IC8vIDQyICAgICAoY29ycmVjdDogc2xvdyBw
YXRoIGNhbGxzIHZhbHVlT2YgZmlyc3QpCg==
</data>

          </attachment>
      

    </bug>

</bugzilla>