Released September 15, 2025

CVE-2025-43368

CVSS 4.3 severity medium CWE-416 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: Pawel Wylecial of REDTEAM.PL working with Trend Micro Zero Day Initiative, Ignacio Sanmillan (@ulexec)

Key insight. Message dispatch called into the Client through a non-owning CheckedPtr, so a handler that tore down the client mid-call left a dangling pointer; retaining the client with a RefPtr for the duration of each call fixes it.

Patch Details

diff --git a/Source/WebKit/Platform/IPC/Connection.cpp b/Source/WebKit/Platform/IPC/Connection.cpp
index 6c8a0b8b5439..922888a03343 100644
--- a/Source/WebKit/Platform/IPC/Connection.cpp
+++ b/Source/WebKit/Platform/IPC/Connection.cpp
@@ -1249,7 +1249,7 @@ void Connection::dispatchSyncMessage(Decoder& decoder)
         } else
             decoder.markInvalid();
     } else
-        checkedClient()->didReceiveSyncMessage(*this, decoder, replyEncoder);
+        protectedClient()->didReceiveSyncMessage(*this, decoder, replyEncoder);
 
     // If the message was not handled, i.e. replyEncoder was not consumed, reply with cancel
     // message. We do not distinquish between a decode failure and failure to find a
@@ -1267,7 +1267,7 @@ void Connection::dispatchDidReceiveInvalidMessage(MessageName messageName, int32
     dispatchToClient([protectedThis = Ref { *this }, messageName, indexOfObjectFailingDecoding] {
         if (!protectedThis->isValid())
             return;
-        protectedThis->checkedClient()->didReceiveInvalidMessage(protectedThis, messageName, indexOfObjectFailingDecoding);
+        protectedThis->protectedClient()->didReceiveInvalidMessage(protectedThis, messageName, indexOfObjectFailingDecoding);
     });
 }
 
@@ -1276,9 +1276,10 @@ void Connection::dispatchDidCloseAndInvalidate()
     dispatchToClient([protectedThis = Ref { *this }] {
         // If the connection has been explicitly invalidated before dispatchConnectionDidClose was called,
         // then the connection client will be nullptr here.
-        if (!protectedThis->m_client)
+        RefPtr client = protectedThis->m_client.get();
+        if (!client)
             return;
-        protectedThis->checkedClient()->didClose(protectedThis);
+        client->didClose(protectedThis);
         protectedThis->invalidate();
     });
 }
