From d3dfab5ee197cf8449b534405147e6b87496fc7c Mon Sep 17 00:00:00 2001 From: James M Snell Date: Sun, 22 Feb 2026 11:03:22 -0800 Subject: [PATCH] Flush pending microtasks on service worker startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Service-worker format scripts (non-module) did not flush the V8 microtask queue after top-level evaluation in the Worker constructor. When two Workers sharing an isolate (same script, different zones) are constructed sequentially, and leave pending microtasks on the shared per-isolate queue, the first request's microtask checkpoint will then drain microtasks from both contexts, executing the other Worker's callbacks under the wrong IoContext — causing things like `TimeoutId::Generator`` mismatch assertions and potential cross-context state corruption. The fix is to call `lock.runMicrotasks()` after `NonModuleScript::run()`, matching the module path. A test in workerd (either C++ or wd-test) is currently not possible so the regression test will be added to the internal project. --- src/workerd/io/worker.c++ | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/workerd/io/worker.c++ b/src/workerd/io/worker.c++ index 2de15705961..22e1f875ab3 100644 --- a/src/workerd/io/worker.c++ +++ b/src/workerd/io/worker.c++ @@ -1845,6 +1845,13 @@ Worker::Worker(kj::Own scriptParam, auto limitScope = script->isolate->getLimitEnforcer().enterStartupJs(lock, limitErrorOrTime); unboundScript.run(lock); + // Flush microtasks enqueued during top-level script evaluation. + // Without this flush, microtasks (e.g. promise continuations from async + // initialization) remain on the per-isolate microtask queue and can leak across + // V8 contexts when multiple Workers share an isolate (same script, different + // zones). The leaked microtasks then execute under the wrong IoContext, making + // things go boom. + lock.runMicrotasks(); } KJ_CASE_ONEOF(mainModule, kj::Path) { KJ_IF_SOME(ns,