Bug 1395240 - Implement stackTest function for JS stack OOM testing. r=jandem
authorChristian Holler <choller@mozilla.com>
Wed, 20 Sep 2017 14:19:21 +0200
changeset 435485 a69946757ded48de0b47d7b11b6268a636255502
parent 435484 880571f390485329eeca41daa02d66a2ccc1e975
child 435486 c065007b752b62c5af0e82176f4c8183cfb0fa56
push id1618
push userCallek@gmail.com
push dateThu, 11 Jan 2018 17:45:48 +0000
treeherdermozilla-release@882ca853e05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjandem
bugs1395240
milestone58.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 1395240 - Implement stackTest function for JS stack OOM testing. r=jandem MozReview-Commit-ID: LRFb0adTFR8
js/public/Utility.h
js/src/builtin/TestingFunctions.cpp
js/src/jsfriendapi.h
js/src/jsutil.cpp
--- a/js/public/Utility.h
+++ b/js/public/Utility.h
@@ -149,35 +149,99 @@ ShouldFailWithOOM()
     return false;
 }
 
 inline bool
 HadSimulatedOOM() {
     return counter >= maxAllocations;
 }
 
+/*
+ * Out of stack space testing support, similar to OOM testing functions.
+ */
+
+extern JS_PUBLIC_DATA(uint32_t) stackTargetThread;
+extern JS_PUBLIC_DATA(uint64_t) maxStackChecks;
+extern JS_PUBLIC_DATA(uint64_t) stackCheckCounter;
+extern JS_PUBLIC_DATA(bool) stackCheckFailAlways;
+
+extern void
+SimulateStackOOMAfter(uint64_t checks, uint32_t thread, bool always);
+
+extern void
+ResetSimulatedStackOOM();
+
+inline bool
+IsThreadSimulatingStackOOM()
+{
+    return js::oom::stackTargetThread && js::oom::stackTargetThread == js::oom::GetThreadType();
+}
+
+inline bool
+IsSimulatedStackOOMCheck()
+{
+    return IsThreadSimulatingStackOOM() &&
+           (stackCheckCounter == maxStackChecks || (stackCheckCounter > maxStackChecks && stackCheckFailAlways));
+}
+
+inline bool
+ShouldFailWithStackOOM()
+{
+    if (!IsThreadSimulatingStackOOM())
+        return false;
+
+    stackCheckCounter++;
+    if (IsSimulatedStackOOMCheck()) {
+        JS_OOM_CALL_BP_FUNC();
+        return true;
+    }
+    return false;
+}
+
+inline bool
+HadSimulatedStackOOM()
+{
+    return stackCheckCounter >= maxStackChecks;
+}
+
 } /* namespace oom */
 } /* namespace js */
 
 #  define JS_OOM_POSSIBLY_FAIL()                                              \
     do {                                                                      \
         if (js::oom::ShouldFailWithOOM())                                     \
             return nullptr;                                                   \
     } while (0)
 
 #  define JS_OOM_POSSIBLY_FAIL_BOOL()                                         \
     do {                                                                      \
         if (js::oom::ShouldFailWithOOM())                                     \
             return false;                                                     \
     } while (0)
 
+#  define JS_STACK_OOM_POSSIBLY_FAIL()                                        \
+    do {                                                                      \
+        if (js::oom::ShouldFailWithStackOOM())                                \
+            return false;                                                     \
+    } while (0)
+
+#  define JS_STACK_OOM_POSSIBLY_FAIL_REPORT()                                 \
+    do {                                                                      \
+        if (js::oom::ShouldFailWithStackOOM()) {                              \
+            ReportOverRecursed(cx);                                           \
+            return false;                                                     \
+        }                                                                     \
+    } 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)
 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
@@ -1622,17 +1622,17 @@ OOMTest(JSContext* cx, unsigned argc, Va
             // would be to have the caller pass some kind of exception
             // specification and to check the exception against it.
 
             cx->clearPendingException();
             cx->runtime()->hadOutOfMemory = false;
 
             // Some tests create a new compartment or zone on every
             // iteration. Our GC is triggered by GC allocations and not by
-            // number of copmartments or zones, so these won't normally get
+            // 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
@@ -1652,16 +1652,156 @@ OOMTest(JSContext* cx, unsigned argc, Va
             fprintf(stderr, "  finished after %d allocations\n", allocation - 2);
         }
     }
 
     cx->runningOOMTest = false;
     args.rval().setUndefined();
     return true;
 }
