TypedValue* methodWrapper(ActRec* ar) { auto func = ar->m_func; auto numArgs = func->numParams(); auto numNonDefault = ar->numArgs(); bool isStatic = func->isStatic(); assert(!func->hasVariadicCaptureParam()); TypedValue* args = ((TypedValue*)ar) - 1; TypedValue rv; rv.m_type = KindOfNull; if (LIKELY(numNonDefault == numArgs) || LIKELY(nativeWrapperCheckArgs(ar))) { if (coerceFCallArgs(args, numArgs, numNonDefault, func)) { // Prepend a context arg for methods // KindOfClass when it's being called statically Foo::bar() // KindOfObject when it's being called on an instance $foo->bar() TypedValue ctx; if (ar->hasThis()) { if (isStatic) { throw_instance_method_fatal(getInvokeName(ar)->data()); } ctx.m_type = KindOfObject; ctx.m_data.pobj = ar->getThis(); } else { if (!isStatic) { throw_instance_method_fatal(getInvokeName(ar)->data()); } ctx.m_type = KindOfClass; ctx.m_data.pcls = const_cast<Class*>(ar->getClass()); } callFunc(func, &ctx, args, numArgs, rv); } else if (func->attrs() & AttrParamCoerceModeFalse) { rv.m_type = KindOfBoolean; rv.m_data.num = 0; } } assert(rv.m_type != KindOfUninit); if (isStatic) { frame_free_locals_no_this_inl(ar, func->numLocals(), &rv); } else { frame_free_locals_inl(ar, func->numLocals(), &rv); } tvCopy(rv, ar->m_r); return &ar->m_r; }
void tearDownEagerAsyncFrame(ActRec*& fp, Stack& stack, PC& pc, ObjectData* e) { auto const func = fp->func(); auto const prevFp = fp->sfp(); auto const soff = fp->m_soff; assert(!fp->resumed()); assert(func->isAsyncFunction()); assert(*reinterpret_cast<const Op*>(pc) != OpRetC); FTRACE(1, "tearDownAsyncFrame: {} ({})\n fp {} prevFp {}\n", func->fullName()->data(), func->unit()->filepath()->data(), implicit_cast<void*>(fp), implicit_cast<void*>(prevFp)); try { frame_free_locals_unwind(fp, func->numLocals()); } catch (...) {} stack.ndiscard(func->numSlotsInFrame()); stack.ret(); assert(stack.topTV() == &fp->m_r); tvWriteObject(c_StaticWaitHandle::CreateFailed(e), &fp->m_r); e->decRefCount(); if (UNLIKELY(!prevFp)) { pc = 0; return; } 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; }
/* * Compute the stack and local type postconditions for a * single-entry/single-exit tracelet. */ std::vector<RegionDesc::TypePred> IRBuilder::getKnownTypes() { // This function is only correct when given a single-exit region, as // in TransProfile. Furthermore, its output is only used to guide // formation of profile-driven regions. assert(tx->mode() == TransProfile); // We want the state for the last block on the "main trace". Figure // out which that is. Block* mainExit = nullptr; for (auto* b : rpoSortCfg(m_unit)) { if (isMainExit(b)) { assert(mainExit == nullptr); mainExit = b; } } assert(mainExit != nullptr); // Load state for mainExit. This feels hacky. FTRACE(1, "mainExit: B{}\n", mainExit->id()); m_state.startBlock(mainExit); // Now use the current state to get all the types. std::vector<RegionDesc::TypePred> result; auto const curFunc = m_state.func(); auto const sp = m_state.sp(); auto const spOffset = m_state.spOffset(); for (unsigned i = 0; i < curFunc->maxStackCells(); ++i) { auto t = getStackValue(sp, i).knownType; if (!t.equals(Type::StackElem)) { result.push_back({ RegionDesc::Location::Stack{i, spOffset - i}, t }); } } for (unsigned i = 0; i < curFunc->numLocals(); ++i) { auto t = m_state.localType(i); if (!t.equals(Type::Gen)) { FTRACE(1, "Local {}: {}\n", i, t.toString()); result.push_back({ RegionDesc::Location::Local{i}, t }); } } return result; }
std::vector<RegionDesc::TypePred> IRBuilder::getKnownTypes() const { std::vector<RegionDesc::TypePred> result; auto const curFunc = m_state.func(); auto const sp = m_state.sp(); auto const spOffset = m_state.spOffset(); for (unsigned i = 0; i < curFunc->maxStackCells(); ++i) { auto t = getStackValue(sp, i).knownType; if (!t.equals(Type::StackElem)) { result.push_back({ RegionDesc::Location::Stack{i, spOffset - i}, t }); } } for (unsigned i = 0; i < curFunc->numLocals(); ++i) { auto t = m_state.localType(i); if (!t.equals(Type::Gen)) { result.push_back({ RegionDesc::Location::Local{i}, t }); } } return result; }
void Generator::copyVars(const ActRec* srcFp) { const auto dstFp = actRec(); const auto func = dstFp->func(); assert(srcFp->func() == dstFp->func()); for (Id i = 0; i < func->numLocals(); ++i) { tvDupFlattenVars(frame_local(srcFp, i), frame_local(dstFp, i)); } if (dstFp->hasThis()) { dstFp->getThis()->incRefCount(); } if (LIKELY(!(srcFp->func()->attrs() & AttrMayUseVV))) return; if (LIKELY(srcFp->m_varEnv == nullptr)) return; if (srcFp->hasExtraArgs()) { dstFp->setExtraArgs(srcFp->getExtraArgs()->clone(dstFp)); } else { assert(srcFp->hasVarEnv()); dstFp->setVarEnv(srcFp->getVarEnv()->clone(dstFp)); } }
void FrameState::trackDefInlineFP(const IRInstruction* inst) { auto const target = inst->extra<DefInlineFP>()->target; auto const savedSPOff = inst->extra<DefInlineFP>()->retSPOff; auto const calleeFP = inst->dst(); auto const calleeSP = inst->src(0); auto const savedSP = inst->src(1); // Saved IRBuilder state will include the "return" fp/sp. // Whatever the current fpValue is is good enough, but we have to be // passed in the StkPtr that represents the stack prior to the // ActRec being allocated. m_spOffset = savedSPOff; m_spValue = savedSP; auto const stackValues = collectStackValues(m_spValue, m_spOffset); for (DEBUG_ONLY auto& val : stackValues) { FTRACE(4, " marking caller stack value available: {}\n", val->toString()); } m_inlineSavedStates.emplace_back(createSnapshot()); /* * Set up the callee state. * * We set m_thisIsAvailable to true on any object method, because we * just don't inline calls to object methods with a null $this. */ m_fpValue = calleeFP; m_spValue = calleeSP; m_thisAvailable = target->cls() != nullptr && !target->isStatic(); m_curFunc = target; m_frameSpansCall = false; m_locals.clear(); m_locals.resize(target->numLocals()); }
TypedValue* functionWrapper(ActRec* ar) { auto func = ar->m_func; auto numArgs = func->numParams(); auto numNonDefault = ar->numArgs(); assert(!func->hasVariadicCaptureParam()); TypedValue* args = ((TypedValue*)ar) - 1; TypedValue rv; rv.m_type = KindOfNull; if (LIKELY(numNonDefault == numArgs) || LIKELY(nativeWrapperCheckArgs(ar))) { if (coerceFCallArgs(args, numArgs, numNonDefault, func)) { callFunc(func, nullptr, args, numArgs, rv); } else if (func->attrs() & AttrParamCoerceModeFalse) { rv.m_type = KindOfBoolean; rv.m_data.num = 0; } } assert(rv.m_type != KindOfUninit); frame_free_locals_no_this_inl(ar, func->numLocals(), &rv); tvCopy(rv, ar->m_r); return &ar->m_r; }
void c_Continuation::copyContinuationVars(ActRec* srcFp) { const auto dstFp = actRec(); const auto func = dstFp->func(); assert(srcFp->func() == dstFp->func()); for (Id i = 0; i < func->numLocals(); ++i) { tvDupFlattenVars(frame_local(srcFp, i), frame_local(dstFp, i)); } if (dstFp->hasThis()) { dstFp->getThis()->incRefCount(); } if (LIKELY(srcFp->m_varEnv == nullptr)) { return; } if (srcFp->hasExtraArgs()) { dstFp->setExtraArgs(srcFp->getExtraArgs()->clone(dstFp)); } else { assert(srcFp->hasVarEnv()); dstFp->setVarEnv(srcFp->getVarEnv()->clone(dstFp)); } }
void tearDownFrame(ActRec*& fp, Stack& stack, PC& pc, Offset& faultOffset) { auto const func = fp->m_func; auto const curOp = *reinterpret_cast<const Op*>(pc); auto const unwindingGeneratorFrame = func->isGenerator(); auto const unwindingReturningFrame = curOp == OpRetC || curOp == OpRetV; auto const prevFp = fp->arGetSfp(); 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 (!unwindingReturningFrame && fp->isFromFPushCtor() && fp->hasThis()) { fp->getThis()->setNoDestruct(); } // A generator's locals don't live on this stack. if (LIKELY(!unwindingGeneratorFrame)) { /* * If we're unwinding through a frame that's returning, it's only * possible that its 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. * */ if (!unwindingReturningFrame) { 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()); } catch (...) {} } stack.ndiscard(func->numSlotsInFrame()); stack.discardAR(); } else { // The generator's locals will be cleaned up when the Continuation // object is destroyed. But we are leaving the generator function // now, so signal that to anyone who cares. try { EventHook::FunctionExit(fp); } catch (...) {} // As above, don't let new exceptions out of unwind. } /* * At the final ActRec in this nesting level. We don't need to set * pc and fp since we're about to re-throw the exception. And we * don't want to dereference prefFp since we just popped it. */ if (prevFp == fp) return; assert(stack.isValidAddress(reinterpret_cast<uintptr_t>(prevFp)) || prevFp->m_func->isGenerator()); auto const prevOff = soff + prevFp->m_func->base(); pc = prevFp->m_func->unit()->at(prevOff); fp = prevFp; faultOffset = prevOff; }
void cgCall(IRLS& env, const IRInstruction* inst) { auto const sp = srcLoc(env, inst, 0).reg(); auto const fp = srcLoc(env, inst, 1).reg(); auto const extra = inst->extra<Call>(); auto const callee = extra->callee; auto const argc = extra->numParams; auto& v = vmain(env); auto& vc = vcold(env); auto const catchBlock = label(env, inst->taken()); auto const calleeSP = sp[cellsToBytes(extra->spOffset.offset)]; auto const calleeAR = calleeSP + cellsToBytes(argc); v << store{fp, calleeAR + AROFF(m_sfp)}; v << storeli{safe_cast<int32_t>(extra->after), calleeAR + AROFF(m_soff)}; if (extra->fcallAwait) { // This clobbers any flags that might have already been set on the callee // AR (e.g., by SpillFrame), but this is okay because there should never be // any conflicts; see the documentation in act-rec.h. auto const imm = static_cast<int32_t>( ActRec::encodeNumArgsAndFlags(argc, ActRec::Flags::IsFCallAwait) ); v << storeli{imm, calleeAR + AROFF(m_numArgsAndFlags)}; } auto const isNativeImplCall = callee && callee->builtinFuncPtr() && !callee->nativeFuncPtr() && argc == callee->numParams(); if (isNativeImplCall) { // The assumption here is that for builtins, the generated func contains // only a single opcode (NativeImpl), and there are no non-argument locals. if (do_assert) { assertx(argc == callee->numLocals()); assertx(callee->numIterators() == 0); auto addr = callee->getEntry(); while (peek_op(addr) == Op::AssertRATL) { addr += instrLen(addr); } assertx(peek_op(addr) == Op::NativeImpl); assertx(addr + instrLen(addr) == callee->unit()->entry() + callee->past()); } v << store{v.cns(mcg->ustubs().retHelper), calleeAR + AROFF(m_savedRip)}; if (callee->attrs() & AttrMayUseVV) { v << storeqi{0, calleeAR + AROFF(m_invName)}; } v << lea{calleeAR, rvmfp()}; emitCheckSurpriseFlagsEnter(v, vc, fp, Fixup(0, argc), catchBlock); auto const builtinFuncPtr = callee->builtinFuncPtr(); TRACE(2, "Calling builtin preClass %p func %p\n", callee->preClass(), builtinFuncPtr); // We sometimes call this while curFunc() isn't really the builtin, so make // sure to record the sync point as if we are inside the builtin. if (FixupMap::eagerRecord(callee)) { auto const syncSP = v.makeReg(); v << lea{calleeSP, syncSP}; emitEagerSyncPoint(v, callee->getEntry(), rvmtl(), rvmfp(), syncSP); } // Call the native implementation. This will free the locals for us in the // normal case. In the case where an exception is thrown, the VM unwinder // will handle it for us. auto const done = v.makeBlock(); v << vinvoke{CallSpec::direct(builtinFuncPtr), v.makeVcallArgs({{rvmfp()}}), v.makeTuple({}), {done, catchBlock}, Fixup(0, argc)}; env.catch_calls[inst->taken()] = CatchCall::CPP; v = done; // The native implementation already put the return value on the stack for // us, and handled cleaning up the arguments. We have to update the frame // pointer and the stack pointer, and load the return value into the return // register so the trace we are returning to has it where it expects. // TODO(#1273094): We should probably modify the actual builtins to return // values via registers using the C ABI and do a reg-to-reg move. loadTV(v, inst->dst(), dstLoc(env, inst, 0), rvmfp()[AROFF(m_r)], true); v << load{rvmfp()[AROFF(m_sfp)], rvmfp()}; emitRB(v, Trace::RBTypeFuncExit, callee->fullName()->data()); return; } v << lea{calleeAR, rvmfp()}; if (RuntimeOption::EvalHHIRGenerateAsserts) { v << syncvmsp{v.cns(0x42)}; constexpr uint64_t kUninitializedRIP = 0xba5eba11acc01ade; emitImmStoreq(v, kUninitializedRIP, rvmfp()[AROFF(m_savedRip)]); } // Emit a smashable call that initially calls a recyclable service request // stub. The stub and the eventual targets take rvmfp() as an argument, // pointing to the callee ActRec. auto const target = callee ? mcg->ustubs().immutableBindCallStub : mcg->ustubs().bindCallStub; auto const done = v.makeBlock(); v << callphp{target, php_call_regs(), {{done, catchBlock}}}; env.catch_calls[inst->taken()] = CatchCall::PHP; v = done; auto const dst = dstLoc(env, inst, 0); v << defvmret{dst.reg(0), dst.reg(1)}; }
/* * Intended to be called after all optimizations are finished on a * single-entry, single-exit tracelet, this collects the types of all stack * slots and locals at the end of the main exit. */ void IRUnit::collectPostConditions() { // This function is only correct when given a single-exit region, as in // TransKind::Profile. Furthermore, its output is only used to guide // formation of profile-driven regions. assert(mcg->tx().mode() == TransKind::Profile); assert(m_postConds.empty()); Timer _t(Timer::collectPostConditions); // We want the state for the last block on the "main trace". Figure // out which that is. Block* mainExit = nullptr; Block* lastMainBlock = nullptr; FrameStateMgr state{*this, entry()->front().marker()}; // TODO(#5678127): this code is wrong for HHIRBytecodeControlFlow state.setLegacyReoptimize(); ITRACE(2, "collectPostConditions starting\n"); Trace::Indent _i; for (auto* block : rpoSortCfg(*this)) { state.startBlock(block, block->front().marker()); for (auto& inst : *block) { state.update(&inst); } if (isMainBlock(block)) lastMainBlock = block; if (isMainExit(block)) { mainExit = block; break; } state.finishBlock(block); } // If we didn't find an obvious exit, then use the last block in the region. always_assert(lastMainBlock != nullptr); if (mainExit == nullptr) mainExit = lastMainBlock; FTRACE(1, "mainExit: B{}\n", mainExit->id()); // state currently holds the state at the end of mainExit auto const curFunc = state.func(); auto const sp = state.sp(); auto const spOffset = state.spOffset(); for (unsigned i = 0; i < spOffset; ++i) { auto t = getStackValue(sp, i).knownType; if (!t.equals(Type::StackElem)) { m_postConds.push_back({ RegionDesc::Location::Stack{i, spOffset - i}, t }); } } for (unsigned i = 0; i < curFunc->numLocals(); ++i) { auto t = state.localType(i); if (!t.equals(Type::Gen)) { FTRACE(1, "Local {}: {}\n", i, t.toString()); m_postConds.push_back({ RegionDesc::Location::Local{i}, t }); } } }
void tearDownFrame(ActRec*& fp, Stack& stack, PC& pc) { 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()); } catch (...) {} } if (LIKELY(!fp->resumed())) { // Free ActRec. stack.ndiscard(func->numSlotsInFrame()); stack.discardAR(); } else if (fp->func()->isAsyncFunction()) { // Do nothing. AsyncFunctionWaitHandle will handle the exception. } else if (fp->func()->isAsyncGenerator()) { // Do nothing. AsyncGeneratorWaitHandle will handle the exception. } else if (fp->func()->isNonAsyncGenerator()) { // Mark the generator as finished. frame_generator(fp)->finish(); } else { not_reached(); } /* * At the final ActRec in this nesting level. We don't need to set * pc and fp since we're about to re-throw the exception. And we * don't want to dereference prefFp since we just popped it. */ if (!prevFp) return; 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; }
/** * 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; }
void tearDownFrame(ActRec*& fp, Stack& stack, PC& pc, Offset& faultOffset) { auto const func = fp->m_func; auto const curOp = *reinterpret_cast<const Op*>(pc); auto const unwindingGeneratorFrame = func->isGenerator(); auto const unwindingReturningFrame = curOp == OpRetC || curOp == OpRetV; auto const prevFp = fp->arGetSfp(); FTRACE(1, "tearDownFrame: {} ({})\n fp {} prevFp {}\n", func->fullName()->data(), func->unit()->filepath()->data(), implicit_cast<void*>(fp), implicit_cast<void*>(prevFp)); if (fp->isFromFPushCtor() && fp->hasThis()) { fp->getThis()->setNoDestruct(); } // A generator's locals don't live on this stack. if (LIKELY(!unwindingGeneratorFrame)) { /* * If we're unwinding through a frame that's returning, it's only * possible that its 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. * */ if (!unwindingReturningFrame) { 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()); } catch (...) {} } stack.ndiscard(func->numSlotsInFrame()); stack.discardAR(); } assert(stack.isValidAddress(reinterpret_cast<uintptr_t>(prevFp)) || prevFp->m_func->isGenerator()); auto const prevOff = fp->m_soff + prevFp->m_func->base(); pc = prevFp->m_func->unit()->at(prevOff); fp = prevFp; faultOffset = prevOff; }