Add sub-controls for Hack array compat runtime checks
[hiphop-php.git] / hphp / runtime / base / backtrace.cpp
blob883adbce6b76890f737b525f8887011070d343f9
1 /*
2 +----------------------------------------------------------------------+
3 | HipHop for PHP |
4 +----------------------------------------------------------------------+
5 | Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com) |
6 +----------------------------------------------------------------------+
7 | This source file is subject to version 3.01 of the PHP license, |
8 | that is bundled with this package in the file LICENSE, and is |
9 | available through the world-wide-web at the following url: |
10 | http://www.php.net/license/3_01.txt |
11 | If you did not receive a copy of the PHP license and are unable to |
12 | obtain it through the world-wide-web, please send a note to |
13 | license@php.net so we can mail you a copy immediately. |
14 +----------------------------------------------------------------------+
16 #include "hphp/runtime/base/backtrace.h"
18 #include "hphp/runtime/base/array-init.h"
19 #include "hphp/runtime/base/execution-context.h"
20 #include "hphp/runtime/base/rds-header.h"
21 #include "hphp/runtime/base/runtime-option.h"
22 #include "hphp/runtime/ext/asio/ext_async-generator-wait-handle.h"
23 #include "hphp/runtime/vm/act-rec.h"
24 #include "hphp/runtime/vm/bytecode.h"
25 #include "hphp/runtime/vm/func.h"
26 #include "hphp/runtime/vm/runtime.h"
27 #include "hphp/runtime/vm/vm-regs.h"
28 #include "hphp/runtime/vm/jit/tc.h"
29 #include "hphp/runtime/vm/jit/unique-stubs.h"
30 #include "hphp/runtime/vm/treadmill.h"
31 #include "hphp/util/concurrent-scalable-cache.h"
32 #include "hphp/util/struct-log.h"
34 #include <folly/small_vector.h>
36 namespace HPHP {
38 const StaticString
39 s_file("file"),
40 s_line("line"),
41 s_function("function"),
42 s_args("args"),
43 s_class("class"),
44 s_object("object"),
45 s_type("type"),
46 s_include("include"),
47 s_main("{main}"),
48 s_metadata("metadata"),
49 s_86metadata("86metadata"),
50 s_arrow("->"),
51 s_double_colon("::");
53 static c_WaitableWaitHandle* getParentWH(
54 c_WaitableWaitHandle* wh,
55 context_idx_t contextIdx,
56 folly::small_vector<c_WaitableWaitHandle*, 64>& visitedWHs) {
57 assertx(!wh->isFinished());
58 auto p = wh->getParentChain().firstInContext(contextIdx);
59 if (p == nullptr ||
60 UNLIKELY(std::find(visitedWHs.begin(), visitedWHs.end(), p)
61 != visitedWHs.end())) {
62 // If the parent exists in our backtrace, it means we have detected a
63 // cycle. We well fall back to savedFP in that case.
64 return nullptr;
66 visitedWHs.push_back(p);
67 return p;
70 // walks up the wait handle dependency chain, until it finds activation record
71 static ActRec* getActRecFromWaitHandle(
72 c_WaitableWaitHandle* currentWaitHandle,
73 context_idx_t contextIdx,
74 Offset* prevPc,
75 folly::small_vector<c_WaitableWaitHandle*, 64>& visitedWHs) {
76 while (currentWaitHandle != nullptr) {
77 assertx(!currentWaitHandle->isFinished());
78 if (currentWaitHandle->getKind() == c_WaitHandle::Kind::AsyncFunction) {
79 auto resumable = currentWaitHandle->asAsyncFunction()->resumable();
80 *prevPc = resumable->resumeOffset();
81 return resumable->actRec();
83 if (currentWaitHandle->getKind() == c_WaitHandle::Kind::AsyncGenerator) {
84 auto resumable = currentWaitHandle->asAsyncGenerator()->resumable();
85 *prevPc = resumable->resumeOffset();
86 return resumable->actRec();
89 currentWaitHandle = getParentWH(currentWaitHandle, contextIdx, visitedWHs);
91 *prevPc = 0;
92 return AsioSession::Get()->getContext(contextIdx)->getSavedFP();
95 static ActRec* getPrevActRec(
96 const ActRec* fp, Offset* prevPc,
97 folly::small_vector<c_WaitableWaitHandle*, 64>& visitedWHs) {
98 c_WaitableWaitHandle* currentWaitHandle = nullptr;
100 if (fp && fp->func() && fp->resumed()) {
101 if (fp->func()->isAsyncFunction()) {
102 currentWaitHandle = frame_afwh(fp);
103 } else if (fp->func()->isAsyncGenerator()) {
104 auto const gen = frame_async_generator(fp);
105 if (gen->isRunning() && !gen->isEagerlyExecuted()) {
106 currentWaitHandle = gen->getWaitHandle();
111 if (currentWaitHandle != nullptr) {
112 if (currentWaitHandle->isFinished()) {
114 * It's possible in very rare cases (it will return a truncated stack):
115 * 1) async function which WaitHandle is not referenced by anything
116 * else finishes
117 * 2) its return value is an object with destructor
118 * 3) this destructor gets called as part of destruction of the
119 * WaitHandleobject, which happens right before FP is adjusted
121 return nullptr;
124 auto const contextIdx = currentWaitHandle->getContextIdx();
126 currentWaitHandle = getParentWH(currentWaitHandle, contextIdx, visitedWHs);
127 return getActRecFromWaitHandle(
128 currentWaitHandle, contextIdx, prevPc, visitedWHs);
131 return g_context->getPrevVMState(fp, prevPc);
134 // wrapper around getActRecFromWaitHandle, which does some extra validation
135 static ActRec* getActRecFromWaitHandleWrapper(
136 c_WaitableWaitHandle* currentWaitHandle, Offset* prevPc,
137 folly::small_vector<c_WaitableWaitHandle*, 64>& visitedWHs) {
138 if (currentWaitHandle->isFinished()) {
139 return nullptr;
142 auto const contextIdx = currentWaitHandle->getContextIdx();
143 if (contextIdx <= 0) {
144 return nullptr;
147 return getActRecFromWaitHandle(
148 currentWaitHandle, contextIdx, prevPc, visitedWHs);
151 Array createBacktrace(const BacktraceArgs& btArgs) {
152 if (btArgs.isCompact()) {
153 return createCompactBacktrace()->extract();
156 auto bt = Array::Create();
157 folly::small_vector<c_WaitableWaitHandle*, 64> visitedWHs;
159 // If there is a parser frame, put it at the beginning of the backtrace.
160 if (btArgs.m_parserFrame) {
161 bt.append(
162 make_map_array(
163 s_file, btArgs.m_parserFrame->filename,
164 s_line, btArgs.m_parserFrame->lineNumber
169 int depth = 0;
170 ActRec* fp = nullptr;
171 Offset pc = 0;
173 if (btArgs.m_fromWaitHandle) {
174 fp = getActRecFromWaitHandleWrapper(
175 btArgs.m_fromWaitHandle, &pc, visitedWHs);
176 // no frames found, we are done
177 if (!fp) return bt;
178 } else {
179 VMRegAnchor _;
180 // If there are no VM frames, we're done.
181 if (!rds::header() || !vmfp()) return bt;
183 // Get the fp and pc of the top frame (possibly skipping one frame).
185 if (btArgs.m_skipTop) {
186 fp = getPrevActRec(vmfp(), &pc, visitedWHs);
187 // We skipped over the only VM frame, we're done.
188 if (!fp) return bt;
189 } else {
190 fp = vmfp();
191 auto const unit = fp->func()->unit();
192 assert(unit);
193 pc = unit->offsetOf(vmpc());
196 if (btArgs.m_skipInlined && RuntimeOption::EvalJit) {
197 while (fp && (jit::TCA)fp->m_savedRip == jit::tc::ustubs().retInlHelper) {
198 fp = getPrevActRec(fp, &pc, visitedWHs);
200 if (!fp) return bt;
204 // Handle the top frame.
205 if (btArgs.m_withSelf) {
206 // Builtins don't have a file and line number, so find the first user frame
207 auto curFp = fp;
208 auto curPc = pc;
209 while (curFp && curFp->func()->isBuiltin()) {
210 curFp = g_context->getPrevVMState(curFp, &curPc);
212 if (curFp) {
213 auto const unit = curFp->func()->unit();
214 assert(unit);
215 auto const filename = curFp->func()->filename();
217 ArrayInit frame(btArgs.m_parserFrame ? 4 : 2, ArrayInit::Map{});
218 frame.set(s_file, Variant{const_cast<StringData*>(filename)});
219 frame.set(s_line, unit->getLineNumber(curPc));
220 if (btArgs.m_parserFrame) {
221 frame.set(s_function, s_include);
222 frame.set(s_args, Array::Create(btArgs.m_parserFrame->filename));
224 bt.append(frame.toVariant());
225 depth++;
229 // Handle the subsequent VM frames.
230 Offset prevPc = 0;
231 for (auto prevFp = getPrevActRec(fp, &prevPc, visitedWHs);
232 fp != nullptr && (btArgs.m_limit == 0 || depth < btArgs.m_limit);
233 fp = prevFp, pc = prevPc,
234 prevFp = getPrevActRec(fp, &prevPc, visitedWHs)) {
235 // Do not capture frame for HPHP only functions.
236 if (fp->func()->isNoInjection()) continue;
238 ArrayInit frame(8, ArrayInit::Map{});
240 auto const curUnit = fp->func()->unit();
241 auto const curOp = curUnit->getOp(pc);
242 auto const isReturning =
243 curOp == Op::RetC || curOp == Op::RetV || curOp == Op::RetM ||
244 curOp == Op::CreateCont || curOp == Op::Await ||
245 fp->localsDecRefd();
247 // Builtins and generators don't have a file and line number
248 if (prevFp && !prevFp->func()->isBuiltin()) {
249 auto const prevUnit = prevFp->func()->unit();
250 auto prevFile = prevUnit->filepath();
251 if (prevFp->func()->originalFilename()) {
252 prevFile = prevFp->func()->originalFilename();
254 assert(prevFile);
255 frame.set(s_file, Variant{const_cast<StringData*>(prevFile)});
257 // In the normal method case, the "saved pc" for line number printing is
258 // pointing at the cell conversion (Unbox/Pop) instruction, not the call
259 // itself. For multi-line calls, this instruction is associated with the
260 // subsequent line which results in an off-by-n. We're subtracting one
261 // in order to look up the line associated with the FCall/FCallArray
262 // instruction. Exception handling and the other opcodes (ex. BoxR)
263 // already do the right thing. The emitter associates object access with
264 // the subsequent expression and this would be difficult to modify.
265 auto const opAtPrevPc = prevUnit->getOp(prevPc);
266 Offset pcAdjust = 0;
267 if (opAtPrevPc == Op::PopR ||
268 opAtPrevPc == Op::UnboxR ||
269 opAtPrevPc == Op::UnboxRNop) {
270 pcAdjust = 1;
272 frame.set(s_line,
273 prevFp->func()->unit()->getLineNumber(prevPc - pcAdjust));
276 // Check for include.
277 String funcname{const_cast<StringData*>(fp->func()->displayName())};
278 if (fp->func()->isClosureBody()) {
279 // Strip the file hash from the closure name.
280 String fullName{const_cast<StringData*>(fp->func()->baseCls()->name())};
281 funcname = fullName.substr(0, fullName.find(';'));
284 // Check for pseudomain.
285 if (funcname.empty()) {
286 if (!prevFp && !btArgs.m_withPseudoMain) continue;
287 else if (!prevFp) funcname = s_main;
288 else funcname = s_include;
291 frame.set(s_function, funcname);
293 if (!funcname.same(s_include)) {
294 // Closures have an m_this but they aren't in object context.
295 auto ctx = arGetContextClass(fp);
296 if (ctx != nullptr && !fp->func()->isClosureBody()) {
297 frame.set(s_class, Variant{const_cast<StringData*>(ctx->name())});
298 if (!isReturning && fp->hasThis()) {
299 if (btArgs.m_withThis) {
300 frame.set(s_object, Object(fp->getThis()));
302 frame.set(s_type, s_arrow);
303 } else {
304 frame.set(s_type, s_double_colon);
309 bool const mayUseVV = fp->func()->attrs() & AttrMayUseVV;
311 auto const withNames = btArgs.m_withArgNames;
312 auto const withValues = btArgs.m_withArgValues;
313 if ((!btArgs.m_withArgNames && !btArgs.m_withArgValues) ||
314 !RuntimeOption::EnableArgsInBacktraces) {
315 // do nothing
316 } else if (funcname.same(s_include)) {
317 auto filepath = const_cast<StringData*>(curUnit->filepath());
318 frame.set(s_args, make_packed_array(filepath));
319 } else if (isReturning) {
320 frame.set(s_args, empty_array());
321 } else {
322 auto args = Array::Create();
323 auto const nparams = fp->func()->numNonVariadicParams();
324 auto const nargs = fp->numArgs();
325 auto const nformals = std::min<int>(nparams, nargs);
327 if (UNLIKELY(mayUseVV) &&
328 UNLIKELY(fp->hasVarEnv() && fp->getVarEnv()->getFP() != fp)) {
329 // VarEnv is attached to eval or debugger frame, other than the current
330 // frame. Access locals thru VarEnv.
331 auto varEnv = fp->getVarEnv();
332 auto func = fp->func();
333 for (int i = 0; i < nformals; i++) {
334 auto const argname = func->localVarName(i);
335 auto const tv = varEnv->lookup(argname);
337 auto val = init_null();
338 if (tv != nullptr) { // the variable hasn't been unset
339 val = withValues ? tvAsVariant(tv) : "";
342 if (withNames) {
343 args.set(String(const_cast<StringData*>(argname)), val);
344 } else {
345 args.append(val);
348 } else {
349 for (int i = 0; i < nformals; i++) {
350 Variant val = withValues ? tvAsVariant(frame_local(fp, i)) : "";
352 if (withNames) {
353 auto const argname = fp->func()->localVarName(i);
354 args.set(String(const_cast<StringData*>(argname)), val);
355 } else {
356 args.append(val);
361 // Builtin extra args are not stored in varenv.
362 if (UNLIKELY(mayUseVV) && nargs > nparams && fp->hasExtraArgs()) {
363 for (int i = nparams; i < nargs; i++) {
364 auto arg = fp->getExtraArg(i - nparams);
365 if (arg->m_type == KindOfUninit) {
366 args.append(init_null());
367 } else {
368 args.append(tvAsVariant(arg));
372 frame.set(s_args, args);
375 if (btArgs.m_withMetadata && !isReturning) {
376 if (UNLIKELY(mayUseVV) && UNLIKELY(fp->hasVarEnv())) {
377 auto tv = fp->getVarEnv()->lookup(s_86metadata.get());
378 if (tv != nullptr && tv->m_type != KindOfUninit) {
379 frame.set(s_metadata, tvAsVariant(tv));
381 } else {
382 auto local = fp->func()->lookupVarId(s_86metadata.get());
383 if (local != kInvalidId) {
384 auto tv = frame_local(fp, local);
385 if (tv->m_type != KindOfUninit) {
386 frame.set(s_metadata, tvAsVariant(tv));
392 bt.append(frame.toVariant());
393 depth++;
396 return bt;
399 void addBacktraceToStructLog(const Array& bt, StructuredLogEntry& cols) {
400 std::set<std::string> strings;
401 std::vector<folly::StringPiece> files;
402 std::vector<folly::StringPiece> functions;
403 std::vector<folly::StringPiece> lines;
404 auto addString = [&] (std::string&& s) -> folly::StringPiece {
405 return *strings.insert(std::move(s)).first;
407 auto addVariant = [&] (const Variant& v) -> folly::StringPiece {
408 if (v.isString()) return v.toCStrRef().slice();
409 return addString(v.toString().toCppString());
411 for (ArrayIter it(bt.get()); it; ++it) {
412 Array frame = it.second().toArray();
413 files.emplace_back(addVariant(frame[s_file]));
414 if (frame.exists(s_class)) {
415 functions.emplace_back(
416 addString(folly::sformat("{}{}{}",
417 frame[s_class].toString().data(),
418 frame[s_type].toString().data(),
419 frame[s_function].toString().data()
422 } else {
423 functions.emplace_back(addVariant(frame[s_function]));
425 lines.emplace_back(addVariant(frame[s_line]));
427 cols.setVec("php_files", files);
428 cols.setVec("php_functions", functions);
429 cols.setVec("php_lines", lines);
432 template<class L>
433 static void walkStack(L func) {
434 VMRegAnchor _;
435 folly::small_vector<c_WaitableWaitHandle*, 64> visitedWHs;
436 ActRec* fp = vmfp();
438 // If there are no VM frames, we're done.
439 if (!fp || !rds::header()) return;
441 // Handle the subsequent VM frames.
442 Offset prevPc = 0;
443 for (; fp != nullptr; fp = getPrevActRec(fp, &prevPc, visitedWHs)) {
445 // Do not capture frame for HPHP only functions.
446 if (fp->func()->isNoInjection()) continue;
448 func(fp, prevPc);
452 int64_t createBacktraceHash() {
453 // Settings constants before looping
454 int64_t hash = 0x9e3779b9;
455 Unit* prev_unit = nullptr;
457 walkStack([&] (ActRec* fp, Offset) {
458 auto const curFunc = fp->func();
459 auto const curUnit = curFunc->unit();
461 // Only do a filehash if the file changed. It is very common
462 // to see sequences of calls within the same file
463 // File paths are already hashed, and the hash bits are random enough
464 // That allows us to do a faster combination of hashes using a known
465 // implementation (boost::hash_combine)
466 if (prev_unit != curUnit) {
467 prev_unit = curUnit;
468 auto filehash = curUnit->filepath()->hash();
469 hash ^= filehash + 0x9e3779b9 + (hash << 6) + (hash >> 2);
472 // Function names are already hashed, and the hash bits are random enough
473 // That allows us to do a faster combination of hashes using a known
474 // implementation (boost::hash_combine)
475 auto funchash = curFunc->fullName()->hash();
476 hash ^= funchash + 0x9e3779b9 + (hash << 6) + (hash >> 2);
479 return hash;
482 req::ptr<CompactTrace> createCompactBacktrace() {
483 auto ret = req::make<CompactTrace>();
484 walkStack([&] (ActRec* fp, Offset prevPc) { ret->insert(fp, prevPc); });
485 return ret;
488 namespace {
490 struct CTKHasher final {
491 uint64_t hash(const CompactTrace::Key& k) const { return k.m_hash; }
492 bool equal(const CompactTrace::Key& k1, const CompactTrace::Key& k2) const;
495 struct CacheDeleter final {
496 void operator()(ArrayData* ad) const {
497 if (!ad->isUncounted()) return;
498 Treadmill::enqueue([ad] {
499 PackedArray::ReleaseUncounted(ad);
504 using CachedArray = std::shared_ptr<ArrayData>;
505 using Cache = ConcurrentScalableCache<CompactTrace::Key,CachedArray,CTKHasher>;
506 Cache s_cache(1024);
508 bool CTKHasher::equal(
509 const CompactTrace::Key& k1,
510 const CompactTrace::Key& k2
511 ) const {
512 if (k1.m_hash != k2.m_hash || k1.m_frames.size() != k2.m_frames.size()) {
513 return false;
515 for (int i = 0; i < k1.m_frames.size(); ++i) {
516 auto& a = k1.m_frames[i];
517 auto& b = k2.m_frames[i];
518 if (a.func != b.func || a.prevPcAndHasThis != b.prevPcAndHasThis) {
519 return false;
522 return true;
526 IMPLEMENT_RESOURCE_ALLOCATION(CompactTrace)
528 void CompactTrace::Key::insert(const ActRec* fp, int32_t prevPc) {
529 auto const funcHash = use_lowptr
530 ? (uintptr_t)fp->func() << 32
531 : (uintptr_t)fp->func();
532 m_hash ^= funcHash + 0x9e3779b9 + (m_hash << 6) + (m_hash >> 2);
533 m_hash ^= prevPc + 0x9e3779b9 + (m_hash << 6) + (m_hash >> 2);
535 auto const curUnit = fp->func()->unit();
536 auto const curOp = curUnit->getOp(prevPc);
537 auto const isReturning =
538 curOp == Op::RetC || curOp == Op::RetV || curOp == Op::RetM ||
539 curOp == Op::CreateCont || curOp == Op::Await ||
540 fp->localsDecRefd();
541 m_frames.push_back(Frame{
542 fp->func(),
543 prevPc,
544 !isReturning && arGetContextClass(fp) && fp->hasThis()
548 Array CompactTrace::Key::extract() const {
549 PackedArrayInit aInit(m_frames.size(), CheckAllocation{});
550 for (int idx = 0; idx < m_frames.size(); ++idx) {
551 auto const prev = idx < m_frames.size() - 1 ? &m_frames[idx + 1] : nullptr;
552 ArrayInit frame(6, ArrayInit::Map{});
553 if (prev && !prev->func->isBuiltin()) {
554 auto const prevUnit = prev->func->unit();
555 auto prevFile = prevUnit->filepath();
556 if (prev->func->originalFilename()) {
557 prevFile = prev->func->originalFilename();
560 auto const prevPc = prev->prevPc;
561 auto const opAtPrevPc = prevUnit->getOp(prevPc);
562 Offset pcAdjust = 0;
563 if (opAtPrevPc == Op::PopR ||
564 opAtPrevPc == Op::UnboxR ||
565 opAtPrevPc == Op::UnboxRNop) {
566 pcAdjust = 1;
568 frame.set(s_file, StrNR(prevFile).asString());
569 frame.set(s_line, prevUnit->getLineNumber(prevPc - pcAdjust));
572 auto const f = m_frames[idx].func;
574 // Check for include.
575 String funcname{const_cast<StringData*>(f->displayName())};
576 if (f->isClosureBody()) {
577 // Strip the file hash from the closure name.
578 String fullName{const_cast<StringData*>(f->baseCls()->name())};
579 funcname = fullName.substr(0, fullName.find(';'));
582 // Check for pseudomain.
583 if (funcname.empty()) {
584 if (!prev) continue;
585 else funcname = s_include;
588 frame.set(s_function, funcname);
590 if (!funcname.same(s_include)) {
591 // Closures have an m_this but they aren't in object context.
592 auto ctx = m_frames[idx].func->cls();
593 if (ctx != nullptr && !f->isClosureBody()) {
594 frame.set(s_class, Variant{const_cast<StringData*>(ctx->name())});
595 if (m_frames[idx].hasThis) {
596 frame.set(s_type, s_arrow);
597 } else {
598 frame.set(s_type, s_double_colon);
601 } else {
602 auto filepath = const_cast<StringData*>(f->unit()->filepath());
603 frame.set(s_args, make_packed_array(filepath));
606 aInit.append(frame.toVariant());
609 return aInit.toArray();
612 Array CompactTrace::extract() const {
613 if (m_key.m_frames.size() == 1) return empty_array();
615 Cache::ConstAccessor acc;
616 if (s_cache.find(acc, m_key)) {
617 return Array(acc.get()->get());
620 auto arr = m_key.extract();
621 auto ins = CachedArray(
622 arr.get()->empty()
623 ? staticEmptyArray()
624 : PackedArray::MakeUncounted(arr.get()),
625 CacheDeleter()
627 if (!s_cache.insert(m_key, ins)) {
628 return arr;
630 return Array(ins.get());
633 } // HPHP