hphp/runtime/base/execution-context.cpp (1,820 lines of code) (raw):
/*
+----------------------------------------------------------------------+
| HipHop for PHP |
+----------------------------------------------------------------------+
| Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com) |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
*/
#include "hphp/runtime/base/execution-context.h"
#define __STDC_LIMIT_MACROS
#include <cstdint>
#include <algorithm>
#include <list>
#include <utility>
#include <folly/MapUtil.h>
#include <folly/Format.h>
#include <folly/Likely.h>
#include "hphp/util/logger.h"
#include "hphp/util/process.h"
#include "hphp/util/text-color.h"
#include "hphp/util/service-data.h"
#include "hphp/runtime/base/request-event-handler.h"
#include "hphp/runtime/base/array-init.h"
#include "hphp/runtime/base/debuggable.h"
#include "hphp/runtime/base/array-iterator.h"
#include "hphp/runtime/base/memory-manager.h"
#include "hphp/runtime/base/sweepable.h"
#include "hphp/runtime/base/backtrace.h"
#include "hphp/runtime/base/builtin-functions.h"
#include "hphp/runtime/base/comparisons.h"
#include "hphp/runtime/base/program-functions.h"
#include "hphp/runtime/base/runtime-option.h"
#include "hphp/runtime/base/tv-refcount.h"
#include "hphp/runtime/base/tv-type.h"
#include "hphp/runtime/base/type-variant.h"
#include "hphp/runtime/base/unit-cache.h"
#include "hphp/runtime/base/system-profiler.h"
#include "hphp/runtime/base/container-functions.h"
#include "hphp/runtime/base/hhprof.h"
#include "hphp/runtime/base/apc-stats.h"
#include "hphp/runtime/base/apc-typed-value.h"
#include "hphp/runtime/base/extended-logger.h"
#include "hphp/runtime/base/implicit-context.h"
#include "hphp/runtime/debugger/debugger.h"
#include "hphp/runtime/debugger/debugger_base.h"
#include "hphp/runtime/ext/std/ext_std_output.h"
#include "hphp/runtime/ext/string/ext_string.h"
#include "hphp/runtime/ext/reflection/ext_reflection.h"
#include "hphp/runtime/ext/apc/ext_apc.h"
#include "hphp/runtime/server/cli-server.h"
#include "hphp/runtime/server/server-stats.h"
#include "hphp/runtime/server/source-root-info.h"
#include "hphp/runtime/vm/debug/debug.h"
#include "hphp/runtime/vm/jit/enter-tc.h"
#include "hphp/runtime/vm/jit/jit-resume-addr-defs.h"
#include "hphp/runtime/vm/jit/tc.h"
#include "hphp/runtime/vm/jit/translator-inline.h"
#include "hphp/runtime/vm/jit/translator.h"
#include "hphp/runtime/vm/jit/unwind-itanium.h"
#include "hphp/runtime/vm/act-rec-defs.h"
#include "hphp/runtime/vm/debugger-hook.h"
#include "hphp/runtime/vm/event-hook.h"
#include "hphp/runtime/vm/hh-utils.h"
#include "hphp/runtime/vm/interp-helpers.h"
#include "hphp/runtime/vm/resumable.h"
#include "hphp/runtime/vm/runtime.h"
#include "hphp/runtime/vm/runtime-compiler.h"
#include "hphp/runtime/vm/treadmill.h"
#include "hphp/runtime/vm/unwind.h"
#include "hphp/runtime/base/php-globals.h"
#include "hphp/runtime/base/exceptions.h"
#include "hphp/util/timer.h"
#include "hphp/zend/zend-math.h"
#include "hphp/runtime/base/bespoke-runtime.h"
namespace HPHP {
///////////////////////////////////////////////////////////////////////////////
TRACE_SET_MOD(bcinterp);
rds::local::AliasedRDSLocal<ExecutionContext,
rds::local::Initialize::Explicitly,
&rds::local::detail::HotRDSLocals::g_context
> g_context;
ExecutionContext::ExecutionContext()
: m_transport(nullptr)
, m_sb(nullptr)
, m_implicitFlush(false)
, m_protectedLevel(0)
, m_stdoutBytesWritten(0)
, m_errorState(ExecutionContext::ErrorState::NoError)
, m_lastErrorNum(0)
, m_deferredErrors(nullptr)
, m_throwAllErrors(false)
, m_pageletTasksStarted(0)
, m_vhost(nullptr)
, m_globalNVTable(nullptr)
, m_lambdaCounter(0)
, m_nesting(0)
, m_dbgNoBreak(false)
, m_inlineInterpState(InlineInterpState::NONE)
, m_lastErrorPath(staticEmptyString())
, m_lastErrorLine(0)
, m_executingSetprofileCallback(false)
, m_logger_hook(*this)
{
m_deferredErrors = staticEmptyVec();
resetCoverageCounters();
// We don't want a new execution context to cause any request-heap
// allocations (because it will cause us to hold a slab, even while idle).
static auto s_cwd = makeStaticString(Process::CurrentWorkingDirectory);
m_cwd = s_cwd;
RID().setMemoryLimit(std::to_string(RuntimeOption::RequestMemoryMaxBytes));
RID().setErrorReportingLevel(RuntimeOption::RuntimeErrorReportingLevel);
VariableSerializer::serializationSizeLimit->value =
RuntimeOption::SerializationSizeLimit;
tvWriteUninit(m_headerCallback);
m_shutdowns[ShutdownType::ShutDown] = empty_vec_array();
m_shutdowns[ShutdownType::PostSend] = empty_vec_array();
m_shutdownsBackup[ShutdownType::ShutDown] = empty_vec_array();
m_shutdownsBackup[ShutdownType::PostSend] = empty_vec_array();
}
namespace rds::local {
// See header for why this is required.
#ifndef _MSC_VER
template<>
#endif
void GContextType::Base::destroy() {
if (!isNull()) {
getNoCheck()->sweep();
nullOut();
}
}
}
void ExecutionContext::cleanup() {
manageAPCHandle();
auto dead = std::move(m_stdoutHooks);
}
void ExecutionContext::sweep() {
cleanup();
}
ExecutionContext::~ExecutionContext() {
cleanup();
}
void ExecutionContext::backupSession() {
m_shutdownsBackup = m_shutdowns;
m_userErrorHandlersBackup = m_userErrorHandlers;
m_userExceptionHandlersBackup = m_userExceptionHandlers;
}
void ExecutionContext::restoreSession() {
m_shutdowns = m_shutdownsBackup;
m_userErrorHandlers = m_userErrorHandlersBackup;
m_userExceptionHandlers = m_userExceptionHandlersBackup;
}
///////////////////////////////////////////////////////////////////////////////
// system functions
String ExecutionContext::getMimeType() const {
String mimetype;
if (m_transport) {
mimetype = m_transport->getMimeType();
}
if (strncasecmp(mimetype.data(), "text/", 5) == 0) {
int pos = mimetype.find(';');
if (pos != String::npos) {
mimetype = mimetype.substr(0, pos);
}
} else if (m_transport && m_transport->getUseDefaultContentType()) {
mimetype = RID().getDefaultMimeType();
}
return mimetype;
}
std::string ExecutionContext::getRequestUrl(size_t szLimit) {
Transport* t = getTransport();
std::string ret = t ? t->getUrl() : "";
if (szLimit != std::string::npos) {
ret = ret.substr(0, szLimit);
}
return ret;
}
void ExecutionContext::setContentType(const String& mimetype,
const String& charset) {
if (m_transport) {
String contentType = mimetype;
contentType += "; ";
contentType += "charset=";
contentType += charset;
m_transport->addHeader("Content-Type", contentType.c_str());
m_transport->setUseDefaultContentType(false);
}
}
///////////////////////////////////////////////////////////////////////////////
// write()
void ExecutionContext::write(const String& s) {
write(s.data(), s.size());
}
void ExecutionContext::addStdoutHook(StdoutHook* hook) {
if (hook != nullptr) {
m_stdoutHooks.insert(hook);
}
}
bool ExecutionContext::removeStdoutHook(StdoutHook* hook) {
if (hook == nullptr) {
return false;
}
return m_stdoutHooks.erase(hook) != 0;
}
static void safe_stdout(const void *ptr, size_t size) {
write(fileno(stdout), ptr, size);
}
void ExecutionContext::writeStdout(const char *s, int len) {
fflush(stdout);
if (m_stdoutHooks.empty()) {
if (s_stdout_color) {
safe_stdout(s_stdout_color, strlen(s_stdout_color));
safe_stdout(s, len);
safe_stdout(ANSI_COLOR_END, strlen(ANSI_COLOR_END));
} else {
safe_stdout(s, len);
}
m_stdoutBytesWritten += len;
} else {
for (auto const hook : m_stdoutHooks) {
assertx(hook != nullptr);
(*hook)(s, len);
}
}
}
void ExecutionContext::writeTransport(const char *s, int len) {
if (m_transport) {
m_transport->sendRaw(s, len, 200, false, true);
} else {
writeStdout(s, len);
}
}
size_t ExecutionContext::getStdoutBytesWritten() const {
return m_stdoutBytesWritten;
}
void ExecutionContext::write(const char *s, int len) {
if (m_sb) {
m_sb->append(s, len);
if (m_out && m_out->chunk_size > 0) {
if (m_sb->size() >= m_out->chunk_size) {
obFlush();
}
}
if (m_implicitFlush) flush();
} else {
writeTransport(s, len);
}
}
///////////////////////////////////////////////////////////////////////////////
// output buffers
void ExecutionContext::obProtect(bool on) {
m_protectedLevel = on ? m_buffers.size() : 0;
}
void ExecutionContext::obStart(const Variant& handler /* = null */,
int chunk_size /* = 0 */,
OBFlags flags /* = OBFlags::Default */) {
if (m_insideOBHandler) {
raise_error("ob_start(): Cannot use output buffering "
"in output buffering display handlers");
}
m_buffers.emplace_back(Variant(handler), chunk_size, flags);
resetCurrentBuffer();
}
String ExecutionContext::obCopyContents() {
if (!m_buffers.empty()) {
StringBuffer &oss = m_buffers.back().oss;
if (!oss.empty()) {
return oss.copy();
}
}
return empty_string();
}
String ExecutionContext::obDetachContents() {
if (!m_buffers.empty()) {
StringBuffer &oss = m_buffers.back().oss;
if (!oss.empty()) {
return oss.detach();
}
}
return empty_string();
}
int ExecutionContext::obGetContentLength() {
if (m_buffers.empty()) {
return 0;
}
return m_buffers.back().oss.size();
}
void ExecutionContext::obClean(int handler_flag) {
if (!m_buffers.empty()) {
OutputBuffer *last = &m_buffers.back();
if (!last->handler.isNull()) {
m_insideOBHandler = true;
SCOPE_EXIT { m_insideOBHandler = false; };
vm_call_user_func(last->handler,
make_vec_array(last->oss.detach(), handler_flag));
}
last->oss.clear();
}
}
bool ExecutionContext::obFlush(bool force /*= false*/) {
assertx(m_protectedLevel >= 0);
if ((int)m_buffers.size() <= m_protectedLevel) {
return false;
}
auto iter = m_buffers.end();
OutputBuffer& last = *(--iter);
if (!force && !(last.flags & OBFlags::Flushable)) {
return false;
}
if (any(last.flags & OBFlags::OutputDisabled)) {
return false;
}
const int flag = k_PHP_OUTPUT_HANDLER_START | k_PHP_OUTPUT_HANDLER_END;
if (iter != m_buffers.begin()) {
OutputBuffer& prev = *(--iter);
if (last.handler.isNull()) {
prev.oss.absorb(last.oss);
} else {
auto str = last.oss.detach();
try {
Variant tout;
{
m_insideOBHandler = true;
SCOPE_EXIT { m_insideOBHandler = false; };
tout = vm_call_user_func(
last.handler, make_vec_array(str, flag)
);
}
prev.oss.append(tout.toString());
} catch (...) {
prev.oss.append(str);
throw;
}
}
return true;
}
auto str = last.oss.detach();
if (!last.handler.isNull()) {
try {
Variant tout;
{
m_insideOBHandler = true;
SCOPE_EXIT { m_insideOBHandler = false; };
tout = vm_call_user_func(
last.handler, make_vec_array(str, flag)
);
}
str = tout.toString();
} catch (...) {
writeTransport(str.data(), str.size());
throw;
}
}
writeTransport(str.data(), str.size());
return true;
}
void ExecutionContext::obFlushAll() {
do {
obFlush(true);
} while (obEnd());
}
bool ExecutionContext::obEnd() {
assertx(m_protectedLevel >= 0);
if ((int)m_buffers.size() > m_protectedLevel) {
m_buffers.pop_back();
resetCurrentBuffer();
if (m_implicitFlush) flush();
return true;
}
if (m_implicitFlush) flush();
return false;
}
void ExecutionContext::obEndAll() {
while (obEnd()) {}
}
int ExecutionContext::obGetLevel() {
assertx((int)m_buffers.size() >= m_protectedLevel);
return m_buffers.size() - m_protectedLevel;
}
const StaticString
s_level("level"),
s_type("type"),
s_flags("flags"),
s_name("name"),
s_args("args"),
s_chunk_size("chunk_size"),
s_buffer_used("buffer_used"),
s_default_output_handler("default output handler");
Array ExecutionContext::obGetStatus(bool full) {
Array ret = empty_vec_array();
int level = 0;
for (auto& buffer : m_buffers) {
Array status = empty_dict_array();
if (level < m_protectedLevel || buffer.handler.isNull()) {
status.set(s_name, s_default_output_handler);
status.set(s_type, 0);
} else {
status.set(s_name, buffer.handler);
status.set(s_type, 1);
}
int flags = 0;
if (any(buffer.flags & OBFlags::Cleanable)) {
flags |= k_PHP_OUTPUT_HANDLER_CLEANABLE;
}
if (any(buffer.flags & OBFlags::Flushable)) {
flags |= k_PHP_OUTPUT_HANDLER_FLUSHABLE;
}
if (any(buffer.flags & OBFlags::Removable)) {
flags |= k_PHP_OUTPUT_HANDLER_REMOVABLE;
}
status.set(s_flags, flags);
status.set(s_level, level);
status.set(s_chunk_size, buffer.chunk_size);
status.set(s_buffer_used, static_cast<uint64_t>(buffer.oss.size()));
if (full) {
ret.append(status);
} else {
ret = std::move(status);
}
level++;
}
return ret;
}
String ExecutionContext::obGetBufferName() {
if (m_buffers.empty()) {
return String();
} else if (m_buffers.size() <= m_protectedLevel) {
return s_default_output_handler;
} else {
auto iter = m_buffers.end();
OutputBuffer& buffer = *(--iter);
if (buffer.handler.isNull()) {
return s_default_output_handler;
} else {
return buffer.handler.toString();
}
}
}
void ExecutionContext::obSetImplicitFlush(bool on) {
m_implicitFlush = on;
}
Array ExecutionContext::obGetHandlers() {
Array ret = empty_vec_array();
for (auto& ob : m_buffers) {
auto& handler = ob.handler;
ret.append(handler.isNull() ? s_default_output_handler : handler);
}
return ret;
}
void ExecutionContext::flush() {
if (!m_buffers.empty() &&
RuntimeOption::EnableEarlyFlush && m_protectedLevel &&
!(m_buffers.front().flags & OBFlags::OutputDisabled)) {
OutputBuffer &buffer = m_buffers.front();
StringBuffer &oss = buffer.oss;
if (!oss.empty()) {
if (any(buffer.flags & OBFlags::WriteToStdout)) {
writeStdout(oss.data(), oss.size());
} else {
writeTransport(oss.data(), oss.size());
}
oss.clear();
}
}
}
void ExecutionContext::resetCurrentBuffer() {
if (m_buffers.empty()) {
m_sb = nullptr;
m_out = nullptr;
} else {
m_sb = &m_buffers.back().oss;
m_out = &m_buffers.back();
}
}
///////////////////////////////////////////////////////////////////////////////
// program executions
void ExecutionContext::registerShutdownFunction(const Variant& function,
ShutdownType type) {
auto& funcs = m_shutdowns[type];
assertx(funcs.isVec());
funcs.append(function);
}
Variant ExecutionContext::pushUserErrorHandler(const Variant& function,
int error_types) {
Variant ret;
if (!m_userErrorHandlers.empty()) {
ret = m_userErrorHandlers.back().first;
}
m_userErrorHandlers.push_back(std::pair<Variant,int>(function, error_types));
return ret;
}
Variant ExecutionContext::pushUserExceptionHandler(const Variant& function) {
Variant ret;
if (!m_userExceptionHandlers.empty()) {
ret = m_userExceptionHandlers.back();
}
m_userExceptionHandlers.push_back(function);
return ret;
}
void ExecutionContext::popUserErrorHandler() {
if (!m_userErrorHandlers.empty()) {
m_userErrorHandlers.pop_back();
}
}
void ExecutionContext::clearUserErrorHandlers() {
while (!m_userErrorHandlers.empty()) m_userErrorHandlers.pop_back();
}
void ExecutionContext::popUserExceptionHandler() {
if (!m_userExceptionHandlers.empty()) {
m_userExceptionHandlers.pop_back();
}
}
void ExecutionContext::acceptRequestEventHandlers(bool enable) {
m_acceptRequestEventHandlers = enable;
}
std::size_t ExecutionContext::registerRequestEventHandler(
RequestEventHandler *handler) {
assertx(handler && handler->getInited());
assertx(m_acceptRequestEventHandlers);
m_requestEventHandlers.push_back(handler);
return m_requestEventHandlers.size()-1;
}
void ExecutionContext::unregisterRequestEventHandler(
RequestEventHandler* handler,
std::size_t index) {
assertx(index < m_requestEventHandlers.size() &&
m_requestEventHandlers[index] == handler);
assertx(!handler->getInited());
if (index == m_requestEventHandlers.size()-1) {
m_requestEventHandlers.pop_back();
} else {
m_requestEventHandlers[index] = nullptr;
}
}
static bool requestEventHandlerPriorityComp(RequestEventHandler *a,
RequestEventHandler *b) {
if (!a) return b;
else if (!b) return false;
else return a->priority() < b->priority();
}
void ExecutionContext::onRequestShutdown() {
while (!m_requestEventHandlers.empty()) {
// handlers could cause other handlers to be registered,
// so need to repeat until done
decltype(m_requestEventHandlers) tmp;
tmp.swap(m_requestEventHandlers);
// Sort handlers by priority so that lower priority values get shutdown
// first
sort(tmp.begin(), tmp.end(),
requestEventHandlerPriorityComp);
for (auto* handler : tmp) {
if (!handler) continue;
assertx(handler->getInited());
handler->requestShutdown();
handler->setInited(false);
}
}
}
void ExecutionContext::executeFunctions(ShutdownType type) {
RID().resetTimers(
RuntimeOption::PspTimeoutSeconds,
RuntimeOption::PspCpuTimeoutSeconds
);
if (RO::EvalEnableImplicitContext) {
// If the main request terminated with a C++ exception, we would not
// have cleared the implicit context since that logic is
// done in a PHP try-finally. Let's clear the implicit context here.
*ImplicitContext::activeCtx = nullptr;
}
// We mustn't destroy any callbacks until we're done with all
// of them. So hold them in tmp.
// XXX still true in a world without destructors?
auto tmp = empty_vec_array();
while (true) {
Array funcs = m_shutdowns[type];
if (funcs.empty()) break;
m_shutdowns[type] = empty_vec_array();
IterateV(
funcs.get(),
[](TypedValue v) {
vm_call_user_func(VarNR{v}, init_null_variant,
RuntimeCoeffects::defaults());
// Implicit context should not have leaked between each call
assertx(!RO::EvalEnableImplicitContext ||
!(*ImplicitContext::activeCtx));
}
);
tmp.append(funcs);
}
}
void ExecutionContext::onShutdownPreSend() {
// in case obStart was called without obFlush
SCOPE_EXIT {
try { obFlushAll(); } catch (...) {}
};
// When host is OOMing, abort abruptly.
if (RID().shouldOOMAbort()) return;
tracing::Block _{"shutdown-pre-send"};
tl_heap->resetCouldOOM(isStandardRequest());
executeFunctions(ShutDown);
}
void ExecutionContext::debuggerExecutePsps() {
try {
executeFunctions(PostSend);
} catch (const ExitException& e) {
// do nothing
} catch (const Exception& e) {
onFatalError(e);
} catch (const Object& e) {
onUnhandledException(e);
} catch (...) {
}
}
void ExecutionContext::onShutdownPostSend() {
// When host is OOMing, abort abruptly.
if (RID().shouldOOMAbort()) return;
tracing::Block _{"shutdown-post-send"};
ServerStats::SetThreadMode(ServerStats::ThreadMode::PostProcessing);
tl_heap->resetCouldOOM(isStandardRequest());
try {
try {
ServerStatsHelper ssh("psp", ServerStatsHelper::TRACK_HWINST);
executeFunctions(PostSend);
} catch (...) {
try {
bump_counter_and_rethrow(true /* isPsp */);
} catch (const ExitException& e) {
// do nothing
} catch (const Exception& e) {
onFatalError(e);
} catch (const Object& e) {
onUnhandledException(e);
}
}
} catch (...) {
Logger::Error("unknown exception was thrown from psp");
}
ServerStats::SetThreadMode(ServerStats::ThreadMode::Idling);
}
///////////////////////////////////////////////////////////////////////////////
// error handling
bool ExecutionContext::errorNeedsHandling(int errnum,
bool callUserHandler,
ErrorThrowMode mode) {
if (UNLIKELY(m_throwAllErrors)) {
throw Exception(folly::sformat("throwAllErrors: {}", errnum));
}
if (mode != ErrorThrowMode::Never || errorNeedsLogging(errnum)) {
return true;
}
if (callUserHandler) {
if (!m_userErrorHandlers.empty() &&
(m_userErrorHandlers.back().second & errnum) != 0) {
return true;
}
}
return false;
}
bool ExecutionContext::errorNeedsLogging(int errnum) {
auto level =
RID().getErrorReportingLevel() |
RuntimeOption::ForceErrorReportingLevel;
return RuntimeOption::NoSilencer || (level & errnum) != 0;
}
struct ErrorStateHelper {
ErrorStateHelper(ExecutionContext *context,
ExecutionContext::ErrorState state) {
m_context = context;
m_originalState = m_context->getErrorState();
m_context->setErrorState(state);
}
~ErrorStateHelper() {
m_context->setErrorState(m_originalState);
}
private:
ExecutionContext *m_context;
ExecutionContext::ErrorState m_originalState;
};
const StaticString
s_class("class"),
s_file("file"),
s_function("function"),
s_line("line"),
s_error_num("error-num"),
s_error_string("error-string"),
s_error_file("error-file"),
s_error_line("error-line"),
s_error_backtrace("error-backtrace"),
s_overflow("overflow");
void ExecutionContext::handleError(const std::string& msg,
int errnum,
bool callUserHandler,
ErrorThrowMode mode,
const std::string& prefix,
bool skipFrame /* = false */) {
SYNC_VM_REGS_SCOPED();
auto newErrorState = ErrorState::ErrorRaised;
switch (getErrorState()) {
case ErrorState::ErrorRaised:
case ErrorState::ErrorRaisedByUserHandler:
return;
case ErrorState::ExecutingUserHandler:
newErrorState = ErrorState::ErrorRaisedByUserHandler;
break;
default:
break;
}
// Potentially upgrade the error to E_USER_ERROR
if (errnum & RuntimeOption::ErrorUpgradeLevel &
static_cast<int>(ErrorMode::UPGRADEABLE_ERROR)) {
errnum = static_cast<int>(ErrorMode::USER_ERROR);
mode = ErrorThrowMode::IfUnhandled;
}
auto const ee = skipFrame ?
ExtendedException(ExtendedException::SkipFrame{}, msg) :
ExtendedException(msg);
bool handled = false;
{
ErrorStateHelper esh(this, newErrorState);
if (callUserHandler) {
handled = callUserErrorHandler(ee, errnum, false);
}
if (!handled) {
recordLastError(ee, errnum);
}
if (g_system_profiler) {
g_system_profiler->errorCallBack(ee, errnum, msg);
}
}
if (mode == ErrorThrowMode::Always ||
(mode == ErrorThrowMode::IfUnhandled && !handled)) {
DEBUGGER_ATTACHED_ONLY(phpDebuggerErrorHook(ee, errnum, msg));
bool isRecoverable =
errnum == static_cast<int>(ErrorMode::RECOVERABLE_ERROR);
raise_fatal_error(msg.c_str(), ee.getBacktrace(), isRecoverable,
!errorNeedsLogging(errnum) /* silent */);
not_reached();
}
if (!handled) {
// If we're inside an error handler already, queue it up on the deferred
// list.
if (getErrorState() == ErrorState::ExecutingUserHandler) {
auto& deferred = m_deferredErrors;
if (deferred.size() < RuntimeOption::EvalMaxDeferredErrors) {
auto fileAndLine = ee.getFileAndLine();
deferred.append(
make_dict_array(
s_error_num, errnum,
s_error_string, msg,
s_error_file, std::move(fileAndLine.first),
s_error_line, fileAndLine.second,
s_error_backtrace, ee.getBacktrace()
)
);
} else if (!deferred.empty()) {
auto const last = deferred.lval(int64_t{deferred.size() - 1});
if (isDictType(type(last))) {
asArrRef(last).set(s_overflow, true);
}
}
}
if (errorNeedsLogging(errnum)) {
DEBUGGER_ATTACHED_ONLY(phpDebuggerErrorHook(ee, errnum, ee.getMessage()));
auto fileAndLine = ee.getFileAndLine();
Logger::Log(Logger::LogError, prefix.c_str(), ee,
fileAndLine.first.c_str(), fileAndLine.second);
}
}
}
bool ExecutionContext::callUserErrorHandler(const Exception& e, int errnum,
bool swallowExceptions) {
switch (getErrorState()) {
case ErrorState::ExecutingUserHandler:
case ErrorState::ErrorRaisedByUserHandler:
return false;
default:
break;
}
if (!m_userErrorHandlers.empty() &&
(m_userErrorHandlers.back().second & errnum) != 0) {
auto fileAndLine = std::make_pair(empty_string(), 0);
Variant backtrace;
if (auto const ee = dynamic_cast<const ExtendedException*>(&e)) {
fileAndLine = ee->getFileAndLine();
backtrace = ee->getBacktrace();
}
try {
ErrorStateHelper esh(this, ErrorState::ExecutingUserHandler);
m_deferredErrors = empty_vec_array();
SCOPE_EXIT { m_deferredErrors = empty_vec_array(); };
if (!same(vm_call_user_func
(m_userErrorHandlers.back().first,
make_vec_array(errnum, String(e.getMessage()),
fileAndLine.first, fileAndLine.second, empty_dict_array(),
backtrace),
RuntimeCoeffects::defaults()),
false)) {
return true;
}
} catch (const RequestTimeoutException&) {
static auto requestErrorHandlerTimeoutCounter =
ServiceData::createTimeSeries("requests_timed_out_error_handler",
{ServiceData::StatsType::COUNT});
requestErrorHandlerTimeoutCounter->addValue(1);
ServerStats::Log("request.timed_out.error_handler", 1);
if (!swallowExceptions) throw;
} catch (const RequestCPUTimeoutException&) {
static auto requestErrorHandlerCPUTimeoutCounter =
ServiceData::createTimeSeries("requests_cpu_timed_out_error_handler",
{ServiceData::StatsType::COUNT});
requestErrorHandlerCPUTimeoutCounter->addValue(1);
ServerStats::Log("request.cpu_timed_out.error_handler", 1);
if (!swallowExceptions) throw;
} catch (const RequestMemoryExceededException&) {
static auto requestErrorHandlerMemoryExceededCounter =
ServiceData::createTimeSeries(
"requests_memory_exceeded_error_handler",
{ServiceData::StatsType::COUNT});
requestErrorHandlerMemoryExceededCounter->addValue(1);
ServerStats::Log("request.memory_exceeded.error_handler", 1);
if (!swallowExceptions) throw;
} catch (...) {
static auto requestErrorHandlerOtherExceptionCounter =
ServiceData::createTimeSeries(
"requests_other_exception_error_handler",
{ServiceData::StatsType::COUNT});
requestErrorHandlerOtherExceptionCounter->addValue(1);
ServerStats::Log("request.other_exception.error_handler", 1);
if (!swallowExceptions) throw;
}
}
return false;
}
bool ExecutionContext::onFatalError(const Exception& e) {
tl_heap->resetCouldOOM(isStandardRequest());
RID().resetTimers();
// need to restore the error reporting level, because the fault
// handler for silencers won't be run on fatals, and we might be
// about to run a user error handler (and psp/shutdown code).
RID().setErrorReportingLevel(RuntimeOption::RuntimeErrorReportingLevel);
auto prefix = "\nFatal error: ";
auto errnum = static_cast<int>(ErrorMode::FATAL_ERROR);
auto const fatal = dynamic_cast<const FatalErrorException*>(&e);
if (fatal && fatal->isRecoverable()) {
prefix = "\nCatchable fatal error: ";
errnum = static_cast<int>(ErrorMode::RECOVERABLE_ERROR);
}
recordLastError(e, errnum);
bool silenced = false;
auto fileAndLine = std::make_pair(empty_string(), 0);
if (auto const ee = dynamic_cast<const ExtendedException*>(&e)) {
silenced = ee->isSilent();
fileAndLine = ee->getFileAndLine();
}
// need to silence even with the AlwaysLogUnhandledExceptions flag set
if (!silenced && RuntimeOption::AlwaysLogUnhandledExceptions) {
Logger::Log(Logger::LogError, prefix, e, fileAndLine.first.c_str(),
fileAndLine.second);
}
bool handled = false;
if (RuntimeOption::CallUserHandlerOnFatals) {
handled = callUserErrorHandler(e, errnum, true);
}
if (!handled && !silenced && !RuntimeOption::AlwaysLogUnhandledExceptions) {
Logger::Log(Logger::LogError, prefix, e, fileAndLine.first.c_str(),
fileAndLine.second);
}
return handled;
}
bool ExecutionContext::onUnhandledException(Object e) {
String err = throwable_to_string(e.get());
if (RuntimeOption::AlwaysLogUnhandledExceptions) {
Logger::Error("\nFatal error: Uncaught %s", err.data());
}
if (e.instanceof(SystemLib::s_ThrowableClass)) {
// user thrown exception
if (!m_userExceptionHandlers.empty()) {
if (!same(vm_call_user_func
(m_userExceptionHandlers.back(),
make_vec_array(e),
RuntimeCoeffects::defaults()),
false)) {
return true;
}
}
} else {
assertx(false);
}
m_lastError = err;
if (!RuntimeOption::AlwaysLogUnhandledExceptions) {
Logger::Error("\nFatal error: Uncaught %s", err.data());
}
return false;
}
///////////////////////////////////////////////////////////////////////////////
void ExecutionContext::debuggerInfo(
std::vector<std::pair<const char*,std::string>>& info) {
static StaticString s_memoryLimit("memory_limit");
int64_t newInt = convert_bytes_to_long(IniSetting::Get(s_memoryLimit));
if (newInt <= 0) {
newInt = std::numeric_limits<int64_t>::max();
}
if (newInt == std::numeric_limits<int64_t>::max()) {
info.emplace_back("Max Memory", "(unlimited)");
} else {
info.emplace_back("Max Memory", IDebuggable::FormatSize(newInt));
}
info.emplace_back("Max Time",
IDebuggable::FormatTime(RID().getTimeout() * 1000));
}
void ExecutionContext::setenv(const String& name, const String& value) {
m_envs.set(m_envs.convertKey<IntishCast::Cast>(name),
make_tv<KindOfString>(value.get()));
}
void ExecutionContext::unsetenv(const String& name) {
m_envs.remove(name);
}
String ExecutionContext::getenv(const String& name) const {
if (m_envs.exists(name)) {
return m_envs[name].toString();
}
if (is_cli_server_mode()) {
auto envs = cli_env();
if (envs.exists(name)) return envs[name].toString();
return String();
}
if (auto value = ::getenv(name.data())) {
return String(value, CopyString);
}
if (RuntimeOption::EnvVariables.find(name.c_str()) != RuntimeOption::EnvVariables.end()) {
return String(RuntimeOption::EnvVariables[name.c_str()].data(), CopyString);
}
return String();
}
TypedValue ExecutionContext::lookupClsCns(const NamedEntity* ne,
const StringData* cls,
const StringData* cns) {
Class* class_ = nullptr;
try {
class_ = Class::load(ne, cls);
} catch (Object& ex) {
// For compatibility with php, throwing through a constant lookup has
// different behavior inside a property initializer (86pinit/86sinit).
auto ar = getStackFrame();
if (ar && ar->func() && Func::isSpecial(ar->func()->name())) {
raise_warning("Uncaught %s", throwable_to_string(ex.get()).data());
raise_error("Couldn't find constant %s::%s", cls->data(), cns->data());
}
throw;
}
if (class_ == nullptr) {
raise_error(Strings::UNKNOWN_CLASS, cls->data());
}
TypedValue clsCns = class_->clsCnsGet(cns);
if (clsCns.m_type == KindOfUninit) {
raise_error("Couldn't find constant %s::%s", cls->data(), cns->data());
}
return clsCns;
}
static Class* loadClass(StringData* clsName) {
Class* class_ = Class::load(clsName);
if (class_ == nullptr) {
raise_error(Strings::UNKNOWN_CLASS, clsName->data());
}
return class_;
}
ObjectData* ExecutionContext::createObject(StringData* clsName,
const Variant& params,
bool init /* = true */) {
return createObject(loadClass(clsName), params, init);
}
ObjectData* ExecutionContext::createObject(const Class* class_,
const Variant& params,
bool init) {
callerDynamicConstructChecks(class_);
auto o = Object::attach(ObjectData::newInstance(const_cast<Class*>(class_)));
if (init) {
initObject(class_, params, o.get());
}
return o.detach();
}
ObjectData* ExecutionContext::createObjectOnly(StringData* clsName) {
return createObject(clsName, init_null_variant, false);
}
ObjectData* ExecutionContext::initObject(StringData* clsName,
const Variant& params,
ObjectData* o) {
return initObject(loadClass(clsName), params, o);
}
ObjectData* ExecutionContext::initObject(const Class* class_,
const Variant& params,
ObjectData* o) {
auto ctor = class_->getCtor();
if (!(ctor->attrs() & AttrPublic)) {
std::string msg = "Access to non-public constructor of class ";
msg += class_->name()->data();
Reflection::ThrowReflectionExceptionObject(msg);
}
// call constructor
if (!isContainerOrNull(params)) {
throw_param_is_not_container();
}
tvDecRefGen(invokeFunc(ctor, params, o, nullptr, RuntimeCoeffects::fixme(),
true, false, true, false, Array()));
return o;
}
ActRec* ExecutionContext::getStackFrame() {
VMRegAnchor _;
return vmfp();
}
ObjectData* ExecutionContext::getThis() {
VMRegAnchor _;
ActRec* fp = vmfp();
if (fp->skipFrame()) fp = getPrevVMStateSkipFrame(fp);
if (fp && fp->func()->cls() && fp->hasThis()) {
return fp->getThis();
}
return nullptr;
}
namespace {
const RepoOptions& getRepoOptionsForDebuggerEval() {
const auto root = SourceRootInfo::GetCurrentSourceRoot();
if (root.size() == 0) {
return RepoOptions::defaults();
}
const auto dummyPath = fmt::format("{}/$$eval$$.php", root);
return RepoOptions::forFile(dummyPath.data());
}
} // namespace
const RepoOptions& ExecutionContext::getRepoOptionsForCurrentFrame() const {
return getRepoOptionsForFrame(0);
}
const RepoOptions& ExecutionContext::getRepoOptionsForFrame(int frame) const {
VMRegAnchor _;
const RepoOptions* ret{nullptr};
walkStack([&] (const BTFrame& frm) {
if (frame--) {
return false;
}
auto const path = frm.func()->unit()->filepath();
if (UNLIKELY(path->empty())) {
// - Systemlib paths always start with `/:systemlib`
// - we make up a bogus filename for eval()
// - but let's assert out of paranoia as we're in a path that's not
// perf-sensitive
assertx(!frm.func()->unit()->isSystemLib());
ret = &getRepoOptionsForDebuggerEval();
return true;
}
ret = &RepoOptions::forFile(path->data());
return true;
});
return ret ? *ret : RepoOptions::defaults();
}
void ExecutionContext::onLoadWithOptions(
const char* f, const RepoOptions& opts
) {
if (!RuntimeOption::EvalFatalOnParserOptionMismatch) return;
if (!m_requestOptions) {
m_requestOptions.emplace(opts);
return;
}
if (m_requestOptions != opts) {
// The data buffer has to stay alive for the call to raise_error.
auto const path_str = opts.path();
auto const path = path_str.empty() ? "{default options}" : path_str.data();
raise_error(
"Attempting to load file %s with incompatible parser settings from %s, "
"this request is using parser settings from %s",
f, path, m_requestOptions->path().data()
);
}
}
StringData* ExecutionContext::getContainingFileName() {
VMRegAnchor _;
ActRec* ar = vmfp();
if (ar == nullptr) return staticEmptyString();
if (ar->skipFrame()) ar = getPrevVMStateSkipFrame(ar);
if (ar == nullptr) return staticEmptyString();
Unit* unit = ar->func()->unit();
auto const path = ar->func()->originalFilename() ?
ar->func()->originalFilename() : unit->filepath();
return const_cast<StringData*>(path);
}
int ExecutionContext::getLine() {
VMRegAnchor _;
ActRec* ar = vmfp();
auto func = ar ? ar->func() : nullptr;
Offset pc = func ? pcOff() : 0;
if (ar == nullptr) return -1;
if (ar->skipFrame()) ar = getPrevVMStateSkipFrame(ar, &pc);
if (ar == nullptr || (func = ar->func()) == nullptr) return -1;
return func->getLineNumber(pc);
}
ActRec* ExecutionContext::getFrameAtDepthForDebuggerUnsafe(int frameDepth) const {
ActRec* ret = nullptr;
walkStack([&] (const BTFrame& frm) {
if (frameDepth == 0) {
if (!frm.isInlined() && frm.localsAvailable()) {
ret = frm.fpInternal();
}
return true;
}
frameDepth--;
return false;
});
return ret;
}
Array ExecutionContext::getLocalDefinedVariablesDebugger(int frame) {
const auto fp = getFrameAtDepthForDebuggerUnsafe(frame);
return getDefinedVariables(fp);
}
bool ExecutionContext::setHeaderCallback(const Variant& callback) {
if (tvAsVariant(g_context->m_headerCallback).toBoolean()) {
// return false if a callback has already been set.
return false;
}
tvAsVariant(g_context->m_headerCallback) = callback;
return true;
}
const static StaticString
s_enter_async_entry_point("__SystemLib\\enter_async_entry_point");
TypedValue ExecutionContext::invokeUnit(const Unit* unit,
bool callByHPHPInvoke) {
checkHHConfig(unit);
const_cast<Unit*>(unit)->merge();
auto it = unit->getEntryPoint();
if (callByHPHPInvoke && it != nullptr) {
if (it->isAsync()) {
invokeFunc(
Func::lookup(s_enter_async_entry_point.get()),
make_vec_array(Variant{it}),
nullptr, nullptr, RuntimeCoeffects::defaults(), false
);
} else {
invokeFunc(it, init_null_variant, nullptr, nullptr,
RuntimeCoeffects::defaults(), false);
}
}
return make_tv<KindOfInt64>(1);
}
void ExecutionContext::syncGdbState() {
if (RuntimeOption::EvalJit && !RuntimeOption::EvalJitNoGdb) {
Debug::DebugInfo::Get()->debugSync();
}
}
void ExecutionContext::pushVMState(TypedValue* savedSP) {
if (UNLIKELY(!vmfp())) {
// first entry
assertx(m_nestedVMs.size() == 0);
return;
}
TRACE(3, "savedVM: %p %p %p %p\n", vmpc(), vmfp(), vmFirstAR(), savedSP);
auto& savedVM = m_nestedVMs.emplace_back(
VMState {
vmpc(),
vmfp(),
vmFirstAR(),
savedSP,
vmMInstrState(),
vmJitCalledFrame(),
vmJitReturnAddr(),
jit::g_unwind_rds->exn,
jit::g_unwind_rds->sideEnter,
}
);
jit::g_unwind_rds->exn = nullptr;
jit::g_unwind_rds->sideEnter = false;
m_nesting++;
if (debug && savedVM.fp &&
savedVM.fp->func() &&
savedVM.fp->func()->unit()) {
// Some asserts and tracing.
const Func* func = savedVM.fp->func();
/* bound-check asserts in offsetOf */
func->offsetOf(savedVM.pc);
TRACE(3, "pushVMState: saving frame %s pc %p off %d fp %p\n",
func->name()->data(),
savedVM.pc,
func->offsetOf(savedVM.pc),
savedVM.fp);
}
}
void ExecutionContext::popVMState() {
if (UNLIKELY(m_nestedVMs.empty())) {
// last exit
vmfp() = nullptr;
vmpc() = nullptr;
vmFirstAR() = nullptr;
return;
}
assertx(m_nestedVMs.size() >= 1);
VMState &savedVM = m_nestedVMs.back();
vmpc() = savedVM.pc;
vmfp() = savedVM.fp;
vmFirstAR() = savedVM.firstAR;
vmStack().top() = savedVM.sp;
vmMInstrState() = savedVM.mInstrState;
vmJitCalledFrame() = savedVM.jitCalledFrame;
vmJitReturnAddr() = savedVM.jitReturnAddr;
jit::g_unwind_rds->exn = savedVM.exn;
jit::g_unwind_rds->sideEnter = savedVM.unwinderSideEnter;
if (debug) {
if (savedVM.fp &&
savedVM.fp->func() &&
savedVM.fp->func()->unit()) {
const Func* func = savedVM.fp->func();
(void) /* bound-check asserts in offsetOf */
func->offsetOf(savedVM.pc);
TRACE(3, "popVMState: restoring frame %s pc %p off %d fp %p\n",
func->name()->data(),
savedVM.pc,
func->offsetOf(savedVM.pc),
savedVM.fp);
}
}
m_nestedVMs.pop_back();
m_nesting--;
TRACE(1, "Reentry: exit fp %p pc %p\n", vmfp(), vmpc());
}
void ExecutionContext::ExcLoggerHook::operator()(
const char* header, const char* msg, const char* ending
) {
ec.write(header);
ec.write(msg);
ec.write(ending);
ec.flush();
}
StaticString
s_hh("<?hh "),
s_namespace("namespace "),
s_curly_start(" { "),
s_curly_end(" }"),
s_function_start("<<__DynamicallyCallable>> function "),
s_evaluate_default_argument("evaluate_default_argument"),
s_function_middle("() { return "),
s_semicolon(";"),
s_stdclass("stdclass");
void ExecutionContext::requestInit() {
assertx(SystemLib::s_unit);
initBlackHole();
createGlobalNVTable();
vmStack().requestInit();
ResourceHdr::resetMaxId();
jit::tc::requestInit();
*rl_num_coeffect_violations = 0;
if (RuntimeOption::EvalJitEnableRenameFunction) {
assertx(SystemLib::s_anyNonPersistentBuiltins);
}
/*
* The normal case for production mode is that all builtins are
* persistent, and every systemlib unit is accordingly going to be
* merge only.
*
* However, if we have rename_function generally enabled, or if any
* builtin functions were specified as interceptable at
* repo-generation time, we'll actually need to merge systemlib on
* every request because some of the builtins will not be marked
* persistent.
*/
if (UNLIKELY(SystemLib::s_anyNonPersistentBuiltins)) {
SystemLib::s_unit->merge();
SystemLib::mergePersistentUnits();
if (SystemLib::s_hhas_unit) SystemLib::s_hhas_unit->merge();
} else {
// System units are merge only, and everything is persistent.
assertx(SystemLib::s_unit->isEmpty());
assertx(!SystemLib::s_hhas_unit || SystemLib::s_hhas_unit->isEmpty());
}
if (RO::EvalEnableImplicitContext) *ImplicitContext::activeCtx = nullptr;
profileRequestStart();
HHProf::Request::StartProfiling();
#ifndef NDEBUG
Class* cls = NamedEntity::get(s_stdclass.get())->clsList();
assertx(cls);
assertx(cls == SystemLib::s_stdclassClass);
#endif
if (Logger::UseRequestLog) Logger::SetThreadHook(&m_logger_hook);
// Needs to be last (or nearly last): might cause unit merging to call an
// extension function in the VM; this is bad if systemlib itself hasn't been
// merged.
autoTypecheckRequestInit();
if (!RO::RepoAuthoritative && RO::EvalSampleRequestTearing) {
if (StructuredLog::coinflip(RO::EvalSampleRequestTearing)) {
m_requestStartForTearing.emplace();
Timer::GetRealtimeTime(*m_requestStartForTearing);
}
}
}
void ExecutionContext::requestExit() {
apcExtension::purgeDeferred(std::move(m_apcDeferredExpire));
autoTypecheckRequestExit();
HHProf::Request::FinishProfiling();
manageAPCHandle();
syncGdbState();
vmStack().requestExit();
profileRequestEnd();
EventHook::Disable();
zend_rand_unseed();
clearBlackHole();
if (m_globalNVTable) {
req::destroy_raw(m_globalNVTable);
m_globalNVTable = nullptr;
}
if (!m_lastError.isNull()) {
clearLastError();
}
{
m_deferredErrors = empty_vec_array();
}
if (Logger::UseRequestLog) Logger::SetThreadHook(nullptr);
if (m_requestTrace) record_trace(std::move(*m_requestTrace));
if (!RO::RepoAuthoritative) m_requestStartForTearing.reset();
}
/**
* Enter VM by calling action(), which invokes a function or resumes
* an async function. The 'ar' argument points to an ActRec of the
* invoked/resumed function.
*/
template<class Action>
static inline void enterVM(ActRec* ar, Action action) {
assertx(ar);
assertx(!ar->sfp());
assertx(isCallToExit(ar->m_savedRip));
assertx(ar->callOffset() == 0);
vmFirstAR() = ar;
vmJitCalledFrame() = nullptr;
vmJitReturnAddr() = 0;
action();
while (vmpc()) {
exception_handler(enterVMAtCurPC);
}
}
/*
* Shared implementation for invokeFunc{,Few}().
*/
ALWAYS_INLINE
TypedValue ExecutionContext::invokeFuncImpl(const Func* f,
ObjectData* thiz, Class* cls,
uint32_t numArgsInclUnpack,
RuntimeCoeffects providedCoeffects,
bool hasGenerics, bool dynamic,
bool allowDynCallNoPointer) {
assertx(f);
// If `f' is a regular function, `thiz' and `cls' must be null.
assertx(IMPLIES(!f->implCls(), (!thiz && !cls)));
// If `f' is a method, either `thiz' or `cls' must be non-null.
assertx(IMPLIES(f->preClass(), thiz || cls));
// If `f' is a static method, thiz must be null.
assertx(IMPLIES(f->isStaticInPrologue(), !thiz));
ActRec* ar = vmStack().indA(numArgsInclUnpack + (hasGenerics ? 1 : 0));
// Callee checks and input initialization.
calleeGenericsChecks(f, hasGenerics);
calleeArgumentArityChecks(f, numArgsInclUnpack);
calleeDynamicCallChecks(f, dynamic, allowDynCallNoPointer);
void* ctx = thiz ? (void*)thiz : (void*)cls;
calleeCoeffectChecks(f, providedCoeffects, numArgsInclUnpack, ctx);
f->recordCall();
initFuncInputs(f, numArgsInclUnpack);
ar->setReturnVMExit();
ar->setFunc(f);
if (thiz) {
thiz->incRefCount();
ar->setThis(thiz);
} else if (cls) {
ar->setClass(cls);
} else {
ar->trashThis();
}
#ifdef HPHP_TRACE
if (vmfp() == nullptr) {
TRACE(1, "Reentry: enter %s(%p) from top-level\n",
f->name()->data(), ar);
} else {
TRACE(1, "Reentry: enter %s(pc %p ar %p) from %s(%p)\n",
f->name()->data(), vmpc(), ar,
vmfp()->func() ? vmfp()->func()->name()->data()
: "unknownBuiltin",
vmfp());
}
#endif
auto const reentrySP =
reinterpret_cast<TypedValue*>(ar) + kNumActRecCells + f->numInOutParams();
pushVMState(reentrySP);
SCOPE_EXIT {
assert_flog(
vmStack().top() == reentrySP,
"vmsp() mismatch around reentry: before @ {}, after @ {}",
reentrySP, vmStack().top()
);
popVMState();
};
enterVM(ar, [&] {
exception_handler([&] {
enterVMAtFunc(ar, numArgsInclUnpack);
});
});
if (UNLIKELY(f->takesInOutParams())) {
VecInit vec(f->numInOutParams() + 1);
for (uint32_t i = 0; i < f->numInOutParams() + 1; ++i) {
vec.append(*vmStack().topTV());
vmStack().popC();
}
return make_array_like_tv(vec.create());
} else {
auto const retval = *vmStack().topTV();
vmStack().discard();
return retval;
}
}
TypedValue ExecutionContext::invokeFunc(const Func* f,
const Variant& args_,
ObjectData* thiz /* = NULL */,
Class* cls /* = NULL */,
RuntimeCoeffects providedCoeffects
/* = RuntimeCoeffects::fixme() */,
bool dynamic /* = true */,
bool checkRefAnnot /* = false */,
bool allowDynCallNoPointer
/* = false */,
bool readonlyReturn /* = false */,
Array&& generics /* = Array() */) {
VMRegAnchor _;
// Check both the native stack and VM stack for overflow, numParams
// is already included in f->maxStackCells().
checkStack(vmStack(), f, f->numInOutParams() + kNumActRecCells);
// Reserve space for inout outputs and ActRec.
for (auto i = f->numInOutParams(); i > 0; --i) vmStack().pushUninit();
for (auto i = kNumActRecCells; i > 0; --i) vmStack().pushUninit();
// Push arguments.
auto const& args = *args_.asTypedValue();
assertx(isContainerOrNull(args));
auto numArgs = tvIsNull(args) ? 0 : getContainerSize(args);
if (numArgs > 0) {
assertx(isContainer(args));
tvDup(args, *vmStack().allocC());
numArgs = prepareUnpackArgs(f, 0, checkRefAnnot);
}
// Push generics.
auto const hasGenerics = !generics.isNull();
GenericsSaver::push(std::move(generics));
// Caller checks.
if (dynamic) callerDynamicCallChecks(f, allowDynCallNoPointer);
if (f->attrs() & AttrReadonlyReturn && !readonlyReturn) {
throwReadonlyMismatch(f, kReadonlyReturnId);
}
return invokeFuncImpl(f, thiz, cls, numArgs, providedCoeffects,
hasGenerics, dynamic, allowDynCallNoPointer);
}
TypedValue ExecutionContext::invokeFuncFew(
const Func* f,
ExecutionContext::ThisOrClass thisOrCls,
uint32_t numArgs,
const TypedValue* argv,
RuntimeCoeffects providedCoeffects,
bool dynamic /* = true */,
bool allowDynCallNoPointer
/* = false */
) {
VMRegAnchor _;
auto& stack = vmStack();
// See comments in invokeFunc().
checkStack(stack, f, f->numInOutParams() + kNumActRecCells);
// Reserve space for inout outputs and ActRec.
for (auto i = f->numInOutParams(); i > 0; --i) stack.pushUninit();
for (auto i = kNumActRecCells; i > 0; --i) stack.pushUninit();
// Push non-variadic arguments.
auto const numParams = f->numNonVariadicParams();
for (auto i = 0; i < numArgs && i < numParams; ++i) {
const TypedValue *from = &argv[i];
TypedValue* to = stack.allocTV();
tvDup(*from, *to);
}
// Push variadic arguments.
if (UNLIKELY(numParams < numArgs)) {
VecInit ai{numArgs - numParams};
for (auto i = numParams; i < numArgs; ++i) ai.append(argv[i]);
stack.pushArrayLikeNoRc(ai.create());
numArgs = numParams + 1;
}
// Caller checks.
if (dynamic) callerDynamicCallChecks(f, allowDynCallNoPointer);
if (f->attrs() & AttrReadonlyReturn) {
throwReadonlyMismatch(f, kReadonlyReturnId);
}
return invokeFuncImpl(f, thisOrCls.left(), thisOrCls.right(), numArgs,
providedCoeffects,
false /* hasGenerics */, dynamic, false);
}
static void prepareAsyncFuncEntry(ActRec* enterFnAr,
Resumable* resumable,
bool willUnwind) {
assertx(enterFnAr);
assertx(enterFnAr->func()->isAsync());
assertx(isResumed(enterFnAr));
assertx(resumable);
vmfp() = enterFnAr;
// If we're going to unwind, we need to resume at the offset we
// suspended at, so we look up the correct catch trace (not the one
// for the next op).
vmpc() = enterFnAr->func()->at(
willUnwind
? resumable->suspendOffset()
: resumable->resumeFromAwaitOffset()
);
EventHook::FunctionResumeAwait(enterFnAr, EventHook::Source::Asio);
}
void ExecutionContext::resumeAsyncFunc(Resumable* resumable,
ObjectData* freeObj,
const TypedValue awaitResult) {
assertx(regState() == VMRegState::CLEAN);
SCOPE_EXIT { assertx(regState() == VMRegState::CLEAN); };
auto fp = resumable->actRec();
if (RO::EvalEnableImplicitContext) {
*ImplicitContext::activeCtx = [&] {
if (!fp->func()->isGenerator()) return frame_afwh(fp)->m_implicitContext;
auto gen = frame_async_generator(fp);
return gen->getWaitHandle()->m_implicitContext;
}();
}
// We don't need to check for space for the ActRec (unlike generally
// in normal re-entry), because the ActRec isn't on the stack.
checkStack(vmStack(), fp->func(), 0);
TypedValue* savedSP = vmStack().top();
tvDup(awaitResult, *vmStack().allocC());
// decref after awaitResult is on the stack
decRefObj(freeObj);
pushVMState(savedSP);
SCOPE_EXIT { popVMState(); };
enterVM(fp, [&] {
exception_handler([&] {
prepareAsyncFuncEntry(fp, resumable, false);
const bool useJit = RID().getJit();
if (LIKELY(useJit && resumable->resumeAddr())) {
Stats::inc(Stats::VMEnter);
jit::enterTC(jit::JitResumeAddr::trans(resumable->resumeAddr()));
} else {
enterVMAtCurPC();
}
});
});
}
void ExecutionContext::resumeAsyncFuncThrow(Resumable* resumable,
ObjectData* freeObj,
ObjectData* exception) {
assertx(exception);
assertx(exception->instanceof(SystemLib::s_ThrowableClass));
assertx(regState() == VMRegState::CLEAN);
SCOPE_EXIT { assertx(regState() == VMRegState::CLEAN); };
auto fp = resumable->actRec();
if (RO::EvalEnableImplicitContext) {
*ImplicitContext::activeCtx = [&] {
if (!fp->func()->isGenerator()) return frame_afwh(fp)->m_implicitContext;
auto gen = frame_async_generator(fp);
return gen->getWaitHandle()->m_implicitContext;
}();
}
checkStack(vmStack(), fp->func(), 0);
// decref after we hold reference to the exception
Object e(exception);
decRefObj(freeObj);
pushVMState(vmStack().top());
SCOPE_EXIT { popVMState(); };
enterVM(fp, [&] {
DEBUG_ONLY auto const success = exception_handler([&] {
prepareAsyncFuncEntry(fp, resumable, true);
});
// Function entry may fail only with a C++ exception thrown by the event
// hook, which would be rethrown by the exception_handler() after unwinding
// the current frame.
assertx(success);
unwindVM(exception);
e.reset();
});
}
ActRec* ExecutionContext::getPrevVMState(const ActRec* fp,
Offset* prevPc /* = NULL */,
TypedValue** prevSp /* = NULL */,
bool* fromVMEntry /* = NULL */,
jit::TCA* jitReturnAddr /* = NULL */) {
if (fp == nullptr) {
return nullptr;
}
ActRec* prevFp = fp->sfp();
if (LIKELY(prevFp != nullptr)) {
if (prevSp) {
if (UNLIKELY(isResumed(fp))) {
assertx(fp->func()->isGenerator());
*prevSp = (TypedValue*)prevFp - prevFp->func()->numSlotsInFrame();
} else {
*prevSp = (TypedValue*)(fp + 1);
}
}
if (prevPc) *prevPc = fp->callOffset();
if (fromVMEntry) *fromVMEntry = false;
if (jitReturnAddr) *jitReturnAddr = (jit::TCA)fp->m_savedRip;
return prevFp;
}
// Linear search from end of m_nestedVMs. In practice, we're probably
// looking for something recently pushed.
int i = m_nestedVMs.size() - 1;
ActRec* firstAR = vmFirstAR();
while (i >= 0 && firstAR != fp) {
firstAR = m_nestedVMs[i--].firstAR;
}
if (i == -1) return nullptr;
const VMState& vmstate = m_nestedVMs[i];
prevFp = vmstate.fp;
assertx(prevFp);
assertx(prevFp->func()->unit());
if (prevSp) *prevSp = vmstate.sp;
if (prevPc) {
*prevPc = prevFp->func()->offsetOf(vmstate.pc);
}
if (fromVMEntry) *fromVMEntry = true;
if (jitReturnAddr) *jitReturnAddr = vmstate.jitReturnAddr;
return prevFp;
}
Variant ExecutionContext::getEvaledArg(const StringData* val,
const String& namespacedName,
const Unit* funcUnit) {
auto key = StrNR(val);
if (m_evaledArgs.get()) {
auto const arg = m_evaledArgs.get()->get(key);
if (arg.is_init()) return Variant::wrap(arg);
}
String code;
String funcName;
int pos = namespacedName.rfind('\\');
if (pos != -1) {
auto ns = namespacedName.substr(0, pos);
code = s_hh + s_namespace + ns + s_curly_start + s_function_start +
s_evaluate_default_argument + s_function_middle + key +
s_semicolon + s_curly_end + s_curly_end;
funcName = ns + "\\" + s_evaluate_default_argument;
} else {
code = s_hh + s_function_start + s_evaluate_default_argument +
s_function_middle + key + s_semicolon + s_curly_end;
funcName = s_evaluate_default_argument;
}
Unit* unit = compileEvalString(code.get());
// This exception will be caught by the caller and proper error message
// will be emitted
if (unit->getFatalInfo()) throw Exception("Parse error");
assertx(unit != nullptr);
unit->setInterpretOnly();
// The evaluate_default_argument function should be the last one
assertx(unit->funcs().size() >= 1);
auto func = *(unit->funcs().end() - 1);
assertx(func->name()->equal(funcName.get()) &&
"We expecting the evaluate_default_argument func");
// Default arg values are not currently allowed to depend on class context.
auto v = Variant::attach(
g_context->invokeFuncFew(func, nullptr, 0, nullptr,
RuntimeCoeffects::defaults(), true, true)
);
m_evaledArgs.set(key, *v.asTypedValue());
return Variant::wrap(m_evaledArgs.lookup(key));
}
void ExecutionContext::recordLastError(const Exception& e, int errnum) {
m_lastError = String(e.getMessage());
m_lastErrorNum = errnum;
m_lastErrorPath = String::attach(getContainingFileName());
m_lastErrorLine = getLine();
if (auto const ee = dynamic_cast<const ExtendedException*>(&e)) {
m_lastErrorPath = ee->getFileAndLine().first;
m_lastErrorLine = ee->getFileAndLine().second;
}
}
void ExecutionContext::clearLastError() {
m_lastError = String();
m_lastErrorNum = 0;
m_lastErrorPath = staticEmptyString();
m_lastErrorLine = 0;
}
void ExecutionContext::enqueueAPCDeferredExpire(const String& key) {
auto keyStr = key.get();
keyStr->incRefCount();
m_apcDeferredExpire.push_back(keyStr);
}
void ExecutionContext::enqueueAPCHandle(APCHandle* handle, size_t size) {
assertx(handle->isUncounted());
m_apcHandles.push_back(handle);
m_apcMemSize += size;
}
// Treadmill solution for the SharedVariant memory management
namespace {
struct FreedAPCHandle {
explicit FreedAPCHandle(std::vector<APCHandle*>&& shandles, size_t size)
: m_memSize(size), m_apcHandles(std::move(shandles))
{}
void operator()() {
for (auto handle : m_apcHandles) {
APCTypedValue::fromHandle(handle)->deleteUncounted();
}
APCStats::getAPCStats().removePendingDelete(m_memSize);
}
private:
size_t m_memSize;
std::vector<APCHandle*> m_apcHandles;
};
}
void ExecutionContext::manageAPCHandle() {
assertx(apcExtension::UseUncounted || m_apcHandles.size() == 0);
if (m_apcHandles.size() > 0) {
Treadmill::enqueue(
FreedAPCHandle(std::move(m_apcHandles), m_apcMemSize)
);
APCStats::getAPCStats().addPendingDelete(m_apcMemSize);
}
}
ExecutionContext::EvaluationResult
ExecutionContext::evalPHPDebugger(StringData* code, int frame) {
// The code has "<?hh" prepended already
auto unit = compile_debugger_string(code->data(), code->size(),
getRepoOptionsForFrame(frame));
if (unit == nullptr) {
raise_error("Syntax error");
return {true, init_null_variant, "Syntax error"};
}
return evalPHPDebugger(unit, frame);
}
const StaticString
s_DebuggerMainAttr("__DebuggerMain"),
s_debuggerThis("__debugger$this"),
s_uninitClsName("__uninitSentinel");
ExecutionContext::EvaluationResult
ExecutionContext::evalPHPDebugger(Unit* unit, int frame) {
always_assert(!RuntimeOption::RepoAuthoritative);
// Do not JIT this unit, we are using it exactly once.
unit->setInterpretOnly();
VMRegAnchor _;
auto fp = getFrameAtDepthForDebuggerUnsafe(frame);
// Continue walking up the stack until we find a frame that can have
// a variable environment context attached to it, or we run out out frames.
while (fp && (fp->skipFrame() || fp->isInlined())) {
fp = getPrevVMStateSkipFrame(fp);
}
if (fp) phpDebuggerEvalHook(fp->func());
const static StaticString s_cppException("Hit an exception");
const static StaticString s_phpException("Hit a php exception");
const static StaticString s_exit("Hit exit");
const static StaticString s_fatal("Hit fatal");
std::ostringstream errorString;
std::string stack;
// Find a suitable PC to use when switching to the target frame. If the target
// is the current frame, this is just vmpc(). For other cases, this will
// generally be the address of a call from that frame's function. If we can't
// find the target frame (because it lies deeper in the stack), then just use
// the target frame's func's entry point.
auto const findSuitablePC = [this](const ActRec* target){
if (auto fp = vmfp()) {
if (fp == target) return vmpc();
while (true) {
auto prevFp = getPrevVMState(fp);
if (!prevFp) break;
if (prevFp == target) {
return prevFp->func()->entry() + fp->callOffset();
}
fp = prevFp;
}
}
return target->func()->entry();
};
try {
// We also need to change vmpc() to match, since we assert in a few places
// that the vmpc() lies within vmfp()'s code.
auto savedFP = vmfp();
auto savedPC = vmpc();
if (fp) {
vmpc() = findSuitablePC(fp);
vmfp() = fp;
}
SCOPE_EXIT { vmpc() = savedPC; vmfp() = savedFP; };
unit->merge();
enum VarAction { StoreFrame, StoreEnv };
auto const uninit_cls = Class::load(s_uninitClsName.get());
auto& env = m_debuggerEnv;
auto const ctx = fp ? fp->func()->cls() : nullptr;
auto const f = [&] () -> Func* {
for (auto orig_f : unit->funcs()) {
if (orig_f->userAttributes().count(s_DebuggerMainAttr.get())) {
auto const f = ctx ? orig_f->clone(ctx) : orig_f;
if (ctx) {
f->setBaseCls(ctx);
if (fp && fp->hasThis()) {
f->setAttrs(Attr(f->attrs() & ~AttrStatic));
} else if (fp && fp->hasClass()) {
f->setAttrs(Attr(f->attrs() | AttrStatic));
}
}
assertx(f->numParams() >= 1 && f->numParams() == f->numInOutParams());
return f;
}
}
return nullptr;
}();
always_assert(f);
VecInit args{f->numParams()};
std::vector<VarAction> actions{f->numParams(), StoreEnv};
std::vector<Id> frameIds;
frameIds.resize(f->numParams(), 0);
auto const appendUninit = [&] {
args.append(make_tv<KindOfObject>(Object{uninit_cls}.detach()));
};
for (Id id = 0; id < f->numParams() - 1; id++) {
assertx(id < f->numNamedLocals());
assertx(f->params()[id].isInOut());
if (f->localVarName(id)->equal(s_debuggerThis.get()) &&
ctx && fp->hasThis()) {
args.append(make_tv<KindOfObject>(fp->getThis()));
continue;
}
if (fp) {
auto const idx = fp->func()->lookupVarId(f->localVarName(id));
if (idx != kInvalidId) {
Variant var{tvAsCVarRef(*frame_local(fp, idx))};
if (var.isInitialized()) args.append(var);
else appendUninit();
actions[id] = StoreFrame;
frameIds[id] = idx;
continue;
}
}
auto const val = env.lookup(StrNR{f->localVarName(id)});
if (val.is_init()) args.append(val);
else appendUninit();
}
args.append(make_tv<KindOfNull>()); // $__debugger_exn$output
auto const obj = ctx && fp->hasThis() ? fp->getThis() : nullptr;
auto const cls = ctx && fp->hasClass() ? fp->getClass() : nullptr;
auto const arr_tv = invokeFunc(f, args.toArray(), obj, cls,
RuntimeCoeffects::defaults(), false);
assertx(isArrayLikeType(type(arr_tv)));
assertx(val(arr_tv).parr->size() == f->numParams() + 1);
Array arr = Array::attach(val(arr_tv).parr);
for (Id id = 0; id < f->numParams() - 1; id++) {
auto const tv = arr.lookup(id + 1);
if (isObjectType(type(tv)) &&
val(tv).pobj->instanceof(uninit_cls)) {
switch (actions[id]) {
case StoreFrame:
variant_ref{frame_local(fp, frameIds[id])}.unset();
break;
case StoreEnv:
env.remove(StrNR{f->localVarName(id)});
break;
}
continue;
}
switch (actions[id]) {
case StoreFrame:
variant_ref{frame_local(fp, frameIds[id])} = arr[id + 1];
break;
case StoreEnv:
env.set(StrNR{f->localVarName(id)}, tv);
break;
}
}
auto const exn = arr[int64_t(f->numParams())];
if (exn.isObject()) {
assertx(exn.toObject().instanceof("Throwable"));
throw_object(exn.toObject());
}
assertx(exn.isNull());
return {false, arr[0], ""};
} catch (FatalErrorException& e) {
errorString << s_fatal.data();
errorString << " : ";
errorString << e.getMessage().c_str();
errorString << "\n";
stack = ExtendedLogger::StringOfStackTrace(e.getBacktrace());
} catch (ExitException& e) {
errorString << s_exit.data();
errorString << " : ";
errorString << *rl_exit_code;
} catch (Eval::DebuggerException& e) {
} catch (Exception& e) {
errorString << s_cppException.data();
errorString << " : ";
errorString << e.getMessage().c_str();
ExtendedException* ee = dynamic_cast<ExtendedException*>(&e);
if (ee) {
errorString << "\n";
stack = ExtendedLogger::StringOfStackTrace(ee->getBacktrace());
}
} catch (Object &e) {
errorString << s_phpException.data();
errorString << " : ";
try {
errorString << throwable_to_string(e.get()).data();
} catch (...) {
errorString << e->getVMClass()->name()->data();
}
} catch (...) {
errorString << s_cppException.data();
}
auto errorStr = errorString.str();
g_context->write(errorStr);
if (!stack.empty()) {
g_context->write(stack.c_str());
}
return {true, init_null_variant, errorStr};
}
const StaticString s_include("include");
void ExecutionContext::enterDebuggerDummyEnv() {
static Unit* s_debuggerDummy = compile_debugger_string(
"<?hh", 4, getRepoOptionsForDebuggerEval()
);
// Ensure that the VM stack is completely empty (vmfp() should be null)
// and that we're not in a nested VM (reentrancy)
assertx(vmfp() == nullptr);
assertx(m_nestedVMs.size() == 0);
assertx(m_nesting == 0);
assertx(vmStack().count() == 0);
ActRec* ar = vmStack().allocA();
ar->setFunc(s_debuggerDummy->lookupFuncId(0));
assertx(ar->func() && ar->func()->name()->equal(s_include.get()));
for (int i = 0; i < ar->func()->numLocals(); ++i) vmStack().pushInt(1);
ar->trashThis();
ar->setReturnVMExit();
vmfp() = ar;
vmpc() = ar->func()->entry();
vmFirstAR() = ar;
}
void ExecutionContext::exitDebuggerDummyEnv() {
assertx(m_globalNVTable);
// Ensure that vmfp() is valid
assertx(vmfp() != nullptr);
// Ensure that vmfp() points to the only frame on the call stack.
// In other words, make sure there are no VM frames directly below
// this one and that we are not in a nested VM (reentrancy)
assertx(!vmfp()->sfp());
assertx(m_nestedVMs.size() == 0);
assertx(m_nesting == 0);
// Teardown the frame we erected by enterDebuggerDummyEnv()
const Func* func = vmfp()->func();
try {
vmfp()->setLocalsDecRefd();
frame_free_locals_no_hook(vmfp());
} catch (...) {}
vmStack().ndiscard(func->numSlotsInFrame());
vmStack().discardAR();
// After tearing down this frame, the VM stack should be completely empty
assertx(vmStack().count() == 0);
vmfp() = nullptr;
vmpc() = nullptr;
}
ThrowAllErrorsSetter::ThrowAllErrorsSetter() {
m_throwAllErrors = g_context->getThrowAllErrors();
g_context->setThrowAllErrors(true);
}
ThrowAllErrorsSetter::~ThrowAllErrorsSetter() {
g_context->setThrowAllErrors(m_throwAllErrors);
}
}