CVE-2026-117963
Overview
Background
- `ServiceWorkerManager` (SWM)
- The main-thread singleton that manages service worker registrations and, among other things, surfaces diagnostic/error messages to a worker’s controlled clients via
LocalizeAndReportToAllClientsindom/serviceworkers/ServiceWorkerManager.cpp. - `ConsoleUtils::ReportForServiceWorkerScope`
- Helper in
dom/console/ConsoleUtils.cppthat posts a console message for a service worker scope; it is main-thread-only and begins with a thread assertion. - Main-thread affinity
ServiceWorkerManager::GetInstance()and the objects it touches (registration tables, client lists, console service) are main-thread-only. Calling into them from another thread is a thread-safety violation: it races main-thread state and can use objects not meant for concurrent access.- `MOZ_ASSERT` vs `MOZ_RELEASE_ASSERT`
MOZ_ASSERTis compiled out of release builds, so a debug-only assertion does nothing to stop the bad path in shipping Firefox.MOZ_RELEASE_ASSERTfires in all builds.
Root Cause Analysis
LocalizeAndReportToAllClients assumed it ran on the main thread — it immediately called ServiceWorkerManager::GetInstance() and operated on SWM state — but it could be invoked from a non-main thread (e.g. a worker/IPC path reporting a service-worker error). On that off-main-thread entry it touched main-thread-only service-worker and console machinery concurrently with the main thread, a thread-affinity violation. The only guard downstream, ConsoleUtils::ReportForServiceWorkerScope, was a MOZ_ASSERT(NS_IsMainThread()), which is a no-op in release builds, so shipping Firefox silently proceeded off-thread into main-thread-only code.
The patch makes LocalizeAndReportToAllClients thread-safe by bouncing to the main thread: if !NS_IsMainThread(), it builds a runnable that copies all arguments (aScope, a fresh nsCString(aStringKey), aParamArray.Clone(), flags, filename, line, line/column numbers) and re-invokes itself on the main thread via NS_DispatchToMainThread, then returns. The deep copies/Clone() ensure the dispatched work owns its data rather than referencing caller buffers that may go away. It also upgrades the ConsoleUtils::ReportForServiceWorkerScope guard from MOZ_ASSERT to MOZ_RELEASE_ASSERT(NS_IsMainThread()) so any remaining off-thread caller fails loudly in release builds instead of corrupting state. The advisory’s “incorrect boundary conditions” label does not match the code; the diff substantiates a thread-safety/affinity defect, not a bounds error.
LocalizeAndReportToAllClients ran main-thread-only service-worker and console code without checking the thread, and the only check downstream was a release-build no-op MOZ_ASSERT, so it could execute on a non-main thread and race/misuse main-thread-only state.Attack Path
- Trigger an SW error report
Web content registers/controls a service worker and induces an error condition that routes through
LocalizeAndReportToAllClients(inferred: reachable from service-worker error reporting). - Off-main-thread entry The report is raised from a non-main thread, so the function runs off the main thread.
- No effective guard (pre-patch)
The debug-only
MOZ_ASSERTinReportForServiceWorkerScopeis compiled out; execution continues intoServiceWorkerManager::GetInstance()and client/console machinery. - Concurrent main-thread access Main-thread-only SWM/console objects are touched off-thread while the main thread may also be using them, a data race.
- Primitive Racy access to main-thread-only objects; depending on timing this can corrupt service-worker/console state or crash (inferred potential use-after-free/memory corruption — not explicitly shown by the diff).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/serviceworkers/ServiceWorkerManager.cpp |
modified |
Files Changed
dom/console/ConsoleUtils.cppdom/serviceworkers/ServiceWorkerManager.cpp
Audit Directions
- Debug-only thread asserts on main-thread-only codeGrep for
MOZ_ASSERT(NS_IsMainThread())guarding functions that touch singletons/DOM state; these are no-ops in release and may hide off-thread callers like this one. - Other SWM entry pointsAudit other
ServiceWorkerManagermethods callable from worker/IPC threads for a missingNS_IsMainThread()marshal. - Argument lifetime on dispatchVerify other
NS_DispatchToMainThreadsites copy/Clone()their arguments rather than capturing references to caller-owned buffers, as this fix does.
Patch
diff --git a/dom/console/ConsoleUtils.cpp b/dom/console/ConsoleUtils.cpp
index 7fab046991f..17c5d8503ab 100644
--- a/dom/console/ConsoleUtils.cpp
+++ b/dom/console/ConsoleUtils.cpp
@@ -47,7 +47,7 @@ void ConsoleUtils::ReportForServiceWorkerScope(const nsAString& aScope,
uint32_t aLineNumber,
uint32_t aColumnNumber,
Level aLevel) {
- MOZ_ASSERT(NS_IsMainThread());
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
RefPtr<ConsoleUtils> service = ConsoleUtils::GetOrCreate();
if (NS_WARN_IF(!service)) {
diff --git a/dom/serviceworkers/ServiceWorkerManager.cpp b/dom/serviceworkers/ServiceWorkerManager.cpp
index 9d78131fddb..94b18851f61 100644
--- a/dom/serviceworkers/ServiceWorkerManager.cpp
+++ b/dom/serviceworkers/ServiceWorkerManager.cpp
@@ -1606,6 +1606,19 @@ void ServiceWorkerManager::LocalizeAndReportToAllClients(
const nsTArray<nsString>& aParamArray, uint32_t aFlags,
const nsCString& aFilename, const nsString& aLine, uint32_t aLineNumber,
uint32_t aColumnNumber) {
+ if (!NS_IsMainThread()) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "ServiceWorkerManager::LocalizeAndReportToAllClients",
+ [scope = aScope, stringKey = nsCString(aStringKey),
+ paramArray = aParamArray.Clone(), aFlags, filename = aFilename,
+ line = aLine, aLineNumber, aColumnNumber]() {
+ LocalizeAndReportToAllClients(scope, stringKey.get(), paramArray,
+ aFlags, filename, line, aLineNumber,
+ aColumnNumber);
+ }));
+ return;
+ }
+
RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
if (!swm) {
return;