+
+static bool
+StackTest(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (args.length() < 1 || args.length() > 2) {
+        JS_ReportErrorASCII(cx, "stackTest() takes between 1 and 2 arguments.");
+        return false;
+    }
+
+    if (!args[0].isObject() || !args[0].toObject().is<JSFunction>()) {
+        JS_ReportErrorASCII(cx, "The first argument to stackTest() must be a function.");
+        return false;
+    }
+
+    if (args.length() == 2 && !args[1].isBoolean()) {
+        JS_ReportErrorASCII(cx, "The optional second argument to stackTest() 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() or stackTest() 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
+
+    for (unsigned thread = threadStart; thread < threadEnd; thread++) {
+        if (verbose)
+            fprintf(stderr, "thread %d\n", thread);
+
+        unsigned check = 1;
+        bool handledOOM;
+        do {
+            if (verbose)
+                fprintf(stderr, "  check %d\n", check);
+
+            MOZ_ASSERT(!cx->isExceptionPending());
+
+            js::oom::SimulateStackOOMAfter(check, thread, false);
+
+            RootedValue result(cx);
+            bool ok = JS_CallFunction(cx, cx->global(), function,
+                                      HandleValueArray::empty(), &result);
+
+            handledOOM = js::oom::HadSimulatedStackOOM();
+            js::oom::ResetSimulatedStackOOM();
+
+            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 (handledOOM);
+
+        if (verbose) {
+            fprintf(stderr, "  finished after %d checks\n", check - 2);
+        }
+    }
+
+    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;
@@ -4616,16 +4756,22 @@ static const JSFunctionSpecWithHelp Test
     JS_FN_HELP("oomTest", OOMTest, 0, 0,
 "oomTest(function, [expectExceptionOnFailure = true])",
 "  Test that the passed function behaves correctly under OOM conditions by\n"
 "  repeatedly executing it and simulating allocation failure at successive\n"
 "  allocations until the function completes without seeing a failure.\n"
 "  By default this tests that an exception is raised if execution fails, but\n"
 "  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)."),
 #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/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -1047,33 +1047,41 @@ GetNativeStackLimit(JSContext* cx, int e
  * allows less space than any other check, including a safety buffer (as in, it
  * uses the untrusted limit and subtracts a little more from it).
  */
 
 MOZ_ALWAYS_INLINE bool
 CheckRecursionLimit(JSContext* cx, uintptr_t limit)
 {
     int stackDummy;
+
+    JS_STACK_OOM_POSSIBLY_FAIL_REPORT();
+
     if (!JS_CHECK_STACK_SIZE(limit, &stackDummy)) {
         ReportOverRecursed(cx);
         return false;
     }
     return true;
 }
 
 MOZ_ALWAYS_INLINE bool
 CheckRecursionLimitDontReport(JSContext* cx, uintptr_t limit)
 {
     int stackDummy;
+
+    JS_STACK_OOM_POSSIBLY_FAIL();
+
     return JS_CHECK_STACK_SIZE(limit, &stackDummy);
 }
 
 MOZ_ALWAYS_INLINE bool
 CheckRecursionLimit(JSContext* cx)
 {
+    JS_STACK_OOM_POSSIBLY_FAIL_REPORT();
+
     // GetNativeStackLimit(cx) is pretty slow because it has to do an uninlined
     // call to RunningWithTrustedPrincipals to determine which stack limit to
     // use. To work around this, check the untrusted limit first to avoid the
     // overhead in most cases.
     uintptr_t untrustedLimit = GetNativeStackLimit(cx, JS::StackForUntrustedScript);
     if (MOZ_LIKELY(CheckRecursionLimitDontReport(cx, untrustedLimit)))
         return true;
     return CheckRecursionLimit(cx, GetNativeStackLimit(cx));
@@ -1083,22 +1091,26 @@ MOZ_ALWAYS_INLINE bool
 CheckRecursionLimitDontReport(JSContext* cx)
 {
     return CheckRecursionLimitDontReport(cx, GetNativeStackLimit(cx));
 }
 
 MOZ_ALWAYS_INLINE bool
 CheckRecursionLimitWithStackPointerDontReport(JSContext* cx, void* sp)
 {
+    JS_STACK_OOM_POSSIBLY_FAIL();
+
     return JS_CHECK_STACK_SIZE(GetNativeStackLimit(cx), sp);
 }
 
 MOZ_ALWAYS_INLINE bool
 CheckRecursionLimitWithStackPointer(JSContext* cx, void* sp)
 {
+    JS_STACK_OOM_POSSIBLY_FAIL_REPORT();
+
     if (!JS_CHECK_STACK_SIZE(GetNativeStackLimit(cx), sp)) {
         ReportOverRecursed(cx);
         return false;
     }
     return true;
 }
 
 MOZ_ALWAYS_INLINE bool
--- a/js/src/jsutil.cpp
+++ b/js/src/jsutil.cpp
@@ -40,16 +40,21 @@ mozilla::Atomic<AutoEnterOOMUnsafeRegion
 namespace oom {
 
 JS_PUBLIC_DATA(uint32_t) targetThread = 0;
 MOZ_THREAD_LOCAL(uint32_t) threadType;
 JS_PUBLIC_DATA(uint64_t) maxAllocations = UINT64_MAX;
 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;
+
 bool
 InitThreadType(void) {
     return threadType.init();
 }
 
 void
 SetThreadType(ThreadType type) {
     threadType.set(type);
@@ -62,43 +67,75 @@ GetThreadType(void) {
 
 static inline bool
 IsHelperThreadType(uint32_t thread)
 {
     return thread != THREAD_TYPE_NONE && thread != THREAD_TYPE_COOPERATING;
 }
 
 void
-SimulateOOMAfter(uint64_t allocations, uint32_t thread, bool always) {
+SimulateOOMAfter(uint64_t allocations, uint32_t thread, bool always)
+{
     Maybe<AutoLockHelperThreadState> lock;
     if (IsHelperThreadType(targetThread) || IsHelperThreadType(thread)) {
         lock.emplace();
         HelperThreadState().waitForAllThreadsLocked(lock.ref());
     }
 
     MOZ_ASSERT(counter + allocations > counter);
     MOZ_ASSERT(thread > js::THREAD_TYPE_NONE && thread < js::THREAD_TYPE_MAX);
     targetThread = thread;
     maxAllocations = counter + allocations;
     failAlways = always;
 }
 
 void
-ResetSimulatedOOM() {
+ResetSimulatedOOM()
+{
     Maybe<AutoLockHelperThreadState> lock;
     if (IsHelperThreadType(targetThread)) {
         lock.emplace();
         HelperThreadState().waitForAllThreadsLocked(lock.ref());
     }
 
     targetThread = THREAD_TYPE_NONE;
     maxAllocations = UINT64_MAX;
     failAlways = false;
 }
 
+void
+SimulateStackOOMAfter(uint64_t checks, uint32_t thread, bool always)
+{
+    Maybe<AutoLockHelperThreadState> lock;
+    if (IsHelperThreadType(stackTargetThread) || IsHelperThreadType(thread)) {
+        lock.emplace();
+        HelperThreadState().waitForAllThreadsLocked(lock.ref());
+    }
+
+    MOZ_ASSERT(stackCheckCounter + checks > stackCheckCounter);
+    MOZ_ASSERT(thread > js::THREAD_TYPE_NONE && thread < js::THREAD_TYPE_MAX);
+    stackTargetThread = thread;
+    maxStackChecks = stackCheckCounter + checks;
+    stackCheckFailAlways = always;
+}
+
+void
+ResetSimulatedStackOOM()
+{
+    Maybe<AutoLockHelperThreadState> lock;
+    if (IsHelperThreadType(stackTargetThread)) {
+        lock.emplace();
+        HelperThreadState().waitForAllThreadsLocked(lock.ref());
+    }
+
+    stackTargetThread = THREAD_TYPE_NONE;
+    maxStackChecks = UINT64_MAX;
+    stackCheckFailAlways = 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)
 {