<?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>312669</bug_id>
          
          <creation_ts>2026-04-18 01:07:33 -0700</creation_ts>
          <short_desc>DFG `operationStringProtoFuncReplaceAllGeneric` Skips Global Flag Check for RegExp</short_desc>
          <delta_ts>2026-05-11 14:11:36 -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>2201816</commentid>
    <comment_count>0</comment_count>
      <attachid>479191</attachid>
    <who name="">parkjuny</who>
    <bug_when>2026-04-18 01:07:33 -0700</bug_when>
    <thetext>Created attachment 479191
poc.js

## Summary

`String.prototype.replaceAll` must throw a TypeError when its search argument is a non-global RegExp. The interpreter and specialized DFG operations enforce this, but `operationStringProtoFuncReplaceAllGeneric` — reached when the replacer is a function — skips the check entirely, producing a classic unopt/opt discrepancy.

**While I think this is a simple correctness bug, I thought it would be safe to report this as a security bug. Feel free to demote this to a bug.**

## Bug

### Summary

When the DFG compiles a `StringReplaceAll` node whose `child2` speculates as `RegExpObject` but `child3` is a function (not String), the fixup phase leaves all edges as `UntypedUse`. The resulting `compileStringReplace` call emits `operationStringProtoFuncReplaceAllGeneric`, which delegates directly to `replace&lt;StringReplaceMode::Global&gt;`. That function dispatches to `replaceUsingRegExpSearch` upon seeing a `RegExpObject` without checking the global flag, silently performing a single-match replacement instead of throwing.

### Detail

`operationStringProtoFuncReplaceAllGeneric` unconditionally delegates:

```cpp
// Source/JavaScriptCore/dfg/DFGOperations.cpp:3558-3566
JSC_DEFINE_JIT_OPERATION(operationStringProtoFuncReplaceAllGeneric, JSCell*, (JSGlobalObject* globalObject, EncodedJSValue thisValue, EncodedJSValue searchValue, EncodedJSValue replaceValue))
{
    VM&amp; vm = globalObject-&gt;vm();
    CallFrame* callFrame = DECLARE_CALL_FRAME(vm);
    JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
    auto scope = DECLARE_THROW_SCOPE(vm);

    OPERATION_RETURN(scope, replace&lt;StringReplaceMode::Global&gt;(vm, globalObject, JSValue::decode(thisValue), JSValue::decode(searchValue), JSValue::decode(replaceValue)));
}
```

Inside `replace&lt;StringReplaceMode::Global&gt;`, the RegExp branch jumps straight to `replaceUsingRegExpSearch` with no global flag check:

```cpp
// Source/JavaScriptCore/runtime/StringPrototypeInlines.h:1589-1590
if (searchValue.inherits&lt;RegExpObject&gt;())
    RELEASE_AND_RETURN(scope, replaceUsingRegExpSearch(vm, globalObject, string, searchValue, replaceValue));
```

(`StringReplaceMode::Global` only affects the plain-string search path lower in the function, not this branch.)

The specialized operations used for the `RegExpObjectUse`+`StringUse` path both carry the required check:

```cpp
// Source/JavaScriptCore/dfg/DFGOperations.cpp:3599-3602 (operationStringProtoFuncReplaceAllRegExpEmptyStr)
if (!regExp-&gt;global()) [[unlikely]] {
    throwTypeError(globalObject, scope, &quot;String.prototype.replaceAll argument must not be a non-global regular expression&quot;_s);
    OPERATION_RETURN(scope, nullptr);
}

// Source/JavaScriptCore/dfg/DFGOperations.cpp:3633-3636 (operationStringProtoFuncReplaceAllRegExpString)
if (!searchValue-&gt;regExp()-&gt;global()) [[unlikely]] {
    throwTypeError(globalObject, scope, &quot;String.prototype.replaceAll argument must not be a non-global regular expression&quot;_s);
    OPERATION_RETURN(scope, nullptr);
}
```

The interpreter path likewise checks (Source/JavaScriptCore/runtime/StringPrototype.cpp:408-409):

```cpp
if (!regExpObject-&gt;regExp()-&gt;global()) [[unlikely]]
    return throwVMTypeError(globalObject, scope, &quot;String.prototype.replaceAll argument must not be a non-global regular expression&quot;_s);
```

The gap is introduced by the fixup phase. The optimized (RegExpObject+String) edge fixup requires all three children to speculate correctly:

