LibWasm: Make traps hold on to externally-managed data

...instead of specially handling JS::Completion.
This makes it possible for LibWeb/LibJS to have full control over how
these things are made, stored, and visited (whenever).

Fixes an issue where we couldn't roundtrip a JS exception through Wasm.
This commit is contained in:
Ali Mohammad Pur 2025-04-22 09:48:26 +02:00 committed by Andrew Kaster
commit 51bab5b186
Notes: github-actions[bot] 2025-04-22 14:45:25 +00:00
11 changed files with 176 additions and 111 deletions

View file

@ -239,6 +239,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
} }
BytecodeInterpreter interpreter(m_stack_info); BytecodeInterpreter interpreter(m_stack_info);
auto handle = register_scoped(interpreter);
for (auto& entry : module.global_section().entries()) { for (auto& entry : module.global_section().entries()) {
Configuration config { m_store }; Configuration config { m_store };
@ -250,9 +251,9 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
entry.expression(), entry.expression(),
1, 1,
}); });
auto result = config.execute(interpreter).assert_wasm_result(); auto result = config.execute(interpreter);
if (result.is_trap()) if (result.is_trap())
return InstantiationError { ByteString::formatted("Global value construction trapped: {}", result.trap().reason) }; return InstantiationError { "Global instantiation trapped", move(result.trap()) };
global_values.append(result.values().first()); global_values.append(result.values().first());
} }
@ -271,9 +272,9 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
entry, entry,
entry.instructions().size(), entry.instructions().size(),
}); });
auto result = config.execute(interpreter).assert_wasm_result(); auto result = config.execute(interpreter);
if (result.is_trap()) if (result.is_trap())
return InstantiationError { ByteString::formatted("Element construction trapped: {}", result.trap().reason) }; return InstantiationError { "Element section initialisation trapped", move(result.trap()) };
for (auto& value : result.values()) { for (auto& value : result.values()) {
auto reference = value.to<Reference>(); auto reference = value.to<Reference>();
@ -306,9 +307,9 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
active_ptr->expression, active_ptr->expression,
1, 1,
}); });
auto result = config.execute(interpreter).assert_wasm_result(); auto result = config.execute(interpreter);
if (result.is_trap()) if (result.is_trap())
return InstantiationError { ByteString::formatted("Element section initialisation trapped: {}", result.trap().reason) }; return InstantiationError { "Element section initialisation trapped", move(result.trap()) };
auto d = result.values().first().to<i32>(); auto d = result.values().first().to<i32>();
auto table_instance = m_store.get(main_module_instance.tables()[active_ptr->index.value()]); auto table_instance = m_store.get(main_module_instance.tables()[active_ptr->index.value()]);
if (current_index >= main_module_instance.elements().size()) if (current_index >= main_module_instance.elements().size())
@ -341,9 +342,9 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
data.offset, data.offset,
1, 1,
}); });
auto result = config.execute(interpreter).assert_wasm_result(); auto result = config.execute(interpreter);
if (result.is_trap()) if (result.is_trap())
return InstantiationError { ByteString::formatted("Data section initialisation trapped: {}", result.trap().reason) }; return InstantiationError { "Data section initialisation trapped", move(result.trap()) };
size_t offset = result.values().first().to<u64>(); size_t offset = result.values().first().to<u64>();
if (main_module_instance.memories().size() <= data.index.value()) { if (main_module_instance.memories().size() <= data.index.value()) {
return InstantiationError { return InstantiationError {
@ -391,7 +392,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
} }
auto result = invoke(functions[index.value()], {}); auto result = invoke(functions[index.value()], {});
if (result.is_trap()) if (result.is_trap())
return InstantiationError { ByteString::formatted("Start function trapped: {}", result.trap().reason) }; return InstantiationError { "Start function trapped", move(result.trap()) };
} }
return InstantiationResult { move(main_module_instance_pointer) }; return InstantiationResult { move(main_module_instance_pointer) };
@ -491,6 +492,7 @@ Optional<InstantiationError> AbstractMachine::allocate_all_final_phase(Module co
Result AbstractMachine::invoke(FunctionAddress address, Vector<Value> arguments) Result AbstractMachine::invoke(FunctionAddress address, Vector<Value> arguments)
{ {
BytecodeInterpreter interpreter(m_stack_info); BytecodeInterpreter interpreter(m_stack_info);
auto handle = register_scoped(interpreter);
return invoke(interpreter, address, move(arguments)); return invoke(interpreter, address, move(arguments));
} }
@ -575,4 +577,10 @@ void Linker::populate()
m_unresolved_imports.set(m_ordered_imports.last()); m_unresolved_imports.set(m_ordered_imports.last());
} }
} }
void AbstractMachine::visit_external_resources(HostVisitOps const& host)
{
for (auto interpreter_ptr : m_active_interpreters)
interpreter_ptr->visit_external_resources(host);
}
} }

