Bug 1402815 - Add interrupTest function. r?jandem draft
authorChristian Holler <choller@mozilla.com>
Fri, 22 Sep 2017 18:28:47 +0200
changeset 669751 d6ecb1c161db1e4c1929c4770cbf9d3bc0638c4e
parent 669750 ff48fab67d3c8e41035cd8ea1cbda1696fdd12fc
child 733045 989d4045746078575267042fa91b9b308e4e593c
push id81421
push usercholler@mozilla.com
push dateMon, 25 Sep 2017 11:21:04 +0000
reviewersjandem
bugs1402815
milestone58.0a1
Bug 1402815 - Add interrupTest function. r?jandem MozReview-Commit-ID: El24BdESnM2
js/public/Utility.h
js/src/builtin/TestingFunctions.cpp
js/src/jscntxtinlines.h
js/src/jsutil.cpp
--- a/js/public/Utility.h
+++ b/js/public/Utility.h
@@ -197,16 +197,64 @@ ShouldFailWithStackOOM()
 }
 
 inline bool
 HadSimulatedStackOOM()
 {
     return stackCheckCounter >= maxStackChecks;
 }
 
+/*
+ * Interrupt testing support, similar to OOM testing functions.
+ */
+
+extern JS_PUBLIC_DATA(uint32_t) interruptTargetThread;
+extern JS_PUBLIC_DATA(uint64_t) maxInterruptChecks;
+extern JS_PUBLIC_DATA(uint64_t) interruptCheckCounter;
+extern JS_PUBLIC_DATA(bool) interruptCheckFailAlways;
+
+extern void
+SimulateInterruptAfter(uint64_t checks, uint32_t thread, bool always);
+
+extern void
+ResetSimulatedInterrupt();
+
+inline bool
+IsThreadSimulatingInterrupt()
+{
+    return js::oom::interruptTargetThread && js::oom::interruptTargetThread == js::oom::GetThreadType();
+}
+
+inline bool
+IsSimulatedInterruptCheck()
+{
+    return IsThreadSimulatingInterrupt() &&
+           (interruptCheckCounter == maxInterruptChecks || (interruptCheckCounter > maxInterruptChecks && interruptCheckFailAlways));
+}
+
+inline bool
+ShouldFailWithInterrupt()
+{
+    if (!IsThreadSimulatingInterrupt())
+        return false;
+
+    interruptCheckCounter++;
+    if (IsSimulatedInterruptCheck()) {
+        JS_OOM_CALL_BP_FUNC();
+        return true;
+    }
+    return false;
+}
+
+inline bool
+HadSimulatedInterrupt()
+{
+    return interruptCheckCounter >= maxInterruptChecks;
+}
+
 } /* namespace oom */
 } /* namespace js */
 
 #  define JS_OOM_POSSIBLY_FAIL()                                              \
     do {                                                                      \
         if (js::oom::ShouldFailWithOOM())                                     \
             return nullptr;                                                   \
     } while (0)
@@ -226,22 +274,31 @@ HadSimulatedStackOOM()
 #  define JS_STACK_OOM_POSSIBLY_FAIL_REPORT()                                 \
     do {                                                                      \
         if (js::oom::ShouldFailWithStackOOM()) {                              \
             ReportOverRecursed(cx);                                           \
             return false;                                                     \
         }                                                                     \
     } while (0)
 
+#  define JS_INTERRUPT_POSSIBLY_FAIL()                                        \
+    do {                                                                      \
+        if (MOZ_UNLIKELY(js::oom::ShouldFailWithInterrupt())) {               \
+            cx->interrupt_ = true;                                            \
+            return cx->handleInterrupt();                                     \
+        }                                                                     \
+    } while (0)
+
 # else
 
 #  define JS_OOM_POSSIBLY_FAIL() do {} while(0)
 #  define JS_OOM_POSSIBLY_FAIL_BOOL() do {} while(0)
 #  define JS_STACK_OOM_POSSIBLY_FAIL() do {} while(0)
 #  define JS_STACK_OOM_POSSIBLY_FAIL_REPORT() do {} while(0)
+#  define JS_INTERRUPT_POSSIBLY_FAIL() do {} while(0)
 namespace js {
 namespace oom {
 static inline bool IsSimulatedOOMAllocation() { return false; }
 static inline bool ShouldFailWithOOM() { return false; }
 } /* namespace oom */
 } /* namespace js */
 
 # endif /* DEBUG || JS_OOM_BREAKPOINT */
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -1792,16 +1792,167 @@ StackTest(JSContext* cx, unsigned argc, 
             fprintf(stderr, "  finished after %d checks\n", check - 2);
         }
     }
 
     cx->runningOOMTest = false;
     args.rval().setUndefined();
     return true;
 }
