1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 10:18:11 +00:00

LibWasm: Give traps a reason and display it when needed

This makes debugging wasm code a bit easier, as we now know what fails
instead of just "too bad, something went wrong".
This commit is contained in:
Ali Mohammad Pur 2021-07-01 17:03:17 +04:30 committed by Ali Mohammad Pur
parent 62ca81fdcc
commit b538e15548
10 changed files with 54 additions and 50 deletions

View file

@ -254,7 +254,7 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke)
auto result = WebAssemblyModule::machine().invoke(function_address, arguments); auto result = WebAssemblyModule::machine().invoke(function_address, arguments);
if (result.is_trap()) { if (result.is_trap()) {
vm.throw_exception<JS::TypeError>(global_object, "Execution trapped"); vm.throw_exception<JS::TypeError>(global_object, String::formatted("Execution trapped: {}", result.trap().reason));
return {}; return {};
} }

View file

@ -136,7 +136,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
}); });
auto result = config.execute(interpreter); auto result = config.execute(interpreter);
if (result.is_trap()) if (result.is_trap())
instantiation_result = InstantiationError { "Global value construction trapped" }; instantiation_result = InstantiationError { String::formatted("Global value construction trapped: {}", result.trap().reason) };
else else
global_values.append(result.values().first()); global_values.append(result.values().first());
} }
@ -161,7 +161,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
}); });
auto result = config.execute(interpreter); auto result = config.execute(interpreter);
if (result.is_trap()) { if (result.is_trap()) {
instantiation_result = InstantiationError { "Element construction trapped" }; instantiation_result = InstantiationError { String::formatted("Element construction trapped: {}", result.trap().reason) };
return IterationDecision::Continue; return IterationDecision::Continue;
} }
@ -212,7 +212,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
}); });
auto result = config.execute(interpreter); auto result = config.execute(interpreter);
if (result.is_trap()) { if (result.is_trap()) {
instantiation_result = InstantiationError { "Element section initialisation trapped" }; instantiation_result = InstantiationError { String::formatted("Element section initialisation trapped: {}", result.trap().reason) };
return IterationDecision::Break; return IterationDecision::Break;
} }
auto d = result.values().first().to<i32>(); auto d = result.values().first().to<i32>();
@ -270,7 +270,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
}); });
auto result = config.execute(interpreter); auto result = config.execute(interpreter);
if (result.is_trap()) { if (result.is_trap()) {
instantiation_result = InstantiationError { "Data section initialisation trapped" }; instantiation_result = InstantiationError { String::formatted("Data section initialisation trapped: {}", result.trap().reason) };
return; return;
} }
size_t offset = 0; size_t offset = 0;

View file

@ -193,28 +193,29 @@ private:
}; };
struct Trap { struct Trap {
// Empty value type String reason;
}; };
class Result { class Result {
public: public:
explicit Result(Vector<Value> values) explicit Result(Vector<Value> values)
: m_values(move(values)) : m_result(move(values))
{ {
} }
Result(Trap) Result(Trap trap)
: m_is_trap(true) : m_result(move(trap))
{ {
} }
auto& values() const { return m_values; } auto is_trap() const { return m_result.has<Trap>(); }
auto& values() { return m_values; } auto& values() const { return m_result.get<Vector<Value>>(); }
auto is_trap() const { return m_is_trap; } auto& values() { return m_result.get<Vector<Value>>(); }
auto& trap() const { return m_result.get<Trap>(); }
auto& trap() { return m_result.get<Trap>(); }
private: private:
Vector<Value> m_values; Variant<Vector<Value>, Trap> m_result;
bool m_is_trap { false };
}; };
using ExternValue = Variant<FunctionAddress, TableAddress, MemoryAddress, GlobalAddress>; using ExternValue = Variant<FunctionAddress, TableAddress, MemoryAddress, GlobalAddress>;

View file

