dart-sdk/runtime/vm/stack_trace.cc
Vyacheslav Egorov 46ac1f653f [vm] Treat Future.then(..., onError:...) as catch all handler
Commit a52f2b9 which reworked awaiter stack unwinding and its
integration with debugger introduced the following regression:
it stopped treating `Future` listeners which had both `onValue`
and `onError` callbacks as catch all exception handlers. Only
listners with `onError` callback (those created with
`Future.onError`) were treated as a catch all handler.

This meant that debugger started to treat exceptions in the
code below as uncaught:

```
Future<void> foo() {
  await 0;
  throw '';
}

await foo().then(..., onError: (e, st) {

});
```

This change fixes this regression by checking if
`FutureListener.state & stateCatchError != 0` instead of
more narrow `FutureListener.state == stateCatchError` which
only detects listeners which only catch errors but do not
handle values. The new predicate matches
`FutureListener.handlesError`.

This relands 38e0046cad
with a fix to ensure that we correctly detect `onError`
callbacks which simply forward to a suspended async
function. We do this by marking FutureListener's that originate
from `await` using a bit in the state.

Fixes https://github.com/dart-lang/sdk/issues/53334

TEST=service/pause_on_unhandled_async_exceptions{_zones,}_test

Fixed: 53334
CoreLibraryReviewExempt: No fundamental changes to _FutureListener implementation, just additional bit to detect that this listener originates from await
Change-Id: I90385fc619cbb52925e075dd5c7b171a31ca4cab
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/323481
Commit-Queue: Slava Egorov <vegorov@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
2023-08-31 11:47:54 +00:00

542 lines
20 KiB
C++

// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#include "vm/stack_trace.h"
#include "vm/dart_api_impl.h"
#include "vm/object_store.h"
#include "vm/stack_frame.h"
#include "vm/symbols.h"
namespace dart {
namespace {
// Keep in sync with:
// - sdk/lib/async/stream_controller.dart:_StreamController._STATE_SUBSCRIBED.
const intptr_t k_StreamController__STATE_SUBSCRIBED = 1;
// - sdk/lib/async/future_impl.dart:_FutureListener.stateThen.
const intptr_t k_FutureListener_stateThen = 1;
// - sdk/lib/async/future_impl.dart:_FutureListener.stateCatchError.
const intptr_t k_FutureListener_stateCatchError = 2;
// - sdk/lib/async/future_impl.dart:_FutureListener.stateWhenComplete.
const intptr_t k_FutureListener_stateWhenComplete = 8;
// - sdk/lib/async/future_impl.dart:_FutureListener.maskAwait.
const intptr_t k_FutureListener_maskAwait = 16;
bool WasPreviouslySuspended(const Function& function,
const Object& suspend_state_var) {
if (!suspend_state_var.IsSuspendState()) {
return false;
}
if (function.IsAsyncFunction()) {
// Error callback is set after both 'then' and 'error' callbacks are
// registered with the Zone. Callback registration may query
// stack trace and should still collect the synchronous stack trace.
return SuspendState::Cast(suspend_state_var).error_callback() !=
Object::null();
} else if (function.IsAsyncGenerator()) {
return true;
} else {
UNREACHABLE();
}
}
// Unwinder which starts by unwinding the synchronous portion of the stack
// until it reaches a frame which has an asynchronous awaiter (e.g. an
// activation of the async function which has a listener attached to the
// corresponding Future object) and then unwinds through the chain
// of awaiters.
class AsyncAwareStackUnwinder : public ValueObject {
public:
explicit AsyncAwareStackUnwinder(Thread* thread)
: zone_(thread->zone()),
sync_frames_(thread, StackFrameIterator::kNoCrossThreadIteration),
sync_frame_(nullptr),
awaiter_frame_{Closure::Handle(zone_), Object::Handle(zone_)},
closure_(Closure::Handle(zone_)),
code_(Code::Handle(zone_)),
context_(Context::Handle(zone_)),
function_(Function::Handle(zone_)),
parent_function_(Function::Handle(zone_)),
object_(Object::Handle(zone_)),
suspend_state_(SuspendState::Handle(zone_)),
controller_(Object::Handle(zone_)),
subscription_(Object::Handle(zone_)),
stream_iterator_(Object::Handle(zone_)),
async_lib_(Library::Handle(zone_, Library::AsyncLibrary())),
null_closure_(Closure::Handle(zone_)) {}
bool Unwind(intptr_t skip_frames,
std::function<void(const StackTraceUtils::Frame&)> handle_frame);
private:
bool HandleSynchronousFrame();
void UnwindAwaiterFrame();
// Returns either the `onData` or the Future awaiter.
ObjectPtr FindCallerInAsyncStarStreamController(
const Object& async_star_stream_controller);
void InitializeAwaiterFrameFromSuspendState();
void InitializeAwaiterFrameFromFutureListener(const Object& listener);
void UnwindToAwaiter();
// |frame.next| is a |_Future| instance. Unwind to the next frame.
void UnwindFrameToFutureListener();
// |frame.next| is an |_AsyncStarStreamController| instance corresponding to
// an async* function. Unwind to the next frame.
void UnwindFrameToStreamListener();
ObjectPtr GetReceiver() const;
#define USED_CLASS_LIST(V) \
V(_AsyncStarStreamController) \
V(_BufferingStreamSubscription) \
V(_Completer) \
V(_AsyncCompleter) \
V(_SyncCompleter) \
V(_ControllerSubscription) \
V(_Future) \
V(_FutureListener) \
V(_StreamController) \
V(_StreamIterator) \
V(_SyncStreamController)
enum ClassId {
#define DECLARE_CONSTANT(symbol) k##symbol,
USED_CLASS_LIST(DECLARE_CONSTANT)
#undef DECLARE_CONSTANT
};
#define USED_FIELD_LIST(V) \
V(_AsyncStarStreamController, controller) \
V(_BufferingStreamSubscription, _onData) \
V(_Completer, future) \
V(_Future, _resultOrListeners) \
V(_FutureListener, callback) \
V(_FutureListener, result) \
V(_FutureListener, state) \
V(_StreamController, _state) \
V(_StreamController, _varData) \
V(_StreamIterator, _hasValue) \
V(_StreamIterator, _stateData)
enum FieldId {
#define DECLARE_CONSTANT(class_symbol, field_symbol) \
k##class_symbol##_##field_symbol,
USED_FIELD_LIST(DECLARE_CONSTANT)
#undef DECLARE_CONSTANT
};
#define PLUS_ONE(...) +1
static constexpr intptr_t kUsedClassCount = 0 USED_CLASS_LIST(PLUS_ONE);
static constexpr intptr_t kUsedFieldCount = 0 USED_FIELD_LIST(PLUS_ONE);
#undef PLUS_ONE
#define DECLARE_GETTER(symbol) \
const Class& symbol() { \
auto& cls = classes_[k##symbol]; \
if (cls == nullptr) { \
cls = &Class::Handle( \
zone_, async_lib_.LookupClassAllowPrivate(Symbols::symbol())); \
ASSERT(!cls->IsNull()); \
} \
return *cls; \
}
USED_CLASS_LIST(DECLARE_GETTER)
#undef DECLARE_GETTER
#define DECLARE_GETTER(class_symbol, field_symbol) \
ObjectPtr Get##class_symbol##_##field_symbol(const Object& obj) { \
auto& field = fields_[k##class_symbol##_##field_symbol]; \
if (field == nullptr) { \
field = &Field::Handle(zone_, class_symbol().LookupFieldAllowPrivate( \
Symbols::field_symbol())); \
ASSERT(!field->IsNull()); \
} \
return Instance::Cast(obj).GetField(*field); \
}
USED_FIELD_LIST(DECLARE_GETTER)
#undef DECLARE_GETTER
struct AwaiterFrame {
Closure& closure;
Object& next;
bool has_catch_error;
};
Zone* zone_;
DartFrameIterator sync_frames_;
StackFrame* sync_frame_;
AwaiterFrame awaiter_frame_;
Closure& closure_;
Code& code_;
Context& context_;
Function& function_;
Function& parent_function_;
Object& object_;
SuspendState& suspend_state_;
Object& controller_;
Object& subscription_;
Object& stream_iterator_;
const Library& async_lib_;
Class* classes_[kUsedClassCount] = {};
Field* fields_[kUsedFieldCount] = {};
const Closure& null_closure_;
DISALLOW_COPY_AND_ASSIGN(AsyncAwareStackUnwinder);
};
bool AsyncAwareStackUnwinder::Unwind(
intptr_t skip_frames,
std::function<void(const StackTraceUtils::Frame&)> handle_frame) {
// First skip the given number of synchronous frames.
sync_frame_ = sync_frames_.NextFrame();
while (skip_frames > 0 && sync_frame_ != nullptr) {
sync_frame_ = sync_frames_.NextFrame();
skip_frames--;
}
// Continue unwinding synchronous portion of the stack looking for
// a synchronous frame which has an awaiter.
while (sync_frame_ != nullptr && awaiter_frame_.closure.IsNull()) {
const bool was_handled = HandleSynchronousFrame();
if (!was_handled) {
code_ = sync_frame_->LookupDartCode();
const uword pc_offset = sync_frame_->pc() - code_.PayloadStart();
handle_frame({sync_frame_, code_, pc_offset, null_closure_, false});
}
sync_frame_ = sync_frames_.NextFrame();
}
// Traverse awaiter frames.
bool any_async = false;
for (; !awaiter_frame_.closure.IsNull(); UnwindToAwaiter()) {
function_ = awaiter_frame_.closure.function();
if (function_.IsNull()) {
continue;
}
any_async = true;
if (awaiter_frame_.next.IsSuspendState()) {
const uword pc = SuspendState::Cast(awaiter_frame_.next).pc();
if (pc == 0) {
// Async function is already resumed.
continue;
}
code_ = SuspendState::Cast(awaiter_frame_.next).GetCodeObject();
const uword pc_offset = pc - code_.PayloadStart();
handle_frame({nullptr, code_, pc_offset, awaiter_frame_.closure,
awaiter_frame_.has_catch_error});
} else {
// This is an asynchronous continuation represented by a closure which
// will handle successful completion. This function is not yet executing
// so we have to use artificial marker offset (1).
code_ = function_.EnsureHasCode();
RELEASE_ASSERT(!code_.IsNull());
const uword pc_offset =
(function_.entry_point() + StackTraceUtils::kFutureListenerPcOffset) -
code_.PayloadStart();
handle_frame({nullptr, code_, pc_offset, awaiter_frame_.closure,
awaiter_frame_.has_catch_error});
}
}
return any_async;
}
ObjectPtr AsyncAwareStackUnwinder::GetReceiver() const {
return *(reinterpret_cast<ObjectPtr*>(sync_frame_->GetCallerSp()) +
function_.num_fixed_parameters() - 1);
}
bool AsyncAwareStackUnwinder::HandleSynchronousFrame() {
function_ = sync_frame_->LookupDartFunction();
if (function_.IsNull()) {
return false;
}
// This is an invocation of an `async` or `async*` function.
if (function_.IsAsyncFunction() || function_.IsAsyncGenerator()) {
InitializeAwaiterFrameFromSuspendState();
return false;
}
// This is an invocation of a closure which has a variable marked
// with `@pragma('vm:awaiter-link')` which points to the awaiter.
if (function_.HasAwaiterLink()) {
object_ = GetReceiver();
if (object_.IsClosure() &&
StackTraceUtils::GetSuspendState(Closure::Cast(object_),
&awaiter_frame_.next)) {
awaiter_frame_.closure ^= object_.ptr();
return true; // Hide this frame from the stack trace.
}
}
// This is `_FutureListener.handleValue(...)` invocation.
if (function_.recognized_kind() ==
MethodRecognizer::kFutureListenerHandleValue) {
object_ = GetReceiver();
InitializeAwaiterFrameFromFutureListener(object_);
UnwindToAwaiter();
return true; // Hide this frame from the stack trace.
}
return false;
}
void AsyncAwareStackUnwinder::InitializeAwaiterFrameFromSuspendState() {
if (function_.IsAsyncFunction() || function_.IsAsyncGenerator()) {
// Check if we reached a resumed asynchronous function. In this case we
// are going to start following async frames after we emit this frame.
object_ = *reinterpret_cast<ObjectPtr*>(LocalVarAddress(
sync_frame_->fp(), runtime_frame_layout.FrameSlotForVariableIndex(
SuspendState::kSuspendStateVarIndex)));
awaiter_frame_.closure = Closure::null();
if (WasPreviouslySuspended(function_, object_)) {
awaiter_frame_.next = object_.ptr();
UnwindToAwaiter();
}
}
}
void AsyncAwareStackUnwinder::UnwindToAwaiter() {
awaiter_frame_.has_catch_error = false;
do {
UnwindAwaiterFrame();
} while (awaiter_frame_.closure.IsNull() && !awaiter_frame_.next.IsNull());
}
void AsyncAwareStackUnwinder::UnwindAwaiterFrame() {
if (awaiter_frame_.next.IsSuspendState()) {
awaiter_frame_.next =
SuspendState::Cast(awaiter_frame_.next).function_data();
} else if (awaiter_frame_.next.GetClassId() == _SyncCompleter().id() ||
awaiter_frame_.next.GetClassId() == _AsyncCompleter().id()) {
awaiter_frame_.next = Get_Completer_future(awaiter_frame_.next);
}
if (awaiter_frame_.next.GetClassId() == _Future().id()) {
UnwindFrameToFutureListener();
} else if (awaiter_frame_.next.GetClassId() ==
_AsyncStarStreamController().id()) {
UnwindFrameToStreamListener();
} else {
awaiter_frame_.closure = Closure::null();
awaiter_frame_.next = Object::null();
return;
}
while (!awaiter_frame_.closure.IsNull()) {
function_ = awaiter_frame_.closure.function();
context_ = awaiter_frame_.closure.context();
const auto awaiter_link = function_.awaiter_link();
if (awaiter_link.depth != ClosureData::kNoAwaiterLinkDepth) {
intptr_t depth = awaiter_link.depth;
while (depth-- > 0) {
context_ = context_.parent();
}
const Object& object = Object::Handle(context_.At(awaiter_link.index));
if (object.IsClosure()) {
awaiter_frame_.closure ^= object.ptr();
continue;
} else {
awaiter_frame_.next = object.ptr();
return;
}
}
break;
}
}
void AsyncAwareStackUnwinder::UnwindFrameToFutureListener() {
object_ = Get_Future__resultOrListeners(awaiter_frame_.next);
if (object_.GetClassId() == _FutureListener().id()) {
InitializeAwaiterFrameFromFutureListener(object_);
} else {
awaiter_frame_.closure = Closure::null();
awaiter_frame_.next = Object::null();
}
}
void AsyncAwareStackUnwinder::InitializeAwaiterFrameFromFutureListener(
const Object& listener) {
if (listener.GetClassId() != _FutureListener().id()) {
awaiter_frame_.closure = Closure::null();
awaiter_frame_.next = Object::null();
return;
}
const auto state =
Smi::Value(Smi::RawCast(Get_FutureListener_state(listener)));
if (state == k_FutureListener_stateThen ||
state == k_FutureListener_stateCatchError ||
state == k_FutureListener_stateWhenComplete ||
state ==
(k_FutureListener_stateThen | k_FutureListener_stateCatchError) ||
state == (k_FutureListener_stateThen | k_FutureListener_stateCatchError |
k_FutureListener_maskAwait)) {
awaiter_frame_.next = Get_FutureListener_result(listener);
} else {
awaiter_frame_.next = Object::null();
}
awaiter_frame_.closure =
Closure::RawCast(Get_FutureListener_callback(listener));
// If the Future has catchError callback attached through either
// `Future.onError` or `Future.then(..., onError: ...)` then we should
// treat this listener as a catch all exception handler. However we should
// ignore the case when these callbacks are simply forwarding errors into a
// suspended async function, because it will be represented by its own async
// frame.
if ((state &
(k_FutureListener_stateCatchError | k_FutureListener_maskAwait)) ==
k_FutureListener_stateCatchError) {
awaiter_frame_.has_catch_error = true;
}
}
void AsyncAwareStackUnwinder::UnwindFrameToStreamListener() {
controller_ = Get_AsyncStarStreamController_controller(awaiter_frame_.next);
// Clear the frame.
awaiter_frame_.closure = Closure::null();
awaiter_frame_.next = Object::null();
const auto state =
Smi::Value(Smi::RawCast(Get_StreamController__state(controller_)));
if (state != k_StreamController__STATE_SUBSCRIBED) {
return;
}
subscription_ = Get_StreamController__varData(controller_);
closure_ ^= Get_BufferingStreamSubscription__onData(subscription_);
// If this is not the "_StreamIterator._onData" tear-off, we return the
// callback we found.
function_ = closure_.function();
if (!function_.IsImplicitInstanceClosureFunction() ||
function_.Owner() != _StreamIterator().ptr()) {
awaiter_frame_.closure = closure_.ptr();
return;
}
// All implicit closure functions (tear-offs) have the "this" receiver
// captured.
context_ = closure_.context();
ASSERT(context_.num_variables() == 1);
stream_iterator_ = context_.At(0);
ASSERT(stream_iterator_.IsInstance());
if (stream_iterator_.GetClassId() != _StreamIterator().id()) {
UNREACHABLE();
}
// If `_hasValue` is true then the `StreamIterator._stateData` field
// contains the iterator's value. In that case we cannot unwind anymore.
//
// Notice: With correct async* semantics this may never be true: The async*
// generator should only be invoked to produce a value if there's an
// in-progress `await streamIterator.moveNext()` call. Once such call has
// finished the async* generator should be paused/yielded until the next
// such call - and being paused/yielded means it should not appear in stack
// traces.
//
// See dartbug.com/48695.
if (Get_StreamIterator__hasValue(stream_iterator_) ==
Object::bool_true().ptr()) {
return;
}
// If we have an await'er for `await streamIterator.moveNext()` we continue
// unwinding there.
//
// Notice: With correct async* semantics this may always contain a Future
// See also comment above as well as dartbug.com/48695.
object_ = Get_StreamIterator__stateData(stream_iterator_);
if (object_.GetClassId() == _Future().id()) {
awaiter_frame_.next = object_.ptr();
}
}
} // namespace
bool StackTraceUtils::IsNeededForAsyncAwareUnwinding(const Function& function) {
// If this is a closure function which specifies an awaiter-link then
// we need to retain both function and the corresponding code.
if (function.HasAwaiterLink()) {
return true;
}
if (function.recognized_kind() ==
MethodRecognizer::kFutureListenerHandleValue) {
return true;
}
return false;
}
void StackTraceUtils::CollectFrames(
Thread* thread,
int skip_frames,
const std::function<void(const StackTraceUtils::Frame&)>& handle_frame) {
const Closure& null_closure = Closure::Handle(thread->zone());
const Frame gap_frame = {nullptr, StubCode::AsynchronousGapMarker(),
/*pc_offset=*/0, null_closure, false};
const Frame gap_frame_with_catch = {nullptr,
StubCode::AsynchronousGapMarker(),
/*pc_offset=*/0, null_closure, true};
AsyncAwareStackUnwinder it(thread);
const bool any_async = it.Unwind(skip_frames, [&](const Frame& frame) {
if (frame.frame == nullptr) {
handle_frame(frame.has_async_catch_error ? gap_frame_with_catch
: gap_frame);
}
handle_frame(frame);
});
if (any_async) {
handle_frame(gap_frame);
}
}
bool StackTraceUtils::GetSuspendState(const Closure& closure,
Object* suspend_state) {
const Function& function = Function::Handle(closure.function());
const auto awaiter_link = function.awaiter_link();
if (awaiter_link.depth != ClosureData::kNoAwaiterLinkDepth) {
Context& context = Context::Handle(closure.context());
intptr_t depth = awaiter_link.depth;
while (depth-- > 0) {
context = context.parent();
}
const Object& link = Object::Handle(context.At(awaiter_link.index));
if (link.IsSuspendState()) {
*suspend_state = link.ptr();
return true;
}
}
return false;
}
} // namespace dart