commit d48a6a97232229679e7b2f70f3521016a55e444a
parent 0e17b2f9c4321f55c4299bc18515e2c27616d2b2
Author: Ryan Hunt <rhunt@eqrion.net>
Date: Tue, 18 Nov 2025 16:57:30 +0000
Bug 1990931 - wasm: Move stack limit from wasm::Instance to wasm::Context. r=yury
This allows us to update the stack limit without acquiring any mutex
or iterating over all the instances on a thread.
Differential Revision: https://phabricator.services.mozilla.com/D271459
Diffstat:
14 files changed, 136 insertions(+), 92 deletions(-)
diff --git a/js/src/jit/MacroAssembler.cpp b/js/src/jit/MacroAssembler.cpp
@@ -5836,16 +5836,20 @@ void MacroAssembler::wasmTrap(wasm::Trap trap,
}
uint32_t MacroAssembler::wasmReserveStackChecked(uint32_t amount, Label* fail) {
+ Register scratch1 = ABINonArgReg0;
+ Register scratch2 = ABINonArgReg1;
+ loadPtr(Address(InstanceReg, wasm::Instance::offsetOfCx()), scratch2);
+
if (amount > MAX_UNCHECKED_LEAF_FRAME_SIZE) {
// The frame is large. Don't bump sp until after the stack limit check so
// that the trap handler isn't called with a wild sp.
- Register scratch = ABINonArgReg0;
- moveStackPtrTo(scratch);
- branchPtr(Assembler::Below, scratch, Imm32(amount), fail);
- subPtr(Imm32(amount), scratch);
+ moveStackPtrTo(scratch1);
+ branchPtr(Assembler::Below, scratch1, Imm32(amount), fail);
+ subPtr(Imm32(amount), scratch1);
branchPtr(Assembler::AboveOrEqual,
- Address(InstanceReg, wasm::Instance::offsetOfStackLimit()),
- scratch, fail);
+ Address(scratch2, JSContext::offsetOfWasm() +
+ wasm::Context::offsetOfStackLimit()),
+ scratch1, fail);
reserveStack(amount);
// The stack amount was reserved after branching to the fail label.
return 0;
@@ -5853,7 +5857,8 @@ uint32_t MacroAssembler::wasmReserveStackChecked(uint32_t amount, Label* fail) {
reserveStack(amount);
branchStackPtrRhs(Assembler::AboveOrEqual,
- Address(InstanceReg, wasm::Instance::offsetOfStackLimit()),
+ Address(scratch2, JSContext::offsetOfWasm() +
+ wasm::Context::offsetOfStackLimit()),
fail);
// The stack amount was reserved before branching to the fail label.
return amount;
diff --git a/js/src/vm/JSContext.cpp b/js/src/vm/JSContext.cpp
@@ -1308,16 +1308,6 @@ JSContext::JSContext(JSRuntime* runtime, const JS::ContextOptions& options)
}
}
-#ifdef ENABLE_WASM_JSPI
-bool js::IsSuspendableStackActive(JSContext* cx) {
- return cx->wasm().suspendableStackLimit != JS::NativeStackLimitMin;
-}
-
-JS::NativeStackLimit js::GetSuspendableStackLimit(JSContext* cx) {
- return cx->wasm().suspendableStackLimit;
-}
-#endif
-
JSContext::~JSContext() {
#ifdef DEBUG
// Clear the initialized_ first, so that ProtectedData checks will allow us to
@@ -1623,7 +1613,10 @@ void JSContext::resetJitStackLimit() {
jitStackLimitNoInterrupt = jitStackLimit;
}
-void JSContext::initJitStackLimit() { resetJitStackLimit(); }
+void JSContext::initJitStackLimit() {
+ resetJitStackLimit();
+ wasm_.initStackLimit(this);
+}
JSScript* JSContext::currentScript(jsbytecode** ppc,
AllowCrossRealm allowCrossRealm) {
diff --git a/js/src/vm/JSContext.h b/js/src/vm/JSContext.h
@@ -634,6 +634,7 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext,
public:
js::wasm::Context& wasm() { return wasm_; }
+ static constexpr size_t offsetOfWasm() { return offsetof(JSContext, wasm_); }
/* Temporary arena pool used while compiling and decompiling. */
static const size_t TEMP_LIFO_ALLOC_PRIMARY_CHUNK_SIZE = 4 * 1024;
diff --git a/js/src/wasm/WasmBCFrame.h b/js/src/wasm/WasmBCFrame.h
@@ -539,11 +539,13 @@ class BaseStackFrame final : public BaseStackFrameAllocator {
// Note the platform scratch register may be used by branchPtr(), so
// generally tmp must be something else.
- void checkStack(Register tmp, Label* stackOverflowTrap) {
- stackAddOffset_ = masm.sub32FromStackPtrWithPatch(tmp);
+ void checkStack(Register tmp1, Register tmp2, Label* stackOverflowTrap) {
+ masm.loadPtr(Address(InstanceReg, wasm::Instance::offsetOfCx()), tmp2);
+ stackAddOffset_ = masm.sub32FromStackPtrWithPatch(tmp1);
masm.branchPtr(Assembler::AboveOrEqual,
- Address(InstanceReg, wasm::Instance::offsetOfStackLimit()),
- tmp, stackOverflowTrap);
+ Address(tmp2, JSContext::offsetOfWasm() +
+ wasm::Context::offsetOfStackLimit()),
+ tmp1, stackOverflowTrap);
}
void patchCheckStack() {
diff --git a/js/src/wasm/WasmBaselineCompile.cpp b/js/src/wasm/WasmBaselineCompile.cpp
@@ -589,7 +589,7 @@ bool BaseCompiler::beginFunction() {
if (!oolStackOverflowTrap) {
return false;
}
- fr.checkStack(ABINonArgReg0, oolStackOverflowTrap->entry());
+ fr.checkStack(ABINonArgReg0, ABINonArgReg1, oolStackOverflowTrap->entry());
OutOfLineCode* oolInterruptTrap = addOutOfLineCode(
new (alloc_) OutOfLineResumableTrap(Trap::CheckInterrupt, trapSiteDesc(),
diff --git a/js/src/wasm/WasmContext.cpp b/js/src/wasm/WasmContext.cpp
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ *
+ * Copyright 2025 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "wasm/WasmContext.h"
+
+#include "js/friend/StackLimits.h"
+#include "vm/JSContext.h"
+
+using namespace js::wasm;
+
+Context::Context()
+ : triedToInstallSignalHandlers(false),
+ haveSignalHandlers(false),
+ stackLimit(JS::NativeStackLimitMin)
+#ifdef ENABLE_WASM_JSPI
+ ,
+ onSuspendableStack(0),
+ suspendableStacksCount(0)
+#endif
+{
+}
+
+void Context::initStackLimit(JSContext* cx) {
+ // The wasm stack limit is the same as the jit stack limit. We also don't
+ // use the stack limit for triggering interrupts.
+ stackLimit = cx->jitStackLimitNoInterrupt;
+}
+
+#ifdef ENABLE_WASM_JSPI
+void Context::enterSuspendableStack(JS::NativeStackLimit newStackLimit) {
+ MOZ_ASSERT(onSuspendableStack == 0);
+ onSuspendableStack = 1;
+ stackLimit = newStackLimit;
+}
+
+void Context::leaveSuspendableStack(JSContext* cx) {
+ MOZ_ASSERT(onSuspendableStack != 0);
+ onSuspendableStack = 0;
+ initStackLimit(cx);
+}
+#endif
+
+#ifdef ENABLE_WASM_JSPI
+bool js::IsSuspendableStackActive(JSContext* cx) {
+ return cx->wasm().onSuspendableStack != 0;
+}
+
+JS::NativeStackLimit js::GetSuspendableStackLimit(JSContext* cx) {
+ MOZ_ASSERT(IsSuspendableStackActive(cx));
+ return cx->wasm().stackLimit;
+}
+#endif
diff --git a/js/src/wasm/WasmContext.h b/js/src/wasm/WasmContext.h
@@ -19,7 +19,13 @@
#ifndef wasm_context_h
#define wasm_context_h
-#include "mozilla/DoublyLinkedList.h"
+#ifdef ENABLE_WASM_JSPI
+# include "mozilla/DoublyLinkedList.h"
+
+# include "gc/Barrier.h"
+#endif // ENABLE_WASM_JSPI
+
+#include "js/NativeStackLimits.h"
namespace js::wasm {
@@ -51,24 +57,38 @@ class SuspenderContext {
class Context {
public:
- Context()
- : triedToInstallSignalHandlers(false),
- haveSignalHandlers(false)
+ Context();
+
+ static constexpr size_t offsetOfStackLimit() {
+ return offsetof(Context, stackLimit);
+ }
#ifdef ENABLE_WASM_JSPI
- ,
- suspendableStackLimit(JS::NativeStackLimitMin),
- suspendableStacksCount(0)
-#endif
- {
+ static constexpr size_t offsetOfOnSuspendableStack() {
+ return offsetof(Context, onSuspendableStack);
}
+#endif
+
+ void initStackLimit(JSContext* cx);
+
+#ifdef ENABLE_WASM_JSPI
+ void enterSuspendableStack(JS::NativeStackLimit newStackLimit);
+ void leaveSuspendableStack(JSContext* cx);
+#endif
// Used by wasm::EnsureThreadSignalHandlers(cx) to install thread signal
// handlers once per JSContext/thread.
bool triedToInstallSignalHandlers;
bool haveSignalHandlers;
+ // Like JSContext::jitStackLimit but used for wasm code. Wasm code doesn't
+ // use the stack limit for interrupts, but it does update it for stack
+ // switching.
+ JS::NativeStackLimit stackLimit;
+
#ifdef ENABLE_WASM_JSPI
- JS::NativeStackLimit suspendableStackLimit;
+ // Boolean value set to true when the top wasm frame is currently executed on
+ // a suspendable stack. Aligned to int32_t to be used on JIT code.
+ int32_t onSuspendableStack;
mozilla::Atomic<uint32_t> suspendableStacksCount;
SuspenderContext promiseIntegration;
#endif
diff --git a/js/src/wasm/WasmFrameIter.cpp b/js/src/wasm/WasmFrameIter.cpp
@@ -1000,15 +1000,18 @@ void wasm::GenerateJitExitPrologue(MacroAssembler& masm,
{
# if defined(JS_CODEGEN_ARM64)
AutoForbidPoolsAndNops afp(&masm,
- /* number of instructions in scope = */ 2);
+ /* number of instructions in scope = */ 3);
# endif
offsets->begin = masm.currentOffset();
Label fallback;
masm.bind(&fallback, BufferOffset(fallbackOffset));
const Register scratch = ABINonArgReg0;
- masm.load32(Address(InstanceReg, Instance::offsetOfOnSuspendableStack()),
- scratch);
+ masm.loadPtr(Address(InstanceReg, Instance::offsetOfCx()), scratch);
+ masm.load32(
+ Address(scratch, JSContext::offsetOfWasm() +
+ wasm::Context::offsetOfOnSuspendableStack()),
+ scratch);
masm.branchTest32(Assembler::NonZero, scratch, scratch, &fallback);
}
diff --git a/js/src/wasm/WasmInstance.cpp b/js/src/wasm/WasmInstance.cpp
@@ -2374,7 +2374,6 @@ Instance::Instance(JSContext* cx, Handle<WasmInstanceObject*> object,
const SharedCode& code, SharedTableVector&& tables,
UniqueDebugState maybeDebug)
: realm_(cx->realm()),
- onSuspendableStack_(false),
allocSites_(nullptr),
jsJitExceptionHandler_(
cx->runtime()->jitRuntime()->getExceptionTail().value),
@@ -2434,7 +2433,6 @@ bool Instance::init(JSContext* cx, const JSObjectVector& funcImports,
cx_ = cx;
valueBoxClass_ = AnyRef::valueBoxClass();
interrupt_ = false;
- stackLimit_ = cx->stackLimitForJitCode(JS::StackForUntrustedScript);
jumpTable_ = code_->tieringJumpTable();
debugFilter_ = nullptr;
callRefMetrics_ = nullptr;
@@ -2833,16 +2831,6 @@ bool Instance::isInterrupted() const { return interrupt_; }
void Instance::resetInterrupt() { interrupt_ = false; }
-void Instance::setTemporaryStackLimit(JS::NativeStackLimit limit) {
- stackLimit_ = limit;
- onSuspendableStack_ = true;
-}
-
-void Instance::resetTemporaryStackLimit(JSContext* cx) {
- stackLimit_ = cx->stackLimitForJitCode(JS::StackForUntrustedScript);
- onSuspendableStack_ = false;
-}
-
int32_t Instance::computeInitialHotnessCounter(uint32_t funcIndex,
size_t codeSectionSize) {
MOZ_ASSERT(code().mode() == CompileMode::LazyTiering);
diff --git a/js/src/wasm/WasmInstance.h b/js/src/wasm/WasmInstance.h
@@ -119,16 +119,9 @@ class alignas(16) Instance {
// The tag object of the pending exception.
GCPtr<AnyRef> pendingExceptionTag_;
- // Equal to cx->stackLimitForJitCode(JS::StackForUntrustedScript).
- JS::NativeStackLimit stackLimit_;
-
// Set to 1 when wasm should call CheckForInterrupt.
mozilla::Atomic<uint32_t, mozilla::Relaxed> interrupt_;
- // Boolean value set to true when instance code is executed on a suspendable
- // stack. Aligned to int32_t to be used on JIT code.
- int32_t onSuspendableStack_;
-
// The address of the realm()->zone()->needsIncrementalBarrier(). This is
// specific to this instance and not a process wide field, and so it cannot
// be linked into code.
@@ -314,15 +307,9 @@ class alignas(16) Instance {
static constexpr size_t offsetOfPendingExceptionTag() {
return offsetof(Instance, pendingExceptionTag_);
}
- static constexpr size_t offsetOfStackLimit() {
- return offsetof(Instance, stackLimit_);
- }
static constexpr size_t offsetOfInterrupt() {
return offsetof(Instance, interrupt_);
}
- static constexpr size_t offsetOfOnSuspendableStack() {
- return offsetof(Instance, onSuspendableStack_);
- }
static constexpr size_t offsetOfAllocSites() {
return offsetof(Instance, allocSites_);
}
@@ -395,9 +382,6 @@ class alignas(16) Instance {
bool isInterrupted() const;
void resetInterrupt();
- void setTemporaryStackLimit(JS::NativeStackLimit limit);
- void resetTemporaryStackLimit(JSContext* cx);
-
void setAllocationMetadataBuilder(
const js::AllocationMetadataBuilder* allocationMetadataBuilder) {
allocationMetadataBuilder_ = allocationMetadataBuilder;
diff --git a/js/src/wasm/WasmPI.cpp b/js/src/wasm/WasmPI.cpp
@@ -52,6 +52,11 @@
#endif
#ifdef XP_WIN
+// We only need the `windows.h` header, but this file can get unified built
+// with WasmSignalHandlers.cpp, which requires `winternal.h` to be included
+// before the `windows.h` header, and so we must include it here for that case.
+# include <winternl.h> // must include before util/WindowsWrapper.h's `#undef`s
+
# include "util/WindowsWrapper.h"
#endif
@@ -433,7 +438,7 @@ void SuspenderObject::trace(JSTracer* trc, JSObject* obj) {
void SuspenderObject::setMoribund(JSContext* cx) {
MOZ_ASSERT(state() == SuspenderState::Active);
- ResetInstanceStackLimits(cx);
+ cx->wasm().leaveSuspendableStack(cx);
# if defined(_WIN32)
data()->restoreTIBStackFields();
# endif
@@ -448,7 +453,7 @@ void SuspenderObject::setMoribund(JSContext* cx) {
void SuspenderObject::setActive(JSContext* cx) {
data()->setState(SuspenderState::Active);
- UpdateInstanceStackLimitsForSuspendableStack(cx, getStackMemoryLimit());
+ cx->wasm().enterSuspendableStack(getStackMemoryLimit());
# if defined(_WIN32)
data()->updateTIBStackFields();
# endif
@@ -456,7 +461,7 @@ void SuspenderObject::setActive(JSContext* cx) {
void SuspenderObject::setSuspended(JSContext* cx) {
data()->setState(SuspenderState::Suspended);
- ResetInstanceStackLimits(cx);
+ cx->wasm().leaveSuspendableStack(cx);
# if defined(_WIN32)
data()->restoreTIBStackFields();
# endif
diff --git a/js/src/wasm/WasmRealm.cpp b/js/src/wasm/WasmRealm.cpp
@@ -135,22 +135,3 @@ void wasm::ResetInterruptState(JSContext* cx) {
instance->resetInterrupt();
}
}
-
-#ifdef ENABLE_WASM_JSPI
-void wasm::UpdateInstanceStackLimitsForSuspendableStack(
- JSContext* cx, JS::NativeStackLimit limit) {
- auto runtimeInstances = cx->runtime()->wasmInstances.lock();
- cx->wasm().suspendableStackLimit = limit;
- for (Instance* instance : runtimeInstances.get()) {
- instance->setTemporaryStackLimit(limit);
- }
-}
-
-void wasm::ResetInstanceStackLimits(JSContext* cx) {
- auto runtimeInstances = cx->runtime()->wasmInstances.lock();
- cx->wasm().suspendableStackLimit = JS::NativeStackLimitMin;
- for (Instance* instance : runtimeInstances.get()) {
- instance->resetTemporaryStackLimit(cx);
- }
-}
-#endif // ENABLE_WASM_JSPI
diff --git a/js/src/wasm/WasmRealm.h b/js/src/wasm/WasmRealm.h
@@ -78,12 +78,6 @@ extern void InterruptRunningCode(JSContext* cx);
void ResetInterruptState(JSContext* cx);
-#ifdef ENABLE_WASM_JSPI
-void UpdateInstanceStackLimitsForSuspendableStack(JSContext* cx,
- JS::NativeStackLimit limit);
-void ResetInstanceStackLimits(JSContext* cx);
-#endif
-
} // namespace wasm
} // namespace js
diff --git a/js/src/wasm/moz.build b/js/src/wasm/moz.build
@@ -24,6 +24,7 @@ UNIFIED_SOURCES += [
"WasmCode.cpp",
"WasmCodegenTypes.cpp",
"WasmCompile.cpp",
+ "WasmContext.cpp",
"WasmDebug.cpp",
"WasmDebugFrame.cpp",
"WasmDump.cpp",