Summary
In the wild exploit targeting Chrome, UAF within the Browser process have frequently been a key vector for sandbox escapes.
In this post, we introduce two newly discovered UAF within the Browser process, identified during our vulnerability research. In the past, these flaws could have led to critical exploits, but thanks to Chrome’s latest security technology, MiraclePtr, they are no longer exploitable.
MiraclePtr is a protection mechanism designed to prevent UAF in the Browser process and is now widely deployed across key components. This article provides an analysis of the two vulnerabilities, along with an overview of how MiraclePtr works and its implementation.
Credit
A researcher working for SSD Labs Korea.
Affected Versions
- Chrome versions after and including 133.0.6835.0 and before 135.0.7016.0
Technical Analysis
If you examine, components/sync/service/sync_service_impl.cc, the following code can be spotted
void SyncServiceImpl::GetLocalDataDescriptions(
DataTypeSet types,
base::OnceCallback<void(std::map<DataType, LocalDataDescription>)>
callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Some code paths in GetLocalDataDescriptionsImpl() are synchronous, e.g.
// if `types` have synchronous DataTypeLocalDataBatchUploader implementations.
// Having an API that is sometime sync and sometimes async can be unexpected
// to the caller and lead to bugs such as crbug.com/361088051. To avoid those,
// post a task here to ensure the call is always async.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&SyncServiceImpl::GetLocalDataDescriptionsImpl,
weak_factory_.GetWeakPtr(), types, std::move(callback)));
}
Where:
- The
GetLocalDataDescriptionsImplfunction ofSyncServiceis bound toWeakPtrand posted to the task list. - The
GetLocalDataDescriptionsImplfunction receives the previously delivered callback as a param.
Inside components/sync/service/sync_service_impl.cc, you can see:
void SyncServiceImpl::GetLocalDataDescriptionsImpl(
DataTypeSet types,
base::OnceCallback<void(std::map<DataType, LocalDataDescription>)>
callback) {
// Syncing users do not use separate local and account storages. Thus, there's
// no local-only data.
if (HasSyncConsent()) {
std::move(callback).Run({});
return;
}
data_type_manager_->GetLocalDataDescriptions(types, std::move(callback));
}
Where:
- The
GetLocalDataDescriptionsImplfunction executes the received callback function. - If the bound cb instance gets destroyed while the posted task is being executed, UAF could occur.
UAF Case #1
When accessing Chrome’s Password Manager page, the HandleGetLocalPasswordCount function is called to retrieve the number of locally stored passwords.
Inside chrome/browser/ui/webui/password_manager/sync_handler.cc, you can see:
void SyncHandler::HandleGetLocalPasswordCount(const base::Value::List& args) {
AllowJavascript();
CHECK_EQ(1U, args.size());
const base::Value& callback_id = args[0];
syncer::SyncService* sync_service = GetSyncService();
if (!sync_service) {
ResolveJavascriptCallback(callback_id, base::Value(false));
return;
}
sync_service->GetLocalDataDescriptions(
{syncer::PASSWORDS},
base::BindOnce(&SyncHandler::HandleOnGetLocalDataDescriptionReceived,
base::Unretained(this), callback_id.Clone()));
}
Where:
- The
HandleOnGetLocalDataDescriptionReceivedfunction ofSyncHandleris bound with a raw pointer and transmitted to theGetLocalDataDescriptionsfunction ofSyncService.
Proof of Concept
chrome.windows.create({ url: 'chrome://password-manager' }, (tab) => {
setTimeout(() => {
chrome.windows.remove(tab.id);
}, 8000);
});
UAF Case #2
When the sync state of Chrome changes, the OnStateChanged function is called, as can be seen in the following code chrome/browser/ui/webui/password_manager/sync_handler.cc:
void SyncHandler::OnStateChanged(syncer::SyncService* sync_service) {
FireWebUIListener("trusted-vault-banner-state-changed",
GetTrustedVaultBannerState());
FireWebUIListener("sync-info-changed", GetSyncInfo());
if (sync_service->GetTransportState() !=
syncer::SyncService::TransportState::CONFIGURING) {
sync_service->GetLocalDataDescriptions(
{syncer::PASSWORDS},
base::BindOnce(&SyncHandler::FireOnGetLocalDataDescriptionReceived,
base::Unretained(this)));
}
}
Therefore:
- The
FireOnGetLocalDataDescriptionReceivedfunction ofSyncHandleris bound with a raw pointer and transmitted to theGetLocalDataDescriptionsfunction ofSyncService.
Proof of Concept
chrome.windows.create({ url: 'chrome://settings/signOut' }, (tab) => {
setTimeout(() => {
chrome.windows.remove(tab.id);
}, 500);
});
- Crash occurs when the
Turn offbutton is clicked once.
Exploitability
If you examine content/browser/webui/web_ui_message_handler.cc, you will notice:
bool WebUIMessageHandler::IsJavascriptAllowed() {
return javascript_allowed_ && web_ui() && web_ui()->CanCallJavascript();
}
Where:
- The first point where the member variables and member functions of the freed
SyncHandlerobject are accessed.
If you examine content/public/browser/web_ui.h you can see that:
virtual bool CanCallJavascript() = 0;
Where:
- The
CanCallJavascriptfunction of the freed object is a virtual function.
An attacker can exploit well-known heap spraying techniques to corrupt the vTable and achieve arbitrary code execution outside the sandbox (before the era of MiraclePtr).
MiraclePtr Status: PROTECTED MiraclePtr is expected to make this crash non-exploitable once fully enabled.
- Both of the vulnerabilities discovered are protected by
MiraclePtr, and thus are not exploitable.
MiraclePtr
MiraclePtr is a technique designed to prevent the exploitation of UAF. Among various implementation approaches, the BackupRefPtr method was chosen as the most reasonable alternative, considering both performance overhead and stability.

