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:
parent
62ca81fdcc
commit
b538e15548
Notes:
sideshowbarker
2024-07-18 11:08:37 +09:00
Author: https://github.com/alimpfard Commit: https://github.com/SerenityOS/serenity/commit/b538e15548c Pull-request: https://github.com/SerenityOS/serenity/pull/8358 Reviewed-by: https://github.com/IdanHo Reviewed-by: https://github.com/linusg
10 changed files with 54 additions and 50 deletions
|
@ -254,7 +254,7 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke)
|
|||
|
||||
auto result = WebAssemblyModule::machine().invoke(function_address, arguments);
|
||||
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 {};
|
||||
}
|
||||
|
||||
|
|
|
@ -136,7 +136,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
|||
});
|
||||
auto result = config.execute(interpreter);
|
||||
if (result.is_trap())
|
||||
instantiation_result = InstantiationError { "Global value construction trapped" };
|
||||
instantiation_result = InstantiationError { String::formatted("Global value construction trapped: {}", result.trap().reason) };
|
||||
else
|
||||
global_values.append(result.values().first());
|
||||
}
|
||||
|
@ -161,7 +161,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
|||
});
|
||||
auto result = config.execute(interpreter);
|
||||
if (result.is_trap()) {
|
||||
instantiation_result = InstantiationError { "Element construction trapped" };
|
||||
instantiation_result = InstantiationError { String::formatted("Element construction trapped: {}", result.trap().reason) };
|
||||
return IterationDecision::Continue;
|
||||
}
|
||||
|
||||
|
@ -212,7 +212,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
|||
});
|
||||
auto result = config.execute(interpreter);
|
||||
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;
|
||||
}
|
||||
auto d = result.values().first().to<i32>();
|
||||
|
@ -270,7 +270,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
|||
});
|
||||
auto result = config.execute(interpreter);
|
||||
if (result.is_trap()) {
|
||||
instantiation_result = InstantiationError { "Data section initialisation trapped" };
|
||||
instantiation_result = InstantiationError { String::formatted("Data section initialisation trapped: {}", result.trap().reason) };
|
||||
return;
|
||||
}
|
||||
size_t offset = 0;
|
||||
|
|
|
@ -193,28 +193,29 @@ private:
|
|||
};
|
||||
|
||||
struct Trap {
|
||||
// Empty value type
|
||||
String reason;
|
||||
};
|
||||
|
||||
class Result {
|
||||
public:
|
||||
explicit Result(Vector<Value> values)
|
||||
: m_values(move(values))
|
||||
: m_result(move(values))
|
||||
{
|
||||
}
|
||||
|
||||
Result(Trap)
|
||||
: m_is_trap(true)
|
||||
Result(Trap trap)
|
||||
: m_result(move(trap))
|
||||
{
|
||||
}
|
||||
|
||||
auto& values() const { return m_values; }
|
||||
auto& values() { return m_values; }
|
||||
auto is_trap() const { return m_is_trap; }
|
||||
auto is_trap() const { return m_result.has<Trap>(); }
|
||||
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:
|
||||
Vector<Value> m_values;
|
||||
bool m_is_trap { false };
|
||||
Variant<Vector<Value>, Trap> m_result;
|
||||
};
|
||||
|
||||
using ExternValue = Variant<FunctionAddress, TableAddress, MemoryAddress, GlobalAddress>;
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace Wasm {
|
|||
|
||||
#define TRAP_IF_NOT(x) \
|
||||
do { \
|
||||
if (trap_if_not(x)) { \
|
||||
if (trap_if_not(x, #x)) { \
|
||||
dbgln_if(WASM_TRACE_DEBUG, "Trapped because {} failed, at line {}", #x, __LINE__); \
|
||||
return; \
|
||||
} \
|
||||
|
@ -25,14 +25,14 @@ namespace Wasm {
|
|||
|
||||
#define TRAP_IF_NOT_NORETURN(x) \
|
||||
do { \
|
||||
if (trap_if_not(x)) { \
|
||||
if (trap_if_not(x, #x)) { \
|
||||
dbgln_if(WASM_TRACE_DEBUG, "Trapped because {} failed, at line {}", #x, __LINE__); \
|
||||
} \
|
||||
} while (false)
|
||||
|
||||
void BytecodeInterpreter::interpret(Configuration& configuration)
|
||||
{
|
||||
m_do_trap = false;
|
||||
m_trap.clear();
|
||||
auto& instructions = configuration.frame().expression().instructions();
|
||||
auto max_ip_value = InstructionPointer { instructions.size() };
|
||||
auto& current_ip_value = configuration.ip();
|
||||
|
@ -40,13 +40,13 @@ void BytecodeInterpreter::interpret(Configuration& configuration)
|
|||
|
||||
while (current_ip_value < max_ip_value) {
|
||||
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;
|
||||
}
|
||||
auto& instruction = instructions[current_ip_value.value()];
|
||||
auto old_ip = current_ip_value;
|
||||
interpret(configuration, current_ip_value, instruction);
|
||||
if (m_do_trap)
|
||||
if (m_trap.has_value())
|
||||
return;
|
||||
if (current_ip_value == old_ip) // If no jump occurred
|
||||
++current_ip_value;
|
||||
|
@ -83,7 +83,7 @@ void BytecodeInterpreter::load_and_push(Configuration& configuration, Instructio
|
|||
auto& address = configuration.frame().module().memories().first();
|
||||
auto memory = configuration.store().get(address);
|
||||
if (!memory) {
|
||||
m_do_trap = true;
|
||||
m_trap = Trap { "Nonexistent memory" };
|
||||
return;
|
||||
}
|
||||
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>());
|
||||
auto base = entry.get<Value>().to<i32>();
|
||||
if (!base.has_value()) {
|
||||
m_do_trap = true;
|
||||
m_trap = Trap { "Memory access out of bounds" };
|
||||
return;
|
||||
}
|
||||
auto instance_address = base.value() + static_cast<i64>(arg.offset);
|
||||
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());
|
||||
return;
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ void BytecodeInterpreter::store_to_memory(Configuration& configuration, Instruct
|
|||
TRAP_IF_NOT(base.has_value());
|
||||
auto instance_address = base.value() + static_cast<i64>(arg.offset);
|
||||
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());
|
||||
return;
|
||||
}
|
||||
|
@ -146,14 +146,14 @@ void BytecodeInterpreter::call_address(Configuration& configuration, FunctionAdd
|
|||
|
||||
configuration.stack().entries().remove(configuration.stack().size() - span.size(), span.size());
|
||||
|
||||
Result result { Trap {} };
|
||||
Result result { Trap { ""sv } };
|
||||
{
|
||||
CallFrameHandle handle { *this, configuration };
|
||||
result = configuration.call(*this, address, move(args));
|
||||
}
|
||||
|
||||
if (result.is_trap()) {
|
||||
m_do_trap = true;
|
||||
m_trap = move(result.trap());
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -254,7 +254,7 @@ T BytecodeInterpreter::read_value(ReadonlyBytes data)
|
|||
stream >> value;
|
||||
if (stream.handle_any_error()) {
|
||||
dbgln("Read from {} failed", data.data());
|
||||
m_do_trap = true;
|
||||
m_trap = Trap { "Read from memory failed" };
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
@ -266,7 +266,7 @@ float BytecodeInterpreter::read_value<float>(ReadonlyBytes data)
|
|||
LittleEndian<u32> raw_value;
|
||||
stream >> raw_value;
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -277,7 +277,7 @@ double BytecodeInterpreter::read_value<double>(ReadonlyBytes data)
|
|||
LittleEndian<u64> raw_value;
|
||||
stream >> raw_value;
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -319,7 +319,7 @@ template<typename V, typename T>
|
|||
MakeSigned<T> BytecodeInterpreter::checked_signed_truncate(V value)
|
||||
{
|
||||
if (isnan(value) || isinf(value)) { // "undefined", let's just trap.
|
||||
m_do_trap = true;
|
||||
m_trap = Trap { "Signed truncation undefined behaviour" };
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -334,7 +334,7 @@ MakeSigned<T> BytecodeInterpreter::checked_signed_truncate(V value)
|
|||
return static_cast<SignedT>(truncated);
|
||||
|
||||
dbgln_if(WASM_TRACE_DEBUG, "Truncate out of range error");
|
||||
m_do_trap = true;
|
||||
m_trap = Trap { "Signed truncation out of range" };
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -342,7 +342,7 @@ template<typename V, typename T>
|
|||
MakeUnsigned<T> BytecodeInterpreter::checked_unsigned_truncate(V value)
|
||||
{
|
||||
if (isnan(value) || isinf(value)) { // "undefined", let's just trap.
|
||||
m_do_trap = true;
|
||||
m_trap = Trap { "Unsigned truncation undefined behaviour" };
|
||||
return 0;
|
||||
}
|
||||
double truncated;
|
||||
|
@ -356,7 +356,7 @@ MakeUnsigned<T> BytecodeInterpreter::checked_unsigned_truncate(V value)
|
|||
return static_cast<UnsignedT>(truncated);
|
||||
|
||||
dbgln_if(WASM_TRACE_DEBUG, "Truncate out of range error");
|
||||
m_do_trap = true;
|
||||
m_trap = Trap { "Unsigned truncation out of range" };
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -490,7 +490,7 @@ void BytecodeInterpreter::interpret(Configuration& configuration, InstructionPoi
|
|||
|
||||
switch (instruction.opcode().value()) {
|
||||
case Instructions::unreachable.value():
|
||||
m_do_trap = true;
|
||||
m_trap = Trap { "Unreachable" };
|
||||
return;
|
||||
case Instructions::nop.value():
|
||||
return;
|
||||
|
@ -1087,7 +1087,7 @@ void BytecodeInterpreter::interpret(Configuration& configuration, InstructionPoi
|
|||
default:
|
||||
unimplemented:;
|
||||
dbgln("Instruction '{}' not implemented", instruction_name(instruction.opcode()));
|
||||
m_do_trap = true;
|
||||
m_trap = Trap { String::formatted("Unimplemented instruction {}", instruction_name(instruction.opcode())) };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1097,7 +1097,7 @@ void DebuggerBytecodeInterpreter::interpret(Configuration& configuration, Instru
|
|||
if (pre_interpret_hook) {
|
||||
auto result = pre_interpret_hook(configuration, ip, instruction);
|
||||
if (!result) {
|
||||
m_do_trap = true;
|
||||
m_trap = Trap { "Trapped by user request" };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -1106,7 +1106,7 @@ void DebuggerBytecodeInterpreter::interpret(Configuration& configuration, Instru
|
|||
if (post_interpret_hook) {
|
||||
auto result = post_interpret_hook(configuration, ip, instruction, *this);
|
||||
if (!result) {
|
||||
m_do_trap = true;
|
||||
m_trap = Trap { "Trapped by user request" };
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,9 @@ namespace Wasm {
|
|||
struct BytecodeInterpreter : public Interpreter {
|
||||
virtual void interpret(Configuration&) override;
|
||||
virtual ~BytecodeInterpreter() override = default;
|
||||
virtual bool did_trap() const override { return m_do_trap; }
|
||||
virtual void clear_trap() override { m_do_trap = false; }
|
||||
virtual bool did_trap() const override { return m_trap.has_value(); }
|
||||
virtual String trap_reason() const override { return m_trap.value().reason; }
|
||||
virtual void clear_trap() override { m_trap.clear(); }
|
||||
|
||||
struct CallFrameHandle {
|
||||
explicit CallFrameHandle(BytecodeInterpreter& interpreter, Configuration& configuration)
|
||||
|
@ -48,13 +49,14 @@ protected:
|
|||
T read_value(ReadonlyBytes data);
|
||||
|
||||
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)
|
||||
m_do_trap = true;
|
||||
return m_do_trap;
|
||||
m_trap = Trap { move(reason) };
|
||||
return m_trap.has_value();
|
||||
}
|
||||
bool m_do_trap { false };
|
||||
|
||||
Optional<Trap> m_trap;
|
||||
};
|
||||
|
||||
struct DebuggerBytecodeInterpreter : public BytecodeInterpreter {
|
||||
|
|
|
@ -68,10 +68,10 @@ Result Configuration::execute(Interpreter& interpreter)
|
|||
{
|
||||
interpreter.interpret(*this);
|
||||
if (interpreter.did_trap())
|
||||
return Trap {};
|
||||
return Trap { interpreter.trap_reason() };
|
||||
|
||||
if (stack().size() <= frame().arity() + 1)
|
||||
return Trap {};
|
||||
return Trap { "Not enough values to return from call" };
|
||||
|
||||
Vector<Value> results;
|
||||
results.ensure_capacity(frame().arity());
|
||||
|
@ -80,7 +80,7 @@ Result Configuration::execute(Interpreter& interpreter)
|
|||
auto label = stack().pop();
|
||||
// ASSERT: label == current frame
|
||||
if (!label.has<Label>())
|
||||
return Trap {};
|
||||
return Trap { "Invalid stack configuration" };
|
||||
return Result { move(results) };
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ struct Interpreter {
|
|||
virtual ~Interpreter() = default;
|
||||
virtual void interpret(Configuration&) = 0;
|
||||
virtual bool did_trap() const = 0;
|
||||
virtual String trap_reason() const = 0;
|
||||
virtual void clear_trap() = 0;
|
||||
};
|
||||
|
||||
|
|
|
@ -38,6 +38,6 @@ static constexpr auto page_size = 64 * KiB;
|
|||
|
||||
// Limits
|
||||
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;
|
||||
|
||||
}
|
||||
|
|
|
@ -432,7 +432,7 @@ JS::NativeFunction* create_native_function(Wasm::FunctionAddress address, String
|
|||
auto result = WebAssemblyObject::s_abstract_machine.invoke(address, move(values));
|
||||
// FIXME: Use the convoluted mapping of errors defined in the spec.
|
||||
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 {};
|
||||
}
|
||||
|
||||
|
|
|
@ -35,10 +35,10 @@ static bool post_interpret_hook(Wasm::Configuration&, Wasm::InstructionPointer&
|
|||
{
|
||||
if (interpreter.did_trap()) {
|
||||
g_continue = false;
|
||||
const_cast<Wasm::Interpreter&>(interpreter).clear_trap();
|
||||
warnln("Trapped when executing ip={}", ip);
|
||||
g_printer.print(instr);
|
||||
warnln("");
|
||||
warnln("Trap reason: {}", interpreter.trap_reason());
|
||||
const_cast<Wasm::Interpreter&>(interpreter).clear_trap();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -206,7 +206,7 @@ static bool pre_interpret_hook(Wasm::Configuration& config, Wasm::InstructionPoi
|
|||
result = config.call(g_interpreter, *address, move(values));
|
||||
}
|
||||
if (result.is_trap())
|
||||
warnln("Execution trapped!");
|
||||
warnln("Execution trapped: {}", result.trap().reason);
|
||||
if (!result.values().is_empty())
|
||||
warnln("Returned:");
|
||||
for (auto& value : result.values()) {
|
||||
|
|
Loading…
Add table
Reference in a new issue