Skip to content

Commit 7a5ee54

Browse files
cdumezJonWBedard
authored andcommitted
UAF due to cross-thread destruction of worker DeferredPromise in WebLockManager::query()
https://bugs.webkit.org/show_bug.cgi?id=312456 rdar://174652399 Reviewed by Ryosuke Niwa. WebLockManager::MainThreadBridge::query() was taking in a CompletionHandler but it may sometimes fail to call its completion handler. This happened when the worker thread is exiting, causing `ScriptExecutionContext::ensureOnContextThread()` to fail. Not calling the completion handler is bad but what's worse is that the completion handler would end up getting destroyed on the main thread. The completion handler was capturing a promise from the worker thread, which led to security bugs. To address the issue: 1. Have WebLockManager::MainThreadBridge::query() take in a Function instead of a CompletionHandler given that it cannot always call its callback. 2. Have WebLockManager store the promise in a HashMap and only capture a promise identifier in the MainThreadBridge::query() lambda instead of the promise itself. This pattern was already used for other promises in this class. Test: workers/weblock-manager-query-crash.html * LayoutTests/workers/weblock-manager-query-crash-expected.txt: Added. * LayoutTests/workers/weblock-manager-query-crash.html: Added. * Source/WebCore/Modules/web-locks/WebLockManager.cpp: (WebCore::WebLockManager::MainThreadBridge::abortLockRequest): (WebCore::WebLockManager::MainThreadBridge::query): (WebCore::WebLockManager::query): (WebCore::WebLockManager::clientIsGoingAway): * Source/WebCore/Modules/web-locks/WebLockManager.h: Originally-landed-as: 305413.688@safari-7624-branch (0b964ced2532). rdar://180436136 Canonical link: https://commits.webkit.org/316195@main
1 parent cfced00 commit 7a5ee54

4 files changed

Lines changed: 72 additions & 21 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This test passes if it doesn't crash.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<script>
5+
globalThis.testRunner?.dumpAsText();
6+
globalThis.testRunner?.waitUntilDone();
7+
8+
const workerSrc = `
9+
for (let i = 0; i < 500; i++) self.navigator.locks.query();
10+
self.postMessage("ready");
11+
for (let i = 0; i < 500; i++) self.navigator.locks.query();
12+
`;
13+
const blob = new Blob([workerSrc], {type: "text/javascript"});
14+
const url = URL.createObjectURL(blob);
15+
16+
function log(msg) {
17+
if (globalThis.$vm) $vm.print(msg);
18+
console.log(msg);
19+
}
20+
21+
let iterations = 0;
22+
function spawn() {
23+
if (iterations++ > 50) {
24+
globalThis.testRunner?.notifyDone();
25+
return;
26+
}
27+
let w = new Worker(url);
28+
w.onmessage = () => {
29+
w.terminate();
30+
w = null;
31+
if (globalThis.$vm) $vm.gc();
32+
setTimeout(spawn, 0);
33+
};
34+
}
35+
36+
window.onload = spawn;
37+
</script>
38+
</head>
39+
<body>
40+
This test passes if it doesn't crash.
41+
</body>
42+
</html>

Source/WebCore/Modules/web-locks/WebLockManager.cpp

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ class WebLockManager::MainThreadBridge : public ThreadSafeRefCounted<MainThreadB
9696

9797
void requestLock(WebLockIdentifier, const String& name, const Options&, Function<void(bool)>&&, Function<void()>&& lockStolenHandler);
9898
void releaseLock(WebLockIdentifier, const String& name);
99-
void abortLockRequest(WebLockIdentifier, const String& name, CompletionHandler<void(bool)>&&);
100-
void query(CompletionHandler<void(Snapshot&&)>&&);
99+
void abortLockRequest(WebLockIdentifier, const String& name, Function<void(bool)>&&);
100+
void query(Function<void(Snapshot&&)>&&);
101101
void clientIsGoingAway();
102102

103103
private:
@@ -137,23 +137,23 @@ void WebLockManager::MainThreadBridge::releaseLock(WebLockIdentifier lockIdentif
137137
});
138138
}
139139

