WebKit Bugzilla
New
Browse
Search+
Log In
×
Sign in with GitHub
or
Remember my login
Create Account
·
Forgot Password
Forgotten password account recovery
RESOLVED FIXED
312672
Array ToPrimitive Fast Path Ignores Object.prototype.valueOf Override
https://bugs.webkit.org/show_bug.cgi?id=312672
Summary
Array ToPrimitive Fast Path Ignores Object.prototype.valueOf Override
parkjuny
Reported
2026-04-18 02:27:28 PDT
Created
attachment 479192
[details]
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& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); if (isJSArray(this)) { auto* array = jsCast<JSArray*>(const_cast<JSObject*>(this)); if (array->isToPrimitiveFastAndNonObservable()) [[likely]] RELEASE_AND_RETURN(scope, array->fastToString(globalObject)); } // falls through to ordinaryToPrimitive } ``` **Guard function (JSArray.cpp:1999–2013):** ```cpp bool JSArray::isToPrimitiveFastAndNonObservable() { JSGlobalObject* globalObject = this->realm(); if (!globalObject->arrayPrototypeChainIsSane()) [[unlikely]] return false; if (!globalObject->arrayToStringWatchpointSet().isStillValid()) [[unlikely]] return false; if (!globalObject->arraySymbolToPrimitiveWatchpointSet().isStillValid()) [[unlikely]] return false; if (!globalObject->arrayJoinWatchpointSet().isStillValid()) [[unlikely]] return false; Structure* structure = this->structure(); return globalObject->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'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., `+`, `==`, `<`, `-`). ## 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 `"1,2,3"` instead of `"42"`; `slowArr` (slow path) correctly returns `"42"`. ### 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("" + arr); // 1,2,3 (bug: fast path ignores valueOf, should be "42") print("" + 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->arrayJoinWatchpointSet().isStillValid()) [[unlikely]] return false; + if (!globalObject->objectPrototypeValueOfWatchpointSet().isStillValid()) [[unlikely]] + return false; Structure* structure = this->structure(); return globalObject->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->iteratorSymbol), m_stringIteratorProtocolWatchpointSet); installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_stringPrototype.get(), vm.propertyNames->toString), m_stringToStringWatchpointSet); installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_stringPrototype.get(), vm.propertyNames->valueOf), m_stringValueOfWatchpointSet); + installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_objectPrototype.get(), vm.propertyNames->valueOf), m_objectPrototypeValueOfWatchpointSet); installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_regExpPrototype.get(), vm.propertyNames->exec), m_regExpPrimordialPropertiesWatchpointSet); installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_regExpPrototype.get(), vm.propertyNames->flags), m_regExpPrimordialPropertiesWatchpointSet); installObjectPropertyChangeAdaptiveWatchpoint(setupAdaptiveWatchpoint(this, m_regExpPrototype.get(), vm.propertyNames->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& arraySymbolToPrimitiveWatchpointSet() LIFETIME_BOUND { return m_arraySymbolToPrimitiveWatchpointSet; } InlineWatchpointSet& stringToStringWatchpointSet() LIFETIME_BOUND { return m_stringToStringWatchpointSet; } InlineWatchpointSet& stringValueOfWatchpointSet() LIFETIME_BOUND { return m_stringValueOfWatchpointSet; } + InlineWatchpointSet& objectPrototypeValueOfWatchpointSet() LIFETIME_BOUND { return m_objectPrototypeValueOfWatchpointSet; } InlineWatchpointSet& regExpPrimordialPropertiesWatchpointSet() LIFETIME_BOUND { return m_regExpPrimordialPropertiesWatchpointSet; } InlineWatchpointSet& mapSetWatchpointSet() LIFETIME_BOUND { return m_mapSetWatchpointSet; } InlineWatchpointSet& setAddWatchpointSet() LIFETIME_BOUND { return m_setAddWatchpointSet; } ``` ### Credit Information Reporter credit: Junyoung Park (@candymate) of KAIST Hacking Lab
Attachments
poc.js
(481 bytes, text/javascript)
2026-04-18 02:27 PDT
,
parkjuny
no flags
Details
View All
Add attachment
proposed patch, testcase, etc.
Radar WebKit Bug Importer
Comment 1
2026-04-19 13:22:15 PDT
<
rdar://problem/175122250
>
Kai Tamkun
Comment 2
2026-04-24 08:23:25 PDT
Pull request:
https://github.com/WebKit/WebKit/pull/63524
EWS
Comment 3
2026-05-11 14:14:01 PDT
Committed
313028@main
(0c9e1d804a0f): <
https://commits.webkit.org/313028@main
> Reviewed commits have been landed. Closing PR #63524 and removing active labels.
Note
You need to
log in
before you can comment on or make changes to this bug.
Top of Page
Format For Printing
XML
Clone This Bug