[wasm] Trap on invalid suspender object

Trap if the suspender argument provided to the JSPI import
wrapper is invalid.

For now, the suspender argument is expected to be the active
suspender. In the future, it will also be possible to suspend
to a parent of the current suspender. This will only be possible
once wasm-to-wasm suspending wrappers are supported, or if and
when JSPI suspenders become compatible with their core
stack-switching counterpart (e.g. Fibers in the fiber proposal).

R=jkummerow@chromium.org

Bug: v8:12191
Change-Id: I650454ed076bd251b0aa18656774d4c4b2d3bfdc
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3892697
Reviewed-by: Jakob Kummerow <jkummerow@chromium.org>
Commit-Queue: Thibaud Michaud <thibaudm@chromium.org>
Cr-Commit-Position: refs/heads/main@{#83173}
This commit is contained in:
Thibaud Michaud 2022-09-13 20:16:46 +02:00 committed by V8 LUCI CQ
parent 2847ad2e20
commit 704c571d90
6 changed files with 52 additions and 8 deletions

View File

@ -3971,11 +3971,6 @@ void Builtins::Generate_WasmSuspend(MacroAssembler* masm) {
__ subq(rsp, Immediate(-(BuiltinWasmWrapperConstants::kGCScanSlotCountOffset - __ subq(rsp, Immediate(-(BuiltinWasmWrapperConstants::kGCScanSlotCountOffset -
TypedFrameConstants::kFixedFrameSizeFromFp))); TypedFrameConstants::kFixedFrameSizeFromFp)));
// TODO(thibaudm): Throw if any of the following holds:
// - caller is null
// - ActiveSuspender is undefined
// - 'suspender' is not the active suspender
// ------------------------------------------- // -------------------------------------------
// Save current state in active jump buffer. // Save current state in active jump buffer.
// ------------------------------------------- // -------------------------------------------

View File

@ -654,6 +654,7 @@ namespace internal {
T(WasmTrapStringInvalidUtf8, "invalid UTF-8 string") \ T(WasmTrapStringInvalidUtf8, "invalid UTF-8 string") \
T(WasmTrapStringInvalidWtf8, "invalid WTF-8 string") \ T(WasmTrapStringInvalidWtf8, "invalid WTF-8 string") \
T(WasmTrapStringOffsetOutOfBounds, "string offset out of bounds") \ T(WasmTrapStringOffsetOutOfBounds, "string offset out of bounds") \
T(WasmTrapBadSuspender, "invalid suspender object for suspend") \
T(WasmTrapStringIsolatedSurrogate, \ T(WasmTrapStringIsolatedSurrogate, \
"Failed to encode string as UTF-8: contains unpaired surrogate") \ "Failed to encode string as UTF-8: contains unpaired surrogate") \
T(WasmExceptionError, "wasm exception") \ T(WasmExceptionError, "wasm exception") \

View File

@ -6891,12 +6891,20 @@ class WasmWrapperGraphBuilder : public WasmGraphBuilder {
// If value is a promise, suspend to the js-to-wasm prompt, and resume later // If value is a promise, suspend to the js-to-wasm prompt, and resume later
// with the promise's resolved value. // with the promise's resolved value.
auto resume = gasm_->MakeLabel(MachineRepresentation::kTagged); auto resume = gasm_->MakeLabel(MachineRepresentation::kTagged);
gasm_->GotoIf(IsSmi(value), &resume, value); // Trap if the suspender argument is not the active suspender or if there is
gasm_->GotoIfNot(gasm_->HasInstanceType(value, JS_PROMISE_TYPE), &resume, // no active suspender.
BranchHint::kTrue, value); auto bad_suspender = gasm_->MakeDeferredLabel();
Node* native_context = gasm_->Load( Node* native_context = gasm_->Load(
MachineType::TaggedPointer(), api_function_ref, MachineType::TaggedPointer(), api_function_ref,
wasm::ObjectAccess::ToTagged(WasmApiFunctionRef::kNativeContextOffset)); wasm::ObjectAccess::ToTagged(WasmApiFunctionRef::kNativeContextOffset));
Node* active_suspender = LOAD_ROOT(ActiveSuspender, active_suspender);
gasm_->GotoIf(gasm_->TaggedEqual(active_suspender, UndefinedValue()),
&bad_suspender, BranchHint::kFalse);
gasm_->GotoIfNot(gasm_->TaggedEqual(suspender, active_suspender),
&bad_suspender, BranchHint::kFalse);
gasm_->GotoIf(IsSmi(value), &resume, value);
gasm_->GotoIfNot(gasm_->HasInstanceType(value, JS_PROMISE_TYPE), &resume,
BranchHint::kTrue, value);
auto* call_descriptor = GetBuiltinCallDescriptor( auto* call_descriptor = GetBuiltinCallDescriptor(
Builtin::kWasmSuspend, zone_, StubCallMode::kCallWasmRuntimeStub); Builtin::kWasmSuspend, zone_, StubCallMode::kCallWasmRuntimeStub);
Node* call_target = mcgraph()->RelocatableIntPtrConstant( Node* call_target = mcgraph()->RelocatableIntPtrConstant(
@ -6907,6 +6915,10 @@ class WasmWrapperGraphBuilder : public WasmGraphBuilder {
Node* resolved = Node* resolved =
gasm_->Call(call_descriptor, call_target, chained_promise, suspender); gasm_->Call(call_descriptor, call_target, chained_promise, suspender);
gasm_->Goto(&resume, resolved); gasm_->Goto(&resume, resolved);
gasm_->Bind(&bad_suspender);
BuildCallToRuntimeWithContext(Runtime::kThrowBadSuspenderError,
native_context, nullptr, 0);
TerminateThrow(effect(), control());
gasm_->Bind(&resume); gasm_->Bind(&resume);
return resume.PhiAt(0); return resume.PhiAt(0);
} }

View File

@ -194,6 +194,15 @@ RUNTIME_FUNCTION(Runtime_WasmThrowJSTypeError) {
isolate, NewTypeError(MessageTemplate::kWasmTrapJSTypeError)); isolate, NewTypeError(MessageTemplate::kWasmTrapJSTypeError));
} }
// This error is thrown from a wasm-to-JS wrapper, so unlike
// Runtime_ThrowWasmError, this function does not check or unset the
// thread-in-wasm flag.
RUNTIME_FUNCTION(Runtime_ThrowBadSuspenderError) {
HandleScope scope(isolate);
DCHECK_EQ(0, args.length());
return ThrowWasmError(isolate, MessageTemplate::kWasmTrapBadSuspender);
}
RUNTIME_FUNCTION(Runtime_WasmThrow) { RUNTIME_FUNCTION(Runtime_WasmThrow) {
ClearThreadInWasmScope clear_wasm_flag(isolate); ClearThreadInWasmScope clear_wasm_flag(isolate);
HandleScope scope(isolate); HandleScope scope(isolate);

View File

@ -598,6 +598,7 @@ namespace internal {
F(TypedArraySortFast, 1, 1) F(TypedArraySortFast, 1, 1)
#define FOR_EACH_INTRINSIC_WASM(F, I) \ #define FOR_EACH_INTRINSIC_WASM(F, I) \
F(ThrowBadSuspenderError, 0, 1) \
F(ThrowWasmError, 1, 1) \ F(ThrowWasmError, 1, 1) \
F(ThrowWasmStackOverflow, 0, 1) \ F(ThrowWasmStackOverflow, 0, 1) \
F(WasmI32AtomicWait, 4, 1) \ F(WasmI32AtomicWait, 4, 1) \

View File

@ -495,3 +495,29 @@ function TestNestedSuspenders(suspend) {
let wrapper = ToPromising(instance.exports.test); let wrapper = ToPromising(instance.exports.test);
assertThrows(wrapper, RangeError, /Maximum call stack size exceeded/); assertThrows(wrapper, RangeError, /Maximum call stack size exceeded/);
})(); })();
(function TestBadSuspender() {
print(arguments.callee.name);
let builder = new WasmModuleBuilder();
let import_index = builder.addImport('m', 'import', kSig_i_r);
builder.addFunction("test", kSig_i_r)
.addBody([
kExprLocalGet, 0,
kExprCallFunction, import_index, // suspend
]).exportFunc();
builder.addFunction("return_suspender", kSig_r_r)
.addBody([
kExprLocalGet, 0
]).exportFunc();
let js_import = new WebAssembly.Function(
{parameters: ['externref'], results: ['i32']},
() => Promise.resolve(42),
{suspending: 'first'});
let instance = builder.instantiate({m: {import: js_import}});
let suspender = ToPromising(instance.exports.return_suspender)();
for (s of [suspender, null, undefined, {}]) {
assertThrows(() => instance.exports.test(s),
WebAssembly.RuntimeError,
/invalid suspender object for suspend/);
}
})();