← All reports

[JSC] Implement `String#split` in C++

Component: JSC | 1a01603

Source/JavaScriptCore/builtins/BuiltinNames.h

- macro(stringSplitFast) \

Source/JavaScriptCore/builtins/StringPrototype.js

-function split(separator, limit)
-{
- "use strict";
- // ... entire JS builtin deleted; replaced by C++ host function
-}

Source/JavaScriptCore/runtime/RegExpObjectInlines.h

+inline bool RegExpObject::isSymbolSplitFastAndNonObservable()
+{
+ // mirrors isSymbolReplaceFastAndNonObservable:
+ // checks structure to rule out per-instance @@split override
+ // callers must also validate m_stringSymbolSplitWatchpointSet
+ // and m_regExpSpeciesWatchpointSet are still intact
+}

JSC optimizes builtins along two tracks: self-hosted JS builtins, which compile to bytecode and get JIT'd generically, and DFG intrinsics, which become type-specialized IR nodes emitting direct C++ calls. This commit moves String.prototype.split from the first track to the second, deleting the JS builtin and introducing a StringSplit DFG node that dispatches to operationStringSplit for string separators or operationStringSplitRegExp for primordial RegExp separators. Skipping RegExp.prototype[@@split] entirely is only sound while nobody has made that call observable, so three watchpoint sets now guard the fast path — m_stringSymbolSplitWatchpointSet, m_regExpSpeciesWatchpointSet, and splitSymbol added to regExpPrimordialPropertiesWatchpointSet. A watchpoint is an invalidation hook: if any of those properties is assigned, it fires and the JIT must deoptimize or fall to the slow path.

String#split(sep) call
        │
  [DFG StringSplit node]
        │
  watchpoints valid?
  ┌─────┴──────────────────────────┐
 yes                               no
  │                                │
  sep type check                  slow path
  │                              (observable @@split)
  ├─ String ──────────────► operationStringSplit
  │
  └─ RegExp?
       │
  isSymbolSplitFastAndNonObservable?
  ├─ yes ──► operationStringSplitRegExp  (C++ fast)
  └─ no  ──► RegExp.prototype[@@split]  (JS, observable)

11-23% speedup on split-heavy workloads, but the security story is the fast-path gate rather than the numbers. The new isSymbolSplitFastAndNonObservable check, three new watchpoint sets, and a new DFG node boundary all introduce edge cases that mirror the historical bug surface of String#replace's analogous C++ migration.

Three areas repay close inspection. First, watchpoint invalidation races: m_stringSymbolSplitWatchpointSet and m_regExpSpeciesWatchpointSet are new. If either can be triggered after the fast-path guard but before the C++ operation completes — or if their invalidation order relative to JIT code patching is wrong — an attacker could make the JIT operate on a regex whose @@split has been swapped without triggering recompilation. This is the same class of bug as older @@replace watchpoint races, so the replace history is the place to start pattern-matching.

Second, the isSymbolSplitFastAndNonObservable structure check: per-instance @@split overrides are caught by a structure check, and structure IDs are recycled after GC — a carefully timed allocation sequence causing structure ID reuse could make a modified RegExp pass. Diff the implementation against its replace counterpart and treat any divergence as the interesting part.

Third, the separator type dispatch boundary: the DFG node selects between the string and RegExp paths on type speculation. What happens when a separator object carries both a toString and a Symbol.split? When the separator is a String object rather than a primitive? The C++ fast path may handle these differently from the old JS builtin, which followed the spec's explicit typeof branches — and this pattern generalizes to every other builtin migrated from self-hosted JS to a C++ host function, where the spec's coercion order is easy to lose in translation.