Released July 29, 2025

CVE-2025-43216

CVSS 6.5 severity medium CWE-416 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H

Overview

Impact:

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

Researchers: Ignacio Sanmillan (@ulexec)

Key insight. A ref-counted object was cached by raw pointer, so the cache could hand back an object that was already being destroyed; switching the cache to a ThreadSafeWeakPtr (and only self-removing) makes destruction observable and closes the UAF.

Patch Details

diff --git a/Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp b/Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp
index 8ba953418987..5715b0ab7abb 100644
--- a/Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp
+++ b/Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp
@@ -40,9 +40,9 @@
 namespace WebCore {
 
 // Keeps track of loaders on a per-sample-rate basis.
-static HashMap<double, HRTFDatabaseLoader*>& loaderMap()
+static HashMap<double, ThreadSafeWeakPtr<HRTFDatabaseLoader>>& loaderMap()
 {
-    static NeverDestroyed<HashMap<double, HRTFDatabaseLoader*>> loaderMap;
+    static NeverDestroyed<HashMap<double, ThreadSafeWeakPtr<HRTFDatabaseLoader>>> loaderMap;
     return loaderMap;
 }
 
@@ -50,13 +50,13 @@ Ref<HRTFDatabaseLoader> HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessa
 {
     ASSERT(isMainThread());
 
-    if (RefPtr<HRTFDatabaseLoader> loader = loaderMap().get(sampleRate)) {
+    if (RefPtr<HRTFDatabaseLoader> loader = loaderMap().get(sampleRate).get()) {
         ASSERT(sampleRate == loader->databaseSampleRate());
         return loader.releaseNonNull();
     }
 
     auto loader = adoptRef(*new HRTFDatabaseLoader(sampleRate));
-    loaderMap().add(sampleRate, loader.ptr());
+    loaderMap().set(sampleRate, loader.ptr());
 
     loader->loadAsynchronously();
 
@@ -76,8 +76,18 @@ HRTFDatabaseLoader::~HRTFDatabaseLoader()
     waitForLoaderThreadCompletion();
     m_hrtfDatabase = nullptr;
 
-    // Remove ourself from the map.
-    loaderMap().remove(m_databaseSampleRate);
+    // Try to remove ourselves from the map, but
+    // not if the cached loader with our sample
+    // rate is not actually the this being destroyed.
+    auto it = loaderMap().find(m_databaseSampleRate);
+    if (it == loaderMap().end())
+        return;
+
+    RefPtr loader = it->value.get();
+    if (loader && loader.get() != this)
+        return;
+
+    loaderMap().remove(it);
 }
 
 void HRTFDatabaseLoader::load()

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-43216
Bugzilla 295382
Component WebCore
Bug Class UAF
Severity medium
Commit 4637324afb310494…
Advisory Apple Advisory

Terminology

  • HRTF panner — Web Audio’s head-related transfer function spatializer; it loads a per-sample-rate HRTF database asynchronously on a loader thread.
  • Raw vs weak pointer — A raw cached pointer does not observe destruction; ThreadSafeWeakPtr becomes null when the referent is freed, safely across threads.
  • adoptRef / ref-counting — HRTFDatabaseLoader is reference counted; a cache holding a non-owning raw pointer can outlive the object.

Root Cause Analysis

This fixes a use-after-free (and a related teardown race) in the Web Audio HRTF panner’s per-sample-rate loader cache. loaderMap() cached each HRTFDatabaseLoader keyed by sample rate as a RAW pointer (HashMap<double, HRTFDatabaseLoader*>). HRTFDatabaseLoader is ref-counted (created via adoptRef), so the map held a non-owning raw pointer to an object whose lifetime is controlled elsewhere. createAndLoadAsynchronouslyIfNecessary looked the loader up with loaderMap().get(sampleRate) and adopted it into a RefPtr; if the cached loader had already reached refcount zero and begun destruction, that raw pointer was dangling and reviving it was a use-after-free. Symmetrically, ~HRTFDatabaseLoader unconditionally did loaderMap().remove(m_databaseSampleRate), which — if a NEW loader for the same sample rate had already replaced the entry — removed the wrong (live) entry. The fix stores ThreadSafeWeakPtr<HRTFDatabaseLoader> in the map: the lookup upgrades with .get() and safely returns null when the object is gone (forcing creation of a fresh loader), add() becomes set(), and the destructor now finds the entry, upgrades it, and only removes it when the cached loader is actually ’this’ (guarding against clobbering a replacement). The restored invariant is that the cache never hands out or removes a loader whose lifetime has ended; the weak pointer makes destruction observable across threads.

Attack Path

1. Create an HRTF panner

Use Web Audio (PannerNode with HRTF panningModel) at a chosen sample rate to trigger HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary.

2. Drop the loader

Release references so the loader’s refcount hits zero and it begins destruction while its raw pointer is still cached in loaderMap().

3. Re-request the same sample rate

Ask for a panner at the same sample rate so the stale raw pointer is fetched from the map and revived via adoptRef — a use-after-free.

4. Groom and exploit

Reclaim the freed HRTFDatabaseLoader allocation with controlled data to turn the UAF into a stronger primitive in the WebContent process.

Impact Assessment

A use-after-free in the WebContent process reachable from ordinary Web Audio content, with a cross-thread component (the loader thread). Reviving a freed ref-counted object via a stale cache pointer is a controllable UAF; with heap grooming it can escalate beyond the crash the advisory describes toward memory disclosure or control-flow hijack in WebContent.

Audit Directions

Same pattern: raw pointers in static caches

Grep for HashMap<…, T*> NeverDestroyed caches of ref-counted T; each is a candidate for the same dangling-entry UAF and should hold a Weak/ThreadSafeWeakPtr.

Unconditional self-removal in destructors

Look for destructors that remove(key) from a shared map without confirming the mapped entry is ’this’ — they can evict a replacement.

Audio loader lifetime

Review other platform/audio singletons (reverb/convolution kernels) for the same per-sample-rate cache lifetime assumptions.

Changed Functions

Function File Change Note
loaderMap Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp modified Changes the cache value type from HRTFDatabaseLoader* to ThreadSafeWeakPtr<HRTFDatabaseLoader> so entries do not dangle.
HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp modified Upgrades the weak entry with .get() (null when destroyed) and uses set() instead of add() when inserting the new loader.
HRTFDatabaseLoader::~HRTFDatabaseLoader Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp modified Only removes the map entry when the cached loader is actually this, preventing removal of a replacement loader for the same sample rate.

Files Changed

Source Files

  • Source/WebCore/platform/audio/HRTFDatabaseLoader.cpp