140-
void WebLockManager::MainThreadBridge::abortLockRequest(WebLockIdentifier lockIdentifier, const String& name, CompletionHandler<void(bool)>&& completionHandler)
140+
void WebLockManager::MainThreadBridge::abortLockRequest(WebLockIdentifier lockIdentifier, const String& name, Function<void(bool)>&& callback)
141141
{
142-
callOnMainThread([this, protectedThis = Ref { *this }, lockIdentifier, name = crossThreadCopy(name), completionHandler = WTF::move(completionHandler)]() mutable {
143-
WebLockRegistry::singleton().abortLockRequest(m_sessionID, m_clientOrigin, lockIdentifier, m_clientID, name, [clientID = m_clientID, completionHandler = WTF::move(completionHandler)](bool wasAborted) mutable {
144-
ScriptExecutionContext::ensureOnContextThread(clientID, [completionHandler = WTF::move(completionHandler), wasAborted](auto&) mutable {
145-
completionHandler(wasAborted);
142+
callOnMainThread([this, protectedThis = Ref { *this }, lockIdentifier, name = crossThreadCopy(name), callback = WTF::move(callback)]() mutable {
143+
WebLockRegistry::singleton().abortLockRequest(m_sessionID, m_clientOrigin, lockIdentifier, m_clientID, name, [clientID = m_clientID, callback = WTF::move(callback)](bool wasAborted) mutable {
144+
ScriptExecutionContext::ensureOnContextThread(clientID, [callback = WTF::move(callback), wasAborted](auto&) mutable {
145+
callback(wasAborted);
146146
});
147147
});
148148
});
149149
}
150150

151-
void WebLockManager::MainThreadBridge::query(CompletionHandler<void(Snapshot&&)>&& completionHandler)
151+
void WebLockManager::MainThreadBridge::query(Function<void(Snapshot&&)>&& callback)
152152
{
153-
callOnMainThread([this, protectedThis = Ref { *this }, completionHandler = WTF::move(completionHandler)]() mutable {
154-
WebLockRegistry::singleton().snapshot(m_sessionID, m_clientOrigin, [clientID = m_clientID, completionHandler = WTF::move(completionHandler)](Snapshot&& snapshot) mutable {
155-
ScriptExecutionContext::ensureOnContextThread(clientID, [completionHandler = WTF::move(completionHandler), snapshot = crossThreadCopy(snapshot)](auto&) mutable {
156-
completionHandler(WTF::move(snapshot));
153+
callOnMainThread([this, protectedThis = Ref { *this }, callback = WTF::move(callback)]() mutable {
154+
WebLockRegistry::singleton().snapshot(m_sessionID, m_clientOrigin, [clientID = m_clientID, callback = WTF::move(callback)](Snapshot&& snapshot) mutable {
155+
ScriptExecutionContext::ensureOnContextThread(clientID, [callback = WTF::move(callback), snapshot = crossThreadCopy(snapshot)](auto&) mutable {
156+
callback(WTF::move(snapshot));
157157
});
158158
});
159159
});
@@ -322,11 +322,17 @@ void WebLockManager::query(Ref<DeferredPromise>&& promise)
322322
return;
323323
}
324324

325-
m_mainThreadBridge->query([weakThis = WeakPtr { *this }, promise = WTF::move(promise)](Snapshot&& snapshot) mutable {
325+
auto promiseIdentifier = WebLockIdentifier::generate();
326+
m_queryPromises.add(promiseIdentifier, WTF::move(promise));
327+
m_mainThreadBridge->query([weakThis = WeakPtr { *this }, promiseIdentifier](Snapshot&& snapshot) mutable {
326328
RefPtr protectedThis = weakThis.get();
327329
if (!protectedThis)
328330
return;
329331

332+
auto promise = protectedThis->m_queryPromises.take(promiseIdentifier);
333+
if (!promise)
334+
return;
335+
330336
queueTaskKeepingObjectAlive(*protectedThis, TaskSource::DOMManipulation, [promise = WTF::move(promise), snapshot = WTF::move(snapshot)](auto&) mutable {
331337
promise->resolve<IDLDictionary<Snapshot>>(WTF::move(snapshot));
332338
});
@@ -373,16 +379,16 @@ void WebLockManager::stop()
373379

374380
void WebLockManager::clientIsGoingAway()
375381
{
376-
if (m_pendingRequests.isEmpty() && m_releasePromises.isEmpty())
377-
return;
378-
379382
for (auto& request : m_pendingRequests.values())
380383
request.removeSignalAlgorithm();
381384

382-
// Reject all pending promises before clearing
383-
for (Ref promise : m_releasePromises.values())
384-
promise->reject(ExceptionCode::AbortError, "Promise was rejected because the browsing context is going away"_s);
385-
385+
// Reject all pending promises before clearing.
386+
auto releasePromises = std::exchange(m_releasePromises, { });
387+
for (auto& promise : releasePromises.values())
388+
protect(promise)->reject(ExceptionCode::AbortError, "Promise was rejected because the browsing context is going away"_s);
389+
auto queryPromises = std::exchange(m_queryPromises, { });
390+
for (auto& promise : queryPromises.values())
391+
protect(promise)->reject(ExceptionCode::AbortError, "Promise was rejected because the browsing context is going away"_s);
386392
m_pendingRequests.clear();
387393
m_releasePromises.clear();
388394

@@ -392,7 +398,7 @@ void WebLockManager::clientIsGoingAway()
392398

393399
bool WebLockManager::virtualHasPendingActivity() const
394400
{
395-
return !m_pendingRequests.isEmpty() || !m_releasePromises.isEmpty();
401+
return !m_pendingRequests.isEmpty() || !m_releasePromises.isEmpty() || !m_queryPromises.isEmpty();
396402
}
397403

398404
void WebLockManager::suspend(ReasonForSuspension reason)

Source/WebCore/Modules/web-locks/WebLockManager.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ class WebLockManager : public RefCounted<WebLockManager>, public ActiveDOMObject
8383

8484
struct LockRequest;
8585
HashMap<WebLockIdentifier, LockRequest> m_pendingRequests;
86+
87+
HashMap<WebLockIdentifier, Ref<DeferredPromise>> m_queryPromises;
8688
};
8789

8890
} // namespace WebCore

0 commit comments

Comments
 (0)