tor-browser

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

commit 51f685e3a63038eb4b7550d1e11194ec8eab0ce2
parent be6b5737dce2b1f0de100036026dc92c39a01189
Author: Matthew Gaudet <mgaudet@mozilla.com>
Date:   Mon,  3 Nov 2025 21:01:29 +0000

Bug 1989115 - Implement OOM stack trace capture r=jandem

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

We don't need to capture OOM stack trace when in unsafe calls with ABI because
those functions either resolve the OOM explicitly or don't throw exceptions (as
checked by AutoUnsafeCallWithABI). We don't have a stack that we can walk
anyway, so we do not try to capture one in those cases.

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

Diffstat:
Mjs/src/builtin/TestingFunctions.cpp | 32++++++++++++++++++++++++++++++++
Ajs/src/jit-test/tests/test_oom_comprehensive.js | 33+++++++++++++++++++++++++++++++++
Mjs/src/vm/JSContext.cpp | 51++++++++++++++++++++++++++++++++++++++++++++++++++-
Mjs/src/vm/JSContext.h | 14++++++++++++++
Mmodules/libpref/init/StaticPrefList.yaml | 7+++++++
5 files changed, 136 insertions(+), 1 deletion(-)

diff --git a/js/src/builtin/TestingFunctions.cpp b/js/src/builtin/TestingFunctions.cpp @@ -10037,6 +10037,32 @@ 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(); + if (!stackTrace) { + args.rval().setNull(); + return true; + } + + JSString* str = JS_NewStringCopyZ(cx, stackTrace); + if (!str) { + return false; + } + + // Clear the stored OOM stack trace after retrieving it once. + cx->unsetOOMStackTrace(); + + args.rval().setString(str); + return true; +} + // clang-format off static const JSFunctionSpecWithHelp TestingFunctions[] = { JS_FN_HELP("gc", ::GC, 0, 0, @@ -11155,6 +11181,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,32 @@ +// |jit-test| --setpref=experimental.capture_oom_stack_trace=true; skip-if: !this.hasOwnProperty("getLastOOMStackTrace") + +function testStack() { + function deepFunction() { + function evenDeeper() { + throwOutOfMemory(); + } + return evenDeeper(); + } + return deepFunction(); +} + +// Clear any existing trace +var initialTrace = getLastOOMStackTrace(); +assertEq(initialTrace, null); + +try { + testStack(); + 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').filter(line => line.trim()); + assertEq(finalTrace.includes("#"), true); +} +\ No newline at end of file diff --git a/js/src/vm/JSContext.cpp b/js/src/vm/JSContext.cpp @@ -57,7 +57,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" @@ -279,6 +281,13 @@ void JSContext::onOutOfMemory() { runtime()->hadOutOfMemory = true; gc::AutoSuppressGC suppressGC(this); + // Capture stack trace before doing anything else that might use memory. + // If we're in an unsafe ABI context, we don't need to capture a stack trace + // because the function will explicitly recover from OOM. + if (!inUnsafeCallWithABI) { + captureOOMStackTrace(); + } + /* Report the oom. */ if (JS::OutOfMemoryCallback oomCallback = runtime()->oomCallback) { oomCallback(this, runtime()->oomCallbackData); @@ -1266,10 +1275,18 @@ JSContext::JSContext(JSRuntime* runtime, const JS::ContextOptions& options) canSkipEnqueuingJobs(this, false), promiseRejectionTrackerCallback(this, nullptr), promiseRejectionTrackerCallbackData(this, nullptr), + oomStackTraceBuffer_(this, nullptr), + oomStackTraceBufferValid_(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)); + } } #ifdef ENABLE_WASM_JSPI @@ -1309,9 +1326,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 @@ -684,6 +684,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; @@ -956,6 +962,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/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -8997,6 +8997,13 @@ mirror: always set_spidermonkey_pref: startup + # Capture stack traces for OOM +- name: javascript.options.experimental.capture_oom_stack_trace + type: bool + value: true + mirror: always + set_spidermonkey_pref: startup + #endif // NIGHTLY_BUILD # Whether to Baseline-compile self-hosted functions the first time they are