BRP is a reference counting based mitigation technique that leverages Chrome’s custom heap allocator, PartitionAlloc. Each allocated memory region is accompanied by a hidden reference counter, which is used to track the object’s usage state.
For example, when delete(/free) is called in Chrome’s codebase, if the object’s reference count is greater than zero, PartitionAlloc does not immediately deallocate the object. Instead, it moves the object to a quarantine area.
In other words, as long as an object is still being referenced, it is not actually freed from memory, making it impossible for an attacker to reclaim that memory region via heap spraying or similar techniques. This effectively blocks UAF-based exploitation at the root.
Additionally, the technique is designed to fill the memory region with specific bit patterns or trigger intentional crashes, ensuring that even if an attacker attempts to exploit it, the result is merely a crash rather than successful code execution.
Implementation of MiraclePtr
(A rough pseudo-code from MiraclePtr team)
void* Alloc(size_t size) {
void* ptr = ActuallyAlloc(size);
if (isSupportedAllocation(ptr)) {
int& ref_count = *(cast<int*>(ptr) - 1);
ref_count = 1;
}
return ptr;
}
- When memory is allocated, the reference counter is set to 1 and the memory is returned.
void Free(void* ptr) {
if (isSupportedAllocation(ptr)) {
atomic_int& ref_count = *(cast<atomic_int*>(ptr) - 1);
if (ref_count != 1)
memset(ptr, 0xcc, getAllocationSize(ptr));
if (--ref_count != 0)
return;
}
ActuallyFree(ptr);
}
- When memory is freed, if there are remaining references to the memory, it is initialized to
0xcc. - Then, the reference counter is decremented, and memory is only freed if the counter reaches zero, ensuring no other references exist.
template <typename T>
class BackupRefPtr {
BackupRefPtr(T* ptr) : ptr_(ptr) {
if (!isSupportedAllocation(ptr))
return;
atomic_int& ref_count = *(cast<atomic_int*>(ptr) - 1);
CHECK(++ref_count);
}
~BackupRefPtr() {
if (!isSupportedAllocation(ptr_))
return;
atomic_int& ref_count = *(cast<atomic_int*>(ptr) - 1);
if (--ref_count == 0)
PartitionAlloc::ActuallyFree(ptr_);
}
T* operator->() { return ptr_; }
T* ptr_;
};
- When a pointer references an existing memory region instead of directly allocating new memory, the reference counter is incremented.
- The memory is only freed if this pointer is the last remaining reference — that is, when decrementing the counter results in zero.