← WebKit — 2026-W35

b8d3efb7851ea3b56cc7902f753493470262d330  Use-after-free of WorkerOrWorkletThread during worker thread startup

severity medium class UAF confidence 0.55 signal score 11.05 Source/WebCore/workers
Primitive: WorkerThread freed while its thread body still runs / shared-worker proxy thread started without an owner
Triage note: WorkerThread::createThread now captures protectedThis = Ref { *this } and releases it on the main thread instead of relying on a bare [this], and SharedWorkerContextManager::registerSharedWorkerThread converts an ASSERT into a real early-return when the entry already exists so an unowned proxy's thread is not started. Both are genuine lifetime/UAF fixes, but attacker reachability is not clearly established.
Structural signals: uaf_lifetime (+4.75), msg_strong (+3.0), msg_medium (+2.0), surgical (+1.3)

Commit message

Use-after-free of WorkerOrWorkletThread during worker thread startup
<https://bugs.webkit.org/show_bug.cgi?id=321872>
<rdar://184581483>

Reviewed by Ryosuke Niwa.

`WorkerThread::createThread()` hands the new thread a raw `this`,
and the only reference the worker holds on itself is the one that
thread takes as the first statement of `workerOrWorkletThread()`.
Nothing owns the worker between `Thread::create()` returning on the
launching thread and the new thread reaching that statement, so
correctness rests on every caller happening to retain a reference
across the handoff rather than on ownership.  For a shared worker,
where `SharedWorkerContextManager`'s worker map is the only
long-lived owner, losing that reference first means the worker takes
its own first reference out of freed memory.

Take the reference at the call site instead, matching the main run
loop branch directly above.  Ownership then spans the handoff, so the
worker no longer depends on callers retaining a reference by chance.

Release that reference on the main thread once the thread body
returns.  `destroyWorkerGlobalScope()` deliberately sends the
worker's own last reference to the main thread, and a reference
dropped on the worker thread afterwards would run the destructor
there instead, releasing members that expect main-thread destruction.

Also, as a drive-by fix, avoid starting a worker thread for a proxy
that nothing owns.  `HashMap::add()` does nothing when the key is
already present, so a repeated `sharedWorkerIdentifier` -- a value
supplied over IPC by the network process -- left the map holding no
reference at all, guarded only by an assertion that compiles out in
release builds.

The test exercises the thread-start handoff but does not lose the race
on its own.  It crashed without this change only after instrumenting
WebCore to delay the worker thread's first access to itself and to
drop the manager's map reference immediately after starting the thread.

Test: http/tests/workers/shared/worker-thread-start-terminate-race.html

* LayoutTests/http/tests/workers/shared/worker-thread-start-terminate-race-expected.txt: Add.
* LayoutTests/http/tests/workers/shared/worker-thread-start-terminate-race.html: Add.
* Source/WebCore/workers/WorkerThread.cpp:
(WebCore::WorkerThread::createThread):
* Source/WebCore/workers/shared/context/SharedWorkerContextManager.cpp:
(WebCore::SharedWorkerContextManager::registerSharedWorkerThread):

Identifier: 316606.512@safari-7625-branch

Before / after

Loading diff…