commit 4a56445424b3f42ac1891220c09294a43817bd68
parent 3437b89d31602ec35a3e70efe20d34b948dff1b7
Author: Iain Ireland <iireland@mozilla.com>
Date: Wed, 5 Nov 2025 18:52:31 +0000
Bug 1881130: Add eval-in-global support to setInterruptCallback for use in fuzzing r=jandem
The purpose of this patch is to fix a recurring problem when fuzzing the shell, which is that setInterruptCallback gives power that our actual interrupt handlers don't use. For example: ArrayJoinKernel could iterate over a large array, so we check for interrupts inside the main loop. However, an interrupt callback written in JS can mutate the array we're joining, invalidating ArrayJoinKernel's assumptions. Our real interrupt handlers won't do this. However, we still want some amount of fuzzing coverage for interrupts. Bug 1423937 and bug 1691184 are examples of legitimate bugs that we've found by fuzzing interrupts.
As a compromise, this patch adds support for passing a string as an argument to setInterruptCallback, and evaluating that string in a fresh global. This prevents the interrupt handler from directly mutating the interrupted code, but still allows it to do interesting things (get a backtrace, trigger a GC, etc). When fuzzing, setting a function as an interrupt handler will no longer be supported.
Differential Revision: https://phabricator.services.mozilla.com/D271330
Diffstat:
2 files changed, 49 insertions(+), 10 deletions(-)
diff --git a/js/src/jit-test/tests/basic/bug1881130.js b/js/src/jit-test/tests/basic/bug1881130.js
@@ -0,0 +1,14 @@
+// |jit-test| --fuzzing-safe
+
+setInterruptCallback("print('hello world'); true");
+interruptIf(true);
+for (var i = 0; i < 10; i++) {}
+
+// Function callback not available in --fuzzing-safe
+let threw = false;
+try {
+ setInterruptCallback(() => { return true; });
+} catch {
+ threw = true;
+}
+assertEq(threw, true);
diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp
@@ -880,6 +880,7 @@ enum class ShellGlobalKind {
WindowProxy,
};
+static void SetStandardRealmOptions(JS::RealmOptions& options);
static JSObject* NewGlobalObject(JSContext* cx, JS::RealmOptions& options,
JSPrincipals* principals, ShellGlobalKind kind,
bool immutablePrototype);
@@ -1131,25 +1132,49 @@ static bool ShellInterruptCallback(JSContext* cx) {
bool result;
if (sc->haveInterruptFunc) {
+ RootedValue rval(cx);
bool wasAlreadyThrowing = cx->isExceptionPending();
JS::AutoSaveExceptionState savedExc(cx);
- JSAutoRealm ar(cx, &sc->interruptFunc.toObject());
- RootedValue rval(cx);
- // Report any exceptions thrown by the JS interrupt callback, but do
- // *not* keep it on the cx. The interrupt handler is invoked at points
- // that are not expected to throw catchable exceptions, like at
- // JSOp::RetRval.
- //
- // If the interrupted JS code was already throwing, any exceptions
- // thrown by the interrupt handler are silently swallowed.
- {
+ if (sc->interruptFunc.isObject()) {
+ JSAutoRealm ar(cx, &sc->interruptFunc.toObject());
+
+ // Report any exceptions thrown by the JS interrupt callback, but do
+ // *not* keep it on the cx. The interrupt handler is invoked at points
+ // that are not expected to throw catchable exceptions, like at
+ // JSOp::RetRval.
+ //
+ // If the interrupted JS code was already throwing, any exceptions
+ // thrown by the interrupt handler are silently swallowed.
Maybe<AutoReportException> are;
if (!wasAlreadyThrowing) {
are.emplace(cx);
}
result = JS_CallFunctionValue(cx, nullptr, sc->interruptFunc,
JS::HandleValueArray::empty(), &rval);
+ } else {
+ RootedString str(cx, sc->interruptFunc.toString());
+
+ Maybe<AutoReportException> are;
+ if (!wasAlreadyThrowing) {
+ are.emplace(cx);
+ }
+
+ JS::RealmOptions options;
+ SetStandardRealmOptions(options);
+ RootedObject glob(
+ cx, NewGlobalObject(cx, options, nullptr, ShellGlobalKind::WindowProxy,
+ /* immutablePrototype = */ true));
+ if (!glob) {
+ return false;
+ }
+
+ RootedObject opts(cx, nullptr);
+ RootedObject cacheEntry(cx, nullptr);
+ JSAutoRealm ar(cx, glob);
+ if (!EvaluateInner(cx, str, &glob, opts, cacheEntry, &rval)) {
+ return false;
+ }
}
savedExc.restore();