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);
}