```cpp
// Source/JavaScriptCore/dfg/DFGFixupPhase.cpp:1796-1804
if (node-&gt;child1()-&gt;shouldSpeculateString()
    &amp;&amp; node-&gt;child2()-&gt;shouldSpeculateRegExpObject()
    &amp;&amp; node-&gt;child3()-&gt;shouldSpeculateString()) {
    fixEdge&lt;StringUse&gt;(node-&gt;child1());
    fixEdge&lt;RegExpObjectUse&gt;(node-&gt;child2());
    fixEdge&lt;StringUse&gt;(node-&gt;child3());
    break;
}
```

When `child3` is a function, none of the specialized paths apply, edges remain `UntypedUse`, and `compileStringReplace` emits the generic call:

```cpp
// Source/JavaScriptCore/dfg/DFGSpeculativeJIT.cpp:13861-13873
case UntypedUse: {
    // ...
    callOperation(node-&gt;op() == StringReplaceAll ? operationStringProtoFuncReplaceAllGeneric : operationStringProtoFuncReplaceGeneric, ...);
    // ...
}
```

### Trigger Conditions

1. A function calls `replaceAll(regexp, fn)` where the second argument is a function (not a string).
2. It is warmed up enough times with a **global** RegExp to trigger DFG compilation (`child2` speculates as `RegExpObject`, `child3` as non-String → `UntypedUse` path).
3. The RegExp primordial properties watchpoint is intact.
4. The DFG-compiled function is then called with a **non-global** RegExp.

## Version

### Reproduced Version

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

## Reproduction Case

### Release Build (Debug build produces identical output)

```bash
jsc poc.js
```

Result:

```
interpreter: String.prototype.replaceAll argument must not be a non-global regular expression
dfg:         hellO world
```

### PoC Code

```js
// replaceAll must throw TypeError for non-global RegExp.
// DFG-compiled path skips this check — unopt/opt discrepancy.

function doReplaceAll(str, pattern, replacer) {
    return str.replaceAll(pattern, replacer);
}

// Interpreter: throws TypeError before any warmup
try { doReplaceAll(&quot;hello world&quot;, /o/, function(m) { return m.toUpperCase(); }); }
catch (e) { print(&quot;interpreter: &quot; + e.message); }

// Warm up: RegExpObject + function replacer → UntypedUse → operationStringProtoFuncReplaceAllGeneric
for (var i = 0; i &lt; 200000; i++)
    doReplaceAll(&quot;hello world&quot;, /o/g, function(m) { return m.toUpperCase(); });

// DFG: silently performs single-match replacement (bug)
try { print(&quot;dfg:         &quot; + doReplaceAll(&quot;hello world&quot;, /o/, function(m) { return m.toUpperCase(); })); }
catch (e) { print(&quot;dfg:         &quot; + e.message); }
```

## Suggested Patch

```diff
--- a/Source/JavaScriptCore/dfg/DFGOperations.cpp
+++ b/Source/JavaScriptCore/dfg/DFGOperations.cpp
@@ -3560,7 +3560,15 @@ JSC_DEFINE_JIT_OPERATION(operationStringProtoFuncReplaceAllGeneric, JSCell*, (JS
     VM&amp; vm = globalObject-&gt;vm();
     CallFrame* callFrame = DECLARE_CALL_FRAME(vm);
     JITOperationPrologueCallFrameTracer tracer(vm, callFrame);
     auto scope = DECLARE_THROW_SCOPE(vm);
 
-    OPERATION_RETURN(scope, replace&lt;StringReplaceMode::Global&gt;(vm, globalObject, JSValue::decode(thisValue), JSValue::decode(searchValue), JSValue::decode(replaceValue)));
+    JSValue decodedSearchValue = JSValue::decode(searchValue);
+    if (decodedSearchValue.inherits&lt;RegExpObject&gt;()) [[unlikely]] {
+        if (!jsCast&lt;RegExpObject*&gt;(decodedSearchValue)-&gt;regExp()-&gt;global()) {
+            throwTypeError(globalObject, scope, &quot;String.prototype.replaceAll argument must not be a non-global regular expression&quot;_s);
+            OPERATION_RETURN(scope, nullptr);
+        }
+    }
+
+    OPERATION_RETURN(scope, replace&lt;StringReplaceMode::Global&gt;(vm, globalObject, JSValue::decode(thisValue), decodedSearchValue, JSValue::decode(replaceValue)));
 }
```

Mirrors the checks already in `operationStringProtoFuncReplaceAllRegExpEmptyStr` (line 3599) and `operationStringProtoFuncReplaceAllRegExpString` (line 3633).

### Credit Information

