CVE-2026-43658
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Do Young Park
Key insight. When repatching a callee’s callsites, keep the owning object (BBQCallee) alive for the whole operation; pinning only the weakly-held OMGOSREntryCallee missed the case where the owner is freed.
Patch Details
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);
}
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43658 |
| Bugzilla | 307669 |
| Component | JSC |
| Bug Class | UAF |
| Severity | medium |
| Commit | 9a16de46f205d3c7… |
| Advisory | Apple Advisory |
Terminology
- BBQ / OMG JIT callees — Wasm tiered-compilation callees; BBQCallee owns its OMGOSREntryCallee.
- Weak vs strong reference — m_osrEntryCallees holds weak refs; keeping the owning BBQCallee alive keeps both alive.
- Callsite repatching — updateCallsitesToCallUs collects callsites then repatches them, needing the callees alive throughout.
Root Cause Analysis
This fixes a use-after-free in WebAssembly callsite repatching (WasmCalleeGroup). CalleeGroup::updateCallsitesToCallUs collects callsites that call a callee it is about to destroy and then repatches them. As the comment notes, Callees are released under Heap::stopThePeriphery(), which stops JS compiler threads but NOT wasm compiler threads, so a weakly-held BBQCallee and the OMGOSREntryCallee it owns could be freed between the moment their callsites are collected and the moment they are actually repatched — dereferencing them during repatch is a use-after-free. Before the fix only the OMGOSREntryCallees were pinned (keepAliveOSREntryCallees). The fix additionally pins the BBQCallee for the duration by appending it to keepAliveBBQCallees (bbqCallee.releaseNonNull()); because BBQCallee owns the OMGOSREntryCallee, keeping the BBQCallee alive keeps both alive. To avoid creating a second owner, the OMGOSREntryCallee is separately kept alive only in the edge case where the BBQCallee has already been freed while its OMGOSREntryCallee sits in the pending-destruction set (guarded by bbqCalleeKeptAlive). The restored invariant is that any callee whose callsites are being repatched stays alive across the whole operation. INFERENCE: the exact field dereferenced after free is in the repatch/collectCallsites path; the commit establishes the missing keep-alive.
Attack Path
1. Instantiate a tiered WebAssembly module
JS builds and runs a module so functions are compiled to BBQ and OMG tiers with OSR-entry callees.
2. Trigger callsite repatching
Tier-up/replacement causes CalleeGroup::updateCallsitesToCallUs to collect and repatch callsites for a callee being destroyed.
3. Free a weakly-held callee concurrently
Because wasm compiler threads are not stopped by stopThePeriphery(), a weakly-held BBQCallee (and its owned OMGOSREntryCallee) is released between callsite collection and repatch.
4. Use-after-free during repatch
Repatching dereferences the freed callee, crashing the process (now prevented by pinning the BBQCallee/OMGOSREntryCallee).
Impact Assessment
A use-after-free during wasm callsite repatching (callees freed by wasm threads mid-operation); medium crash, a UAF that could be shaped with tier-up timing.
Audit Directions
Weakly-held callees used after collection
Audit CalleeGroup for weak refs dereferenced between collection and repatch; ensure keep-alive spans the operation.
Owner vs owned lifetime
Where an owned object is kept alive via its owner, verify the owner (not just the owned) is pinned.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
CalleeGroup::updateCallsitesToCallUs |
Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp |
modified | Adds keepAliveBBQCallees and pins each collected BBQCallee (releaseNonNull) for the repatch duration; only separately pins the OMGOSREntryCallee in the edge case where the owning BBQCallee is already freed, avoiding a double owner. |
Files Changed
Source Files
Source/JavaScriptCore/wasm/WasmCalleeGroup.cpp