[vm] Runtime type check in await

'await e' should check that e is a Future<flatten(S)>, where S is a
static type of e before awaiting e. If e is not a Future<flatten(S)>,
then 'await e' should await Future.value(e) instead of e. So futures
of incompatible type are not awaited and soundness is not violated.

TEST=tests/language/async/await_type_check_test.dart
(Based on https://dart-review.git.corp.google.com/c/sdk/+/267422.)

Fixes https://github.com/dart-lang/sdk/issues/50529
Part of https://github.com/dart-lang/sdk/issues/49396

Change-Id: Ia418db1be6736710abc9be87d95584c50cbc677e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/273002
Reviewed-by: Erik Ernst <eernst@google.com>
Commit-Queue: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Alexander Markov 2022-12-07 22:46:22 +00:00 committed by Commit Queue
parent 38cab10852
commit 66f1dee48c
25 changed files with 1328 additions and 990 deletions

View file

@ -255,6 +255,20 @@ If an argument of `await` is a Future, then `_SuspendState._await` attaches 'the
callbacks to that Future. Otherwise it schedules a micro-task to continue execution of
the suspended function later.
### AwaitWithTypeCheck stub
AwaitWithTypeCheck is a variant of Await stub which additionally passes type argument `T`
and calls `_SuspendState._awaitWithTypeCheck` in order to test if the value has a
correct `Future<T>` type before awaiting.
This runtime check is needed to maintain soundness in case value is a Future of an
incompatible type, for example:
```
final FutureOr<Object> f = Future<Object?>.value(null);
Object x = await f; // x == f, not null.
```
### ReturnAsync stub
ReturnAsync stub = Return stub which calls `_SuspendState._returnAsync`.

View file

@ -7678,12 +7678,24 @@ void Call1ArgStubInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
locs(), deopt_id(), env());
}
Definition* SuspendInstr::Canonicalize(FlowGraph* flow_graph) {
if (stub_id() == StubId::kAwaitWithTypeCheck &&
!operand()->Type()->CanBeFuture()) {
type_args()->RemoveFromUseList();
stub_id_ = StubId::kAwait;
}
return this;
}
LocationSummary* SuspendInstr::MakeLocationSummary(Zone* zone, bool opt) const {
const intptr_t kNumInputs = 1;
const intptr_t kNumInputs = has_type_args() ? 2 : 1;
const intptr_t kNumTemps = 0;
LocationSummary* locs = new (zone)
LocationSummary(zone, kNumInputs, kNumTemps, LocationSummary::kCall);
locs->set_in(0, Location::RegisterLocation(SuspendStubABI::kArgumentReg));
if (has_type_args()) {
locs->set_in(1, Location::RegisterLocation(SuspendStubABI::kTypeArgsReg));
}
locs->set_out(0, Location::RegisterLocation(CallingConventions::kReturnReg));
return locs;
}
@ -7698,6 +7710,9 @@ void SuspendInstr::EmitNativeCode(FlowGraphCompiler* compiler) {
case StubId::kAwait:
stub = object_store->await_stub();
break;
case StubId::kAwaitWithTypeCheck:
stub = object_store->await_with_type_check_stub();
break;
case StubId::kYieldAsyncStar:
stub = object_store->yield_async_star_stub();
break;

View file

@ -10623,10 +10623,11 @@ class Call1ArgStubInstr : public TemplateDefinition<1, Throws> {
};
// Suspends execution using the suspend stub specified using [StubId].
class SuspendInstr : public TemplateDefinition<1, Throws> {
class SuspendInstr : public TemplateDefinition<2, Throws> {
public:
enum class StubId {
kAwait,
kAwaitWithTypeCheck,
kYieldAsyncStar,
kSuspendSyncStarAtStart,
kSuspendSyncStarAtYield,
@ -10635,6 +10636,7 @@ class SuspendInstr : public TemplateDefinition<1, Throws> {
SuspendInstr(const InstructionSource& source,
StubId stub_id,
Value* operand,
Value* type_args,
intptr_t deopt_id,
intptr_t resume_deopt_id)
: TemplateDefinition(source, deopt_id),
@ -10642,9 +10644,22 @@ class SuspendInstr : public TemplateDefinition<1, Throws> {
resume_deopt_id_(resume_deopt_id),
token_pos_(source.token_pos) {
SetInputAt(0, operand);
if (has_type_args()) {
SetInputAt(1, type_args);
} else {
ASSERT(type_args == nullptr);
}
}
bool has_type_args() const { return stub_id_ == StubId::kAwaitWithTypeCheck; }
virtual intptr_t InputCount() const { return has_type_args() ? 2 : 1; }
Value* operand() const { return inputs_[0]; }
Value* type_args() const {
ASSERT(has_type_args());
return inputs_[1];
}
StubId stub_id() const { return stub_id_; }
intptr_t resume_deopt_id() const { return resume_deopt_id_; }
virtual TokenPosition token_pos() const { return token_pos_; }
@ -10659,8 +10674,10 @@ class SuspendInstr : public TemplateDefinition<1, Throws> {
DECLARE_INSTRUCTION(Suspend);
PRINT_OPERANDS_TO_SUPPORT
virtual Definition* Canonicalize(FlowGraph* flow_graph);
#define FIELD_LIST(F) \
F(const StubId, stub_id_) \
F(StubId, stub_id_) \
F(const intptr_t, resume_deopt_id_) \
F(const TokenPosition, token_pos_)

View file

@ -1385,6 +1385,9 @@ void SuspendInstr::PrintOperandsTo(BaseTextBuffer* f) const {
case StubId::kAwait:
name = "Await";
break;
case StubId::kAwaitWithTypeCheck:
name = "AwaitWithTypeCheck";
break;
case StubId::kYieldAsyncStar:
name = "YieldAsyncStar";
break;
@ -1396,7 +1399,7 @@ void SuspendInstr::PrintOperandsTo(BaseTextBuffer* f) const {
break;
}
f->Printf("%s(", name);
operand()->PrintTo(f);
Definition::PrintOperandsTo(f);
f->AddString(")");
}

View file

@ -4481,15 +4481,28 @@ Fragment StreamingFlowGraphBuilder::BuildAwaitExpression(
instructions += BuildExpression(); // read operand.
SuspendInstr::StubId stub_id = SuspendInstr::StubId::kAwait;
if (ReadTag() == kSomething) {
// TODO(50529): Use runtime check type when present.
SkipDartType(); // read runtime check type.
const AbstractType& type = T.BuildType(); // read runtime check type.
if (!type.IsType() ||
!Class::Handle(Z, type.type_class()).IsFutureClass()) {
FATAL("Unexpected type for runtime check in await: %s", type.ToCString());
}
ASSERT(type.IsFinalized());
const auto& type_args = TypeArguments::ZoneHandle(Z, type.arguments());
if (!type_args.IsNull()) {
const auto& type_arg = AbstractType::Handle(Z, type_args.TypeAt(0));
if (!type_arg.IsTopTypeForSubtyping()) {
instructions += TranslateInstantiatedTypeArguments(type_args);
stub_id = SuspendInstr::StubId::kAwaitWithTypeCheck;
}
}
}
if (NeedsDebugStepCheck(parsed_function()->function(), pos)) {
instructions += DebugStepCheck(pos);
}
instructions += B->Suspend(pos, SuspendInstr::StubId::kAwait);
instructions += B->Suspend(pos, stub_id);
return instructions;
}

View file

@ -673,7 +673,7 @@ void KernelFingerprintHelper::CalculateExpressionFingerprint() {
ReadPosition(); // read position.
CalculateExpressionFingerprint(); // read operand.
if (ReadTag() == kSomething) {
SkipDartType(); // read runtime check type.
CalculateDartTypeFingerprint(); // read runtime check type.
}
return;
case kConstStaticInvocation:

View file

@ -4323,9 +4323,12 @@ Fragment FlowGraphBuilder::Call1ArgStub(TokenPosition position,
Fragment FlowGraphBuilder::Suspend(TokenPosition position,
SuspendInstr::StubId stub_id) {
Value* type_args =
(stub_id == SuspendInstr::StubId::kAwaitWithTypeCheck) ? Pop() : nullptr;
Value* operand = Pop();
SuspendInstr* instr =
new (Z) SuspendInstr(InstructionSource(position), stub_id, Pop(),
GetNextDeoptId(), GetNextDeoptId());
new (Z) SuspendInstr(InstructionSource(position), stub_id, operand,
type_args, GetNextDeoptId(), GetNextDeoptId());
Push(instr);
return Fragment(instr);
}

View file

@ -983,7 +983,7 @@ void ScopeBuilder::VisitExpression() {
helper_.ReadPosition(); // read position.
VisitExpression(); // read operand.
if (helper_.ReadTag() == kSomething) {
helper_.SkipDartType(); // read runtime check type.
VisitDartType(); // read runtime check type.
}
return;
case kConstStaticInvocation:

View file

@ -1274,6 +1274,7 @@ class Thread : public AllStatic {
static word suspend_state_init_async_entry_point_offset();
static word suspend_state_await_entry_point_offset();
static word suspend_state_await_with_type_check_entry_point_offset();
static word suspend_state_return_async_entry_point_offset();
static word suspend_state_return_async_not_future_entry_point_offset();
@ -1312,6 +1313,7 @@ class ObjectStore : public AllStatic {
static word type_type_offset();
static word suspend_state_await_offset();
static word suspend_state_await_with_type_check_offset();
static word suspend_state_handle_exception_offset();
static word suspend_state_init_async_offset();
static word suspend_state_init_async_star_offset();

File diff suppressed because it is too large Load diff

View file

@ -182,6 +182,7 @@
FIELD(ObjectStore, string_type_offset) \
FIELD(ObjectStore, type_type_offset) \
FIELD(ObjectStore, suspend_state_await_offset) \
FIELD(ObjectStore, suspend_state_await_with_type_check_offset) \
FIELD(ObjectStore, suspend_state_handle_exception_offset) \
FIELD(ObjectStore, suspend_state_init_async_offset) \
FIELD(ObjectStore, suspend_state_init_async_star_offset) \
@ -306,6 +307,7 @@
FIELD(Thread, stack_overflow_shared_without_fpu_regs_stub_offset) \
FIELD(Thread, store_buffer_block_offset) \
FIELD(Thread, suspend_state_await_entry_point_offset) \
FIELD(Thread, suspend_state_await_with_type_check_entry_point_offset) \
FIELD(Thread, suspend_state_init_async_entry_point_offset) \
FIELD(Thread, suspend_state_return_async_entry_point_offset) \
FIELD(Thread, suspend_state_return_async_not_future_entry_point_offset) \

View file

@ -1892,9 +1892,11 @@ static void GenerateAllocateSuspendState(Assembler* assembler,
void StubCodeCompiler::GenerateSuspendStub(
Assembler* assembler,
bool call_suspend_function,
bool pass_type_arguments,
intptr_t suspend_entry_point_offset_in_thread,
intptr_t suspend_function_offset_in_object_store) {
const Register kArgument = SuspendStubABI::kArgumentReg;
const Register kTypeArgs = SuspendStubABI::kTypeArgsReg;
const Register kTemp = SuspendStubABI::kTempReg;
const Register kFrameSize = SuspendStubABI::kFrameSizeReg;
const Register kSuspendState = SuspendStubABI::kSuspendStateReg;
@ -1917,6 +1919,10 @@ void StubCodeCompiler::GenerateSuspendStub(
__ EnterStubFrame();
if (pass_type_arguments) {
__ PushRegister(kTypeArgs);
}
__ CompareClassId(kSuspendState, kSuspendStateCid, kTemp);
if (FLAG_precompiled_mode) {
@ -2042,8 +2048,14 @@ void StubCodeCompiler::GenerateSuspendStub(
__ Bind(&call_dart);
if (call_suspend_function) {
__ Comment("Call suspend Dart function");
if (pass_type_arguments) {
__ LoadObject(ARGS_DESC_REG,
ArgumentsDescriptorBoxed(/*type_args_len=*/1,
/*num_arguments=*/2));
}
CallDartCoreLibraryFunction(assembler, suspend_entry_point_offset_in_thread,
suspend_function_offset_in_object_store);
suspend_function_offset_in_object_store,
/*uses_args_desc=*/pass_type_arguments);
} else {
// SuspendStub returns either the result of Dart callback,
// or SuspendStub argument (if Dart callback is not used).
@ -2137,14 +2149,25 @@ void StubCodeCompiler::GenerateSuspendStub(
void StubCodeCompiler::GenerateAwaitStub(Assembler* assembler) {
GenerateSuspendStub(assembler,
/*call_suspend_function=*/true,
/*pass_type_arguments=*/false,
target::Thread::suspend_state_await_entry_point_offset(),
target::ObjectStore::suspend_state_await_offset());
}
void StubCodeCompiler::GenerateAwaitWithTypeCheckStub(Assembler* assembler) {
GenerateSuspendStub(
assembler,
/*call_suspend_function=*/true,
/*pass_type_arguments=*/true,
target::Thread::suspend_state_await_with_type_check_entry_point_offset(),
target::ObjectStore::suspend_state_await_with_type_check_offset());
}
void StubCodeCompiler::GenerateYieldAsyncStarStub(Assembler* assembler) {
GenerateSuspendStub(
assembler,
/*call_suspend_function=*/true,
/*pass_type_arguments=*/false,
target::Thread::suspend_state_yield_async_star_entry_point_offset(),
target::ObjectStore::suspend_state_yield_async_star_offset());
}
@ -2154,6 +2177,7 @@ void StubCodeCompiler::GenerateSuspendSyncStarAtStartStub(
GenerateSuspendStub(
assembler,
/*call_suspend_function=*/true,
/*pass_type_arguments=*/false,
target::Thread::
suspend_state_suspend_sync_star_at_start_entry_point_offset(),
target::ObjectStore::suspend_state_suspend_sync_star_at_start_offset());
@ -2162,7 +2186,8 @@ void StubCodeCompiler::GenerateSuspendSyncStarAtStartStub(
void StubCodeCompiler::GenerateSuspendSyncStarAtYieldStub(
Assembler* assembler) {
GenerateSuspendStub(assembler,
/*call_suspend_function=*/false, -1, -1);
/*call_suspend_function=*/false,
/*pass_type_arguments=*/false, -1, -1);
}
void StubCodeCompiler::GenerateInitSuspendableFunctionStub(

View file

@ -207,6 +207,7 @@ class StubCodeCompiler : public AllStatic {
static void GenerateSuspendStub(
Assembler* assembler,
bool call_suspend_function,
bool pass_type_arguments,
intptr_t suspend_entry_point_offset_in_thread,
intptr_t suspend_function_offset_in_object_store);
static void GenerateInitSuspendableFunctionStub(

View file

@ -573,10 +573,11 @@ struct DoubleToIntegerStubABI {
static const Register kResultReg = R0;
};
// ABI for SuspendStub (AwaitStub, YieldAsyncStarStub,
// ABI for SuspendStub (AwaitStub, AwaitWithTypeCheckStub, YieldAsyncStarStub,
// SuspendSyncStarAtStartStub, SuspendSyncStarAtYieldStub).
struct SuspendStubABI {
static const Register kArgumentReg = R0;
static const Register kTypeArgsReg = R1; // Can be the same as kTempReg
static const Register kTempReg = R1;
static const Register kFrameSizeReg = R2;
static const Register kSuspendStateReg = R3;

View file

@ -403,10 +403,11 @@ struct DoubleToIntegerStubABI {
static const Register kResultReg = R0;
};
// ABI for SuspendStub (AwaitStub, YieldAsyncStarStub,
// ABI for SuspendStub (AwaitStub, AwaitWithTypeCheckStub, YieldAsyncStarStub,
// SuspendSyncStarAtStartStub, SuspendSyncStarAtYieldStub).
struct SuspendStubABI {
static const Register kArgumentReg = R0;
static const Register kTypeArgsReg = R1; // Can be the same as kTempReg
static const Register kTempReg = R1;
static const Register kFrameSizeReg = R2;
static const Register kSuspendStateReg = R3;

View file

@ -292,10 +292,11 @@ struct DoubleToIntegerStubABI {
static const Register kResultReg = EAX;
};
// ABI for SuspendStub (AwaitStub, YieldAsyncStarStub,
// ABI for SuspendStub (AwaitStub, AwaitWithTypeCheckStub, YieldAsyncStarStub,
// SuspendSyncStarAtStartStub, SuspendSyncStarAtYieldStub).
struct SuspendStubABI {
static const Register kArgumentReg = EAX;
static const Register kTypeArgsReg = EDX; // Can be the same as kTempReg
static const Register kTempReg = EDX;
static const Register kFrameSizeReg = ECX;
static const Register kSuspendStateReg = EBX;

View file

@ -412,10 +412,11 @@ struct DoubleToIntegerStubABI {
static constexpr Register kResultReg = A0;
};
// ABI for SuspendStub (AwaitStub, YieldAsyncStarStub,
// ABI for SuspendStub (AwaitStub, AwaitWithTypeCheckStub, YieldAsyncStarStub,
// SuspendSyncStarAtStartStub, SuspendSyncStarAtYieldStub).
struct SuspendStubABI {
static const Register kArgumentReg = A0;
static const Register kTypeArgsReg = T0; // Can be the same as kTempReg
static const Register kTempReg = T0;
static const Register kFrameSizeReg = T1;
static const Register kSuspendStateReg = T2;

View file

@ -373,10 +373,11 @@ struct DoubleToIntegerStubABI {
static const Register kResultReg = RAX;
};
// ABI for SuspendStub (AwaitStub, YieldAsyncStarStub,
// ABI for SuspendStub (AwaitStub, AwaitWithTypeCheckStub, YieldAsyncStarStub,
// SuspendSyncStarAtStartStub, SuspendSyncStarAtYieldStub).
struct SuspendStubABI {
static const Register kArgumentReg = RAX;
static const Register kTypeArgsReg = RDX; // Can be the same as kTempReg
static const Register kTempReg = RDX;
static const Register kFrameSizeReg = RCX;
static const Register kSuspendStateReg = RBX;

View file

@ -300,6 +300,10 @@ void ObjectStore::InitKnownObjects() {
ASSERT(!function.IsNull());
set_suspend_state_await(function);
function = cls.LookupFunctionAllowPrivate(Symbols::_awaitWithTypeCheck());
ASSERT(!function.IsNull());
set_suspend_state_await_with_type_check(function);
function = cls.LookupFunctionAllowPrivate(Symbols::_returnAsync());
ASSERT(!function.IsNull());
set_suspend_state_return_async(function);

View file

@ -177,6 +177,7 @@ class ObjectPointerVisitor;
RW(Function, async_star_stream_controller_add_stream) \
RW(Function, suspend_state_init_async) \
RW(Function, suspend_state_await) \
RW(Function, suspend_state_await_with_type_check) \
RW(Function, suspend_state_return_async) \
RW(Function, suspend_state_return_async_not_future) \
RW(Function, suspend_state_init_async_star) \
@ -272,6 +273,7 @@ class ObjectPointerVisitor;
RW(Code, slow_tts_stub) \
/* Roots for JIT/AOT snapshots are up until here (see to_snapshot() below)*/ \
RW(Code, await_stub) \
RW(Code, await_with_type_check_stub) \
RW(Code, clone_suspend_state_stub) \
RW(Code, init_async_stub) \
RW(Code, resume_stub) \
@ -366,6 +368,7 @@ class ObjectPointerVisitor;
DO(init_late_instance_field_stub, InitLateInstanceField) \
DO(init_late_final_instance_field_stub, InitLateFinalInstanceField) \
DO(await_stub, Await) \
DO(await_with_type_check_stub, AwaitWithTypeCheck) \
DO(clone_suspend_state_stub, CloneSuspendState) \
DO(init_async_stub, InitAsync) \
DO(resume_stub, Resume) \

View file

@ -160,6 +160,7 @@ namespace dart {
V(InstantiateTypeArgumentsMayShareFunctionTA) \
V(NoSuchMethodDispatcher) \
V(Await) \
V(AwaitWithTypeCheck) \
V(InitAsync) \
V(Resume) \
V(ReturnAsync) \

View file

@ -418,6 +418,7 @@ class ObjectPointerVisitor;
V(_WeakProperty, "_WeakProperty") \
V(_WeakReference, "_WeakReference") \
V(_await, "_await") \
V(_awaitWithTypeCheck, "_awaitWithTypeCheck") \
V(_classRangeCheck, "_classRangeCheck") \
V(_current, "_current") \
V(_ensureScheduleImmediate, "_ensureScheduleImmediate") \

View file

@ -184,6 +184,7 @@ class Thread;
#define CACHED_FUNCTION_ENTRY_POINTS_LIST(V) \
V(suspend_state_init_async) \
V(suspend_state_await) \
V(suspend_state_await_with_type_check) \
V(suspend_state_return_async) \
V(suspend_state_return_async_not_future) \
V(suspend_state_init_async_star) \

View file

@ -309,6 +309,33 @@ class _SuspendState {
return _functionData;
}
@pragma("vm:entry-point", "call")
@pragma("vm:invisible")
Object? _awaitWithTypeCheck<T>(Object? object) {
if (_thenCallback == null) {
_createAsyncCallbacks();
}
// Declare a new variable to avoid type promotion of 'object' to
// 'Future<T>', as it would disable further type promotion to '_Future'.
final obj = object;
if (obj is Future<T>) {
if (object is _Future) {
if (object._isComplete) {
_awaitCompletedFuture(object);
} else {
object._thenAwait<dynamic>(
unsafeCast<dynamic Function(dynamic)>(_thenCallback),
unsafeCast<dynamic Function(Object, StackTrace)>(_errorCallback));
}
} else {
_awaitUserDefinedFuture(obj);
}
} else {
_awaitNotFuture(object);
}
return _functionData;
}
@pragma("vm:entry-point", "call")
@pragma("vm:invisible")
static Future _returnAsync(Object suspendState, Object? returnValue) {

View file

@ -0,0 +1,81 @@
// Copyright (c) 2022, 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.
// Verifies that 'await' should check if the awaited value has a correct
// Future<T> type before awaiting it. A Future of incompatible type
// should not be awaited.
//
// Regression test for https://github.com/dart-lang/sdk/issues/49396.
// Requirements=nnbd-strong
import 'dart:async';
import 'package:expect/expect.dart';
import '../static_type_helper.dart';
class A {}
abstract class B<X> extends A implements Future<X> {
Future<X> get fut;
asStream() => fut.asStream();
catchError(error, {test}) => fut.catchError(error, test: test);
then<R>(onValue, {onError}) => fut.then(onValue, onError: onError);
timeout(timeLimit, {onTimeout}) =>
fut.timeout(timeLimit, onTimeout: onTimeout);
whenComplete(action) => fut.whenComplete(action);
}
class C extends B<Object?> {
final Future<Object?> fut = Future.value(CompletelyUnrelated());
}
class CompletelyUnrelated {}
class C1 extends A {}
class C2 extends B<C1> {
final Future<C1> fut = Future.value(C1());
}
void main() async {
final Object o = Future<Object?>.value();
var o2 = await o; // Remains a future.
o2.expectStaticType<Exactly<Object>>();
Expect.isTrue(o2 is Future<Object?>);
Expect.identical(o2, o);
final FutureOr<Object> x = Future<Object?>.value();
var x2 = await x; // Remains a future.
x2.expectStaticType<Exactly<Object>>();
Expect.isTrue(x2 is Future<Object?>);
Expect.identical(x2, x);
final FutureOr<Future<int>> y = Future<int>.value(1);
var y2 = await y; // Remains a `Future<int>`.
y2.expectStaticType<Exactly<Future<int>>>();
Expect.isTrue(y2 is Future<int>);
Expect.identical(y2, y);
A a = C();
var a2 = await a; // Remains an `A`.
a2.expectStaticType<Exactly<A>>();
Expect.isTrue(a2 is A);
Expect.identical(a2, a);
Future<void> f<X extends Object>(X x) async {
var x2 = await x; // Remains a `Future<Object?>`.
Expect.isTrue(x2 is Future<Object?>);
Expect.identical(x2, x);
}
await f(Future<Object?>.value(null));
Future<void> g<X extends A>(X x) async {
var x2 = await x; // The future is awaited.
Expect.isTrue(x2 is C1);
Expect.notIdentical(x2, x);
}
await g<A>(C2());
}