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
Notes: sideshowbarker 2024-07-18 11:08:37 +09:00
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);
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 {};
}

View file

@ -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;

View file

@ -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>;

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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) };
}

View file

@ -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;
};

View file

@ -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;
}

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));
// 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 {};
}

View file

@ -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()) {