@ -17,7 +17,7 @@ namespace Wasm {
#define TRAP_IF_NOT(x) \ #define TRAP_IF_NOT(x) \
do { \ do { \
if (trap_if_not(x)) { \ if (trap_if_not(x, #x)) { \
dbgln_if(WASM_TRACE_DEBUG, "Trapped because {} failed, at line {}", #x, __LINE__); \ dbgln_if(WASM_TRACE_DEBUG, "Trapped because {} failed, at line {}", #x, __LINE__); \
return; \ return; \
} \ } \
@ -25,14 +25,14 @@ namespace Wasm {
#define TRAP_IF_NOT_NORETURN(x) \ #define TRAP_IF_NOT_NORETURN(x) \
do { \ do { \
if (trap_if_not(x)) { \ if (trap_if_not(x, #x)) { \
dbgln_if(WASM_TRACE_DEBUG, "Trapped because {} failed, at line {}", #x, __LINE__); \ dbgln_if(WASM_TRACE_DEBUG, "Trapped because {} failed, at line {}", #x, __LINE__); \
} \ } \
} while (false) } while (false)
void BytecodeInterpreter::interpret(Configuration& configuration) void BytecodeInterpreter::interpret(Configuration& configuration)
{ {
m_do_trap = false; m_trap.clear();
auto& instructions = configuration.frame().expression().instructions(); auto& instructions = configuration.frame().expression().instructions();
auto max_ip_value = InstructionPointer { instructions.size() }; auto max_ip_value = InstructionPointer { instructions.size() };
auto& current_ip_value = configuration.ip(); auto& current_ip_value = configuration.ip();
@ -40,13 +40,13 @@ void BytecodeInterpreter::interpret(Configuration& configuration)
while (current_ip_value < max_ip_value) { while (current_ip_value < max_ip_value) {
if (executed_instructions++ >= Constants::max_allowed_executed_instructions_per_call) [[unlikely]] { if (executed_instructions++ >= Constants::max_allowed_executed_instructions_per_call) [[unlikely]] {
m_do_trap = true; m_trap = Trap { "Exceeded maximum allowed number of instructions" };
return; return;
} }
auto& instruction = instructions[current_ip_value.value()]; auto& instruction = instructions[current_ip_value.value()];
auto old_ip = current_ip_value; auto old_ip = current_ip_value;
interpret(configuration, current_ip_value, instruction); interpret(configuration, current_ip_value, instruction);
if (m_do_trap) if (m_trap.has_value())
return; return;
if (current_ip_value == old_ip) // If no jump occurred if (current_ip_value == old_ip) // If no jump occurred
++current_ip_value; ++current_ip_value;
@ -83,7 +83,7 @@ void BytecodeInterpreter::load_and_push(Configuration& configuration, Instructio
auto& address = configuration.frame().module().memories().first(); auto& address = configuration.frame().module().memories().first();
auto memory = configuration.store().get(address); auto memory = configuration.store().get(address);
if (!memory) { if (!memory) {
m_do_trap = true; m_trap = Trap { "Nonexistent memory" };
return; return;
} }
auto& arg = instruction.arguments().get<Instruction::MemoryArgument>(); auto& arg = instruction.arguments().get<Instruction::MemoryArgument>();
@ -91,12 +91,12 @@ void BytecodeInterpreter::load_and_push(Configuration& configuration, Instructio
TRAP_IF_NOT(entry.has<Value>()); TRAP_IF_NOT(entry.has<Value>());
auto base = entry.get<Value>().to<i32>(); auto base = entry.get<Value>().to<i32>();
if (!base.has_value()) { if (!base.has_value()) {
m_do_trap = true; m_trap = Trap { "Memory access out of bounds" };
return; return;
} }
auto instance_address = base.value() + static_cast<i64>(arg.offset); auto instance_address = base.value() + static_cast<i64>(arg.offset);
if (instance_address < 0 || static_cast<u64>(instance_address + sizeof(ReadType)) > memory->size()) { if (instance_address < 0 || static_cast<u64>(instance_address + sizeof(ReadType)) > memory->size()) {
m_do_trap = true; m_trap = Trap { "Memory access out of bounds" };
dbgln("LibWasm: Memory access out of bounds (expected 0 <= {} and {} <= {})", instance_address, instance_address + sizeof(ReadType), memory->size()); dbgln("LibWasm: Memory access out of bounds (expected 0 <= {} and {} <= {})", instance_address, instance_address + sizeof(ReadType), memory->size());
return; return;
} }
@ -117,7 +117,7 @@ void BytecodeInterpreter::store_to_memory(Configuration& configuration, Instruct
TRAP_IF_NOT(base.has_value()); TRAP_IF_NOT(base.has_value());
auto instance_address = base.value() + static_cast<i64>(arg.offset); auto instance_address = base.value() + static_cast<i64>(arg.offset);
if (instance_address < 0 || static_cast<u64>(instance_address + data.size()) > memory->size()) { if (instance_address < 0 || static_cast<u64>(instance_address + data.size()) > memory->size()) {
m_do_trap = true; m_trap = Trap { "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;
} }
@ -146,14 +146,14 @@ void BytecodeInterpreter::call_address(Configuration& configuration, FunctionAdd
configuration.stack().entries().remove(configuration.stack().size() - span.size(), span.size()); configuration.stack().entries().remove(configuration.stack().size() - span.size(), span.size());
Result result { Trap {} }; Result result { Trap { ""sv } };
{ {
CallFrameHandle handle { *this, configuration }; CallFrameHandle handle { *this, configuration };
result = configuration.call(*this, address, move(args)); result = configuration.call(*this, address, move(args));
} }
if (result.is_trap()) { if (result.is_trap()) {
m_do_trap = true; m_trap = move(result.trap());
return; return;
} }
@ -254,7 +254,7 @@ T BytecodeInterpreter::read_value(ReadonlyBytes data)
stream >> value; stream >> value;
if (stream.handle_any_error()) { if (stream.handle_any_error()) {
dbgln("Read from {} failed", data.data()); dbgln("Read from {} failed", data.data());
m_do_trap = true; m_trap = Trap { "Read from memory failed" };
} }
return value; return value;
} }
@ -266,7 +266,7 @@ float BytecodeInterpreter::read_value<float>(ReadonlyBytes data)
LittleEndian<u32> raw_value; LittleEndian<u32> raw_value;
stream >> raw_value; stream >> raw_value;
if (stream.handle_any_error()) if (stream.handle_any_error())
m_do_trap = true; m_trap = Trap { "Read from memory failed" };
return bit_cast<float>(static_cast<u32>(raw_value)); return bit_cast<float>(static_cast<u32>(raw_value));
} }
@ -277,7 +277,7 @@ double BytecodeInterpreter::read_value<double>(ReadonlyBytes data)
LittleEndian<u64> raw_value; LittleEndian<u64> raw_value;
stream >> raw_value; stream >> raw_value;
if (stream.handle_any_error()) if (stream.handle_any_error())
m_do_trap = true; m_trap = Trap { "Read from memory failed" };
return bit_cast<double>(static_cast<u64>(raw_value)); return bit_cast<double>(static_cast<u64>(raw_value));
} }
@ -319,7 +319,7 @@ template<typename V, typename T>
MakeSigned<T> BytecodeInterpreter::checked_signed_truncate(V value) MakeSigned<T> BytecodeInterpreter::checked_signed_truncate(V value)
{ {
if (isnan(value) || isinf(value)) { // "undefined", let's just trap. if (isnan(value) || isinf(value)) { // "undefined", let's just trap.
m_do_trap = true; m_trap = Trap { "Signed truncation undefined behaviour" };
return 0; return 0;
} }
@ -334,7 +334,7 @@ MakeSigned<T> BytecodeInterpreter::checked_signed_truncate(V value)
return static_cast<SignedT>(truncated); return static_cast<SignedT>(truncated);
dbgln_if(WASM_TRACE_DEBUG, "Truncate out of range error"); dbgln_if(WASM_TRACE_DEBUG, "Truncate out of range error");
m_do_trap = true; m_trap = Trap { "Signed truncation out of range" };
return true; return true;
} }
@ -342,7 +342,7 @@ template<typename V, typename T>
MakeUnsigned<T> BytecodeInterpreter::checked_unsigned_truncate(V value) MakeUnsigned<T> BytecodeInterpreter::checked_unsigned_truncate(V value)
{ {
if (isnan(value) || isinf(value)) { // "undefined", let's just trap. if (isnan(value) || isinf(value)) { // "undefined", let's just trap.
m_do_trap = true; m_trap = Trap { "Unsigned truncation undefined behaviour" };
return 0; return 0;
} }
double truncated; double truncated;
@ -356,7 +356,7 @@ MakeUnsigned<T> BytecodeInterpreter::checked_unsigned_truncate(V value)
return static_cast<UnsignedT>(truncated); return static_cast<UnsignedT>(truncated);
dbgln_if(WASM_TRACE_DEBUG, "Truncate out of range error"); dbgln_if(WASM_TRACE_DEBUG, "Truncate out of range error");
m_do_trap = true; m_trap = Trap { "Unsigned truncation out of range" };
return true; return true;
} }
@ -490,7 +490,7 @@ void BytecodeInterpreter::interpret(Configuration& configuration, InstructionPoi
switch (instruction.opcode().value()) { switch (instruction.opcode().value()) {
case Instructions::unreachable.value(): case Instructions::unreachable.value():
m_do_trap = true; m_trap = Trap { "Unreachable" };
return; return;
case Instructions::nop.value(): case Instructions::nop.value():
return; return;
@ -1087,7 +1087,7 @@ void BytecodeInterpreter::interpret(Configuration& configuration, InstructionPoi
default: default:
unimplemented:; unimplemented:;
dbgln("Instruction '{}' not implemented", instruction_name(instruction.opcode())); dbgln("Instruction '{}' not implemented", instruction_name(instruction.opcode()));
m_do_trap = true; m_trap = Trap { String::formatted("Unimplemented instruction {}", instruction_name(instruction.opcode())) };
return; return;
} }
} }
@ -1097,7 +1097,7 @@ void DebuggerBytecodeInterpreter::interpret(Configuration& configuration, Instru
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_do_trap = true; m_trap = Trap { "Trapped by user request" };
return; return;
} }
} }
@ -1106,7 +1106,7 @@ void DebuggerBytecodeInterpreter::interpret(Configuration& configuration, Instru
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_do_trap = true; m_trap = Trap { "Trapped by user request" };
return; return;
} }
} }

View file

@ -14,8 +14,9 @@ namespace Wasm {
struct BytecodeInterpreter : public Interpreter { struct BytecodeInterpreter : public Interpreter {
virtual void interpret(Configuration&) override; virtual void interpret(Configuration&) override;
virtual ~BytecodeInterpreter() override = default; virtual ~BytecodeInterpreter() override = default;
virtual bool did_trap() const override { return m_do_trap; } virtual bool did_trap() const override { return m_trap.has_value(); }
virtual void clear_trap() override { m_do_trap = false; } virtual String trap_reason() const override { return m_trap.value().reason; }
virtual void clear_trap() override { m_trap.clear(); }
struct CallFrameHandle { struct CallFrameHandle {
explicit CallFrameHandle(BytecodeInterpreter& interpreter, Configuration& configuration) explicit CallFrameHandle(BytecodeInterpreter& interpreter, Configuration& configuration)
@ -48,13 +49,14 @@ protected:
T read_value(ReadonlyBytes data); T read_value(ReadonlyBytes data);
Vector<Value> pop_values(Configuration& configuration, size_t count); Vector<Value> pop_values(Configuration& configuration, size_t count);
bool trap_if_not(bool value) bool trap_if_not(bool value, String reason)
{ {
if (!value) if (!value)
m_do_trap = true; m_trap = Trap { move(reason) };
return m_do_trap; return m_trap.has_value();
} }
bool m_do_trap { false };
Optional<Trap> m_trap;
}; };
struct DebuggerBytecodeInterpreter : public BytecodeInterpreter { struct DebuggerBytecodeInterpreter : public BytecodeInterpreter {

View file

@ -68,10 +68,10 @@ Result Configuration::execute(Interpreter& interpreter)
{ {
interpreter.interpret(*this); interpreter.interpret(*this);
if (interpreter.did_trap()) if (interpreter.did_trap())
return Trap {}; return Trap { interpreter.trap_reason() };
if (stack().size() <= frame().arity() + 1) if (stack().size() <= frame().arity() + 1)
return Trap {}; return Trap { "Not enough values to return from call" };
Vector<Value> results; Vector<Value> results;
results.ensure_capacity(frame().arity()); results.ensure_capacity(frame().arity());
@ -80,7 +80,7 @@ Result Configuration::execute(Interpreter& interpreter)
auto label = stack().pop(); auto label = stack().pop();
// ASSERT: label == current frame // ASSERT: label == current frame
if (!label.has<Label>()) if (!label.has<Label>())
return Trap {}; return Trap { "Invalid stack configuration" };
return Result { move(results) }; return Result { move(results) };
} }

View file

@ -14,6 +14,7 @@ struct Interpreter {
virtual ~Interpreter() = default; virtual ~Interpreter() = default;
virtual void interpret(Configuration&) = 0; virtual void interpret(Configuration&) = 0;
virtual bool did_trap() const = 0; virtual bool did_trap() const = 0;
virtual String trap_reason() const = 0;
virtual void clear_trap() = 0; virtual void clear_trap() = 0;
}; };

View file

@ -38,6 +38,6 @@ static constexpr auto page_size = 64 * KiB;
// Limits // Limits
static constexpr auto max_allowed_call_stack_depth = 1000; static constexpr auto max_allowed_call_stack_depth = 1000;
static constexpr auto max_allowed_executed_instructions_per_call = 64 * 1024 * 1024; static constexpr auto max_allowed_executed_instructions_per_call = 1024 * 1024 * 1024;
} }

View file

@ -432,7 +432,7 @@ JS::NativeFunction* create_native_function(Wasm::FunctionAddress address, String
auto result = WebAssemblyObject::s_abstract_machine.invoke(address, move(values)); auto result = WebAssemblyObject::s_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()) {
vm.throw_exception<JS::TypeError>(global_object, "Wasm execution trapped (WIP)"); vm.throw_exception<JS::TypeError>(global_object, String::formatted("Wasm execution trapped (WIP): {}", result.trap().reason));
return {}; return {};
} }

View file

@ -35,10 +35,10 @@ static bool post_interpret_hook(Wasm::Configuration&, Wasm::InstructionPointer&
{ {
if (interpreter.did_trap()) { if (interpreter.did_trap()) {
g_continue = false; g_continue = false;
const_cast<Wasm::Interpreter&>(interpreter).clear_trap();
warnln("Trapped when executing ip={}", ip); warnln("Trapped when executing ip={}", ip);
g_printer.print(instr); g_printer.print(instr);
warnln(""); warnln("Trap reason: {}", interpreter.trap_reason());
const_cast<Wasm::Interpreter&>(interpreter).clear_trap();
} }
return true; return true;
} }
@ -206,7 +206,7 @@ static bool pre_interpret_hook(Wasm::Configuration& config, Wasm::InstructionPoi
result = config.call(g_interpreter, *address, move(values)); result = config.call(g_interpreter, *address, move(values));
} }
if (result.is_trap()) if (result.is_trap())
warnln("Execution trapped!"); warnln("Execution trapped: {}", result.trap().reason);
if (!result.values().is_empty()) if (!result.values().is_empty())
warnln("Returned:"); warnln("Returned:");
for (auto& value : result.values()) { for (auto& value : result.values()) {