Skip to content

Commit 907d7bc

Browse files
bmeurerCommit Bot
authored andcommitted
[promise] Implement Swallowed Rejection Hook.
This extends the current Promise Rejection Hook with two new events kPromiseRejectAfterResolved kPromiseResolveAfterResolved which are used to detect (and signal) misuse of the Promise constructor. Specifically the common bug like new Promise((res, rej) => { res(1); throw new Error("something") }); where the error is silently swallowed by the Promise constructor without the user ever noticing can be caught via this hook. Doc: https://goo.gl/2stLUY Bug: v8:7919 Cq-Include-Trybots: luci.chromium.try:linux_chromium_rel_ng Change-Id: I890a7e766cdd1be88db94844fb744f72823dba33 Reviewed-on: https://chromium-review.googlesource.com/1126099 Reviewed-by: Maya Lekova <[email protected]> Reviewed-by: Benedikt Meurer <[email protected]> Reviewed-by: Yang Guo <[email protected]> Commit-Queue: Benedikt Meurer <[email protected]> Cr-Commit-Position: refs/heads/master@{#54309}
1 parent 055a139 commit 907d7bc

8 files changed

Lines changed: 148 additions & 44 deletions

File tree

include/v8.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6570,7 +6570,9 @@ typedef void (*PromiseHook)(PromiseHookType type, Local<Promise> promise,
65706570
// --- Promise Reject Callback ---
65716571
enum PromiseRejectEvent {
65726572
kPromiseRejectWithNoHandler = 0,
6573-
kPromiseHandlerAddedAfterReject = 1
6573+
kPromiseHandlerAddedAfterReject = 1,
6574+
kPromiseRejectAfterResolved = 2,
6575+
kPromiseResolveAfterResolved = 3,
65746576
};
65756577

65766578
class PromiseRejectMessage {

src/builtins/builtins-promise-gen.cc

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ Node* PromiseBuiltinsAssembler::CreatePromiseResolvingFunctionsContext(
247247
Node* const context =
248248
CreatePromiseContext(native_context, kPromiseContextLength);
249249
StoreContextElementNoWriteBarrier(context, kPromiseSlot, promise);
250+
StoreContextElementNoWriteBarrier(context, kAlreadyResolvedSlot,
251+
FalseConstant());
250252
StoreContextElementNoWriteBarrier(context, kDebugEventSlot, debug_event);
251253
return context;
252254
}
@@ -737,17 +739,27 @@ TF_BUILTIN(PromiseCapabilityDefaultReject, PromiseBuiltinsAssembler) {
737739
Node* const promise = LoadContextElement(context, kPromiseSlot);
738740

739741
// 3. Let alreadyResolved be F.[[AlreadyResolved]].
742+
Label if_already_resolved(this, Label::kDeferred);
743+
Node* const already_resolved =
744+
LoadContextElement(context, kAlreadyResolvedSlot);
745+
740746
// 4. If alreadyResolved.[[Value]] is true, return undefined.
741-
// We use undefined as a marker for the [[AlreadyResolved]] state.
742-
ReturnIf(IsUndefined(promise), UndefinedConstant());
747+
GotoIf(IsTrue(already_resolved), &if_already_resolved);
743748

744749
// 5. Set alreadyResolved.[[Value]] to true.
745-
StoreContextElementNoWriteBarrier(context, kPromiseSlot, UndefinedConstant());
750+
StoreContextElementNoWriteBarrier(context, kAlreadyResolvedSlot,
751+
TrueConstant());
746752

747753
// 6. Return RejectPromise(promise, reason).
748754
Node* const debug_event = LoadContextElement(context, kDebugEventSlot);
749755
Return(CallBuiltin(Builtins::kRejectPromise, context, promise, reason,
750756
debug_event));
757+
758+
BIND(&if_already_resolved);
759+
{
760+
Return(CallRuntime(Runtime::kPromiseRejectAfterResolved, context, promise,
761+
reason));
762+
}
751763
}
752764

753765
// ES #sec-promise-resolve-functions
@@ -759,16 +771,26 @@ TF_BUILTIN(PromiseCapabilityDefaultResolve, PromiseBuiltinsAssembler) {
759771
Node* const promise = LoadContextElement(context, kPromiseSlot);
760772

761773
// 3. Let alreadyResolved be F.[[AlreadyResolved]].
774+
Label if_already_resolved(this, Label::kDeferred);
775+
Node* const already_resolved =
776+
LoadContextElement(context, kAlreadyResolvedSlot);
777+
762778
// 4. If alreadyResolved.[[Value]] is true, return undefined.
763-
// We use undefined as a marker for the [[AlreadyResolved]] state.
764-
ReturnIf(IsUndefined(promise), UndefinedConstant());
779+
GotoIf(IsTrue(already_resolved), &if_already_resolved);
765780

766781
// 5. Set alreadyResolved.[[Value]] to true.
767-
StoreContextElementNoWriteBarrier(context, kPromiseSlot, UndefinedConstant());
782+
StoreContextElementNoWriteBarrier(context, kAlreadyResolvedSlot,
783+
TrueConstant());
768784

769785
// The rest of the logic (and the catch prediction) is
770786
// encapsulated in the dedicated ResolvePromise builtin.
771787
Return(CallBuiltin(Builtins::kResolvePromise, context, promise, resolution));
788+
789+
BIND(&if_already_resolved);
790+
{
791+
Return(CallRuntime(Runtime::kPromiseResolveAfterResolved, context, promise,
792+
resolution));
793+
}
772794
}
773795

774796
TF_BUILTIN(PromiseConstructorLazyDeoptContinuation, PromiseBuiltinsAssembler) {

src/builtins/builtins-promise-gen.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ typedef compiler::CodeAssemblerState CodeAssemblerState;
1717
class PromiseBuiltinsAssembler : public CodeStubAssembler {
1818
public:
1919
enum PromiseResolvingFunctionContextSlot {
20-
// The promise which resolve/reject callbacks fulfill. If this is
21-
// undefined, then we've already visited this callback and it
22-
// should be a no-op.
20+
// The promise which resolve/reject callbacks fulfill.
2321
kPromiseSlot = Context::MIN_CONTEXT_SLOTS,
2422

23+
// Whether the callback was already invoked.
24+
kAlreadyResolvedSlot,
25+
2526
// Whether to trigger a debug event or not. Used in catch
2627
// prediction.
2728
kDebugEventSlot,

src/compiler/js-call-reducer.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5456,6 +5456,10 @@ Reduction JSCallReducer::ReducePromiseConstructor(Node* node) {
54565456
graph()->NewNode(simplified()->StoreField(AccessBuilder::ForContextSlot(
54575457
PromiseBuiltinsAssembler::kPromiseSlot)),
54585458
promise_context, promise, effect, control);
5459+
effect = graph()->NewNode(
5460+
simplified()->StoreField(AccessBuilder::ForContextSlot(
5461+
PromiseBuiltinsAssembler::kAlreadyResolvedSlot)),
5462+
promise_context, jsgraph()->FalseConstant(), effect, control);
54595463
effect = graph()->NewNode(
54605464
simplified()->StoreField(AccessBuilder::ForContextSlot(
54615465
PromiseBuiltinsAssembler::kDebugEventSlot)),

src/isolate.cc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3931,10 +3931,9 @@ void Isolate::SetPromiseRejectCallback(PromiseRejectCallback callback) {
39313931
void Isolate::ReportPromiseReject(Handle<JSPromise> promise,
39323932
Handle<Object> value,
39333933
v8::PromiseRejectEvent event) {
3934-
DCHECK_EQ(v8::Promise::kRejected, promise->status());
39353934
if (promise_reject_callback_ == nullptr) return;
39363935
Handle<FixedArray> stack_trace;
3937-
if (event == v8::kPromiseRejectWithNoHandler && value->IsJSObject()) {
3936+
if (event != v8::kPromiseHandlerAddedAfterReject && value->IsJSObject()) {
39383937
stack_trace = GetDetailedStackTrace(Handle<JSObject>::cast(value));
39393938
}
39403939
promise_reject_callback_(v8::PromiseRejectMessage(

src/runtime/runtime-promise.cc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,26 @@ RUNTIME_FUNCTION(Runtime_PromiseRejectEventFromStack) {
3939
return ReadOnlyRoots(isolate).undefined_value();
4040
}
4141

42+
RUNTIME_FUNCTION(Runtime_PromiseRejectAfterResolved) {
43+
DCHECK_EQ(2, args.length());
44+
HandleScope scope(isolate);
45+
CONVERT_ARG_HANDLE_CHECKED(JSPromise, promise, 0);
46+
CONVERT_ARG_HANDLE_CHECKED(Object, reason, 1);
47+
isolate->ReportPromiseReject(promise, reason,
48+
v8::kPromiseRejectAfterResolved);
49+
return ReadOnlyRoots(isolate).undefined_value();
50+
}
51+
52+
RUNTIME_FUNCTION(Runtime_PromiseResolveAfterResolved) {
53+
DCHECK_EQ(2, args.length());
54+
HandleScope scope(isolate);
55+
CONVERT_ARG_HANDLE_CHECKED(JSPromise, promise, 0);
56+
CONVERT_ARG_HANDLE_CHECKED(Object, resolution, 1);
57+
isolate->ReportPromiseReject(promise, resolution,
58+
v8::kPromiseResolveAfterResolved);
59+
return ReadOnlyRoots(isolate).undefined_value();
60+
}
61+
4262
RUNTIME_FUNCTION(Runtime_PromiseRevokeReject) {
4363
DCHECK_EQ(1, args.length());
4464
HandleScope scope(isolate);

src/runtime/runtime.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,9 @@ namespace internal {
384384
F(PromiseRevokeReject, 1, 1) \
385385
F(PromiseStatus, 1, 1) \
386386
F(RejectPromise, 3, 1) \
387-
F(ResolvePromise, 2, 1)
387+
F(ResolvePromise, 2, 1) \
388+
F(PromiseRejectAfterResolved, 2, 1) \
389+
F(PromiseResolveAfterResolved, 2, 1)
388390

389391
#define FOR_EACH_INTRINSIC_PROXY(F) \
390392
F(CheckProxyGetSetTrapResult, 2, 1) \

test/cctest/test-api.cc

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17718,6 +17718,8 @@ TEST(RethrowBogusErrorStackTrace) {
1771817718
v8::PromiseRejectEvent reject_event = v8::kPromiseRejectWithNoHandler;
1771917719
int promise_reject_counter = 0;
1772017720
int promise_revoke_counter = 0;
17721+
int promise_reject_after_resolved_counter = 0;
17722+
int promise_resolve_after_resolved_counter = 0;
1772117723
int promise_reject_msg_line_number = -1;
1772217724
int promise_reject_msg_column_number = -1;
1772317725
int promise_reject_line_number = -1;
@@ -17727,40 +17729,56 @@ int promise_reject_frame_count = -1;
1772717729
void PromiseRejectCallback(v8::PromiseRejectMessage reject_message) {
1772817730
v8::Local<v8::Object> global = CcTest::global();
1772917731
v8::Local<v8::Context> context = CcTest::isolate()->GetCurrentContext();
17730-
CHECK_EQ(v8::Promise::PromiseState::kRejected,
17732+
CHECK_NE(v8::Promise::PromiseState::kPending,
1773117733
reject_message.GetPromise()->State());
17732-
if (reject_message.GetEvent() == v8::kPromiseRejectWithNoHandler) {
17733-
promise_reject_counter++;
17734-
global->Set(context, v8_str("rejected"), reject_message.GetPromise())
17735-
.FromJust();
17736-
global->Set(context, v8_str("value"), reject_message.GetValue()).FromJust();
17737-
v8::Local<v8::Message> message = v8::Exception::CreateMessage(
17738-
CcTest::isolate(), reject_message.GetValue());
17739-
v8::Local<v8::StackTrace> stack_trace = message->GetStackTrace();
17740-
17741-
promise_reject_msg_line_number = message->GetLineNumber(context).FromJust();
17742-
promise_reject_msg_column_number =
17743-
message->GetStartColumn(context).FromJust() + 1;
17744-
17745-
if (!stack_trace.IsEmpty()) {
17746-
promise_reject_frame_count = stack_trace->GetFrameCount();
17747-
if (promise_reject_frame_count > 0) {
17748-
CHECK(stack_trace->GetFrame(0)
17749-
->GetScriptName()
17750-
->Equals(context, v8_str("pro"))
17751-
.FromJust());
17752-
promise_reject_line_number = stack_trace->GetFrame(0)->GetLineNumber();
17753-
promise_reject_column_number = stack_trace->GetFrame(0)->GetColumn();
17754-
} else {
17755-
promise_reject_line_number = -1;
17756-
promise_reject_column_number = -1;
17734+
switch (reject_message.GetEvent()) {
17735+
case v8::kPromiseRejectWithNoHandler: {
17736+
promise_reject_counter++;
17737+
global->Set(context, v8_str("rejected"), reject_message.GetPromise())
17738+
.FromJust();
17739+
global->Set(context, v8_str("value"), reject_message.GetValue())
17740+
.FromJust();
17741+
v8::Local<v8::Message> message = v8::Exception::CreateMessage(
17742+
CcTest::isolate(), reject_message.GetValue());
17743+
v8::Local<v8::StackTrace> stack_trace = message->GetStackTrace();
17744+
17745+
promise_reject_msg_line_number =
17746+
message->GetLineNumber(context).FromJust();
17747+
promise_reject_msg_column_number =
17748+
message->GetStartColumn(context).FromJust() + 1;
17749+
17750+
if (!stack_trace.IsEmpty()) {
17751+
promise_reject_frame_count = stack_trace->GetFrameCount();
17752+
if (promise_reject_frame_count > 0) {
17753+
CHECK(stack_trace->GetFrame(0)
17754+
->GetScriptName()
17755+
->Equals(context, v8_str("pro"))
17756+
.FromJust());
17757+
promise_reject_line_number =
17758+
stack_trace->GetFrame(0)->GetLineNumber();
17759+
promise_reject_column_number = stack_trace->GetFrame(0)->GetColumn();
17760+
} else {
17761+
promise_reject_line_number = -1;
17762+
promise_reject_column_number = -1;
17763+
}
1775717764
}
17765+
break;
17766+
}
17767+
case v8::kPromiseHandlerAddedAfterReject: {
17768+
promise_revoke_counter++;
17769+
global->Set(context, v8_str("revoked"), reject_message.GetPromise())
17770+
.FromJust();
17771+
CHECK(reject_message.GetValue().IsEmpty());
17772+
break;
17773+
}
17774+
case v8::kPromiseRejectAfterResolved: {
17775+
promise_reject_after_resolved_counter++;
17776+
break;
17777+
}
17778+
case v8::kPromiseResolveAfterResolved: {
17779+
promise_resolve_after_resolved_counter++;
17780+
break;
1775817781
}
17759-
} else {
17760-
promise_revoke_counter++;
17761-
global->Set(context, v8_str("revoked"), reject_message.GetPromise())
17762-
.FromJust();
17763-
CHECK(reject_message.GetValue().IsEmpty());
1776417782
}
1776517783
}
1776617784

@@ -17783,6 +17801,8 @@ v8::Local<v8::Value> RejectValue() {
1778317801
void ResetPromiseStates() {
1778417802
promise_reject_counter = 0;
1778517803
promise_revoke_counter = 0;
17804+
promise_reject_after_resolved_counter = 0;
17805+
promise_resolve_after_resolved_counter = 0;
1778617806
promise_reject_msg_line_number = -1;
1778717807
promise_reject_msg_column_number = -1;
1778817808
promise_reject_line_number = -1;
@@ -18008,6 +18028,40 @@ TEST(PromiseRejectCallback) {
1800818028
CHECK_EQ(0, promise_revoke_counter);
1800918029
CHECK(RejectValue()->Equals(env.local(), v8_str("sss")).FromJust());
1801018030

18031+
ResetPromiseStates();
18032+
18033+
// Swallowed exceptions in the Promise constructor.
18034+
CompileRun(
18035+
"var v0 = new Promise(\n"
18036+
" function(res, rej) {\n"
18037+
" res(1);\n"
18038+
" throw new Error();\n"
18039+
" }\n"
18040+
");\n");
18041+
CHECK(!GetPromise("v0")->HasHandler());
18042+
CHECK_EQ(0, promise_reject_counter);
18043+
CHECK_EQ(0, promise_revoke_counter);
18044+
CHECK_EQ(1, promise_reject_after_resolved_counter);
18045+
CHECK_EQ(0, promise_resolve_after_resolved_counter);
18046+
18047+
ResetPromiseStates();
18048+
18049+
// Duplication resolve.
18050+
CompileRun(
18051+
"var r;\n"
18052+
"var y0 = new Promise(\n"
18053+
" function(res, rej) {\n"
18054+
" r = res;\n"
18055+
" throw new Error();\n"
18056+
" }\n"
18057+
");\n"
18058+
"r(1);\n");
18059+
CHECK(!GetPromise("y0")->HasHandler());
18060+
CHECK_EQ(1, promise_reject_counter);
18061+
CHECK_EQ(0, promise_revoke_counter);
18062+
CHECK_EQ(0, promise_reject_after_resolved_counter);
18063+
CHECK_EQ(1, promise_resolve_after_resolved_counter);
18064+
1801118065
// Test stack frames.
1801218066
env->GetIsolate()->SetCaptureStackTraceForUncaughtExceptions(true);
1801318067

0 commit comments

Comments
 (0)