← All issues

[5] DFG RegExp constructor miscompilation via missing newTarget propagation

Severity: Medium | Component: JSC DFG JIT | 47b17ca

Rated Medium because the observable effect is a JavaScript-level object identity violation (constructor returns the same object instead of a new one) confirmed at confidence 0.92 by the regression test, with no direct memory corruption — escalation depends on how downstream JavaScript code uses the confused identity, which is application-dependent and requires chaining with specific validation patterns.

operationNewRegExpUntyped in DFG now passes regExpConstructor as both the callee and newTarget parameter to constructRegExp, aligning with the ECMAScript spec requirement that new RegExp(...) invocations carry a defined newTarget.

Source/JavaScriptCore/dfg/DFGOperations.cpp

JSGlobalObject* regExpGlobalObject = structure->globalObject();
- OPERATION_RETURN(scope, constructRegExp(regExpGlobalObject, ArgList { args, 2 }, regExpGlobalObject->regExpConstructor()));
+ JSObject* regExpConstructor = regExpGlobalObject->regExpConstructor();
+ OPERATION_RETURN(scope, constructRegExp(regExpGlobalObject, ArgList { args, 2 }, regExpConstructor, regExpConstructor));

JSTests/stress/dfg-miscompiles-new-regexp.js

+function opt(regExp) {
+ return new RegExp(regExp, undefined);
+}
+
+function main() {
+ const regExp = /a/;
+ for (let i = 0; i < testLoopCount; i++) {
+ if (opt(regExp) === regExp) {
+ throw new Error(`Bug triggered at ${i}`);
+ break;
+ }
+ }
+}
+main();

The fix adds a single argument to the constructRegExp call. Before, the call passed three arguments: regExpGlobalObject, ArgList { args, 2 }, and regExpGlobalObject->regExpConstructor(). After, a fourth argument — the same regExpConstructor — is passed as the newTarget parameter. The regExpConstructor is now extracted into a local variable to avoid calling the getter twice.

JSC compiles hot functions through an interpreter (LLInt), then a baseline JIT, then the DFG (mid-tier optimizing JIT). The NewRegExpUntyped node is emitted by the DFG bytecode parser for new RegExp(...) calls where the newTarget differs from the call target, indicating a constructor invocation rather than a plain function call.

Per ECMAScript §22.2.3.1, when RegExp is called as a function (no new), the spec permits returning the input pattern directly if the pattern is already a RegExp and flags is undefined. When called as a constructor (new), it must always create a new RegExp object. The distinction hinges on whether NewTarget is defined. The constructRegExp function uses its fourth parameter as the newTarget — when absent, it follows the function-call path.

Missing newTarget propagation in a DFG operation stub, causing constructor semantics to degrade to function-call semantics.

Without newTarget, constructRegExp follows the ES2025 §22.2.3.1 function-call path: when the pattern is already a RegExp and flags is undefined, the spec permits returning the same RegExp object. This means DFG-compiled code for new RegExp(existingRegExp, undefined) could return the original object instead of a fresh copy, violating the invariant that the new operator always produces a distinct object. The bytecode parser correctly identifies this as a new invocation (by checking newTargetNode != callTargetNode, as described in the commit message), but the information is lost at the operation boundary.

🔒

Explores the downstream security implications of object identity confusion and how shared mutable state could be weaponized

Subscribe to read more

🔒

Multiple audit patterns identified for DFG call-vs-construct semantic mismatches, with concrete search targets across operation stubs

Subscribe to read more