+
+static bool
+FailingInterruptCallback(JSContext* cx)
+{
+    return false;
+}
+
+static bool
+InterruptTest(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (args.length() < 1 || args.length() > 2) {
+        JS_ReportErrorASCII(cx, "interruptTest() takes between 1 and 2 arguments.");
+        return false;
+    }
+
+    if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()) {
+        JS_ReportErrorASCII(cx, "The first argument to interruptTest() must be a function.");
+        return false;
+    }
+
+    if (args.length() == 2 && !args[1].isBoolean()) {
+        JS_ReportErrorASCII(cx, "The optional second argument to interruptTest() must be a boolean.");
+        return false;
+    }
+
+    bool expectExceptionOnFailure = true;
+    if (args.length() == 2)
+        expectExceptionOnFailure = args[1].toBoolean();
+
+    // There are some places where we do fail without raising an exception, so
+    // we can't expose this to the fuzzers by default.
+    if (fuzzingSafe)
+        expectExceptionOnFailure = false;
+
+    if (disableOOMFunctions) {
+        args.rval().setUndefined();
+        return true;
+    }
+
+    RootedFunction function(cx, &args[0].toObject().as<JSFunction>());
+
+    bool verbose = EnvVarIsDefined("OOM_VERBOSE");
+
+    unsigned threadStart = THREAD_TYPE_COOPERATING;
+    unsigned threadEnd = THREAD_TYPE_MAX;
+
+    // Test a single thread type if specified by the OOM_THREAD environment variable.
+    int threadOption = 0;
+    if (EnvVarAsInt("OOM_THREAD", &threadOption)) {
+        if (threadOption < THREAD_TYPE_COOPERATING || threadOption > THREAD_TYPE_MAX) {
+            JS_ReportErrorASCII(cx, "OOM_THREAD value out of range.");
+            return false;
+        }
+
+        threadStart = threadOption;
+        threadEnd = threadOption + 1;
+    }
+
+    if (cx->runningOOMTest) {
+        JS_ReportErrorASCII(cx, "Nested call to oomTest(), stackTest() or interruptTest() is not allowed.");
+        return false;
+    }
+    cx->runningOOMTest = true;
+
+    MOZ_ASSERT(!cx->isExceptionPending());
+
+    size_t compartmentCount = CountCompartments(cx);
+
+#ifdef JS_GC_ZEAL
+    JS_SetGCZeal(cx, 0, JS_DEFAULT_ZEAL_FREQ);
+#endif
+
+    JSInterruptCallback *prevEnd = cx->interruptCallbacks().end();
+    JS_AddInterruptCallback(cx, FailingInterruptCallback);
+
+    for (unsigned thread = threadStart; thread < threadEnd; thread++) {
+        if (verbose)
+            fprintf(stderr, "thread %d\n", thread);
+
+        unsigned check = 1;
+        bool handledInterrupt;
+        do {
+            if (verbose)
+                fprintf(stderr, "  check %d\n", check);
+
+            MOZ_ASSERT(!cx->isExceptionPending());
+
+            js::oom::SimulateInterruptAfter(check, thread, false);
+
+            RootedValue result(cx);
+            bool ok = JS_CallFunction(cx, cx->global(), function,
+                                      HandleValueArray::empty(), &result);
+
+            handledInterrupt = js::oom::HadSimulatedInterrupt();
+            js::oom::ResetSimulatedInterrupt();
+
+            MOZ_ASSERT_IF(ok, !cx->isExceptionPending());
+
+            if (ok) {
+                MOZ_ASSERT(!cx->isExceptionPending(),
+                           "Thunk execution succeeded but an exception was raised - "
+                           "missing error check?");
+            } else if (expectExceptionOnFailure) {
+                MOZ_ASSERT(cx->isExceptionPending(),
+                           "Thunk execution failed but no exception was raised - "
+                           "missing call to js::ReportOutOfMemory()?");
+            }
+
+            // Note that it is possible that the function throws an exception
+            // unconnected to OOM, in which case we ignore it. More correct
+            // would be to have the caller pass some kind of exception
+            // specification and to check the exception against it.
+
+            cx->clearPendingException();
+
+            // Some tests create a new compartment or zone on every
+            // iteration. Our GC is triggered by GC allocations and not by
+            // number of compartments or zones, so these won't normally get
+            // cleaned up. The check here stops some tests running out of
+            // memory.
+            if (CountCompartments(cx) > compartmentCount + 100) {
+                JS_GC(cx);
+                compartmentCount = CountCompartments(cx);
+            }
+
+#ifdef JS_TRACE_LOGGING
+            // Reset the TraceLogger state if enabled.
+            TraceLoggerThread* logger = TraceLoggerForCurrentThread(cx);
+            if (logger->enabled()) {
+                while (logger->enabled())
+                    logger->disable();
+                logger->enable(cx);
+            }
+#endif
+
+            check++;
+        } while (handledInterrupt);
+
+        if (verbose) {
+            fprintf(stderr, "  finished after %d checks\n", check - 2);
+        }
+    }
+
+    // Clear any interrupt callbacks we added within this function
+    cx->interruptCallbacks().erase(prevEnd, cx->interruptCallbacks().end());
+    cx->runningOOMTest = false;
+    args.rval().setUndefined();
+    return true;
+}
 #endif
 
 static bool
 SettlePromiseNow(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     if (!args.requireAtLeast(cx, "settlePromiseNow", 1))
         return false;
@@ -4762,16 +4913,20 @@ static const JSFunctionSpecWithHelp Test
 "  this can be disabled by passing false as the optional second parameter.\n"
 "  This is also disabled when --fuzzing-safe is specified."),
 
     JS_FN_HELP("stackTest", StackTest, 0, 0,
 "stackTest(function, [expectExceptionOnFailure = true])",
 "  This function behaves exactly like oomTest with the difference that\n"
 "  instead of simulating regular OOM conditions, it simulates the engine\n"
 "  running out of stack space (failing recursion check)."),
