Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions src/runtime/runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -667,11 +667,34 @@ void Runtime::callWasmFunctionInInterpMode(Instance &Inst, uint32_t FuncIdx,
#ifdef ZEN_ENABLE_EVM
void Runtime::callEVMInInterpMode(EVMInstance &Inst, evmc_message &Msg,
evmc::Result &Result) {
evm::InterpreterExecContext Ctx(&Inst);
evm::BaseInterpreter Interpreter(Ctx);
Ctx.allocTopFrame(&Msg);
Interpreter.interpret();
Result = std::move(const_cast<evmc::Result &>(Ctx.getExeResult()));
// Reuse a thread-local InterpreterExecContext for top-level calls to avoid
// re-allocating the ~33 KB EVMFrame (1024 × uint256 stack) on every call.
// For nested calls (CALL/CREATE re-entering this function via Host->call()),
// we must create a fresh context to avoid corrupting the outer call's state.
static thread_local evm::InterpreterExecContext *TLCtx = nullptr;
static thread_local bool TLCtxInUse = false;

if (!TLCtxInUse) {
// Top-level call: reuse the cached context
if (!TLCtx) {
TLCtx = new evm::InterpreterExecContext(&Inst);
} else {
TLCtx->resetForNewCall(&Inst);
}
Comment on lines +674 to +683
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cached InterpreterExecContext is allocated with new into a thread_local raw pointer and is never freed. Even though it’s “only” once per thread, this is still an intentional leak and can matter for short-lived worker threads / fuzzing. Prefer static thread_local std::unique_ptr<evm::InterpreterExecContext> (or std::optional if default-constructible) so the context is reclaimed on thread exit and ownership is explicit.

Copilot uses AI. Check for mistakes.
TLCtxInUse = true;
evm::BaseInterpreter Interpreter(*TLCtx);
TLCtx->allocTopFrame(&Msg);
Interpreter.interpret();
Result = std::move(const_cast<evmc::Result &>(TLCtx->getExeResult()));
TLCtxInUse = false;
Comment on lines +684 to +689
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TLCtxInUse is not exception-safe: if allocTopFrame() or interpret() throws, the flag will remain true for the rest of the thread lifetime. That can permanently disable reuse (or change reentrancy behavior) and makes later calls take the nested path unexpectedly. Consider using an RAII guard (scope-exit) or try { ... } catch (...) { TLCtxInUse = false; throw; } to ensure the flag is reset on all exit paths.

Suggested change
TLCtxInUse = true;
evm::BaseInterpreter Interpreter(*TLCtx);
TLCtx->allocTopFrame(&Msg);
Interpreter.interpret();
Result = std::move(const_cast<evmc::Result &>(TLCtx->getExeResult()));
TLCtxInUse = false;
struct TLCtxGuard {
bool &Flag;
explicit TLCtxGuard(bool &F) : Flag(F) { Flag = true; }
~TLCtxGuard() { Flag = false; }
} Guard(TLCtxInUse);
evm::BaseInterpreter Interpreter(*TLCtx);
TLCtx->allocTopFrame(&Msg);
Interpreter.interpret();
Result = std::move(const_cast<evmc::Result &>(TLCtx->getExeResult()));

Copilot uses AI. Check for mistakes.
} else {
// Nested call: use a stack-local context to avoid corrupting the outer one
evm::InterpreterExecContext Ctx(&Inst);
evm::BaseInterpreter Interpreter(Ctx);
Ctx.allocTopFrame(&Msg);
Interpreter.interpret();
Result = std::move(const_cast<evmc::Result &>(Ctx.getExeResult()));
}
}

#ifdef ZEN_ENABLE_VIRTUAL_STACK
Expand Down Expand Up @@ -710,7 +733,15 @@ void Runtime::callEVMMain(EVMInstance &Inst, evmc_message &Msg,
#endif

#ifdef ZEN_ENABLE_VIRTUAL_STACK
if (Msg.depth == 0) {
// Interpreter mode does not need a virtual stack: it manages call depth
// via InterpreterExecContext::FrameStack and never emits native code that
// could overflow the physical stack in an unbounded way. Skipping the
// virtual-stack allocation/mprotect/setjmp round-trip on every call
// eliminates ~50 % of the per-execution overhead measured on ERC-20
// transfers.
Comment on lines +736 to +741
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says interpreter mode “manages call depth via InterpreterExecContext::FrameStack”, but CALL/CREATE in the interpreter go through Host->call(NewMsg) (see CallHandler::doExecute()), i.e. they re-enter execution rather than pushing additional frames onto FrameStack. Since FrameStack is currently only used for the single top frame (allocTopFrame()/freeBackFrame()), this rationale seems inaccurate/misleading—please reword to reflect the actual depth management mechanism (evmc_message.depth / host recursion).

Suggested change
// Interpreter mode does not need a virtual stack: it manages call depth
// via InterpreterExecContext::FrameStack and never emits native code that
// could overflow the physical stack in an unbounded way. Skipping the
// virtual-stack allocation/mprotect/setjmp round-trip on every call
// eliminates ~50 % of the per-execution overhead measured on ERC-20
// transfers.
// Interpreter mode does not need a virtual stack: CALL/CREATE re-enter
// execution via the host with an incremented evmc_message.depth rather
// than pushing additional frames onto InterpreterExecContext::FrameStack,
// so call depth is bounded by the EVM depth limit rather than unbounded
// native recursion. Skipping the virtual-stack allocation/mprotect/setjmp
// round-trip on every call eliminates ~50 % of the per-execution overhead
// measured on ERC-20 transfers.

Copilot uses AI. Check for mistakes.
if (getConfig().Mode == RunMode::InterpMode) {
callEVMMainOnPhysStack(Inst, Msg, Result);
} else if (Msg.depth == 0) {
VirtualStackInfo StackInfo;
StackInfo.SavedPtr1 = &Inst;
StackInfo.SavedPtr2 = &Msg;
Expand Down
Loading