View file

@ -6,6 +6,7 @@
#pragma once #pragma once
#include <AK/ByteBuffer.h>
#include <AK/Function.h> #include <AK/Function.h>
#include <AK/HashMap.h> #include <AK/HashMap.h>
#include <AK/HashTable.h> #include <AK/HashTable.h>
@ -14,18 +15,13 @@
#include <AK/UFixedBigInt.h> #include <AK/UFixedBigInt.h>
#include <LibWasm/Types.h> #include <LibWasm/Types.h>
// NOTE: Special case for Wasm::Result.
#include <LibJS/Runtime/Completion.h>
namespace Wasm { namespace Wasm {
class Configuration; class Configuration;
class Result; class Result;
struct Interpreter; struct Interpreter;
struct Trap;
struct InstantiationError {
ByteString error { "Unknown error" };
};
struct LinkError { struct LinkError {
enum OtherErrors { enum OtherErrors {
InvalidImportedModule, InvalidImportedModule,
@ -198,37 +194,43 @@ private:
u128 m_value; u128 m_value;
}; };
struct Trap { struct ExternallyManagedTrap {
ByteString reason; Array<u8, 64> data;
template<typename T>
T const& unsafe_external_object_as() const
{
static_assert(sizeof(T) <= sizeof(data), "Object size is too large for ExternallyManagedTrap");
return *reinterpret_cast<T const*>(data.data());
}
}; };
// A variant of Result that does not include external reasons for error (JS::Completion, for now). struct Trap {
class PureResult { Variant<ByteString, ExternallyManagedTrap> data;
public:
explicit PureResult(Vector<Value> values) ByteString format() const
: m_result(move(values))
{ {
if (auto const* ptr = data.get_pointer<ByteString>())
return *ptr;
return "<Externally managed Trap Data>";
} }
PureResult(Trap trap) template<typename T>
: m_result(move(trap)) static Trap from_external_object(T&& object)
{ {
static_assert(sizeof(T) <= sizeof(ExternallyManagedTrap::data), "Object size is too large for ExternallyManagedTrap");
static_assert(IsTriviallyCopyable<T>, "Object must be trivially copyable");
static_assert(IsTriviallyDestructible<T>, "Object must be trivially destructible");
ExternallyManagedTrap externally_managed_trap;
new (externally_managed_trap.data.data()) T(forward<T>(object));
return Trap { externally_managed_trap };
} }
auto is_trap() const { return m_result.has<Trap>(); } static Trap from_string(ByteString string)
auto& values() const { return m_result.get<Vector<Value>>(); }
auto& values() { return m_result.get<Vector<Value>>(); }
auto& trap() const { return m_result.get<Trap>(); }
auto& trap() { return m_result.get<Trap>(); }
private:
friend class Result;
explicit PureResult(Variant<Vector<Value>, Trap>&& result)
: m_result(move(result))
{ {
return Trap { move(string) };
} }
Variant<Vector<Value>, Trap> m_result;
}; };
class Result { class Result {
@ -243,34 +245,24 @@ public:
{ {
} }
Result(JS::Completion completion)
: m_result(move(completion))
{
VERIFY(m_result.get<JS::Completion>().is_abrupt());
}
Result(PureResult&& result)
: m_result(result.m_result.downcast<decltype(m_result)>())
{
}
auto is_trap() const { return m_result.has<Trap>(); } auto is_trap() const { return m_result.has<Trap>(); }
auto is_completion() const { return m_result.has<JS::Completion>(); }
auto& values() const { return m_result.get<Vector<Value>>(); } auto& values() const { return m_result.get<Vector<Value>>(); }
auto& values() { return m_result.get<Vector<Value>>(); } auto& values() { return m_result.get<Vector<Value>>(); }
auto& trap() const { return m_result.get<Trap>(); } auto& trap() const { return m_result.get<Trap>(); }
auto& trap() { return m_result.get<Trap>(); } auto& trap() { return m_result.get<Trap>(); }
auto& completion() { return m_result.get<JS::Completion>(); }
auto& completion() const { return m_result.get<JS::Completion>(); }
PureResult assert_wasm_result() &&
{
VERIFY(!is_completion());
return PureResult(move(m_result).downcast<Vector<Value>, Trap>());
}
private: private:
Variant<Vector<Value>, Trap, JS::Completion> m_result; explicit Result(Variant<Vector<Value>, Trap>&& result)
: m_result(move(result))
{
}
Variant<Vector<Value>, Trap> m_result;
};
struct InstantiationError {
ByteString error { "Unknown error" };
Optional<Trap> relevant_trap {};
}; };
using ExternValue = Variant<FunctionAddress, TableAddress, MemoryAddress, GlobalAddress>; using ExternValue = Variant<FunctionAddress, TableAddress, MemoryAddress, GlobalAddress>;
@ -628,6 +620,10 @@ private:
using InstantiationResult = AK::ErrorOr<NonnullOwnPtr<ModuleInstance>, InstantiationError>; using InstantiationResult = AK::ErrorOr<NonnullOwnPtr<ModuleInstance>, InstantiationError>;
struct HostVisitOps {
Function<void(ExternallyManagedTrap&)> visit_trap;
};
class AbstractMachine { class AbstractMachine {
public: public:
explicit AbstractMachine() = default; explicit AbstractMachine() = default;
@ -644,11 +640,38 @@ public:
void enable_instruction_count_limit() { m_should_limit_instruction_count = true; } void enable_instruction_count_limit() { m_should_limit_instruction_count = true; }
void visit_external_resources(HostVisitOps const&);
private: private:
class InterpreterHandle {
public:
explicit InterpreterHandle(AbstractMachine& machine, Interpreter& interpreter)
: m_machine(machine)
, m_interpreter(interpreter)
{
m_machine.m_active_interpreters.set(&m_interpreter);
}
~InterpreterHandle()
{
m_machine.m_active_interpreters.remove(&m_interpreter);
}
private:
AbstractMachine& m_machine;
Interpreter& m_interpreter;
};
[[nodiscard]] InterpreterHandle register_scoped(Interpreter& interpreter)
{
return InterpreterHandle(*this, interpreter);
}
Optional<InstantiationError> allocate_all_initial_phase(Module const&, ModuleInstance&, Vector<ExternValue>&, Vector<Value>& global_values, Vector<FunctionAddress>& own_functions); Optional<InstantiationError> allocate_all_initial_phase(Module const&, ModuleInstance&, Vector<ExternValue>&, Vector<Value>& global_values, Vector<FunctionAddress>& own_functions);
Optional<InstantiationError> allocate_all_final_phase(Module const&, ModuleInstance&, Vector<Vector<Reference>>& elements); Optional<InstantiationError> allocate_all_final_phase(Module const&, ModuleInstance&, Vector<Vector<Reference>>& elements);
Store m_store; Store m_store;
StackInfo m_stack_info; StackInfo m_stack_info;
HashTable<Interpreter*> m_active_interpreters;
bool m_should_limit_instruction_count { false }; bool m_should_limit_instruction_count { false };
}; };

View file

@ -42,7 +42,7 @@ void BytecodeInterpreter::interpret(Configuration& configuration)
while (current_ip_value < max_ip_value) { while (current_ip_value < max_ip_value) {
if (should_limit_instruction_count) { if (should_limit_instruction_count) {
if (executed_instructions++ >= Constants::max_allowed_executed_instructions_per_call) [[unlikely]] { if (executed_instructions++ >= Constants::max_allowed_executed_instructions_per_call) [[unlikely]] {
m_trap = Trap { "Exceeded maximum allowed number of instructions" }; m_trap = Trap::from_string("Exceeded maximum allowed number of instructions");
return; return;
} }
} }
@ -78,7 +78,7 @@ void BytecodeInterpreter::load_and_push(Configuration& configuration, Instructio
auto base = entry.to<i32>(); auto base = entry.to<i32>();
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset; u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
if (instance_address + sizeof(ReadType) > memory->size()) { if (instance_address + sizeof(ReadType) > memory->size()) {
m_trap = Trap { "Memory access out of bounds" }; m_trap = Trap::from_string("Memory access out of bounds");
dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + sizeof(ReadType), memory->size()); dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + sizeof(ReadType), memory->size());
return; return;
} }
@ -103,7 +103,7 @@ void BytecodeInterpreter::load_and_push_mxn(Configuration& configuration, Instru
auto base = entry.to<i32>(); auto base = entry.to<i32>();
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset; u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
if (instance_address + M * N / 8 > memory->size()) { if (instance_address + M * N / 8 > memory->size()) {
m_trap = Trap { "Memory access out of bounds" }; m_trap = Trap::from_string("Memory access out of bounds");
dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + M * N / 8, memory->size()); dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + M * N / 8, memory->size());
return; return;
} }
@ -131,7 +131,7 @@ void BytecodeInterpreter::load_and_push_lane_n(Configuration& configuration, Ins
auto base = configuration.value_stack().take_last().to<u32>(); auto base = configuration.value_stack().take_last().to<u32>();
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + memarg_and_lane.memory.offset; u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + memarg_and_lane.memory.offset;
if (instance_address + N / 8 > memory->size()) { if (instance_address + N / 8 > memory->size()) {
m_trap = Trap { "Memory access out of bounds" }; m_trap = Trap::from_string("Memory access out of bounds");
return; return;
} }
auto slice = memory->data().bytes().slice(instance_address, N / 8); auto slice = memory->data().bytes().slice(instance_address, N / 8);
@ -149,7 +149,7 @@ void BytecodeInterpreter::load_and_push_zero_n(Configuration& configuration, Ins
auto base = configuration.value_stack().take_last().to<u32>(); auto base = configuration.value_stack().take_last().to<u32>();
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + memarg_and_lane.offset; u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + memarg_and_lane.offset;
if (instance_address + N / 8 > memory->size()) { if (instance_address + N / 8 > memory->size()) {
m_trap = Trap { "Memory access out of bounds" }; m_trap = Trap::from_string("Memory access out of bounds");
return; return;
} }
auto slice = memory->data().bytes().slice(instance_address, N / 8); auto slice = memory->data().bytes().slice(instance_address, N / 8);
@ -168,7 +168,7 @@ void BytecodeInterpreter::load_and_push_m_splat(Configuration& configuration, In
auto base = entry.to<i32>(); auto base = entry.to<i32>();
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset; u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
if (instance_address + M / 8 > memory->size()) { if (instance_address + M / 8 > memory->size()) {
m_trap = Trap { "Memory access out of bounds" }; m_trap = Trap::from_string("Memory access out of bounds");
dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + M / 8, memory->size()); dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + M / 8, memory->size());
return; return;
} }
@ -238,7 +238,7 @@ void BytecodeInterpreter::call_address(Configuration& configuration, FunctionAdd
configuration.value_stack().remove(configuration.value_stack().size() - span.size(), span.size()); configuration.value_stack().remove(configuration.value_stack().size() - span.size(), span.size());
Result result { Trap { ""sv } }; Result result { Trap::from_string("") };
if (instance->has<WasmFunction>()) { if (instance->has<WasmFunction>()) {
CallFrameHandle handle { *this, configuration }; CallFrameHandle handle { *this, configuration };
result = configuration.call(*this, address, move(args)); result = configuration.call(*this, address, move(args));
@ -251,11 +251,6 @@ void BytecodeInterpreter::call_address(Configuration& configuration, FunctionAdd
return; return;
} }
if (result.is_completion()) {
m_trap = move(result.completion());
return;
}
configuration.value_stack().ensure_capacity(configuration.value_stack().size() + result.values().size()); configuration.value_stack().ensure_capacity(configuration.value_stack().size() + result.values().size());
for (auto& entry : result.values().in_reverse()) for (auto& entry : result.values().in_reverse())
configuration.value_stack().unchecked_append(entry); configuration.value_stack().unchecked_append(entry);
@ -361,7 +356,7 @@ void BytecodeInterpreter::store_to_memory(Configuration& configuration, Instruct
Checked addition { instance_address }; Checked addition { instance_address };
addition += data.size(); addition += data.size();
if (addition.has_overflow() || addition.value() > memory->size()) { if (addition.has_overflow() || addition.value() > memory->size()) {
m_trap = Trap { "Memory access out of bounds" }; m_trap = Trap::from_string("Memory access out of bounds");
dbgln("LibWasm: Memory access out of bounds (expected 0 <= {} and {} <= {})", instance_address, instance_address + data.size(), memory->size()); dbgln("LibWasm: Memory access out of bounds (expected 0 <= {} and {} <= {})", instance_address, instance_address + data.size(), memory->size());
return; return;
} }
@ -376,7 +371,7 @@ T BytecodeInterpreter::read_value(ReadonlyBytes data)
auto value_or_error = stream.read_value<LittleEndian<T>>(); auto value_or_error = stream.read_value<LittleEndian<T>>();
if (value_or_error.is_error()) { if (value_or_error.is_error()) {
dbgln("Read from {} failed", data.data()); dbgln("Read from {} failed", data.data());
m_trap = Trap { "Read from memory failed" }; m_trap = Trap::from_string("Read from memory failed");
} }
return value_or_error.release_value(); return value_or_error.release_value();
} }
@ -387,7 +382,7 @@ float BytecodeInterpreter::read_value<float>(ReadonlyBytes data)
FixedMemoryStream stream { data }; FixedMemoryStream stream { data };
auto raw_value_or_error = stream.read_value<LittleEndian<u32>>(); auto raw_value_or_error = stream.read_value<LittleEndian<u32>>();
if (raw_value_or_error.is_error()) if (raw_value_or_error.is_error())
m_trap = Trap { "Read from memory failed" }; m_trap = Trap::from_string("Read from memory failed");
auto raw_value = raw_value_or_error.release_value(); auto raw_value = raw_value_or_error.release_value();
return bit_cast<float>(static_cast<u32>(raw_value)); return bit_cast<float>(static_cast<u32>(raw_value));
} }
@ -398,7 +393,7 @@ double BytecodeInterpreter::read_value<double>(ReadonlyBytes data)
FixedMemoryStream stream { data }; FixedMemoryStream stream { data };
auto raw_value_or_error = stream.read_value<LittleEndian<u64>>(); auto raw_value_or_error = stream.read_value<LittleEndian<u64>>();
if (raw_value_or_error.is_error()) if (raw_value_or_error.is_error())
m_trap = Trap { "Read from memory failed" }; m_trap = Trap::from_string("Read from memory failed");
auto raw_value = raw_value_or_error.release_value(); auto raw_value = raw_value_or_error.release_value();
return bit_cast<double>(static_cast<u64>(raw_value)); return bit_cast<double>(static_cast<u64>(raw_value));
} }
@ -409,7 +404,7 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
switch (instruction.opcode().value()) { switch (instruction.opcode().value()) {
case Instructions::unreachable.value(): case Instructions::unreachable.value():
m_trap = Trap { "Unreachable" }; m_trap = Trap::from_string("Unreachable");
return; return;
case Instructions::nop.value(): case Instructions::nop.value():
return; return;
@ -1659,7 +1654,7 @@ void DebuggerBytecodeInterpreter::interpret_instruction(Configuration& configura
if (pre_interpret_hook) { if (pre_interpret_hook) {
auto result = pre_interpret_hook(configuration, ip, instruction); auto result = pre_interpret_hook(configuration, ip, instruction);
if (!result) { if (!result) {
m_trap = Trap { "Trapped by user request" }; m_trap = Trap::from_string("Trapped by user request");
return; return;
} }
} }
@ -1669,7 +1664,7 @@ void DebuggerBytecodeInterpreter::interpret_instruction(Configuration& configura
if (post_interpret_hook) { if (post_interpret_hook) {
auto result = post_interpret_hook(configuration, ip, instruction, *this); auto result = post_interpret_hook(configuration, ip, instruction, *this);
if (!result) { if (!result) {
m_trap = Trap { "Trapped by user request" }; m_trap = Trap::from_string("Trapped by user request");
return; return;
} }
} }

View file

@ -22,14 +22,17 @@ struct BytecodeInterpreter : public Interpreter {
virtual ~BytecodeInterpreter() override = default; virtual ~BytecodeInterpreter() override = default;
virtual bool did_trap() const final { return !m_trap.has<Empty>(); } virtual bool did_trap() const final { return !m_trap.has<Empty>(); }
virtual ByteString trap_reason() const final virtual Trap trap() const final
{ {
return m_trap.visit( return m_trap.get<Trap>();
[](Empty) -> ByteString { VERIFY_NOT_REACHED(); },
[](Trap const& trap) { return trap.reason; },
[](JS::Completion const& completion) { return completion.value().to_string_without_side_effects().to_byte_string(); });
} }
virtual void clear_trap() final { m_trap = Empty {}; } virtual void clear_trap() final { m_trap = Empty {}; }
virtual void visit_external_resources(HostVisitOps const& host) override
{
if (auto ptr = m_trap.get_pointer<Trap>())
if (auto data = ptr->data.get_pointer<ExternallyManagedTrap>())
host.visit_trap(*data);
}
struct CallFrameHandle { struct CallFrameHandle {
explicit CallFrameHandle(BytecodeInterpreter& interpreter, Configuration& configuration) explicit CallFrameHandle(BytecodeInterpreter& interpreter, Configuration& configuration)
@ -82,11 +85,11 @@ protected:
ALWAYS_INLINE bool trap_if_not(bool value, StringView reason) ALWAYS_INLINE bool trap_if_not(bool value, StringView reason)
{ {
if (!value) if (!value)
m_trap = Trap { reason }; m_trap = Trap { ByteString(reason) };
return !m_trap.has<Empty>(); return !m_trap.has<Empty>();
} }
Variant<Trap, JS::Completion, Empty> m_trap; Variant<Trap, Empty> m_trap;
StackInfo const& m_stack_info; StackInfo const& m_stack_info;
}; };

View file

@ -22,7 +22,7 @@ Result Configuration::call(Interpreter& interpreter, FunctionAddress address, Ve
{ {
auto* function = m_store.get(address); auto* function = m_store.get(address);
if (!function) if (!function)
return Trap {}; return Trap::from_string("Attempt to call nonexistent function by address");
if (auto* wasm_function = function->get_pointer<WasmFunction>()) { if (auto* wasm_function = function->get_pointer<WasmFunction>()) {
Vector<Value> locals = move(arguments); Vector<Value> locals = move(arguments);
locals.ensure_capacity(locals.size() + wasm_function->code().func().locals().size()); locals.ensure_capacity(locals.size() + wasm_function->code().func().locals().size());
@ -50,7 +50,7 @@ Result Configuration::execute(Interpreter& interpreter)
{ {
interpreter.interpret(*this); interpreter.interpret(*this);
if (interpreter.did_trap()) if (interpreter.did_trap())
return Trap { interpreter.trap_reason() }; return interpreter.trap();
Vector<Value> results; Vector<Value> results;
results.ensure_capacity(frame().arity()); results.ensure_capacity(frame().arity());

View file

@ -13,9 +13,10 @@ namespace Wasm {
struct Interpreter { struct Interpreter {
virtual ~Interpreter() = default; virtual ~Interpreter() = default;
virtual void interpret(Configuration&) = 0; virtual void interpret(Configuration&) = 0;
virtual Trap trap() const = 0;
virtual bool did_trap() const = 0; virtual bool did_trap() const = 0;
virtual ByteString trap_reason() const = 0;
virtual void clear_trap() = 0; virtual void clear_trap() = 0;
virtual void visit_external_resources(HostVisitOps const&) { }
}; };
} }

View file

@ -12,6 +12,6 @@ if (NOT WIN32)
endif() endif()
serenity_lib(LibWasm wasm) serenity_lib(LibWasm wasm)
target_link_libraries(LibWasm PRIVATE LibCore LibGC LibJS) target_link_libraries(LibWasm PRIVATE LibCore)
include(wasm_spec_tests) include(wasm_spec_tests)

View file

@ -59,6 +59,10 @@ void visit_edges(JS::Object& object, JS::Cell::Visitor& visitor)
visitor.visit(cache.imported_objects()); visitor.visit(cache.imported_objects());
visitor.visit(cache.extern_values()); visitor.visit(cache.extern_values());
visitor.visit(cache.global_instances()); visitor.visit(cache.global_instances());
cache.abstract_machine().visit_external_resources({ .visit_trap = [&visitor](Wasm::ExternallyManagedTrap const& trap) {
auto& completion = trap.unsafe_external_object_as<JS::Completion>();
visitor.visit(completion.value());
} });
} }
} }
@ -156,6 +160,32 @@ WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> instantiate_streaming(JS::VM& vm,
namespace Detail { namespace Detail {
#define TRY_OR_RETURN_TRAP(...) \
({ \
/* Ignore -Wshadow to allow nesting the macro. */ \
AK_IGNORE_DIAGNOSTIC("-Wshadow", \
auto&& _temporary_result = (__VA_ARGS__)); \
static_assert(!::AK::Detail::IsLvalueReference<decltype(_temporary_result.release_value())>, \
"Do not return a reference from a fallible expression"); \
if (_temporary_result.is_error()) [[unlikely]] \
return Wasm::Trap::from_external_object(_temporary_result.release_error()); \
_temporary_result.release_value(); \
})
#define TRY_OR_RETURN_OOM_TRAP(vm, ...) \
({ \
/* Ignore -Wshadow to allow nesting the macro. */ \
AK_IGNORE_DIAGNOSTIC("-Wshadow", \
auto&& _temporary_result = (__VA_ARGS__)); \
if (_temporary_result.is_error()) { \
VERIFY(_temporary_result.error().code() == ENOMEM); \
return Wasm::Trap::from_external_object((vm).throw_completion<JS::InternalError>((vm).error_message(::JS::VM::ErrorMessage::OutOfMemory))); \
} \
static_assert(!::AK::Detail::IsLvalueReference<decltype(_temporary_result.release_value())>, \
"Do not return a reference from a fallible expression"); \
_temporary_result.release_value(); \
})
JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS::VM& vm, Wasm::Module const& module, GC::Ptr<JS::Object> import_object) JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS::VM& vm, Wasm::Module const& module, GC::Ptr<JS::Object> import_object)
{ {
Wasm::Linker linker { module }; Wasm::Linker linker { module };
@ -216,28 +246,28 @@ JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS
++index; ++index;
} }
auto result = TRY(JS::call(vm, function, JS::js_undefined(), argument_values.span())); auto result = TRY_OR_RETURN_TRAP(JS::call(vm, function, JS::js_undefined(), argument_values.span()));
if (type.results().is_empty()) if (type.results().is_empty())
return Wasm::Result { Vector<Wasm::Value> {} }; return Wasm::Result { Vector<Wasm::Value> {} };
if (type.results().size() == 1) if (type.results().size() == 1)
return Wasm::Result { Vector<Wasm::Value> { TRY(to_webassembly_value(vm, result, type.results().first())) } }; return Wasm::Result { Vector<Wasm::Value> { TRY_OR_RETURN_TRAP(to_webassembly_value(vm, result, type.results().first())) } };
auto method = TRY(result.get_method(vm, vm.names.iterator)); auto method = TRY_OR_RETURN_TRAP(result.get_method(vm, vm.names.iterator));
if (method == JS::js_undefined()) if (method == JS::js_undefined())
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotIterable, result.to_string_without_side_effects()); return Wasm::Trap::from_external_object(vm.throw_completion<JS::TypeError>(JS::ErrorType::NotIterable, result.to_string_without_side_effects()));
auto values = TRY(JS::iterator_to_list(vm, TRY(JS::get_iterator_from_method(vm, result, *method)))); auto values = TRY_OR_RETURN_TRAP(JS::iterator_to_list(vm, TRY_OR_RETURN_TRAP(JS::get_iterator_from_method(vm, result, *method))));
if (values.size() != type.results().size()) if (values.size() != type.results().size())
return vm.throw_completion<JS::TypeError>(ByteString::formatted("Invalid number of return values for multi-value wasm return of {} objects", type.results().size())); return Wasm::Trap::from_external_object(vm.throw_completion<JS::TypeError>(ByteString::formatted("Invalid number of return values for multi-value wasm return of {} objects", type.results().size())));
Vector<Wasm::Value> wasm_values; Vector<Wasm::Value> wasm_values;
TRY_OR_THROW_OOM(vm, wasm_values.try_ensure_capacity(values.size())); TRY_OR_RETURN_OOM_TRAP(vm, wasm_values.try_ensure_capacity(values.size()));
size_t i = 0; size_t i = 0;
for (auto& value : values) for (auto& value : values)
wasm_values.append(TRY(to_webassembly_value(vm, value, type.results()[i++]))); wasm_values.append(TRY_OR_RETURN_TRAP(to_webassembly_value(vm, value, type.results()[i++])));
return Wasm::Result { move(wasm_values) }; return Wasm::Result { move(wasm_values) };
}, },
@ -415,8 +445,11 @@ JS::NativeFunction* create_native_function(JS::VM& vm, Wasm::FunctionAddress add
auto& cache = get_cache(realm); auto& cache = get_cache(realm);
auto result = cache.abstract_machine().invoke(address, move(values)); auto result = cache.abstract_machine().invoke(address, move(values));
// FIXME: Use the convoluted mapping of errors defined in the spec. // FIXME: Use the convoluted mapping of errors defined in the spec.
if (result.is_trap()) if (result.is_trap()) {
return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("Wasm execution trapped (WIP): {}", result.trap().reason))); if (auto ptr = result.trap().data.get_pointer<Wasm::ExternallyManagedTrap>())
return ptr->unsafe_external_object_as<JS::Completion>();
return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("Wasm execution trapped (WIP): {}", result.trap().format())));
}
if (result.values().is_empty()) if (result.values().is_empty())
return JS::js_undefined(); return JS::js_undefined();

View file

@ -471,7 +471,7 @@ if (ASSERT_FAIL_HAS_INT OR EMSCRIPTEN)
target_compile_definitions(test262-runner PRIVATE ASSERT_FAIL_HAS_INT) target_compile_definitions(test262-runner PRIVATE ASSERT_FAIL_HAS_INT)
endif() endif()
lagom_utility(wasm SOURCES ../../Utilities/wasm.cpp LIBS LibFileSystem LibWasm LibLine LibMain LibJS) lagom_utility(wasm SOURCES ../../Utilities/wasm.cpp LIBS LibFileSystem LibWasm LibLine LibMain)
lagom_utility(xml SOURCES ../../Utilities/xml.cpp LIBS LibFileSystem LibMain LibXML LibURL) lagom_utility(xml SOURCES ../../Utilities/xml.cpp LIBS LibFileSystem LibMain LibXML LibURL)
include(CTest) include(CTest)

View file

@ -401,11 +401,11 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke)
auto functype = WebAssemblyModule::machine().store().get(function_address)->visit([&](auto& func) { return func.type(); }); auto functype = WebAssemblyModule::machine().store().get(function_address)->visit([&](auto& func) { return func.type(); });
auto result = WebAssemblyModule::machine().invoke(function_address, arguments); auto result = WebAssemblyModule::machine().invoke(function_address, arguments);
if (result.is_trap()) if (result.is_trap()) {
return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("Execution trapped: {}", result.trap().reason))); if (auto ptr = result.trap().data.get_pointer<Wasm::ExternallyManagedTrap>())
return ptr->unsafe_external_object_as<JS::Completion>();
if (result.is_completion()) return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("Execution trapped: {}", result.trap().format())));
return result.completion(); }
if (result.values().is_empty()) if (result.values().is_empty())
return JS::js_null(); return JS::js_null();

