diff --git a/JSTests/stress/direct-tail-call-to-host-function-code-freed-during-call.js b/JSTests/stress/direct-tail-call-to-host-function-code-freed-during-call.js new file mode 100644 index 0000000000000..c3a55bc7dd58e --- /dev/null +++ b/JSTests/stress/direct-tail-call-to-host-function-code-freed-during-call.js @@ -0,0 +1,48 @@ +//@ runDefault("--zeroExecutableMemoryOnFree=1", "--useConcurrentJIT=0", "--thresholdForOptimizeAfterWarmUp=100", "--thresholdForFTLOptimizeAfterWarmUp=1000") +//@ runDefault("--zeroExecutableMemoryOnFree=1", "--useConcurrentJIT=0", "--thresholdForOptimizeAfterWarmUp=100", "--thresholdForFTLOptimizeAfterWarmUp=1000", "--useFTLJIT=0") + +// A direct tail call to a host function must not run the call thunk from the +// caller's own JIT code. The tail call destroys the caller's frame, so nothing +// on the stack refers to the caller's CodeBlock while the host function runs. +// A jettison plus a collection inside the host function then frees the code +// that the host call returns into. +// +// The host function here is encodeURIComponent. It calls toString on its +// argument, and that hook: +// 1. adds a property to the object whose structure the caller's code watches, +// which jettisons the caller's optimized code, and +// 2. collects, which frees the jettisoned code. +// --zeroExecutableMemoryOnFree fills the freed code with zeroes, so a return +// into it crashes every time instead of once in a while. + +"use strict"; + +const o = { f: encodeURIComponent }; + +let armed = false; +let hookCalls = 0; +const arg = { + toString() { + hookCalls++; + if (!armed) + return "x"; + o.g = 1; + gc(); + return "x"; + } +}; + +function hot(v) { return o.f(v); } + +for (let i = 0; i < testLoopCount; i++) + hot(arg); + +armed = true; +for (let i = 0; i < 3; i++) { + const result = hot(arg); + if (result !== "x") + throw new Error(`expected "x", got ${result}`); +} + +if (hookCalls !== testLoopCount + 3) + throw new Error(`expected ${testLoopCount + 3} hook calls, got ${hookCalls}`); diff --git a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp index 08ca976c6a2f5..24be64066b424 100644 --- a/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp +++ b/Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp @@ -1018,7 +1018,13 @@ void SpeculativeJIT::emitCall(Node* node) nativeFunction = uncheckedDowncast(executable)->function(); } - if (nativeFunction && !vm().isDebuggerHookInjected()) { + // A tail call must not run the thunk from this CodeBlock's own code. The tail call + // destroys this frame, so the conservative stack scan no longer finds this CodeBlock and + // cannot keep it alive (CodeBlockSet::m_currentlyExecuting). A jettison plus a collection + // inside the host function then frees the code the host call returns into. A linked direct + // tail call jumps to the executable's host call thunk, which lives as long as the VM, and + // that thunk returns to our caller. + if (nativeFunction && !isTail && !vm().isDebuggerHookInjected()) { auto emitCallTarget = [&]() { emitFunctionPrologue(); emitPutToCallFrameHeader(nullptr, CallFrameSlot::codeBlock); @@ -1040,15 +1046,6 @@ void SpeculativeJIT::emitCall(Node* node) emitFunctionEpilogue(); }; - if (isTail) { - emitStoreCallSiteIndex(callSite); - CallFrameShuffler(*this, shuffleData).prepareForTailCall(); - emitCallTarget(); - ret(); - useChildren(node); - return; - } - auto done = jump(); auto callTarget = label(); emitCallTarget(); diff --git a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp index 427fea3385b83..ab257b234b2a9 100644 --- a/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp +++ b/Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp @@ -14129,16 +14129,13 @@ IGNORE_CLANG_WARNINGS_END shuffleData.numParameters = jit.codeBlock()->numParameters(); shuffleData.setupCalleeSaveRegisters(state->jitCode->calleeSaveRegisters()); - if (nativeFunction && !vm->isDebuggerHookInjected()) { - jit.store32( - CCallHelpers::TrustedImm32(callSiteIndex.bits()), - CCallHelpers::highWordFor(CallFrameSlot::argumentCountIncludingThis)); - CallFrameShuffler(jit, shuffleData).prepareForTailCall(); - emitCallTarget(); - jit.ret(); - return; - } - + // A tail call must not run the thunk from this CodeBlock's own code. The tail + // call destroys this frame, so the conservative stack scan no longer finds + // this CodeBlock and cannot keep it alive + // (CodeBlockSet::m_currentlyExecuting). A jettison plus a collection inside + // the host function then frees the code the host call returns into. A linked + // direct tail call jumps to the executable's host call thunk, which lives as + // long as the VM, and that thunk returns to our caller. auto* callLinkInfo = state->jitCode->common.m_directCallLinkInfos.add(semanticNodeOrigin, CallLinkInfo::UseDataIC::No, state->graph.m_codeBlock, executable); callLinkInfo->setCallType(CallLinkInfo::DirectTailCall); if (numAllocatedArgs > numPassedArgs)