@@ -1314,7 +1315,7 @@ void Connection::enqueueIncomingMessage(UniqueRef<Decoder> incomingMessage)
         if (isIncomingMessagesThrottlingEnabled() && m_incomingMessages.size() >= maxPendingIncomingMessagesKillingThreshold) {
             m_didRequestProcessTermination = true;
             dispatchToClientWithIncomingMessagesLock([protectedThis = Ref { *this }] {
-                CheckedPtr client = protectedThis->m_client;
+                RefPtr client = protectedThis->m_client.get();
                 if (!client)
                     return;
                 client->requestRemoteProcessTermination();
@@ -1348,7 +1349,8 @@ void Connection::enqueueIncomingMessage(UniqueRef<Decoder> incomingMessage)
 void Connection::dispatchMessage(Decoder& decoder)
 {
     assertIsCurrent(dispatcher());
-    RELEASE_ASSERT(m_client);
+    RefPtr client = m_client.get();
+    RELEASE_ASSERT(client);
     if (decoder.messageReceiverName() == ReceiverName::AsyncReply) {
         auto handler = takeAsyncReplyHandler(AtomicObjectIdentifier<AsyncReplyIDType>(decoder.destinationID()));
         if (!handler) {
@@ -1378,7 +1380,7 @@ void Connection::dispatchMessage(Decoder& decoder)
     }
 #endif
 
-    checkedClient()->didReceiveMessage(*this, decoder);
+    client->didReceiveMessage(*this, decoder);
 }
 
 void Connection::dispatchMessage(UniqueRef<Decoder> message)
@@ -1406,7 +1408,7 @@ void Connection::dispatchMessage(UniqueRef<Decoder> message)
             if (m_ignoreInvalidMessageForTesting)
                 return;
 #endif
-            checkedClient()->didReceiveInvalidMessage(*this, message->messageName(), message->indexOfObjectFailingDecoding());
+            protectedClient()->didReceiveInvalidMessage(*this, message->messageName(), message->indexOfObjectFailingDecoding());
             return;
         }
         m_inDispatchMessageMarkedToUseFullySynchronousModeForTesting++;
@@ -1458,7 +1460,7 @@ void Connection::dispatchMessage(UniqueRef<Decoder> message)
     }
 #endif
     if (didReceiveInvalidMessage && isValid())
-        checkedClient()->didReceiveInvalidMessage(*this, message->messageName(), message->indexOfObjectFailingDecoding());
+        protectedClient()->didReceiveInvalidMessage(*this, message->messageName(), message->indexOfObjectFailingDecoding());
 }
 
 size_t Connection::numberOfMessagesToProcess(size_t totalMessages)
diff --git a/Source/WebKit/Platform/IPC/Connection.h b/Source/WebKit/Platform/IPC/Connection.h
index 0c0eb5c21d41..eda198806654 100644
--- a/Source/WebKit/Platform/IPC/Connection.h
+++ b/Source/WebKit/Platform/IPC/Connection.h
@@ -338,7 +338,7 @@ public:
     ~Connection();
 
     Client* client() const { return m_client.get(); }
-    CheckedPtr<Client> checkedClient() const { return m_client; }
+    RefPtr<Client> protectedClient() const { return m_client.get(); }
 
     enum UniqueIDType { };
     using UniqueID = AtomicObjectIdentifier<UniqueIDType>;
@@ -884,7 +884,7 @@ template<typename T> Error Connection::waitForAndDispatchImmediately(uint64_t de
         return Error::InvalidConnection;
 
     ASSERT(decoderOrError.value()->destinationID() == destinationID);
-    checkedClient()->didReceiveMessage(*this, decoderOrError.value());
+    protectedClient()->didReceiveMessage(*this, decoderOrError.value());
     return Error::NoError;
 }
 

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-43368
Bugzilla 296276
Component WebKit
Bug Class UAF
Severity medium
Commit 674611789255a98f…
Advisory Apple Advisory

Terminology

  • IPC Connection/Client — WebKit’s inter-process message channel delivers decoded messages to a Client object via callbacks like didReceiveMessage.
  • CheckedPtr vs RefPtr — CheckedPtr detects use-after-free in debug but does not extend lifetime; RefPtr holds a reference that keeps the object alive.
  • Re-entrant teardown — A message handler can invalidate/close the connection and destroy the client while a dispatch call is still executing.

Root Cause Analysis

This fixes a use-after-free of the IPC Connection’s Client during message dispatch by replacing non-owning CheckedPtr access with a ref-holding RefPtr. Connection dispatches incoming messages to its Client (didReceiveMessage, didReceiveSyncMessage, didClose, didReceiveInvalidMessage) via checkedClient(), which returned a CheckedPtr<Client>. A CheckedPtr only verifies (in debug) that the pointee has not been freed; it does NOT keep the Client alive. Message handlers can run code that re-entrantly tears down the connection’s client (or the client can be destroyed on another thread), so a call like checkedClient()->didReceiveMessage(*this, decoder) could dereference a Client that is being or has been destroyed — a use-after-free. The fix renames checkedClient() to protectedClient() returning RefPtr<Client> (m_client.get()), and updates the hot dispatch paths to take a local RefPtr client = m_client.get(), null-check it, and call through it, so the Client is guaranteed to stay alive for the duration of each dispatch call. dispatchDidCloseAndInvalidate and the throttling-termination path likewise load a RefPtr and bail if null. The restored invariant is that the Client outlives every dispatch call made on it; a checked (non-owning) pointer was insufficient across calls that can drop the last reference.

Attack Path

1. Send messages that re-enter

A process on one end of the IPC connection sends messages whose handlers cause the connection’s Client to be torn down (invalidate/close) during dispatch.

2. Dispatch through a non-owning pointer

Connection calls checkedClient()->didReceive… which does not retain the Client across the call.

3. Free during the call

The Client is destroyed while a dispatch call is in flight, leaving a dangling pointer.

4. Use-after-free

Continued use of the freed Client corrupts memory in the receiving process.

Impact Assessment

A use-after-free on the IPC Client in whichever process receives the messages (UI, GPU, Network or WebContent), reachable when a peer sends messages that trigger re-entrant client teardown during dispatch. UAFs on core IPC objects are broadly reachable and exploitable with grooming; the advisory rates it a crash, but the class routinely escalates toward code execution in the receiving process.

Audit Directions

Same file: remaining checkedClient users

Grep Connection.cpp/.h for any residual checkedClient()/CheckedPtr client uses on paths that can run client code and confirm they hold a RefPtr.

CheckedPtr across callbacks

Across WebKit, look for CheckedPtr members dereferenced across virtual callbacks that can free the pointee; these need protected*/RefPtr upgrades.

Changed Functions

Function File Change Note
Connection::protectedClient (was checkedClient) Source/WebKit/Platform/IPC/Connection.h modified Returns RefPtr<Client> (retaining) instead of CheckedPtr<Client> (non-owning).
Connection::dispatchMessage Source/WebKit/Platform/IPC/Connection.cpp modified Loads a local RefPtr client, RELEASE_ASSERTs it, and calls didReceiveMessage through it, keeping the client alive across dispatch.
Connection::dispatchSyncMessage / dispatchDidReceiveInvalidMessage / dispatchDidCloseAndInvalidate / enqueueIncomingMessage Source/WebKit/Platform/IPC/Connection.cpp modified Switch from checkedClient() to protectedClient()/RefPtr client with null checks before invoking client callbacks.
Connection::waitForAndDispatchImmediately Source/WebKit/Platform/IPC/Connection.cpp modified Uses protectedClient()->didReceiveMessage to retain the client across the synchronous dispatch.

Files Changed

Source Files

  • Source/WebKit/Platform/IPC/Connection.cpp
  • Source/WebKit/Platform/IPC/Connection.h