Reporter credit: Junyoung Park (@candymate) of KAIST Hacking Lab</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2201817</commentid>
    <comment_count>1</comment_count>
    <who name="Radar WebKit Bug Importer">webkit-bug-importer</who>
    <bug_when>2026-04-18 01:07:39 -0700</bug_when>
    <thetext>&lt;rdar://problem/175067313&gt;</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2207763</commentid>
    <comment_count>2</comment_count>
    <who name="Shu-yu Guo">syg</who>
    <bug_when>2026-05-05 13:00:24 -0700</bug_when>
    <thetext>This is a correctness bug, not a security bug.</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2207768</commentid>
    <comment_count>3</comment_count>
    <who name="Kai Tamkun">k_tamkun</who>
    <bug_when>2026-05-05 13:09:26 -0700</bug_when>
    <thetext>Pull request: https://github.com/WebKit/WebKit/pull/64290</thetext>
  </long_desc><long_desc isprivate="0" >
    <commentid>2209841</commentid>
    <comment_count>4</comment_count>
    <who name="EWS">ews-feeder</who>
    <bug_when>2026-05-11 14:11:34 -0700</bug_when>
    <thetext>Committed 313027@main (9cfacfaca909): &lt;https://commits.webkit.org/313027@main&gt;

Reviewed commits have been landed. Closing PR #64290 and removing active labels.</thetext>
  </long_desc>
      
          <attachment
              isobsolete="0"
              ispatch="0"
              isprivate="0"
          >
            <attachid>479191</attachid>
            <date>2026-04-18 01:07:33 -0700</date>
            <delta_ts>2026-04-18 01:07:33 -0700</delta_ts>
            <desc>poc.js</desc>
            <filename>poc.js</filename>
            <type>text/javascript</type>
            <size>841</size>
            <attacher>parkjuny</attacher>
            
              <data encoding="base64">Ly8gcmVwbGFjZUFsbCBtdXN0IHRocm93IFR5cGVFcnJvciBmb3Igbm9uLWdsb2JhbCBSZWdFeHAu
Ci8vIERGRy1jb21waWxlZCBwYXRoIHNraXBzIHRoaXMgY2hlY2sg4oCUIHVub3B0L29wdCBkaXNj
cmVwYW5jeS4KCmZ1bmN0aW9uIGRvUmVwbGFjZUFsbChzdHIsIHBhdHRlcm4sIHJlcGxhY2VyKSB7
CiAgICByZXR1cm4gc3RyLnJlcGxhY2VBbGwocGF0dGVybiwgcmVwbGFjZXIpOwp9CgovLyBJbnRl
cnByZXRlcjogdGhyb3dzIFR5cGVFcnJvciBiZWZvcmUgYW55IHdhcm11cAp0cnkgeyBkb1JlcGxh
Y2VBbGwoImhlbGxvIHdvcmxkIiwgL28vLCBmdW5jdGlvbihtKSB7IHJldHVybiBtLnRvVXBwZXJD
YXNlKCk7IH0pOyB9CmNhdGNoIChlKSB7IHByaW50KCJpbnRlcnByZXRlcjogIiArIGUubWVzc2Fn
ZSk7IH0KCi8vIFdhcm0gdXA6IFJlZ0V4cE9iamVjdCArIGZ1bmN0aW9uIHJlcGxhY2VyIOKGkiBV
bnR5cGVkVXNlIOKGkiBvcGVyYXRpb25TdHJpbmdQcm90b0Z1bmNSZXBsYWNlQWxsR2VuZXJpYwpm
b3IgKHZhciBpID0gMDsgaSA8IDIwMDAwMDsgaSsrKQogICAgZG9SZXBsYWNlQWxsKCJoZWxsbyB3
b3JsZCIsIC9vL2csIGZ1bmN0aW9uKG0pIHsgcmV0dXJuIG0udG9VcHBlckNhc2UoKTsgfSk7Cgov
LyBERkc6IHNpbGVudGx5IHBlcmZvcm1zIHNpbmdsZS1tYXRjaCByZXBsYWNlbWVudCAoYnVnKQp0
cnkgeyBwcmludCgiZGZnOiAgICAgICAgICIgKyBkb1JlcGxhY2VBbGwoImhlbGxvIHdvcmxkIiwg
L28vLCBmdW5jdGlvbihtKSB7IHJldHVybiBtLnRvVXBwZXJDYXNlKCk7IH0pKTsgfQpjYXRjaCAo
ZSkgeyBwcmludCgiZGZnOiAgICAgICAgICIgKyBlLm1lc3NhZ2UpOyB9Cg==
</data>

          </attachment>
      

    </bug>

</bugzilla>