CVE-2025-43368
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.cppSource/WebKit/Platform/IPC/Connection.h