tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 902433d0875dfe09025b6db82accbd2c358735d1
parent 041f7b579046f2b608f553b70886b74ad5970e85
Author: Matthew Gaudet <mgaudet@mozilla.com>
Date:   Fri, 19 Dec 2025 16:59:48 +0000

Bug 1989115 - Implement OOM stack trace capture r=jandem,iain

As well as a "get OOM stack trace" test function.

CheckForInterruptNoCallbacks in the ErrorReturnContinuation case means we can
capture OOM stack traces before the bottom-most frames are popped.

CheckForInterruptCallbacks in HandleException means we can capture OOM stack
traces for JITs before the bottom-most frames are popped.

Differential Revision: https://phabricator.services.mozilla.com/D265373

Diffstat:
Mjs/src/builtin/TestingFunctions.cpp | 30++++++++++++++++++++++++++++++
Ajs/src/jit-test/tests/test_oom_comprehensive.js | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mjs/src/jit/JitFrames.cpp | 4++++
Mjs/src/vm/Interpreter.cpp | 1+
Mjs/src/vm/JSContext-inl.h | 11+++++++++++
Mjs/src/vm/JSContext.cpp | 46+++++++++++++++++++++++++++++++++++++++++++++-
Mjs/src/vm/JSContext.h | 16++++++++++++++++
Mjs/src/vm/Runtime.cpp | 26+++++++++++++++++++++++---
Mmodules/libpref/init/StaticPrefList.yaml | 9++++++++-
9 files changed, 193 insertions(+), 5 deletions(-)

diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp @@ -10043,6 +10043,30 @@ static bool TestingFunc_SupportDifferentialTesting(JSContext* cx, unsigned argc, return true; } +static bool GetLastOOMStackTrace(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (!cx->hasOOMStackTrace()) { + args.rval().setNull(); + return true; + } + + const char* stackTrace = cx->getOOMStackTrace(); + MOZ_ASSERT(stackTrace); + + // The stackTrace persists, so we can clear the flag here in case copying the + // string fails + cx->unsetOOMStackTrace(); + + JSString* str = JS_NewStringCopyZ(cx, stackTrace); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + // clang-format off static const JSFunctionSpecWithHelp TestingFunctions[] = { JS_FN_HELP("gc", ::GC, 0, 0, @@ -11161,6 +11185,12 @@ JS_FN_HELP("isSmallFunction", IsSmallFunction, 1, 0, "popAllFusesInRealm()", " Pops all the fuses in the current realm"), + JS_FN_HELP("getLastOOMStackTrace", GetLastOOMStackTrace, 0, 0, +"getLastOOMStackTrace()", +" Returns the stack trace captured from the most recent out-of-memory exception,\n" +" or null if no OOM stack trace is available. The stack trace shows the JavaScript\n" +" call stack at the time the out-of-memory condition occurred."), + JS_FN_HELP("popAllFusesInRuntime", PopAllFusesInRuntime, 0, 0, "popAllFusesInRuntime()", " Pops all the fuses in the runtime"), diff --git a/js/src/jit-test/tests/test_oom_comprehensive.js b/js/src/jit-test/tests/test_oom_comprehensive.js @@ -0,0 +1,55 @@ +// |jit-test| --setpref=experimental.capture_oom_stack_trace=true; skip-if: !this.hasOwnProperty("getLastOOMStackTrace") + +function assertCapturesOOMStackTrace(f, lineNo) { + // Clear any existing trace + var initialTrace = getLastOOMStackTrace(); + assertEq(initialTrace, null); + + try { + f(); + assertEq(true, false, "Expected an OOM exception"); + } catch (e) { + print("✓ Exception caught: " + e); + + // Check for captured stack trace + var finalTrace = getLastOOMStackTrace(); + assertEq(finalTrace !== null, true, "Expected a stack trace after OOM"); + + print(finalTrace); + + // Detailed analysis + var lines = finalTrace.split('\n'); + let re = new RegExp("#0.*test_oom_comprehensive\.js:" + lineNo) + assertEq(re.test(lines[0]), true, "Missing innermost frame"); + } +} + +// Interpreter captures innermost frame +assertCapturesOOMStackTrace(function () { + function deepFunction() { + function evenDeeper() { + throwOutOfMemory(); + } + evenDeeper(); + } + deepFunction(); +}, 31); + +if (getJitCompilerOptions()['baseline.enable']){ + // JITs capture innermost frame + assertCapturesOOMStackTrace(function () { + function deepFunction(shouldOOM) { + function evenDeeper() { + if (shouldOOM) { + assertEq(inJit(), true, "Should be JIT-compiled"); + throwOutOfMemory(); + } + } + evenDeeper(); + } + const MAX = 150; + for (let i = 0; i < MAX; i++) { + deepFunction(i >= MAX - 1); + } + }, 45); +} diff --git a/js/src/jit/JitFrames.cpp b/js/src/jit/JitFrames.cpp @@ -691,6 +691,10 @@ static JitFrameLayout* GetLastProfilingFrame(ResumeFromException* rfe) { void HandleException(ResumeFromException* rfe) { JSContext* cx = TlsContext.get(); + if (!CheckForOOMStackTraceInterrupt(cx)) { + return; + } + cx->realm()->localAllocSite = nullptr; #ifdef DEBUG if (!IsPortableBaselineInterpreterEnabled()) { diff --git a/js/src/vm/Interpreter.cpp b/js/src/vm/Interpreter.cpp @@ -4472,6 +4472,7 @@ error: goto successful_return_continuation; case ErrorReturnContinuation: + CheckForOOMStackTraceInterrupt(cx); interpReturnOK = false; goto return_continuation; diff --git a/js/src/vm/JSContext-inl.h b/js/src/vm/JSContext-inl.h @@ -258,6 +258,17 @@ MOZ_ALWAYS_INLINE bool CallNativeImpl(JSContext* cx, NativeImpl impl, return ok; } +// OOM interrupts don't call the interrupt callbacks, so we don't need +// to worry about if there is an exception pending. We do want to handle +// the interrupt sooner than for other interrupts so we capture a precise +// stack trace. +MOZ_ALWAYS_INLINE bool CheckForOOMStackTraceInterrupt(JSContext* cx) { + if (MOZ_UNLIKELY(cx->hasPendingInterrupt(InterruptReason::OOMStackTrace))) { + return cx->handleInterruptNoCallbacks(); + } + return true; +} + 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 diff --git a/js/src/vm/JSContext.cpp b/js/src/vm/JSContext.cpp @@ -56,7 +56,9 @@ #include "util/NativeStack.h" #include "util/Text.h" #include "util/WindowsWrapper.h" -#include "vm/BytecodeUtil.h" // JSDVG_IGNORE_STACK +#include "js/friend/DumpFunctions.h" // for stack trace utilities +#include "js/Printer.h" // for FixedBufferPrinter +#include "vm/BytecodeUtil.h" // JSDVG_IGNORE_STACK #include "vm/ErrorObject.h" #include "vm/ErrorReporting.h" #include "vm/FrameIter.h" @@ -278,6 +280,8 @@ void JSContext::onOutOfMemory() { runtime()->hadOutOfMemory = true; gc::AutoSuppressGC suppressGC(this); + requestInterrupt(js::InterruptReason::OOMStackTrace); + /* Report the oom. */ if (JS::OutOfMemoryCallback oomCallback = runtime()->oomCallback) { oomCallback(this, runtime()->oomCallbackData); @@ -1290,11 +1294,19 @@ JSContext::JSContext(JSRuntime* runtime, const JS::ContextOptions& options) canSkipEnqueuingJobs(this, false), promiseRejectionTrackerCallback(this, nullptr), promiseRejectionTrackerCallbackData(this, nullptr), + oomStackTraceBuffer_(this, nullptr), + oomStackTraceBufferValid_(this, false), bypassCSPForDebugger(this, false), insideExclusiveDebuggerOnEval(this, nullptr), microTaskQueues(this) { MOZ_ASSERT(static_cast<JS::RootingContext*>(this) == JS::RootingContext::get(this)); + + if (JS::Prefs::experimental_capture_oom_stack_trace()) { + // Allocate pre-allocated buffer for OOM stack traces + oomStackTraceBuffer_ = + static_cast<char*>(js_calloc(OOMStackTraceBufferSize)); + } } JSContext::~JSContext() { @@ -1324,9 +1336,41 @@ JSContext::~JSContext() { irregexp::DestroyIsolate(isolate.ref()); } + // Free the pre-allocated OOM stack trace buffer + if (oomStackTraceBuffer_) { + js_free(oomStackTraceBuffer_); + } + TlsContext.set(nullptr); } +void JSContext::unsetOOMStackTrace() { oomStackTraceBufferValid_ = false; } + +const char* JSContext::getOOMStackTrace() const { + if (!oomStackTraceBufferValid_ || !oomStackTraceBuffer_) { + return nullptr; + } + return oomStackTraceBuffer_; +} + +bool JSContext::hasOOMStackTrace() const { return oomStackTraceBufferValid_; } + +void JSContext::captureOOMStackTrace() { + // Clear any existing stack trace + oomStackTraceBufferValid_ = false; + + if (!oomStackTraceBuffer_) { + return; // Buffer not available + } + + // Write directly to pre-allocated buffer to avoid any memory allocation + FixedBufferPrinter fbp(oomStackTraceBuffer_, OOMStackTraceBufferSize); + js::DumpBacktrace(this, fbp); + MOZ_ASSERT(strlen(oomStackTraceBuffer_) < OOMStackTraceBufferSize); + + oomStackTraceBufferValid_ = true; +} + void JSContext::setRuntime(JSRuntime* rt) { MOZ_ASSERT(!resolvingList); MOZ_ASSERT(!compartment()); diff --git a/js/src/vm/JSContext.h b/js/src/vm/JSContext.h @@ -151,6 +151,7 @@ enum class InterruptReason : uint32_t { AttachOffThreadCompilations = 1 << 2, CallbackUrgent = 1 << 3, CallbackCanWait = 1 << 4, + OOMStackTrace = 1 << 5, }; enum class ShouldCaptureStack { Maybe, Always }; @@ -709,6 +710,12 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext, #endif } + // OOM stack trace buffer management + void unsetOOMStackTrace(); + const char* getOOMStackTrace() const; + bool hasOOMStackTrace() const; + void captureOOMStackTrace(); + js::ContextData<int32_t> reportGranularity; /* see vm/Probes.h */ js::ContextData<js::AutoResolving*> resolvingList; @@ -927,6 +934,7 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext, // that's fine. void requestInterrupt(js::InterruptReason reason); bool handleInterrupt(); + bool handleInterruptNoCallbacks(); MOZ_ALWAYS_INLINE bool hasAnyPendingInterrupt() const { static_assert(sizeof(interruptBits_) == sizeof(uint32_t), @@ -981,6 +989,14 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext, promiseRejectionTrackerCallback; js::ContextData<void*> promiseRejectionTrackerCallbackData; + // Pre-allocated buffer for storing out-of-memory stack traces. + // This buffer is allocated during context initialization to avoid + // allocation during OOM conditions. The buffer stores a formatted + // stack trace string that can be retrieved by privileged JavaScript. + static constexpr size_t OOMStackTraceBufferSize = 4096; + js::ContextData<char*> oomStackTraceBuffer_; + js::ContextData<bool> oomStackTraceBufferValid_; + JSObject* getIncumbentGlobal(JSContext* cx); bool enqueuePromiseJob(JSContext* cx, js::HandleFunction job, js::HandleObject promise, diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp @@ -394,7 +394,7 @@ static bool InvokeInterruptCallbacks(JSContext* cx) { return stop; } -static bool HandleInterrupt(JSContext* cx, bool invokeCallback) { +static bool HandleInterrupt(JSContext* cx, bool invokeCallback, bool oomStackTrace) { MOZ_ASSERT(!cx->zone()->isAtomsZone()); cx->runtime()->gc.gcIfRequested(); @@ -403,7 +403,13 @@ static bool HandleInterrupt(JSContext* cx, bool invokeCallback) { // offthread compilation. jit::AttachFinishedCompilations(cx); - // Don't call the interrupt callback if we only interrupted for GC or Ion. + if (oomStackTrace) { + // Capture OOM stack trace this way because we don't have memory to handle + // it the way ComputeStackString does. + cx->captureOOMStackTrace(); + } + + // Don't call the interrupt callback if we only interrupted for GC, Ion, or OOM. if (!invokeCallback) { return true; } @@ -482,9 +488,23 @@ bool JSContext::handleInterrupt() { bool invokeCallback = hasPendingInterrupt(InterruptReason::CallbackUrgent) || hasPendingInterrupt(InterruptReason::CallbackCanWait); + bool oomStackTrace = hasPendingInterrupt(InterruptReason::OOMStackTrace); interruptBits_ = 0; resetJitStackLimit(); - return HandleInterrupt(this, invokeCallback); + return HandleInterrupt(this, invokeCallback, oomStackTrace); + } + return true; +} + +bool JSContext::handleInterruptNoCallbacks() { + MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime())); + if (hasAnyPendingInterrupt() || jitStackLimit == JS::NativeStackLimitMin) { + bool oomStackTrace = hasPendingInterrupt(InterruptReason::OOMStackTrace); + clearPendingInterrupt(js::InterruptReason::OOMStackTrace); + if (!hasAnyPendingInterrupt()) { + resetJitStackLimit(); + } + return HandleInterrupt(this, false, oomStackTrace); } return true; } diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -9075,7 +9075,7 @@ mirror: always set_spidermonkey_pref: startup - # Experimental support for Iterator Chunking in JavaScript. + # Experimental support for Iterator Chunking in JavaScript. - name: javascript.options.experimental.iterator_chunking type: bool value: false @@ -9097,6 +9097,13 @@ set_spidermonkey_pref: startup #endif // NIGHTLY_BUILD + # Capture stack traces for OOM +- name: javascript.options.experimental.capture_oom_stack_trace + type: bool + value: @IS_NIGHTLY_BUILD@ + mirror: always + set_spidermonkey_pref: startup + # Whether to Baseline-compile self-hosted functions the first time they are # used and cache the result. - name: javascript.options.experimental.self_hosted_cache