Bug 1145201: Use AutoDebuggerJobQueueInterruption in Debugger. r=jorendorff
authorJim Blandy <jimb@mozilla.com>
Tue, 12 Feb 2019 08:10:54 +0000
changeset 458951 3ae9f2b94f97b66353e0121351dd444959f5a8b7
parent 458950 b6216c391d4177539fce7096db9e226147556bf9
child 458952 1f792a3cc1d0bd7aa3da66c1aa5e287e36977bc1
push id78122
push userjblandy@mozilla.com
push dateWed, 13 Feb 2019 17:48:01 +0000
treeherderautoland@3ae9f2b94f97 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjorendorff
bugs1145201
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1145201: Use AutoDebuggerJobQueueInterruption in Debugger. r=jorendorff Modify the Debugger API implementation to ensure that debugger code's promise activity (resolving promises; running reaction jobs) cannot interfere with the debuggee's. Specifically, ensure that there is an `AutoDebuggerJobQueueInterruption` in scope at every site in the Debugger implementation that might invoke a debugger hook function. This saves the debuggee's job queue, emplaces a fresh empty queue, lets the hooks run, drains the queue, and then restores the debuggee's original queue. Since we must already mark sites that could invoke hooks with `EnterDebuggeeNoExecute`, we ensure our job queue protection coverage is complete statically by having `EnterDebuggeeNoExecute`'s constructor require a reference to an `AutoDebuggerJobQueueInterruption`. Differential Revision: https://phabricator.services.mozilla.com/D17547
js/src/jit-test/tests/debug/job-queue-01.js
js/src/jit-test/tests/debug/job-queue-03.js
js/src/jit-test/tests/debug/onNewScript-wasm-01.js
js/src/jit-test/tests/debug/onNewScript-wasm-02.js
js/src/jit-test/tests/debug/wasm-responseurls.js
js/src/vm/Debugger.cpp
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/job-queue-01.js
@@ -0,0 +1,122 @@
+// Debuggee promise reaction jobs should not run from debugger callbacks.
+// This covers:
+// - onDebuggerStatement
+// - onStep
+// - onEnterFrame
+// - onPop
+// - onExceptionUnwind
+// - breakpoint handlers
+// - uncaughtExceptionHook
+
+var g = newGlobal({ newCompartment: true });
+g.parent = this;
+var dbg = new Debugger;
+var gDO = dbg.addDebuggee(g);
+var log = '';
+
+// Exercise the promise machinery: resolve a promise and drain the job queue (or
+// in HTML terms, perform a microtask checkpoint). When called from a debugger
+// hook, the debuggee's microtasks should not run.
+function exercise(name) {
+  log += `${name}-handler`;
+  Promise.resolve(42).then(v => {
+    assertEq(v, 42);
+    log += `${name}-react`;
+  });
+  log += `(`;
+  drainJobQueue();
+  log += `)`;
+
+  // This should be run by the implicit microtask checkpoint after each Debugger
+  // hook call.
+  Promise.resolve(42).then(v => {
+    assertEq(v, 42);
+    log += `(${name}-tail)`;
+  });
+}
+
+dbg.onDebuggerStatement = function (frame) {
+  exercise('debugger');
+
+  frame.onStep = function () {
+    this.onStep = undefined;
+    exercise('step');
+  };
+
+  dbg.onEnterFrame = function (frame) {
+    dbg.onEnterFrame = undefined;
+    frame.onPop = function(completion) {
+      assertEq(completion.return, 'recompense');
+      exercise('pop');
+    }
+
+    exercise('enter');
+  }
+
+  dbg.onExceptionUnwind = function(frame, value) {
+    dbg.onExceptionUnwind = undefined;
+    assertEq(value, 'recidivism');
+    exercise('exception');
+    return { return: 'recompense' };
+  };
+
+  // Set a breakpoint on entry to g.breakpoint_here.
+  const script = gDO.getOwnPropertyDescriptor('breakpoint_here').value.script;
+  const handler = {
+    hit(frame) {
+      script.clearAllBreakpoints();
+      exercise('bp');
+    }
+  };
+  script.setBreakpoint(0, handler);
+
+  dbg.uncaughtExceptionHook = function (ex) {
+    assertEq(ex, 'turncoat');
+    exercise('uncaught');
+  };
+
+  // Throw an uncaught exception from the Debugger handler. This should reach
+  // uncaughtExceptionHook, but shouldn't affect the debuggee.
+  throw 'turncoat';
+};
+
+g.eval(`
+  function breakpoint_here() {
+    throw 'recidivism';
+  }
+
+  parent.log += 'eval(';
+
+  // DebuggeeWouldRun detection may prevent this callback from running at all if
+  // bug 1145201 is present. SpiderMonkey will try to run the promise reaction
+  // job from the Debugger hook's microtask checkpoint, triggering
+  // DebuggeeWouldRun. This is a little difficult to observe, since the callback
+  // never even begins execution. But it should cause the 'then' promise to be
+  // rejected, which the shell will report (if the assertEq(log, ...) doesn't
+  // kill the test first).
+
+  Promise.resolve(84).then(function(v) {
+    assertEq(v, 84);
+    parent.log += 'eval-react';
+  });
+  debugger;
+  parent.log += '...';
+  breakpoint_here();
+  parent.log += ')';
+`);
+
+log += 'main-drain('
+drainJobQueue();
+log += ')';
+
+assertEq(log, `eval(\
+debugger-handler(debugger-react)\
+uncaught-handler((debugger-tail)uncaught-react)(uncaught-tail)\
+step-handler(step-react)(step-tail)\
+...\
+enter-handler(enter-react)(enter-tail)\
+bp-handler(bp-react)(bp-tail)\
+exception-handler(exception-react)(exception-tail)\
+pop-handler(pop-react)(pop-tail)\
+)\
+main-drain(eval-react)`);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/job-queue-03.js
@@ -0,0 +1,173 @@
+// Multiple debuggers get their job queues drained after each hook.
+// This covers:
+// - onDebuggerStatement
+// - onStep
+// - onEnterFrame
+// - onPop
+// - onExceptionUnwind
+// - breakpoint handlers
+// - uncaughtExceptionHook
+
+const g = newGlobal({ newCompartment: true });
+g.parent = this;
+
+var log = '';
+let expected_throws = 0;
+
+function setup(global, label) {
+  const dbg = new Debugger;
+  dbg.gDO = dbg.addDebuggee(global);
+  dbg.log = '';
+
+  dbg.onDebuggerStatement = function (frame) {
+    // Exercise the promise machinery: resolve a promise and perform a microtask
+    // checkpoint. When called from a debugger hook, the debuggee's microtasks
+    // should not run.
+    function exercise(name) {
+      dbg.log += name + ',';
+      log += `${label}-${name}-handler\n`;
+      Promise.resolve(42).then(v => {
+        assertEq(v, 42);
+        log += `${label}-${name}-tail\n`;
+      });
+    }
+
+    exercise('debugger');
+
+    frame.onStep = function () {
+      this.onStep = undefined;
+      exercise('step');
+    };
+
+    dbg.onEnterFrame = function (frame) {
+      dbg.onEnterFrame = undefined;
+      frame.onPop = function(completion) {
+        assertEq(completion.return, 'escutcheon');
+        exercise('pop');
+      }
+
+      exercise('enter');
+    }
+
+    expected_throws++;
+    dbg.onExceptionUnwind = function(frame, value) {
+      dbg.onExceptionUnwind = undefined;
+      assertEq(value, 'myrmidon');
+      exercise('exception');
+      if (--expected_throws > 0) {
+        return undefined;
+      } else {
+        return { return: 'escutcheon' };
+      }
+    };
+
+    // Set a breakpoint on entry to g.breakpoint_here.
+    const script = dbg.gDO.getOwnPropertyDescriptor('breakpoint_here').value.script;
+    const handler = {
+      hit(frame) {
+        script.clearAllBreakpoints();
+        exercise('bp');
+      }
+    };
+    script.setBreakpoint(0, handler);
+
+    dbg.uncaughtExceptionHook = function (ex) {
+      assertEq(ex, 'turncoat');
+      exercise('uncaught');
+    };
+
+    // Throw an uncaught exception from the Debugger handler. This should reach
+    // uncaughtExceptionHook, but shouldn't affect the debuggee.
+    throw 'turncoat';
+  };
+
+  return dbg;
+}
+
+const dbg1 = setup(g, '1');
+const dbg2 = setup(g, '2');
+const dbg3 = setup(g, '3');
+
+g.eval(`
+  function breakpoint_here() {
+    throw 'myrmidon';
+  }
+
+  parent.log += 'eval-start\\n';
+
+  // DebuggeeWouldRun detection may prevent this callback from running at all if
+  // bug 1145201 is present. SpiderMonkey will try to run the promise reaction
+  // job from the Debugger hook's microtask checkpoint, triggering
+  // DebuggeeWouldRun. This is a little difficult to observe, since the callback
+  // never even begins execution. But it should cause the 'then' promise to be
+  // rejected, which the shell will report (if the assertEq(log, ...) doesn't
+  // kill the test first).
+
+  Promise.resolve(84).then(function(v) {
+    assertEq(v, 84);
+    parent.log += 'eval-react';
+  });
+  debugger;
+  parent.log += 'stuff to step over\\n';
+  breakpoint_here();
+  parent.log += 'eval-end\\n';
+`);
+
+log += 'main-drain\n'
+drainJobQueue();
+log += 'main-drain-done\n';
+
+const regex = new RegExp(`eval-start
+.-debugger-handler
+.-uncaught-handler
+.-debugger-tail
+.-uncaught-tail
+.-debugger-handler
+.-uncaught-handler
+.-debugger-tail
+.-uncaught-tail
+.-debugger-handler
+.-uncaught-handler
+.-debugger-tail
+.-uncaught-tail
+.-step-handler
+.-step-tail
+.-step-handler
+.-step-tail
+.-step-handler
+.-step-tail
+stuff to step over
+.-enter-handler
+.-enter-tail
+.-enter-handler
+.-enter-tail
+.-enter-handler
+.-enter-tail
+.-bp-handler
+.-bp-tail
+.-bp-handler
+.-bp-tail
+.-bp-handler
+.-bp-tail
+.-exception-handler
+.-exception-tail
+.-exception-handler
+.-exception-tail
+.-exception-handler
+.-exception-tail
+.-pop-handler
+.-pop-tail
+.-pop-handler
+.-pop-tail
+.-pop-handler
+.-pop-tail
+eval-end
+main-drain
+eval-reactmain-drain-done
+`);
+
+assertEq(!!log.match(regex), true)
+
+assertEq(dbg1.log, 'debugger,uncaught,step,enter,bp,exception,pop,');
+assertEq(dbg2.log, 'debugger,uncaught,step,enter,bp,exception,pop,');
+assertEq(dbg3.log, 'debugger,uncaught,step,enter,bp,exception,pop,');
--- a/js/src/jit-test/tests/debug/onNewScript-wasm-01.js
+++ b/js/src/jit-test/tests/debug/onNewScript-wasm-01.js
@@ -1,16 +1,20 @@
 // |jit-test| skip-if: !wasmDebuggingIsSupported()
 // Draining the job queue from an onNewScript hook reporting a streamed wasm
 // module should not deadlock.
 
 ignoreUnhandledRejections();
 
 try {
     WebAssembly.compileStreaming();
+    // Avoid mixing the test's jobs with the debuggee's, so that
+    // automated checks can make sure AutoSaveJobQueue only
+    // suspends debuggee work.
+    drainJobQueue();
 } catch (err) {
     assertEq(String(err).indexOf("not supported with --no-threads") !== -1, true);
     quit();
 }
 
 load(libdir + "asserts.js");
 
 var g = newGlobal({newCompartment: true});
