void EventHook::onFunctionSuspendE(ActRec* suspending, const ActRec* resumableAR) { // When we're suspending an eagerly executing resumable, we've already // teleported the ActRec from suspending over to resumableAR, so we need to // make sure the unwinder knows not to touch the locals, $this, or // VarEnv/ExtraArgs. suspending->setThisOrClassAllowNull(nullptr); suspending->setLocalsDecRefd(); suspending->setVarEnv(nullptr); try { ssize_t flags = CheckSurprise(); onFunctionExit(resumableAR, nullptr, nullptr, flags); if ((flags & RequestInjectionData::AsyncEventHookFlag) && resumableAR->func()->isAsyncFunction()) { assert(resumableAR->resumed()); auto const afwh = frame_afwh(resumableAR); auto const session = AsioSession::Get(); if (session->hasOnResumableCreateCallback()) { session->onResumableCreate(afwh, afwh->getChild()); } } } catch (...) { auto const resumableObj = [&]() -> ObjectData* { if (resumableAR->func()->isAsyncFunction()) { return frame_afwh(resumableAR); } assert(resumableAR->func()->isGenerator()); return frame_base_generator(resumableAR); }(); decRefObj(resumableObj); throw; } }
static ActRec* getPrevActRec(const ActRec* fp, Offset* prevPc) { if (fp && fp->func() && fp->resumed() && fp->func()->isAsyncFunction()) { c_WaitableWaitHandle* currentWaitHandle = frame_afwh(fp); if (currentWaitHandle->isFinished()) { /* * It's possible in very rare cases (it will return a truncated stack): * 1) async function which WaitHandle is not referenced by anything * else finishes * 2) its return value is an object with destructor * 3) this destructor gets called as part of destruction of the * WaitHandleobject, which happens right before FP is adjusted */ return nullptr; } auto const contextIdx = currentWaitHandle->getContextIdx(); while (currentWaitHandle != nullptr) { auto p = currentWaitHandle->getParentChain().firstInContext(contextIdx); if (p == nullptr) break; if (p->getKind() == c_WaitHandle::Kind::AsyncFunction) { auto wh = p->asAsyncFunction(); *prevPc = wh->resumable()->resumeOffset(); return wh->actRec(); } currentWaitHandle = p; } *prevPc = 0; return AsioSession::Get()->getContext(contextIdx)->getSavedFP(); } return g_context->getPrevVMState(fp, prevPc); }
void EventHook::onFunctionReturn(ActRec* ar, const TypedValue& retval) { // Null out $this for the exiting function, it has been decref'd so it's // garbage. ar->setThisOrClassAllowNull(nullptr); // The locals are already gone. Mark them as decref'd so that if this hook // fails and unwinder kicks in, it won't try to decref them again. ar->setLocalsDecRefd(); // TODO(#5758054): does this need setVarEnv(nullptr) ? ssize_t flags = CheckSurprise(); onFunctionExit(ar, &retval, nullptr, flags); // Async profiler if ((flags & RequestInjectionData::AsyncEventHookFlag) && ar->func()->isAsyncFunction() && ar->resumed()) { auto session = AsioSession::Get(); // Return @ resumed execution => AsyncFunctionWaitHandle succeeded. if (session->hasOnResumableSuccessCallback()) { auto afwh = frame_afwh(ar); session->onResumableSuccess(afwh, cellAsCVarRef(retval)); } } }
void EventHook::onFunctionReturn(ActRec* ar, TypedValue retval) { // The locals are already gone. Null out everything. ar->setThisOrClassAllowNull(nullptr); ar->setLocalsDecRefd(); ar->setVarEnv(nullptr); try { ssize_t flags = CheckSurprise(); onFunctionExit(ar, &retval, nullptr, flags); // Async profiler if ((flags & AsyncEventHookFlag) && ar->func()->isAsyncFunction() && ar->resumed()) { auto session = AsioSession::Get(); // Return @ resumed execution => AsyncFunctionWaitHandle succeeded. if (session->hasOnResumableSuccessCallback()) { auto afwh = frame_afwh(ar); session->onResumableSuccess(afwh, cellAsCVarRef(retval)); } } } catch (...) { /* * We're responsible for freeing the return value if we exit with an * exception. See irgen-ret. */ tvRefcountedDecRef(retval); throw; } }
c_ResumableWaitHandle* c_ResumableWaitHandle::getRunning(ActRec* fp) { for (; fp; fp = g_context->getPrevVMState(fp)) { if (fp->resumed() && fp->func()->isAsync()) { if (fp->func()->isGenerator()) { // async generator auto generator = frame_async_generator(fp); if (!generator->isEagerlyExecuted()) { return generator->getWaitHandle(); } } else { // async function return frame_afwh(fp); } } } return nullptr; }
// Child is the AFWH we're going to block on, nullptr iff this is a suspending // generator. void EventHook::onFunctionSuspendR(ActRec* suspending, ObjectData* child) { ssize_t flags = CheckSurprise(); onFunctionExit(suspending, nullptr, nullptr, flags); if ((flags & RequestInjectionData::AsyncEventHookFlag) && suspending->func()->isAsyncFunction()) { assert(child != nullptr); // This isn't a generator assert(child->instanceof(c_WaitableWaitHandle::classof())); assert(suspending->resumed()); auto const afwh = frame_afwh(suspending); auto const session = AsioSession::Get(); if (session->hasOnResumableAwaitCallback()) { session->onResumableAwait( afwh, static_cast<c_WaitableWaitHandle*>(child) ); } } }
void EventHook::onFunctionSuspend(const ActRec* ar, bool suspendingResumed) { ssize_t flags = CheckSurprise(); onFunctionExit(ar, nullptr, nullptr, flags); // Async profiler if ((flags & RequestInjectionData::AsyncEventHookFlag) && ar->func()->isAsyncFunction()) { assert(ar->resumed()); auto afwh = frame_afwh(ar); auto session = AsioSession::Get(); // Blocking await @ eager execution => AsyncFunctionWaitHandle created. if (!suspendingResumed && session->hasOnAsyncFunctionCreateCallback()) { session->onAsyncFunctionCreate(afwh, afwh->getChild()); } // Blocking await @ resumed execution => AsyncFunctionWaitHandle awaiting. if (suspendingResumed && session->hasOnAsyncFunctionAwaitCallback()) { session->onAsyncFunctionAwait(afwh, afwh->getChild()); } } }
static ActRec* getPrevActRec( const ActRec* fp, Offset* prevPc, folly::small_vector<c_WaitableWaitHandle*, 64>& visitedWHs) { if (fp && fp->func() && fp->resumed() && fp->func()->isAsyncFunction()) { c_WaitableWaitHandle* currentWaitHandle = frame_afwh(fp); if (currentWaitHandle->isFinished()) { /* * It's possible in very rare cases (it will return a truncated stack): * 1) async function which WaitHandle is not referenced by anything * else finishes * 2) its return value is an object with destructor * 3) this destructor gets called as part of destruction of the * WaitHandleobject, which happens right before FP is adjusted */ return nullptr; } auto const contextIdx = currentWaitHandle->getContextIdx(); while (currentWaitHandle != nullptr) { auto p = currentWaitHandle->getParentChain().firstInContext(contextIdx); if (p == nullptr || UNLIKELY(std::find(visitedWHs.begin(), visitedWHs.end(), p) != visitedWHs.end())) { // If the parent exists in our backtrace, it means we have detected a // cycle. Fall back to savedFP in that case. break; } visitedWHs.push_back(p); if (p->getKind() == c_WaitHandle::Kind::AsyncFunction) { auto wh = p->asAsyncFunction(); *prevPc = wh->resumable()->resumeOffset(); return wh->actRec(); } currentWaitHandle = p; } *prevPc = 0; return AsioSession::Get()->getContext(contextIdx)->getSavedFP(); } return g_context->getPrevVMState(fp, prevPc); }
void c_AsyncFunctionWaitHandle::PrepareChild(const ActRec* fp, c_WaitableWaitHandle* child) { frame_afwh(fp)->prepareChild(child); }
/** * Discard the current frame, assuming that a PHP exception given in * phpException argument, or C++ exception (phpException == nullptr) * is being thrown. Returns an exception to propagate, or nulltpr * if the VM execution should be resumed. */ ObjectData* tearDownFrame(ActRec*& fp, Stack& stack, PC& pc, ObjectData* phpException) { auto const func = fp->func(); auto const curOp = peek_op(pc); auto const prevFp = fp->sfp(); auto const soff = fp->m_soff; ITRACE(1, "tearDownFrame: {} ({})\n", func->fullName()->data(), func->unit()->filepath()->data()); ITRACE(1, " fp {} prevFp {}\n", implicit_cast<void*>(fp), implicit_cast<void*>(prevFp)); // When throwing from a constructor, we normally want to avoid running the // destructor on an object that hasn't been fully constructed yet. But if // we're unwinding through the constructor's RetC, the constructor has // logically finished and we're unwinding for some internal reason (timeout // or user profiler, most likely). More importantly, fp->m_this may have // already been destructed and/or overwritten due to sharing space with // fp->m_r. if (curOp != OpRetC && fp->hasThis() && fp->getThis()->getVMClass()->getCtor() == func && fp->getThis()->getVMClass()->getDtor()) { /* * Looks like an FPushCtor call, but it could still have been called * directly. Check the fpi region to be sure. */ Offset prevPc; auto outer = g_context->getPrevVMState(fp, &prevPc); if (outer) { auto fe = outer->func()->findPrecedingFPI(prevPc); if (fe && isFPushCtor(outer->func()->unit()->getOp(fe->m_fpushOff))) { fp->getThis()->setNoDestruct(); } } } auto const decRefLocals = [&] { /* * It is possible that locals have already been decref'd. * * Here's why: * * - If a destructor for any of these things throws a php * exception, it's swallowed at the dtor boundary and we keep * running php. * * - If the destructor for any of these things throws a fatal, * it's swallowed, and we set surprise flags to throw a fatal * from now on. * * - If the second case happened and we have to run another * destructor, its enter hook will throw, but it will be * swallowed again. * * - Finally, the exit hook for the returning function can * throw, but this happens last so everything is destructed. * * - When that happens, exit hook sets localsDecRefd flag. */ if (!fp->localsDecRefd()) { try { // Note that we must convert locals and the $this to // uninit/zero during unwind. This is because a backtrace // from another destructing object during this unwind may try // to read them. frame_free_locals_unwind(fp, func->numLocals(), phpException); } catch (...) {} } }; if (LIKELY(!fp->resumed())) { decRefLocals(); if (UNLIKELY(func->isAsyncFunction()) && phpException && !fp->isFCallAwait()) { // If in an eagerly executed async function, wrap the user exception // into a failed StaticWaitHandle and return it to the caller. auto const waitHandle = c_StaticWaitHandle::CreateFailed(phpException); phpException = nullptr; stack.ndiscard(func->numSlotsInFrame()); stack.ret(); assert(stack.topTV() == &fp->m_r); cellCopy(make_tv<KindOfObject>(waitHandle), fp->m_r); } else { // Free ActRec. stack.ndiscard(func->numSlotsInFrame()); stack.discardAR(); } } else if (func->isAsyncFunction()) { auto const waitHandle = frame_afwh(fp); if (phpException) { // Handle exception thrown by async function. decRefLocals(); waitHandle->fail(phpException); phpException = nullptr; } else if (waitHandle->isRunning()) { // Let the C++ exception propagate. If the current frame represents async // function that is running, mark it as abruptly interrupted. Some opcodes // like Await may change state of the async function just before exit hook // decides to throw C++ exception. decRefLocals(); waitHandle->failCpp(); } } else if (func->isAsyncGenerator()) { auto const gen = frame_async_generator(fp); if (phpException) { // Handle exception thrown by async generator. decRefLocals(); auto eagerResult = gen->fail(phpException); phpException = nullptr; if (eagerResult) { stack.pushObjectNoRc(eagerResult); } } else if (gen->isEagerlyExecuted() || gen->getWaitHandle()->isRunning()) { // Fail the async generator and let the C++ exception propagate. decRefLocals(); gen->failCpp(); } } else if (func->isNonAsyncGenerator()) { // Mark the generator as finished. decRefLocals(); frame_generator(fp)->fail(); } else { not_reached(); } /* * At the final ActRec in this nesting level. */ if (UNLIKELY(!prevFp)) { pc = nullptr; fp = nullptr; return phpException; } assert(stack.isValidAddress(reinterpret_cast<uintptr_t>(prevFp)) || prevFp->resumed()); auto const prevOff = soff + prevFp->func()->base(); pc = prevFp->func()->unit()->at(prevOff); fp = prevFp; return phpException; }
UnwindAction tearDownFrame(ActRec*& fp, Stack& stack, PC& pc, const Fault& fault) { auto const func = fp->func(); auto const curOp = *reinterpret_cast<const Op*>(pc); auto const prevFp = fp->sfp(); auto const soff = fp->m_soff; FTRACE(1, "tearDownFrame: {} ({})\n fp {} prevFp {}\n", func->fullName()->data(), func->unit()->filepath()->data(), implicit_cast<void*>(fp), implicit_cast<void*>(prevFp)); // When throwing from a constructor, we normally want to avoid running the // destructor on an object that hasn't been fully constructed yet. But if // we're unwinding through the constructor's RetC, the constructor has // logically finished and we're unwinding for some internal reason (timeout // or user profiler, most likely). More importantly, fp->m_this may have // already been destructed and/or overwritten due to sharing space with // fp->m_r. if (fp->isFromFPushCtor() && fp->hasThis() && curOp != OpRetC) { fp->getThis()->setNoDestruct(); } /* * It is possible that locals have already been decref'd. * * Here's why: * * - If a destructor for any of these things throws a php * exception, it's swallowed at the dtor boundary and we keep * running php. * * - If the destructor for any of these things throws a fatal, * it's swallowed, and we set surprise flags to throw a fatal * from now on. * * - If the second case happened and we have to run another * destructor, its enter hook will throw, but it will be * swallowed again. * * - Finally, the exit hook for the returning function can * throw, but this happens last so everything is destructed. * * - When that happens, exit hook sets localsDecRefd flag. */ if (!fp->localsDecRefd()) { try { // Note that we must convert locals and the $this to // uninit/zero during unwind. This is because a backtrace // from another destructing object during this unwind may try // to read them. frame_free_locals_unwind(fp, func->numLocals(), fault); } catch (...) {} } auto action = UnwindAction::Propagate; if (LIKELY(!fp->resumed())) { if (UNLIKELY(func->isAsyncFunction()) && fault.m_faultType == Fault::Type::UserException) { // If in an eagerly executed async function, wrap the user exception // into a failed StaticWaitHandle and return it to the caller. auto const e = fault.m_userException; stack.ndiscard(func->numSlotsInFrame()); stack.ret(); assert(stack.topTV() == &fp->m_r); tvWriteObject(c_StaticWaitHandle::CreateFailed(e), &fp->m_r); e->decRefCount(); action = UnwindAction::ResumeVM; } else { // Free ActRec. stack.ndiscard(func->numSlotsInFrame()); stack.discardAR(); } } else if (func->isAsyncFunction()) { auto const waitHandle = frame_afwh(fp); if (fault.m_faultType == Fault::Type::UserException) { // Handle exception thrown by async function. waitHandle->fail(fault.m_userException); action = UnwindAction::ResumeVM; } else { // Fail the async function and let the C++ exception propagate. waitHandle->fail(AsioSession::Get()->getAbruptInterruptException()); } } else if (func->isAsyncGenerator()) { // Do nothing. AsyncGeneratorWaitHandle will handle the exception. } else if (func->isNonAsyncGenerator()) { // Mark the generator as finished. frame_generator(fp)->finish(); } else { not_reached(); } /* * At the final ActRec in this nesting level. */ if (UNLIKELY(!prevFp)) { pc = nullptr; fp = nullptr; return action; } assert(stack.isValidAddress(reinterpret_cast<uintptr_t>(prevFp)) || prevFp->resumed()); auto const prevOff = soff + prevFp->func()->base(); pc = prevFp->func()->unit()->at(prevOff); fp = prevFp; return action; }