// Copyright 2016 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "src/common/assert-scope.h" #include "src/common/message-template.h" #include "src/compiler/wasm-compiler.h" #include "src/debug/debug.h" #include "src/execution/arguments-inl.h" #include "src/execution/frames.h" #include "src/heap/factory.h" #include "src/numbers/conversions.h" #include "src/objects/objects-inl.h" #include "src/strings/unicode-inl.h" #include "src/trap-handler/trap-handler.h" #include "src/wasm/module-compiler.h" #include "src/wasm/value-type.h" #include "src/wasm/wasm-code-manager.h" #include "src/wasm/wasm-constants.h" #include "src/wasm/wasm-debug.h" #include "src/wasm/wasm-engine.h" #include "src/wasm/wasm-objects.h" #include "src/wasm/wasm-subtyping.h" #include "src/wasm/wasm-value.h" namespace v8 { namespace internal { // TODO(13036): See if we can find a way to have the stack walker visit // tagged values being passed from Wasm to runtime functions. In the meantime, // disallow access to safe-looking-but-actually-unsafe stack-backed handles // and thereby force manual creation of safe handles (backed by HandleScope). class RuntimeArgumentsWithoutHandles : public RuntimeArguments { public: RuntimeArgumentsWithoutHandles(int length, Address* arguments) : RuntimeArguments(length, arguments) {} private: // Disallowing the superclass method. template V8_INLINE Handle at(int index) const; }; #define RuntimeArguments RuntimeArgumentsWithoutHandles // (End of TODO(13036)-related hackery.) namespace { template class FrameFinder { public: explicit FrameFinder(Isolate* isolate, std::initializer_list skipped_frame_types = {StackFrame::EXIT}) : frame_iterator_(isolate, isolate->thread_local_top()) { // We skip at least one frame. DCHECK_LT(0, skipped_frame_types.size()); for (auto type : skipped_frame_types) { DCHECK_EQ(type, frame_iterator_.frame()->type()); USE(type); frame_iterator_.Advance(); } // Type check the frame where the iterator stopped now. DCHECK_NOT_NULL(frame()); } FrameType* frame() { return FrameType::cast(frame_iterator_.frame()); } private: StackFrameIterator frame_iterator_; }; WasmInstanceObject GetWasmInstanceOnStackTop( Isolate* isolate, std::initializer_list skipped_frame_types = { StackFrame::EXIT}) { return FrameFinder(isolate, skipped_frame_types) .frame() ->wasm_instance(); } Context GetNativeContextFromWasmInstanceOnStackTop(Isolate* isolate) { return GetWasmInstanceOnStackTop(isolate).native_context(); } class V8_NODISCARD ClearThreadInWasmScope { public: explicit ClearThreadInWasmScope(Isolate* isolate) : isolate_(isolate) { DCHECK_IMPLIES(trap_handler::IsTrapHandlerEnabled(), trap_handler::IsThreadInWasm()); trap_handler::ClearThreadInWasm(); } ~ClearThreadInWasmScope() { DCHECK_IMPLIES(trap_handler::IsTrapHandlerEnabled(), !trap_handler::IsThreadInWasm()); if (!isolate_->has_pending_exception()) { trap_handler::SetThreadInWasm(); } // Otherwise we only want to set the flag if the exception is caught in // wasm. This is handled by the unwinder. } private: Isolate* isolate_; }; Object ThrowWasmError(Isolate* isolate, MessageTemplate message, Handle arg0 = Handle()) { Handle error_obj = isolate->factory()->NewWasmRuntimeError(message, arg0); JSObject::AddProperty(isolate, error_obj, isolate->factory()->wasm_uncatchable_symbol(), isolate->factory()->true_value(), NONE); return isolate->Throw(*error_obj); } } // namespace // Takes a JS object and a wasm type as Smi. Type checks the object against the // type; if the check succeeds, returns the object in its wasm representation; // otherwise throws a type error. RUNTIME_FUNCTION(Runtime_WasmJSToWasmObject) { // This code is called from wrappers, so the "thread is wasm" flag is not set. DCHECK_IMPLIES(trap_handler::IsTrapHandlerEnabled(), !trap_handler::IsThreadInWasm()); HandleScope scope(isolate); DCHECK_EQ(3, args.length()); // 'raw_instance' can be either a WasmInstanceObject or undefined. Object raw_instance = args[0]; Handle value(args[1], isolate); // Make sure ValueType fits properly in a Smi. static_assert(wasm::ValueType::kLastUsedBit + 1 <= kSmiValueSize); int raw_type = args.smi_value_at(2); const wasm::WasmModule* module = raw_instance.IsWasmInstanceObject() ? WasmInstanceObject::cast(raw_instance).module() : nullptr; wasm::ValueType type = wasm::ValueType::FromRawBitField(raw_type); const char* error_message; Handle result; bool success = internal::wasm::JSToWasmObject(isolate, module, value, type, &error_message) .ToHandle(&result); if (success) return *result; THROW_NEW_ERROR_RETURN_FAILURE( isolate, NewTypeError(MessageTemplate::kWasmTrapJSTypeError)); } RUNTIME_FUNCTION(Runtime_WasmMemoryGrow) { ClearThreadInWasmScope flag_scope(isolate); HandleScope scope(isolate); DCHECK_EQ(2, args.length()); WasmInstanceObject instance = WasmInstanceObject::cast(args[0]); // {delta_pages} is checked to be a positive smi in the WasmMemoryGrow builtin // which calls this runtime function. uint32_t delta_pages = args.positive_smi_value_at(1); int ret = WasmMemoryObject::Grow( isolate, handle(instance.memory_object(), isolate), delta_pages); // The WasmMemoryGrow builtin which calls this runtime function expects us to // always return a Smi. DCHECK(!isolate->has_pending_exception()); return Smi::FromInt(ret); } RUNTIME_FUNCTION(Runtime_ThrowWasmError) { ClearThreadInWasmScope flag_scope(isolate); HandleScope scope(isolate); DCHECK_EQ(1, args.length()); int message_id = args.smi_value_at(0); return ThrowWasmError(isolate, MessageTemplateFromInt(message_id)); } RUNTIME_FUNCTION(Runtime_ThrowWasmStackOverflow) { ClearThreadInWasmScope clear_wasm_flag(isolate); SealHandleScope shs(isolate); DCHECK_LE(0, args.length()); return isolate->StackOverflow(); } RUNTIME_FUNCTION(Runtime_WasmThrowJSTypeError) { // The caller may be wasm or JS. Only clear the thread_in_wasm flag if the // caller is wasm, and let the unwinder set it back depending on the handler. if (trap_handler::IsTrapHandlerEnabled() && trap_handler::IsThreadInWasm()) { trap_handler::ClearThreadInWasm(); } HandleScope scope(isolate); DCHECK_EQ(0, args.length()); THROW_NEW_ERROR_RETURN_FAILURE( 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) { ClearThreadInWasmScope clear_wasm_flag(isolate); HandleScope scope(isolate); DCHECK_EQ(2, args.length()); isolate->set_context(GetNativeContextFromWasmInstanceOnStackTop(isolate)); Handle tag(WasmExceptionTag::cast(args[0]), isolate); Handle values(FixedArray::cast(args[1]), isolate); Handle exception = WasmExceptionPackage::New(isolate, tag, values); wasm::GetWasmEngine()->SampleThrowEvent(isolate); return isolate->Throw(*exception); } RUNTIME_FUNCTION(Runtime_WasmReThrow) { ClearThreadInWasmScope clear_wasm_flag(isolate); HandleScope scope(isolate); DCHECK_EQ(1, args.length()); wasm::GetWasmEngine()->SampleRethrowEvent(isolate); return isolate->ReThrow(args[0]); } RUNTIME_FUNCTION(Runtime_WasmStackGuard) { ClearThreadInWasmScope wasm_flag(isolate); SealHandleScope shs(isolate); DCHECK_EQ(0, args.length()); // Check if this is a real stack overflow. StackLimitCheck check(isolate); if (check.JsHasOverflowed()) return isolate->StackOverflow(); return isolate->stack_guard()->HandleInterrupts(); } RUNTIME_FUNCTION(Runtime_WasmCompileLazy) { ClearThreadInWasmScope wasm_flag(isolate); HandleScope scope(isolate); DCHECK_EQ(3, args.length()); Handle instance(WasmInstanceObject::cast(args[0]), isolate); int func_index = args.smi_value_at(1); // Save the native_module on the stack, where the GC will use it to scan // WasmCompileLazy stack frames. wasm::NativeModule** native_module_stack_slot = reinterpret_cast(args.address_of_arg_at(2)); *native_module_stack_slot = instance->module_object().native_module(); DCHECK(isolate->context().is_null()); isolate->set_context(instance->native_context()); bool success = wasm::CompileLazy(isolate, instance, func_index); if (!success) { wasm::ThrowLazyCompilationError( isolate, instance->module_object().native_module(), func_index); DCHECK(isolate->has_pending_exception()); return ReadOnlyRoots{isolate}.exception(); } return Smi::FromInt(wasm::JumpTableOffset(instance->module(), func_index)); } namespace { void ReplaceWrapper(Isolate* isolate, Handle instance, int function_index, Handle wrapper_code) { Handle internal = WasmInstanceObject::GetWasmInternalFunction(isolate, instance, function_index) .ToHandleChecked(); Handle exported_function = handle(WasmExternalFunction::cast(internal->external()), isolate); exported_function->set_code(*wrapper_code, kReleaseStore); WasmExportedFunctionData function_data = exported_function->shared().wasm_exported_function_data(); function_data.set_wrapper_code(*wrapper_code); } } // namespace RUNTIME_FUNCTION(Runtime_WasmCompileWrapper) { HandleScope scope(isolate); DCHECK_EQ(2, args.length()); Handle instance(WasmInstanceObject::cast(args[0]), isolate); Handle function_data( WasmExportedFunctionData::cast(args[1]), isolate); DCHECK(isolate->context().is_null()); isolate->set_context(instance->native_context()); const wasm::WasmModule* module = instance->module(); const int function_index = function_data->function_index(); const wasm::WasmFunction& function = module->functions[function_index]; const wasm::FunctionSig* sig = function.sig; const uint32_t canonical_sig_index = module->isorecursive_canonical_type_ids[function.sig_index]; // The start function is not guaranteed to be registered as // an exported function (although it is called as one). // If there is no entry for the start function, // the tier-up is abandoned. if (WasmInstanceObject::GetWasmInternalFunction(isolate, instance, function_index) .is_null()) { DCHECK_EQ(function_index, module->start_function_index); return ReadOnlyRoots(isolate).undefined_value(); } Handle wrapper_code = wasm::JSToWasmWrapperCompilationUnit::CompileSpecificJSToWasmWrapper( isolate, sig, canonical_sig_index, module); // Replace the wrapper for the function that triggered the tier-up. // This is to verify that the wrapper is replaced, even if the function // is implicitly exported and is not part of the export_table. ReplaceWrapper(isolate, instance, function_index, wrapper_code); // Iterate over all exports to replace eagerly the wrapper for all functions // that share the signature of the function that tiered up. for (wasm::WasmExport exp : module->export_table) { if (exp.kind != wasm::kExternalFunction) { continue; } int index = static_cast(exp.index); const wasm::WasmFunction& exp_function = module->functions[index]; if (exp_function.sig == sig && index != function_index) { ReplaceWrapper(isolate, instance, index, wrapper_code); } } return ReadOnlyRoots(isolate).undefined_value(); } RUNTIME_FUNCTION(Runtime_WasmTriggerTierUp) { ClearThreadInWasmScope clear_wasm_flag(isolate); SealHandleScope shs(isolate); // We're reusing this interrupt mechanism to interrupt long-running loops. StackLimitCheck check(isolate); DCHECK(!check.JsHasOverflowed()); if (check.InterruptRequested()) { Object result = isolate->stack_guard()->HandleInterrupts(); if (result.IsException()) return result; } DisallowGarbageCollection no_gc; DCHECK_EQ(1, args.length()); WasmInstanceObject instance = WasmInstanceObject::cast(args[0]); FrameFinder frame_finder(isolate); int func_index = frame_finder.frame()->function_index(); DCHECK_EQ(instance, frame_finder.frame()->wasm_instance()); wasm::TriggerTierUp(instance, func_index); return ReadOnlyRoots(isolate).undefined_value(); } RUNTIME_FUNCTION(Runtime_WasmAtomicNotify) { ClearThreadInWasmScope clear_wasm_flag(isolate); HandleScope scope(isolate); DCHECK_EQ(3, args.length()); WasmInstanceObject instance = WasmInstanceObject::cast(args[0]); double offset_double = args.number_value_at(1); uintptr_t offset = static_cast(offset_double); uint32_t count = NumberToUint32(args[2]); Handle array_buffer{instance.memory_object().array_buffer(), isolate}; // Should have trapped if address was OOB. DCHECK_LT(offset, array_buffer->byte_length()); if (!array_buffer->is_shared()) return Smi::FromInt(0); return FutexEmulation::Wake(array_buffer, offset, count); } RUNTIME_FUNCTION(Runtime_WasmI32AtomicWait) { ClearThreadInWasmScope clear_wasm_flag(isolate); HandleScope scope(isolate); DCHECK_EQ(4, args.length()); WasmInstanceObject instance = WasmInstanceObject::cast(args[0]); double offset_double = args.number_value_at(1); uintptr_t offset = static_cast(offset_double); int32_t expected_value = NumberToInt32(args[2]); BigInt timeout_ns = BigInt::cast(args[3]); Handle array_buffer{instance.memory_object().array_buffer(), isolate}; // Should have trapped if address was OOB. DCHECK_LT(offset, array_buffer->byte_length()); // Trap if memory is not shared, or wait is not allowed on the isolate if (!array_buffer->is_shared() || !isolate->allow_atomics_wait()) { return ThrowWasmError( isolate, MessageTemplate::kAtomicsOperationNotAllowed, isolate->factory()->NewStringFromAsciiChecked("Atomics.wait")); } return FutexEmulation::WaitWasm32(isolate, array_buffer, offset, expected_value, timeout_ns.AsInt64()); } RUNTIME_FUNCTION(Runtime_WasmI64AtomicWait) { ClearThreadInWasmScope clear_wasm_flag(isolate); HandleScope scope(isolate); DCHECK_EQ(4, args.length()); WasmInstanceObject instance = WasmInstanceObject::cast(args[0]); double offset_double = args.number_value_at(1); uintptr_t offset = static_cast(offset_double); BigInt expected_value = BigInt::cast(args[2]); BigInt timeout_ns = BigInt::cast(args[3]); Handle array_buffer{instance.memory_object().array_buffer(), isolate}; // Should have trapped if address was OOB. DCHECK_LT(offset, array_buffer->byte_length()); // Trap if memory is not shared, or if wait is not allowed on the isolate if (!array_buffer->is_shared() || !isolate->allow_atomics_wait()) { return ThrowWasmError( isolate, MessageTemplate::kAtomicsOperationNotAllowed, isolate->factory()->NewStringFromAsciiChecked("Atomics.wait")); } return FutexEmulation::WaitWasm64(isolate, array_buffer, offset, expected_value.AsInt64(), timeout_ns.AsInt64()); } namespace { Object ThrowTableOutOfBounds(Isolate* isolate, Handle instance) { // Handle out-of-bounds access here in the runtime call, rather // than having the lower-level layers deal with JS exceptions. if (isolate->context().is_null()) { isolate->set_context(instance->native_context()); } return ThrowWasmError(isolate, MessageTemplate::kWasmTrapTableOutOfBounds); } } // namespace RUNTIME_FUNCTION(Runtime_WasmRefFunc) { ClearThreadInWasmScope flag_scope(isolate); HandleScope scope(isolate); DCHECK_EQ(2, args.length()); Handle instance(WasmInstanceObject::cast(args[0]), isolate); uint32_t function_index = args.positive_smi_value_at(1); return *WasmInstanceObject::GetOrCreateWasmInternalFunction(isolate, instance, function_index); } RUNTIME_FUNCTION(Runtime_WasmFunctionTableGet) { ClearThreadInWasmScope flag_scope(isolate); HandleScope scope(isolate); DCHECK_EQ(3, args.length()); WasmInstanceObject instance = WasmInstanceObject::cast(args[0]); uint32_t table_index = args.positive_smi_value_at(1); uint32_t entry_index = args.positive_smi_value_at(2); DCHECK_LT(table_index, instance.tables().length()); auto table = handle(WasmTableObject::cast(instance.tables().get(table_index)), isolate); // We only use the runtime call for lazily initialized function references. DCHECK( table->instance().IsUndefined() ? table->type() == wasm::kWasmFuncRef : IsSubtypeOf(table->type(), wasm::kWasmFuncRef, WasmInstanceObject::cast(table->instance()).module())); if (!WasmTableObject::IsInBounds(isolate, table, entry_index)) { return ThrowWasmError(isolate, MessageTemplate::kWasmTrapTableOutOfBounds); } return *WasmTableObject::Get(isolate, table, entry_index); } RUNTIME_FUNCTION(Runtime_WasmFunctionTableSet) { ClearThreadInWasmScope flag_scope(isolate); HandleScope scope(isolate); DCHECK_EQ(4, args.length()); WasmInstanceObject instance = WasmInstanceObject::cast(args[0]); uint32_t table_index = args.positive_smi_value_at(1); uint32_t entry_index = args.positive_smi_value_at(2); Handle element(args[3], isolate); DCHECK_LT(table_index, instance.tables().length()); auto table = handle(WasmTableObject::cast(instance.tables().get(table_index)), isolate); // We only use the runtime call for lazily initialized function references. DCHECK( table->instance().IsUndefined() ? table->type() == wasm::kWasmFuncRef : IsSubtypeOf(table->type(), wasm::kWasmFuncRef, WasmInstanceObject::cast(table->instance()).module())); if (!WasmTableObject::IsInBounds(isolate, table, entry_index)) { return ThrowWasmError(isolate, MessageTemplate::kWasmTrapTableOutOfBounds); } WasmTableObject::Set(isolate, table, entry_index, element); return ReadOnlyRoots(isolate).undefined_value(); } RUNTIME_FUNCTION(Runtime_WasmTableInit) { ClearThreadInWasmScope flag_scope(isolate); HandleScope scope(isolate); DCHECK_EQ(6, args.length()); Handle instance(WasmInstanceObject::cast(args[0]), isolate); uint32_t table_index = args.positive_smi_value_at(1); uint32_t elem_segment_index = args.positive_smi_value_at(2); static_assert( wasm::kV8MaxWasmTableSize < kSmiMaxValue, "Make sure clamping to Smi range doesn't make an invalid call valid"); uint32_t dst = args.positive_smi_value_at(3); uint32_t src = args.positive_smi_value_at(4); uint32_t count = args.positive_smi_value_at(5); DCHECK(!isolate->context().is_null()); base::Optional opt_error = WasmInstanceObject::InitTableEntries(isolate, instance, table_index, elem_segment_index, dst, src, count); if (opt_error.has_value()) { return ThrowWasmError(isolate, opt_error.value()); } return ReadOnlyRoots(isolate).undefined_value(); } RUNTIME_FUNCTION(Runtime_WasmTableCopy) { ClearThreadInWasmScope flag_scope(isolate); HandleScope scope(isolate); DCHECK_EQ(6, args.length()); Handle instance(WasmInstanceObject::cast(args[0]), isolate); uint32_t table_dst_index = args.positive_smi_value_at(1); uint32_t table_src_index = args.positive_smi_value_at(2); static_assert( wasm::kV8MaxWasmTableSize < kSmiMaxValue, "Make sure clamping to Smi range doesn't make an invalid call valid"); uint32_t dst = args.positive_smi_value_at(3); uint32_t src = args.positive_smi_value_at(4); uint32_t count = args.positive_smi_value_at(5); DCHECK(!isolate->context().is_null()); bool oob = !WasmInstanceObject::CopyTableEntries( isolate, instance, table_dst_index, table_src_index, dst, src, count); if (oob) return ThrowTableOutOfBounds(isolate, instance); return ReadOnlyRoots(isolate).undefined_value(); } RUNTIME_FUNCTION(Runtime_WasmTableGrow) { ClearThreadInWasmScope flag_scope(isolate); HandleScope scope(isolate); DCHECK_EQ(4, args.length()); WasmInstanceObject instance = WasmInstanceObject::cast(args[0]); uint32_t table_index = args.positive_smi_value_at(1); Handle value(args[2], isolate); uint32_t delta = args.positive_smi_value_at(3); Handle table( WasmTableObject::cast(instance.tables().get(table_index)), isolate); int result = WasmTableObject::Grow(isolate, table, delta, value); return Smi::FromInt(result); } RUNTIME_FUNCTION(Runtime_WasmTableFill) { ClearThreadInWasmScope flag_scope(isolate); HandleScope scope(isolate); DCHECK_EQ(5, args.length()); Handle instance(WasmInstanceObject::cast(args[0]), isolate); uint32_t table_index = args.positive_smi_value_at(1); uint32_t start = args.positive_smi_value_at(2); Handle value(args[3], isolate); uint32_t count = args.positive_smi_value_at(4); Handle table( WasmTableObject::cast(instance->tables().get(table_index)), isolate); uint32_t table_size = table->current_length(); if (start > table_size) { return ThrowTableOutOfBounds(isolate, instance); } // Even when table.fill goes out-of-bounds, as many entries as possible are // put into the table. Only afterwards we trap. uint32_t fill_count = std::min(count, table_size - start); if (fill_count < count) { return ThrowTableOutOfBounds(isolate, instance); } WasmTableObject::Fill(isolate, table, start, value, fill_count); return ReadOnlyRoots(isolate).undefined_value(); } namespace { // Returns true if any breakpoint was hit, false otherwise. bool ExecuteWasmDebugBreaks(Isolate* isolate, Handle instance, WasmFrame* frame) { Handle