--- a/js/src/jit-test/tests/debug/onNewScript-wasm-02.js
+++ b/js/src/jit-test/tests/debug/onNewScript-wasm-02.js
@@ -1,16 +1,20 @@
 // |jit-test| skip-if: !wasmDebuggingIsSupported()
 // Draining the job queue from an onNewScript hook reporting a streamed wasm
 // module should not deadlock.
 
 ignoreUnhandledRejections();
 
 try {
     WebAssembly.compileStreaming();
+    // Avoid mixing the test's jobs with the debuggee's, so that
+    // automated checks can make sure AutoSaveJobQueue only
+    // suspends debuggee work.
+    drainJobQueue();
 } catch (err) {
     assertEq(String(err).indexOf("not supported with --no-threads") !== -1, true);
     quit();
 }
 
 var g = newGlobal({newCompartment: true});
 
 var source = new g.Uint8Array(wasmTextToBinary('(module (func unreachable))'));
--- a/js/src/jit-test/tests/debug/wasm-responseurls.js
+++ b/js/src/jit-test/tests/debug/wasm-responseurls.js
@@ -1,16 +1,20 @@
 // |jit-test| test-also=--wasm-compiler=ion; skip-if: !wasmDebuggingIsSupported()
 // Tests that wasm module can accept URL and sourceMapURL from response
 // when instantiateStreaming is used.
 
 ignoreUnhandledRejections();
 
 try {
     WebAssembly.compileStreaming();
+    // Avoid mixing the test's jobs with the debuggee's, so that
+    // automated checks can make sure AutoSaveJobQueue only
+    // suspends debuggee work.
+    drainJobQueue();
 } catch (err) {
     assertEq(String(err).indexOf("not supported with --no-threads") !== -1, true);
     quit();
 }
 
 load(libdir + "asserts.js");
 
 var g = newGlobal({newCompartment: true});
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -22,16 +22,17 @@
 #include "gc/HashUtil.h"
 #include "gc/Marking.h"
 #include "gc/Policy.h"
 #include "gc/PublicIterators.h"
 #include "jit/BaselineDebugModeOSR.h"
 #include "jit/BaselineJIT.h"
 #include "js/CharacterEncoding.h"
 #include "js/Date.h"