+
+    JS_FN_HELP("interruptTest", InterruptTest, 0, 0,
+"interruptTest(function, [expectExceptionOnFailure = true])",
+"  This function simulates interrupts similar to how oomTest simulates OOM conditions."),
 #endif
 
     JS_FN_HELP("settlePromiseNow", SettlePromiseNow, 1, 0,
 "settlePromiseNow(promise)",
 "  'Settle' a 'promise' immediately. This just marks the promise as resolved\n"
 "  with a value of `undefined` and causes the firing of any onPromiseSettled\n"
 "  hooks set on Debugger instances that are observing the given promise's\n"
 "  global as a debuggee."),
--- a/js/src/jscntxtinlines.h
+++ b/js/src/jscntxtinlines.h
@@ -402,16 +402,19 @@ CallJSDeletePropertyOp(JSContext* cx, JS
 MOZ_ALWAYS_INLINE bool
 CheckForInterrupt(JSContext* cx)
 {
     MOZ_ASSERT(!cx->isExceptionPending());
     // Add an inline fast-path since we have to check for interrupts in some hot
     // C++ loops of library builtins.
     if (MOZ_UNLIKELY(cx->hasPendingInterrupt()))
         return cx->handleInterrupt();
+
+    JS_INTERRUPT_POSSIBLY_FAIL();
+
     return true;
 }
 
 }  /* namespace js */
 
 inline js::LifoAlloc&
 JSContext::typeLifoAlloc()
 {
--- a/js/src/jsutil.cpp
+++ b/js/src/jsutil.cpp
@@ -45,16 +45,21 @@ JS_PUBLIC_DATA(uint64_t) maxAllocations 
 JS_PUBLIC_DATA(uint64_t) counter = 0;
 JS_PUBLIC_DATA(bool) failAlways = true;
 
 JS_PUBLIC_DATA(uint32_t) stackTargetThread = 0;
 JS_PUBLIC_DATA(uint64_t) maxStackChecks = UINT64_MAX;
 JS_PUBLIC_DATA(uint64_t) stackCheckCounter = 0;
 JS_PUBLIC_DATA(bool) stackCheckFailAlways = true;
 
+JS_PUBLIC_DATA(uint32_t) interruptTargetThread = 0;
+JS_PUBLIC_DATA(uint64_t) maxInterruptChecks = UINT64_MAX;
+JS_PUBLIC_DATA(uint64_t) interruptCheckCounter = 0;
+JS_PUBLIC_DATA(bool) interruptCheckFailAlways = true;
+
 bool
 InitThreadType(void) {
     return threadType.init();
 }
 
 void
 SetThreadType(ThreadType type) {
     threadType.set(type);
@@ -126,16 +131,45 @@ ResetSimulatedStackOOM()
         HelperThreadState().waitForAllThreadsLocked(lock.ref());
     }
 
     stackTargetThread = THREAD_TYPE_NONE;
     maxStackChecks = UINT64_MAX;
     stackCheckFailAlways = false;
 }
 
+void
+SimulateInterruptAfter(uint64_t checks, uint32_t thread, bool always)
+{
+    Maybe<AutoLockHelperThreadState> lock;
+    if (IsHelperThreadType(interruptTargetThread) || IsHelperThreadType(thread)) {
+        lock.emplace();
+        HelperThreadState().waitForAllThreadsLocked(lock.ref());
+    }
+
+    MOZ_ASSERT(interruptCheckCounter + checks > interruptCheckCounter);
+    MOZ_ASSERT(thread > js::THREAD_TYPE_NONE && thread < js::THREAD_TYPE_MAX);
+    interruptTargetThread = thread;
+    maxInterruptChecks = interruptCheckCounter + checks;
+    interruptCheckFailAlways = always;
+}
+
+void
+ResetSimulatedInterrupt()
+{
+    Maybe<AutoLockHelperThreadState> lock;
+    if (IsHelperThreadType(interruptTargetThread)) {
+        lock.emplace();
+        HelperThreadState().waitForAllThreadsLocked(lock.ref());
+    }
+
+    interruptTargetThread = THREAD_TYPE_NONE;
+    maxInterruptChecks = UINT64_MAX;
+    interruptCheckFailAlways = false;
+}
 
 } // namespace oom
 } // namespace js
 #endif // defined(DEBUG) || defined(JS_OOM_BREAKPOINT)
 
 JS_PUBLIC_API(void)
 JS_Assert(const char* s, const char* file, int ln)
 {