CVE-2026-43658

Overview

Advisory: Apple Security Advisory

Impact:

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

Researchers: Do Young Park

Attribute Value
CVE CVE-2026-43658
Bugzilla 307669
Component JSC
Bug Class UAF
Severity medium
Commit 9a16de46f205d3c7…
Advisory Apple Advisory

Root Cause Analysis

WebAssembly BBQCallee could be freed while its OMGOSREntryCallee was still being referenced during callsite repatching, causing use-after-free. Fix: updateCallsitesToCallUs now keeps BBQCallee alive via keepAliveBBQCallees vector and only keeps OMGOSREntryCallee alive if the BBQ callee was not already tracked.

Attack Path

1. Step 1

Attacker triggers WebAssembly JIT compilation (BBQ tier).

2. Step 2

Concurrent OSR entry causes callsite repatching.

3. Step 3

BBQCallee is freed during repatching while OMGOSREntryCallee still references it.

4. Step 4

Subsequent call accesses freed BBQCallee memory.

5. Step 5

Use-after-free leads to code execution or sandbox escape.

Files Changed

Source Files

  • Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp

Patch Preview

diff --git a/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp b/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp
index 3b271fc5daf3..4cf13d7638e8 100644
--- a/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp
+++ b/Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp
@@ -345,9 +345,18 @@ void CalleeGroup::updateCallsitesToCallUs(const AbstractLocker& locker, CodeLoca
     };
 
     // This is necessary since Callees are released under `Heap::stopThePeriphery()`, but that only stops JS compiler
-    // threads and not wasm ones. So the OMGOSREntryCallee could die between the time we collect the callsites and when
-    // we actually repatch its callsites.
+    // threads and not wasm ones. So a weakly held BBQCallee and its OMGOSREntryCallee could die between the time we
+    // collect the callsites and when we actually repatch its callsites. Since BBQCallee owns OMGOSREntryCallee,
+    // keeping BBQCallee alive is enough to ensure that both are alive for the required duration.
+    //
+    // There is however an edge case here - it can happen that a BBQCallee has been freed but its OMGOSREntryCallee
+    // has been added to the pending-destruction set and not yet free'd. This means that m_osrEntryCallees will still
+    // hold a weak ref to it. In this scenario, BBQCallee won't be kept alive since it does not exist so we manually
+    // have to keep the OMGOSREntryCallee alive separately. This should only be done in this scenario else we will
+    // end up with multiple owners for OMGOSREntryCallee.
+
     // FIXME: These inline capacities were picked semi-randomly. We should figure out if there's a better number.
+    Vector<Ref<BBQCallee>, 4> keepAliveBBQCallees;
     Vector<Ref<OMGOSREntryCallee>, 4> keepAliveOSREntryCallees;
     Vector<Callsite, 16> callsites;
 
@@ -373,6 +382,8 @@ void CalleeGroup::updateCallsitesToCallUs(const AbstractLocker& locker, CodeLoca
         if (!tuple)
             return;
 
+        bool bbqCalleeKeptAlive = false;
+        UNUSED_VARIABLE(bbqCalleeKeptAlive);
 #if ENABLE(WEBASSEMBLY_BBQJIT)
         // This callee could be weak but we still need to update it since it could call our BBQ callee
         // that we're going to want to destroy.
@@ -384,6 +395,8 @@ void CalleeGroup::updateCallsitesToCallUs(const AbstractLocker& locker, CodeLoca
         if (bbqCallee) {
             collectCallsites(bbqCallee.get());
             ASSERT(!bbqCallee->osrEntryCallee() || m_osrEntryCallees.find(callerIndex) != m_osrEntryCallees.end());
+            keepAliveBBQCallees.append(bbqCallee.releaseNonNull());
+            bbqCalleeKeptAlive = true;
         }
 #endif
 #if ENABLE(WEBASSEMBLY_OMGJIT)
@@ -391,7 +404,17 @@ void CalleeGroup::updateCallsitesToCallUs(const AbstractLocker& locker, CodeLoca
         if (auto iter = m_osrEntryCallees.find(callerIndex); iter != m_osrEntryCallees.end()) {
             if (RefPtr callee = iter->value.get()) {
                 collectCallsites(callee.get());
-                keepAliveOSREntryCallees.append(callee.releaseNonNull());
+                // If we track the OMGOSREntryCallee as a callsite there are 2 possibilities -
+                // 1. The BBQCallee is already being tracked - in this case we don't have to
+                //    track the OMGOSREntryCallee since the BBQCallee owns it and keeping the
+                //    BBQCallee alive is good enough to keep the OMGOSREntryCallee alive. Also,
+                //    OMGOSREntryCallee is only supposed to be owned by BBQCallee
+                // 2. The BBQCallee is not tracked - This happens if the BBQCallee is already
+                //    released but the OMGOSREntryCallee is still alive. In this case there is
+                //    no other strong reference to OMGOSREntryCallee so we have to keep it
+                //    alive here.
+                if (!bbqCalleeKeptAlive)
+                    keepAliveOSREntryCallees.append(callee.releaseNonNull());
             } else
                 m_osrEntryCallees.remove(iter);
         }