+#include "js/Promise.h"
 #include "js/PropertyDescriptor.h"
 #include "js/PropertySpec.h"
 #include "js/SourceText.h"
 #include "js/StableStringChars.h"
 #include "js/UbiNodeBreadthFirst.h"
 #include "js/Vector.h"
 #include "js/Wrapper.h"
 #include "proxy/ScriptedProxyHandler.h"
@@ -292,18 +293,23 @@ class MOZ_RAII js::EnterDebuggeeNoExecut
   // Non-nullptr when unlocked temporarily by a LeaveDebuggeeNoExecute.
   LeaveDebuggeeNoExecute* unlocked_;
 
   // When DebuggeeWouldRun is a warning instead of an error, whether we've
   // reported a warning already.
   bool reported_;
 
  public:
-  explicit EnterDebuggeeNoExecute(JSContext* cx, Debugger& dbg)
+  // Mark execution in dbg's debuggees as forbidden, for the lifetime of this
+  // object. Require an AutoDebuggerJobQueueInterruption in scope.
+  explicit EnterDebuggeeNoExecute(
+      JSContext* cx, Debugger& dbg,
+      const JS::AutoDebuggerJobQueueInterruption& adjqiProof)
       : dbg_(dbg), unlocked_(nullptr), reported_(false) {
+    MOZ_ASSERT(adjqiProof.initialized());
     stack_ = &cx->noExecuteDebuggerTop.ref();
     prev_ = *stack_;
     *stack_ = this;
   }
 
   ~EnterDebuggeeNoExecute() {
     MOZ_ASSERT(*stack_ == this);
     *stack_ = prev_;
@@ -1036,26 +1042,34 @@ class MOZ_RAII AutoSetGeneratorRunning {
   }
 
   // Save the frame's completion value.
   ResumeMode resumeMode;
   RootedValue value(cx);
   Debugger::resultToCompletion(cx, frameOk, frame.returnValue(), &resumeMode,
                                &value);
 
+  // Preserve the debuggee's microtask event queue while we run the hooks, so
+  // the debugger's microtask checkpoints don't run from the debuggee's
+  // microtasks, and vice versa.
+  JS::AutoDebuggerJobQueueInterruption adjqi;
+  if (!adjqi.init(cx)) {
+    return false;
+  }
+
   // This path can be hit via unwinding the stack due to over-recursion or
   // OOM. In those cases, don't fire the frames' onPop handlers, because
   // invoking JS will only trigger the same condition. See
   // slowPathOnExceptionUnwind.
   if (!cx->isThrowingOverRecursed() && !cx->isThrowingOutOfMemory()) {
     // For each Debugger.Frame, fire its onPop handler, if any.
     for (size_t i = 0; i < frames.length(); i++) {
       HandleDebuggerFrame frameobj = frames[i];
       Debugger* dbg = Debugger::fromChildJSObject(frameobj);
-      EnterDebuggeeNoExecute nx(cx, *dbg);
+      EnterDebuggeeNoExecute nx(cx, *dbg, adjqi);
 
       if (dbg->enabled && frameobj->onPopHandler()) {
         OnPopHandler* handler = frameobj->onPopHandler();
 
         Maybe<AutoRealm> ar;
         ar.emplace(cx, dbg->object);
 
         RootedValue wrappedValue(cx, value);
@@ -1068,16 +1082,17 @@ class MOZ_RAII AutoSetGeneratorRunning {
         // Call the onPop handler.
         ResumeMode nextResumeMode = resumeMode;
         RootedValue nextValue(cx, wrappedValue);
         bool success;
         {
           AutoSetGeneratorRunning asgr(cx, genObj);
           success = handler->onPop(cx, frameobj, nextResumeMode, &nextValue);
         }
+        adjqi.runJobs();
         nextResumeMode = dbg->processParsedHandlerResult(
             ar, frame, pc, success, nextResumeMode, &nextValue);
 
         // At this point, we are back in the debuggee compartment, and
         // any error has been wrapped up as a completion value.
         MOZ_ASSERT(cx->compartment() == debuggeeGlobal->compartment());
         MOZ_ASSERT(!cx->isExceptionPending());
 
@@ -1649,18 +1664,19 @@ ResumeMode Debugger::reportUncaughtExcep
   return ResumeMode::Terminate;
 }
 
 ResumeMode Debugger::handleUncaughtExceptionHelper(
     Maybe<AutoRealm>& ar, MutableHandleValue* vp,
     const Maybe<HandleValue>& thisVForCheck, AbstractFramePtr frame) {
   JSContext* cx = ar->context();
 
-  // Uncaught exceptions arise from Debugger code, and so we must already be
-  // in an NX section.
+  // Uncaught exceptions arise from Debugger code, and so we must already be in
+  // an NX section. This also establishes that we are already within the scope
+  // of an AutoDebuggerJobQueueInterruption object.
   MOZ_ASSERT(EnterDebuggeeNoExecute::isLockedInStack(cx, *this));
 
   if (cx->isExceptionPending()) {
     if (uncaughtExceptionHook) {
       RootedValue exc(cx);
       if (!cx->getPendingException(&exc)) {
         return ResumeMode::Terminate;
       }
@@ -2041,23 +2057,32 @@ template <typename HookIsEnabledFun /* b
       if (dbg->enabled && hookIsEnabled(dbg)) {
         if (!triggered.append(ObjectValue(*dbg->toJSObject()))) {
           return ResumeMode::Terminate;
         }
       }
     }
   }
 
+  // Preserve the debuggee's microtask event queue while we run the hooks, so
+  // the debugger's microtask checkpoints don't run from the debuggee's
+  // microtasks, and vice versa.
+  JS::AutoDebuggerJobQueueInterruption adjqi;
+  if (!adjqi.init(cx)) {
+    return ResumeMode::Terminate;
+  }
+
   // Deliver the event to each debugger, checking again to make sure it
   // should still be delivered.
   for (Value* p = triggered.begin(); p != triggered.end(); p++) {
     Debugger* dbg = Debugger::fromJSObject(&p->toObject());
-    EnterDebuggeeNoExecute nx(cx, *dbg);
+    EnterDebuggeeNoExecute nx(cx, *dbg, adjqi);
     if (dbg->debuggees.has(global) && dbg->enabled && hookIsEnabled(dbg)) {
       ResumeMode resumeMode = fireHook(dbg);
+      adjqi.runJobs();
       if (resumeMode != ResumeMode::Continue) {
         return resumeMode;
       }
     }
   }
   return ResumeMode::Continue;
 }
 
@@ -2142,61 +2167,70 @@ void Debugger::slowPathOnNewWasmInstance
         &bp->asWasm()->wasmInstance->instance() != iter.wasmInstance()) {
       continue;
     }
     if (!triggered.append(bp)) {
       return ResumeMode::Terminate;
     }
   }
 
-  for (Breakpoint** p = triggered.begin(); p != triggered.end(); p++) {
-    Breakpoint* bp = *p;
-
-    // Handlers can clear breakpoints. Check that bp still exists.
-    if (!site || !site->hasBreakpoint(bp)) {
-      continue;
-    }
-
-    // There are two reasons we have to check whether dbg is enabled and
-    // debugging global.
-    //
-    // One is just that one breakpoint handler can disable other Debuggers
-    // or remove debuggees.
-    //
-    // The other has to do with non-compile-and-go scripts, which have no
-    // specific global--until they are executed. Only now do we know which
-    // global the script is running against.
-    Debugger* dbg = bp->debugger;
-    bool hasDebuggee = dbg->enabled && dbg->debuggees.has(global);
-    if (hasDebuggee) {
-      Maybe<AutoRealm> ar;
-      ar.emplace(cx, dbg->object);
-      EnterDebuggeeNoExecute nx(cx, *dbg);
-
-      RootedValue scriptFrame(cx);
-      if (!dbg->getFrame(cx, iter, &scriptFrame)) {
-        return dbg->reportUncaughtException(ar);
-      }
-      RootedValue rv(cx);
-      Rooted<JSObject*> handler(cx, bp->handler);
-      bool ok = CallMethodIfPresent(cx, handler, "hit", 1,
-                                    scriptFrame.address(), &rv);
-      ResumeMode resumeMode = dbg->processHandlerResult(
-          ar, ok, rv, iter.abstractFramePtr(), iter.pc(), vp);
-      if (resumeMode != ResumeMode::Continue) {
-        savedExc.drop();
-        return resumeMode;
-      }
-
-      // Calling JS code invalidates site. Reload it.
-      if (isJS) {
-        site = iter.script()->getBreakpointSite(pc);
-      } else {
-        site = iter.wasmInstance()->debug().getOrCreateBreakpointSite(
-            cx, bytecodeOffset);
+  if (triggered.length() > 0) {
+    // Preserve the debuggee's microtask event queue while we run the hooks, so
+    // the debugger's microtask checkpoints don't run from the debuggee's
+    // microtasks, and vice versa.
+    JS::AutoDebuggerJobQueueInterruption adjqi;
+    if (!adjqi.init(cx)) {
+      return ResumeMode::Terminate;
+    }
+
+    for (Breakpoint* bp : triggered) {
+      // Handlers can clear breakpoints. Check that bp still exists.
+      if (!site || !site->hasBreakpoint(bp)) {
+        continue;
+      }
+
+      // There are two reasons we have to check whether dbg is enabled and
+      // debugging global.
+      //
+      // One is just that one breakpoint handler can disable other Debuggers
+      // or remove debuggees.
+      //
+      // The other has to do with non-compile-and-go scripts, which have no
+      // specific global--until they are executed. Only now do we know which
+      // global the script is running against.
+      Debugger* dbg = bp->debugger;
+      bool hasDebuggee = dbg->enabled && dbg->debuggees.has(global);
+      if (hasDebuggee) {
+        Maybe<AutoRealm> ar;
+        ar.emplace(cx, dbg->object);
+        EnterDebuggeeNoExecute nx(cx, *dbg, adjqi);
+
+        RootedValue scriptFrame(cx);
+        if (!dbg->getFrame(cx, iter, &scriptFrame)) {
+          return dbg->reportUncaughtException(ar);
+        }
+        RootedValue rv(cx);
+        Rooted<JSObject*> handler(cx, bp->handler);
+        bool ok = CallMethodIfPresent(cx, handler, "hit", 1,
+                                      scriptFrame.address(), &rv);
+        adjqi.runJobs();
+        ResumeMode resumeMode = dbg->processHandlerResult(
+            ar, ok, rv, iter.abstractFramePtr(), iter.pc(), vp);
+        if (resumeMode != ResumeMode::Continue) {
+          savedExc.drop();
+          return resumeMode;
+        }
+
+        // Calling JS code invalidates site. Reload it.
+        if (isJS) {
+          site = iter.script()->getBreakpointSite(pc);
+        } else {
+          site = iter.wasmInstance()->debug().getOrCreateBreakpointSite(
+              cx, bytecodeOffset);
+        }
       }
     }
   }
 
   // By convention, return the true op to the interpreter in vp, and return
   // undefined in vp to the wasm debug trap.
   if (isJS) {
     vp.setInt32(JSOp(*pc));
@@ -2250,37 +2284,48 @@ void Debugger::slowPathOnNewWasmInstance
           }
         }
       }
     }
     MOZ_ASSERT(stepperCount == trappingScript->stepModeCount());
   }
 #endif
 
-  // Call onStep for frames that have the handler set.
-  for (size_t i = 0; i < frames.length(); i++) {
-    HandleDebuggerFrame frame = frames[i];
-    OnStepHandler* handler = frame->onStepHandler();
-    if (!handler) {
-      continue;
-    }
-
-    Debugger* dbg = Debugger::fromChildJSObject(frame);
-    EnterDebuggeeNoExecute nx(cx, *dbg);
-
-    Maybe<AutoRealm> ar;
-    ar.emplace(cx, dbg->object);
-
-    ResumeMode resumeMode = ResumeMode::Continue;
-    bool success = handler->onStep(cx, frame, resumeMode, vp);
-    resumeMode = dbg->processParsedHandlerResult(
-        ar, iter.abstractFramePtr(), iter.pc(), success, resumeMode, vp);
-    if (resumeMode != ResumeMode::Continue) {
-      savedExc.drop();
-      return resumeMode;
+  if (frames.length() > 0) {
+    // Preserve the debuggee's microtask event queue while we run the hooks, so
+    // the debugger's microtask checkpoints don't run from the debuggee's
+    // microtasks, and vice versa.
+    JS::AutoDebuggerJobQueueInterruption adjqi;
+    if (!adjqi.init(cx)) {
+      return ResumeMode::Terminate;
+    }
+
+    // Call onStep for frames that have the handler set.
+    for (size_t i = 0; i < frames.length(); i++) {
+      HandleDebuggerFrame frame = frames[i];
+      OnStepHandler* handler = frame->onStepHandler();
+      if (!handler) {
+        continue;
+      }
+
+      Debugger* dbg = Debugger::fromChildJSObject(frame);
+      EnterDebuggeeNoExecute nx(cx, *dbg, adjqi);
+
+      Maybe<AutoRealm> ar;
+      ar.emplace(cx, dbg->object);
+
+      ResumeMode resumeMode = ResumeMode::Continue;
+      bool success = handler->onStep(cx, frame, resumeMode, vp);
+      adjqi.runJobs();
+      resumeMode = dbg->processParsedHandlerResult(
+          ar, iter.abstractFramePtr(), iter.pc(), success, resumeMode, vp);
+      if (resumeMode != ResumeMode::Continue) {
+        savedExc.drop();
+        return resumeMode;
+      }
     }
   }
 
   vp.setUndefined();
   return ResumeMode::Continue;
 }
 
 ResumeMode Debugger::fireNewGlobalObject(JSContext* cx,
@@ -2343,30 +2388,40 @@ void Debugger::slowPathOnNewGlobalObject
       }
       return;
     }
   }
 
   ResumeMode resumeMode = ResumeMode::Continue;
   RootedValue value(cx);
 
+  // Preserve the debuggee's microtask event queue while we run the hooks, so
+  // the debugger's microtask checkpoints don't run from the debuggee's
+  // microtasks, and vice versa.
+  JS::AutoDebuggerJobQueueInterruption adjqi;
+  if (!adjqi.init(cx)) {
+    cx->clearPendingException();
+    return;
+  }
+
   for (size_t i = 0; i < watchers.length(); i++) {
     Debugger* dbg = fromJSObject(watchers[i]);
-    EnterDebuggeeNoExecute nx(cx, *dbg);
+    EnterDebuggeeNoExecute nx(cx, *dbg, adjqi);
 
     // We disallow resumption values from onNewGlobalObject hooks, because we
     // want the debugger hooks for global object creation to be infallible.
     // But if an onNewGlobalObject hook throws, and the uncaughtExceptionHook
     // decides to raise an error, we want to at least avoid invoking the rest
     // of the onNewGlobalObject handlers in the list (not for any super
     // compelling reason, just because it seems like the right thing to do).
     // So we ignore whatever comes out in |value|, but break out of the loop
     // if a non-success resume mode is returned.
     if (dbg->observesNewGlobalObject()) {
       resumeMode = dbg->fireNewGlobalObject(cx, global, &value);
+      adjqi.runJobs();
       if (resumeMode != ResumeMode::Continue &&
           resumeMode != ResumeMode::Return) {
         break;
       }
     }
   }
   MOZ_ASSERT(!cx->isExceptionPending());
 }