View file

@ -20,6 +20,7 @@
#include <LibWasm/Printer/Printer.h> #include <LibWasm/Printer/Printer.h>
#include <LibWasm/Types.h> #include <LibWasm/Types.h>
#include <LibWasm/Wasi.h> #include <LibWasm/Wasi.h>
#include <math.h>
#include <signal.h> #include <signal.h>
#include <unistd.h> #include <unistd.h>
@ -264,7 +265,7 @@ static bool post_interpret_hook(Wasm::Configuration&, Wasm::InstructionPointer&
g_continue = false; g_continue = false;
warnln("Trapped when executing ip={}", ip); warnln("Trapped when executing ip={}", ip);
g_printer->print(instr); g_printer->print(instr);
warnln("Trap reason: {}", interpreter.trap_reason()); warnln("Trap reason: {}", interpreter.trap().format());
const_cast<Wasm::Interpreter&>(interpreter).clear_trap(); const_cast<Wasm::Interpreter&>(interpreter).clear_trap();
} }
return true; return true;
@ -446,13 +447,13 @@ static bool pre_interpret_hook(Wasm::Configuration& config, Wasm::InstructionPoi
if (!ok) if (!ok)
continue; continue;
Wasm::Result result { Wasm::Trap {} }; Wasm::Result result { Wasm::Trap::from_string("") };
{ {
Wasm::BytecodeInterpreter::CallFrameHandle handle { g_interpreter, config }; Wasm::BytecodeInterpreter::CallFrameHandle handle { g_interpreter, config };
result = config.call(g_interpreter, *address, move(values)).assert_wasm_result(); result = config.call(g_interpreter, *address, move(values));
} }
if (result.is_trap()) { if (result.is_trap()) {
warnln("Execution trapped: {}", result.trap().reason); warnln("Execution trapped: {}", result.trap().format());
} else { } else {
if (!result.values().is_empty()) if (!result.values().is_empty())
warnln("Returned:"); warnln("Returned:");
@ -833,15 +834,16 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
outln(); outln();
} }
auto result = machine.invoke(g_interpreter, run_address.value(), move(values)).assert_wasm_result(); auto result = machine.invoke(g_interpreter, run_address.value(), move(values));
if (debug) if (debug)
launch_repl(); launch_repl();
if (result.is_trap()) { if (result.is_trap()) {
if (result.trap().reason.starts_with("exit:"sv)) auto trap_reason = result.trap().format();
return -result.trap().reason.substring_view(5).to_number<i32>().value_or(-1); if (trap_reason.starts_with("exit:"sv))
warnln("Execution trapped: {}", result.trap().reason); return -trap_reason.substring_view(5).to_number<i32>().value_or(-1);
warnln("Execution trapped: {}", trap_reason);
} else { } else {
if (!result.values().is_empty()) if (!result.values().is_empty())
warnln("Returned:"); warnln("Returned:");