diff --git a/src/lib_scoru_wasm/test/test_encodings_util.ml b/src/lib_scoru_wasm/test/test_encodings_util.ml new file mode 100644 index 0000000000000000000000000000000000000000..72f6e66942a1443c68281b1bedb9609e9b057622 --- /dev/null +++ b/src/lib_scoru_wasm/test/test_encodings_util.ml @@ -0,0 +1,81 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(* Use context-binary for testing. *) +module Context = Tezos_context_memory.Context_binary +include Tree_encoding + +type Lazy_containers.Lazy_map.tree += Tree of Context.tree + +module Tree = struct + type t = Context.t + + type tree = Context.tree + + type key = Context.key + + type value = Context.value + + include Context.Tree + + let select = function + | Tree t -> t + | _ -> raise Tree_encoding.Incorrect_tree_type + + let wrap t = Tree t +end + +module Tree_encoding_runner = Tree_encoding.Runner.Make (Tree) + +let empty_tree () = + let open Lwt_syntax in + let* index = Context.init "/tmp" in + let empty_store = Context.empty index in + return @@ Context.Tree.empty empty_store + +let test_encode_decode enc value f = + let open Lwt_result_syntax in + let*! empty_tree = empty_tree () in + let*! tree = Tree_encoding_runner.encode enc value empty_tree in + let*! value' = Tree_encoding_runner.decode enc tree in + f value' + +let encode_decode enc value = test_encode_decode enc value Lwt.return + +let qcheck ?count ?print gen f = + let open Lwt_result_syntax in + let test = + QCheck2.Test.make ?count ?print gen (fun x -> + Result.is_ok @@ Lwt_main.run (f x)) + in + let res = QCheck_base_runner.run_tests ~verbose:true [test] in + if res = 0 then return_unit else failwith "QCheck tests failed" + +let make_test ?print encoding gen check () = + qcheck ?print gen (fun value -> + let open Lwt_result_syntax in + let*! value' = encode_decode encoding value in + let* res = check value value' in + if res then return_unit else fail ()) diff --git a/src/lib_scoru_wasm/test/test_parser_encoding.ml b/src/lib_scoru_wasm/test/test_parser_encoding.ml index 106726d8aeb3ad4d53401095f6f537373a76690d..cd98cdde441aa654a3e57e6cc5a298aaae221b9e 100644 --- a/src/lib_scoru_wasm/test/test_parser_encoding.ml +++ b/src/lib_scoru_wasm/test/test_parser_encoding.ml @@ -36,58 +36,12 @@ open Tztest open Lazy_containers open Tezos_webassembly_interpreter open Tezos_scoru_wasm - -(* Use context-binary for testing. *) -module Context = Tezos_context_memory.Context_binary - -type Lazy_containers.Lazy_map.tree += Tree of Context.tree - -module Tree : Tree_encoding.TREE with type tree = Context.tree = struct - type tree = Context.tree - - include Context.Tree - - let select = function - | Tree t -> t - | _ -> raise Tree_encoding.Incorrect_tree_type - - let wrap t = Tree t -end - -module Tree_encoding = struct - include Tree_encoding - include Lazy_map_encoding.Make (Instance.NameMap) -end - -module Tree_encoding_runner = Tree_encoding.Runner.Make (Tree) module Parser = Binary_parser_encodings module Utils = struct - include Tree_encoding module V = Lazy_vector.Int32Vector module C = Chunked_byte_vector - - let empty_tree () = - let open Lwt_syntax in - let* index = Context.init "/tmp" in - let empty_store = Context.empty index in - return @@ Context.Tree.empty empty_store - - let test_encode_decode enc value f = - let open Lwt_result_syntax in - let*! empty_tree = empty_tree () in - let*! tree = Tree_encoding_runner.encode enc value empty_tree in - let*! value' = Tree_encoding_runner.decode enc tree in - f value' - - let encode_decode enc value = test_encode_decode enc value Lwt.return - - let make_test encoding gen check () = - Test_wasm_encoding.qcheck gen (fun value -> - let open Lwt_result_syntax in - let*! value' = encode_decode encoding value in - let* res = check value value' in - if res then return_unit else fail ()) + include Test_encodings_util end module Byte_vector = struct diff --git a/src/lib_scoru_wasm/test/test_scoru_wasm.ml b/src/lib_scoru_wasm/test/test_scoru_wasm.ml index 89e4d0e95a9acca5a6a7a97a8260319be08c7771..4cfb3abfbf1fc0850340a84ca9e7499ea304a1ab 100644 --- a/src/lib_scoru_wasm/test/test_scoru_wasm.ml +++ b/src/lib_scoru_wasm/test/test_scoru_wasm.ml @@ -38,6 +38,7 @@ let () = ("Output", Test_output.tests); ("AST Generators", Test_ast_generators.tests); ("WASM Encodings", Test_wasm_encoding.tests); + ("WASM PVM Encodings", Test_wasm_pvm_encodings.tests); ("Parser Encodings", Test_parser_encoding.tests); ] |> Lwt_main.run diff --git a/src/lib_scoru_wasm/test/test_wasm_encoding.ml b/src/lib_scoru_wasm/test/test_wasm_encoding.ml index 3df7f675148eefa7908471b7156b3d452e4f4af1..1e62e97950f63f6b2827aa579d6fffc33c947f8f 100644 --- a/src/lib_scoru_wasm/test/test_wasm_encoding.ml +++ b/src/lib_scoru_wasm/test/test_wasm_encoding.ml @@ -34,52 +34,7 @@ open Tztest open Tezos_scoru_wasm open Tezos_webassembly_interpreter - -let qcheck ?count ?print gen f = - let open Lwt_result_syntax in - let test = - QCheck2.Test.make ?count ?print gen (fun x -> - Result.is_ok @@ Lwt_main.run (f x)) - in - let res = QCheck_base_runner.run_tests ~verbose:true [test] in - if res = 0 then return_unit else failwith "QCheck tests failed" - -(* Use context-binary for testing. *) -module Context = Tezos_context_memory.Context_binary - -type Lazy_containers.Lazy_map.tree += Tree of Context.tree - -module Tree = struct - type t = Context.t - - type tree = Context.tree - - type key = Context.key - - type value = Context.value - - include Context.Tree - - let select = function - | Tree t -> t - | _ -> raise Tree_encoding.Incorrect_tree_type - - let wrap t = Tree t -end - -module Tree_encoding_runner = Tree_encoding.Runner.Make (Tree) - -let empty_tree () = - let open Lwt_syntax in - let* index = Context.init "/tmp" in - let empty_store = Context.empty index in - return @@ Context.Tree.empty empty_store - -let encode_decode enc value = - let open Lwt_syntax in - let* empty_tree = empty_tree () in - let* tree = Tree_encoding_runner.encode enc value empty_tree in - Tree_encoding_runner.decode enc tree +open Test_encodings_util (** Test serialize/deserialize instructions. *) let test_instr_roundtrip () = diff --git a/src/lib_scoru_wasm/test/test_wasm_pvm_encodings.ml b/src/lib_scoru_wasm/test/test_wasm_pvm_encodings.ml new file mode 100644 index 0000000000000000000000000000000000000000..76ec21331815e3d2ad4afb65560101b68553e3fd --- /dev/null +++ b/src/lib_scoru_wasm/test/test_wasm_pvm_encodings.ml @@ -0,0 +1,224 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** Testing + ------- + Component: Tree_encoding_decoding + Invocation: dune exec src/lib_scoru_wasm/test/test_scoru_wasm.exe \ + -- test "^WASM PVM Encodings$" + Subject: WASM PVM encoding tests for the tezos-scoru-wasm library +*) + +open Tztest +open Tezos_scoru_wasm +open Tezos_webassembly_interpreter +module Contest = Tezos_context_memory.Context_binary + +let decode_state_gen = + let open QCheck2.Gen in + oneofl + [ + Decode.Byte_vector_step; + Instr_step; + Instr_block_step; + Block_step; + Name_step; + Func_type_step; + Import_step; + Export_step; + Code_step; + Elem_step; + Data_step; + Module_step; + ] + +let init_state_gen = + let open QCheck2.Gen in + oneofl [Eval.Init_step; Map_step; Map_concat_step; Join_step; Section_step] + +let exn_gen = + let open QCheck2.Gen in + let open Lazy_containers in + let value_type_error = + let* i = int in + let* num = Ast_generators.(value_op_gen int32 int64) in + let+ num_type = Ast_generators.num_type_gen in + Values.TypeError (i, num, num_type) + in + let decode_error = + let+ msg = string in + Binary_exn.Decode_error.Error (Source.no_region, msg) + in + let encode_error = + let+ msg = string in + Binary_exn.Encode_error.Error (Source.no_region, msg) + in + let decode_step_error = + let+ state = decode_state_gen in + Decode.Step_error state + in + let init_step_error = + let+ state = init_state_gen in + Eval.Init_step_error state + in + let invalid = + let+ msg = string in + Valid.Invalid (Source.no_region, msg) + in + let link = + let+ msg = string in + Eval.Link (Source.no_region, msg) + in + let trap = + let+ msg = string in + Eval.Trap (Source.no_region, msg) + in + let crash = + let+ msg = string in + Eval.Crash (Source.no_region, msg) + in + let exhaustion = + let+ msg = string in + Eval.Exhaustion (Source.no_region, msg) + in + let import_unknown = + let+ msg = string in + Import.Unknown (Source.no_region, msg) + in + oneof + [ + value_type_error; + return Binary_exn.EOS; + return Binary_exn.Utf8; + decode_error; + encode_error; + decode_step_error; + init_step_error; + return Lazy_map.UnexpectedAccess; + return Lazy_vector.Bounds; + return Lazy_vector.SizeOverflow; + return Chunked_byte_vector.Bounds; + return Chunked_byte_vector.SizeOverflow; + invalid; + return Table.Type; + return Table.SizeLimit; + return Table.OutOfMemory; + return Table.Bounds; + return Table.SizeOverflow; + return Memory.Type; + return Memory.SizeLimit; + return Memory.OutOfMemory; + return Memory.Bounds; + return Memory.SizeOverflow; + return Global.Type; + return Global.NotMutable; + link; + trap; + crash; + exhaustion; + return Ixx.Overflow; + return Ixx.DivideByZero; + return Ixx.InvalidConversion; + return Input_buffer.Bounds; + return Input_buffer.SizeOverflow; + return Input_buffer.Cannot_store_an_earlier_message; + return Input_buffer.Dequeue_from_empty_queue; + import_unknown; + ] + +let error_state_gen = + let open QCheck2.Gen in + let decode_error = + let+ exn = exn_gen in + let error = Wasm_pvm_errors.refine_error exn in + Wasm_pvm_errors.Decode_error error + in + let init_error = + let+ exn = exn_gen in + let error = Wasm_pvm_errors.refine_error exn in + Wasm_pvm_errors.Init_error error + in + let eval_error = + let+ exn = exn_gen in + let error = Wasm_pvm_errors.refine_error exn in + Wasm_pvm_errors.Eval_error error + in + let unknown_error = + let+ err = string in + Wasm_pvm_errors.Unknown_error err + in + let invalid_state = + let+ err = string in + Wasm_pvm_errors.Invalid_state err + in + oneof [decode_error; init_error; eval_error; invalid_state; unknown_error] + +let pp_interpreter_error out Wasm_pvm_errors.{raw_exception; explanation} = + Format.fprintf + out + "@[{ raw_exception: %s; explanation: %s }@]" + raw_exception + (match explanation with None -> "None" | Some s -> "Some: " ^ s) + +let pp_error_state out = function + | Wasm_pvm_errors.Eval_error error -> + Format.fprintf out "@[Eval_error %a@]" pp_interpreter_error error + | Wasm_pvm_errors.Decode_error error -> + Format.fprintf out "@[Decode_error %a@]" pp_interpreter_error error + | Wasm_pvm_errors.Init_error error -> + Format.fprintf out "@[Init_error %a@]" pp_interpreter_error error + | Wasm_pvm_errors.Invalid_state err -> + Format.fprintf out "@[Invalid_state (%s)@]" err + | Wasm_pvm_errors.Unknown_error err -> + Format.fprintf out "@[Unknown_error (%s)@]" err + +let print_error_state = Format.asprintf "%a" pp_error_state + +let error_state_check state state' = + let check_interpreter_error error error' = + error.Wasm_pvm_errors.raw_exception = error'.Wasm_pvm_errors.raw_exception + && error.explanation = error'.explanation + in + match (state, state') with + | Wasm_pvm_errors.Decode_error error, Wasm_pvm_errors.Decode_error error' + | Init_error error, Init_error error' + | Eval_error error, Eval_error error' -> + Lwt.return_ok (check_interpreter_error error error') + | Invalid_state err, Invalid_state err' + | Unknown_error err, Unknown_error err' -> + Lwt.return_ok (err = err') + | _, _ -> Lwt.return_ok false + +let tests = + [ + tztest + "Wasm_pvm_errors" + `Quick + (Test_encodings_util.make_test + ~print:print_error_state + (Tree_encoding.value [] Wasm_pvm_errors.encoding) + error_state_gen + error_state_check); + ] diff --git a/src/lib_scoru_wasm/wasm_pvm.ml b/src/lib_scoru_wasm/wasm_pvm.ml index 6b50d2acda65d03af30501dac73061e94d1e96fc..11378f8fdb9268f266fba1e6fbadc2909d231658 100644 --- a/src/lib_scoru_wasm/wasm_pvm.ml +++ b/src/lib_scoru_wasm/wasm_pvm.ml @@ -41,6 +41,7 @@ type tick_state = init_kont : Tezos_webassembly_interpreter.Eval.init_kont; } | Eval of Wasm.Eval.config + | Stuck of Wasm_pvm_errors.t type pvm_state = { last_input_info : Wasm_pvm_sig.input_info option; @@ -102,6 +103,11 @@ module Make (T : Tree_encoding.TREE) : (Wasm_encoding.config_encoding ~host_funcs) (function Eval eval_config -> Some eval_config | _ -> None) (fun eval_config -> Eval eval_config); + case + "stuck" + (value [] Wasm_pvm_errors.encoding) + (function Stuck err -> Some err | _ -> None) + (fun err -> Stuck err); ] let input_request_encoding = @@ -154,7 +160,7 @@ module Make (T : Tree_encoding.TREE) : (scope ["wasm"] tick_state_encoding) (scope ["input"; "consuming"] input_request_encoding)) - let next_tick_state {module_reg; kernel; tick_state; _} = + let unsafe_next_tick_state {module_reg; kernel; tick_state; _} = let open Lwt_syntax in match tick_state with | Decode {module_kont = MKStop ast_module; _} -> @@ -181,7 +187,7 @@ module Make (T : Tree_encoding.TREE) : Lwt.return (Init {self; ast_module; init_kont}) | Eval ({Wasm.Eval.frame; code; _} as eval_config) -> ( match code with - | _values, [] -> + | _values, [] -> ( (* We have an empty set of admin instructions so we create one that invokes the main function. *) let* module_inst = @@ -195,37 +201,51 @@ module Make (T : Tree_encoding.TREE) : main_name module_inst.Wasm.Instance.exports in - let main_func = - match extern with - | Wasm.Instance.ExternFunc func -> func - | _ -> - (* We require a function with the name [main] to be exported - rather than any other structure. *) - (* TODO: https://gitlab.com/tezos/tezos/-/issues/3448 - Avoid throwing exceptions. - Possibly use a a new state to indicate, such as - [Invalid_module]. - *) - assert false - in - let admin_instr' = Wasm.Eval.Invoke main_func in - let admin_instr = - Wasm.Source.{it = admin_instr'; at = no_region} - in - (* Clear the values and the locals in the frame. *) - let code = ([], [admin_instr]) in - let eval_config = - { - eval_config with - Wasm.Eval.frame = {frame with locals = []}; - code; - } - in - Lwt.return (Eval eval_config) + + match extern with + | Wasm.Instance.ExternFunc main_func -> + let admin_instr' = Wasm.Eval.Invoke main_func in + let admin_instr = + Wasm.Source.{it = admin_instr'; at = no_region} + in + (* Clear the values and the locals in the frame. *) + let code = ([], [admin_instr]) in + let eval_config = + { + eval_config with + Wasm.Eval.frame = {frame with locals = []}; + code; + } + in + Lwt.return (Eval eval_config) + | _ -> + (* We require a function with the name [main] to be exported + rather than any other structure. *) + Lwt.return + (Stuck + (Invalid_state + "Invalid_module: no `main` function exported"))) | _ -> (* Continue execution. *) let* eval_config = Wasm.Eval.step module_reg eval_config in Lwt.return (Eval eval_config)) + | Stuck e -> Lwt.return (Stuck e) + + let next_tick_state pvm_state = + let to_stuck exn = + let error = Wasm_pvm_errors.refine_error exn in + let wasm_error = + if Wasm_pvm_errors.is_interpreter_error exn then + match pvm_state.tick_state with + | Decode _ -> Wasm_pvm_errors.Decode_error error + | Init _ -> Init_error error + | Eval _ -> Eval_error error + | Stuck _ -> Unknown_error error.raw_exception + else Unknown_error error.raw_exception + in + Lwt.return (Stuck wasm_error) + in + Lwt.catch (fun () -> unsafe_next_tick_state pvm_state) to_stuck let compute_step tree = let open Lwt_syntax in @@ -234,7 +254,7 @@ module Make (T : Tree_encoding.TREE) : let* tick_state = next_tick_state pvm_state in let input_request = match pvm_state.tick_state with - | Eval {code = _, []; _} -> + | Eval {code = _, []; _} | Stuck _ -> (* Ask for more input if the kernel has yielded (empty admin instructions). *) Wasm_pvm_sig.Input_required @@ -270,26 +290,30 @@ module Make (T : Tree_encoding.TREE) : let level = Int32.to_string raw_level in let id = Z.to_string message_counter in let* pvm_state = Tree_encoding_runner.decode pvm_state_encoding tree in - let* () = + let* tick_state = match pvm_state.tick_state with | Eval {input; _} -> - Wasm.Input_buffer.( - enqueue - input - { - (* This is to distinguish (0) Inbox inputs from (1) - DAL/Slot_header inputs. *) - rtype = 0l; - raw_level; - message_counter; - payload = String.to_bytes message; - }) - | Decode _ | Init _ -> - (* TODO: https://gitlab.com/tezos/tezos/-/issues/3448 - Avoid throwing exceptions. - Possibly use a a new state to indicate, such as [Stuck]. - *) - assert false + let+ () = + Wasm.Input_buffer.( + enqueue + input + { + (* This is to distinguish (0) Inbox inputs from (1) + DAL/Slot_header inputs. *) + rtype = 0l; + raw_level; + message_counter; + payload = String.to_bytes message; + }) + in + pvm_state.tick_state + | Decode _ -> + Lwt.return + (Stuck (Invalid_state "No input required during decoding")) + | Init _ -> + Lwt.return + (Stuck (Invalid_state "No input required during initialization")) + | Stuck _ -> Lwt.return pvm_state.tick_state in (* Encode the input in the tree under [input/level/id]. *) let* tree = @@ -302,6 +326,7 @@ module Make (T : Tree_encoding.TREE) : let pvm_state = { pvm_state with + tick_state; current_tick = Z.succ pvm_state.current_tick; input_request = Wasm_pvm_sig.No_input_required; } diff --git a/src/lib_scoru_wasm/wasm_pvm_errors.ml b/src/lib_scoru_wasm/wasm_pvm_errors.ml new file mode 100644 index 0000000000000000000000000000000000000000..e0d9ea940d19bf85ca74a5512951b305da6a2b20 --- /dev/null +++ b/src/lib_scoru_wasm/wasm_pvm_errors.ml @@ -0,0 +1,163 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +open Tezos_webassembly_interpreter + +type interpreter_error = {raw_exception : string; explanation : string option} + +type t = + | Decode_error of interpreter_error + | Init_error of interpreter_error + | Eval_error of interpreter_error + | Invalid_state of string + | Unknown_error of string + +let is_interpreter_error = + let open Lazy_containers in + function + (* Decoder errors. *) + | Values.TypeError _ | Binary_exn.EOS | Binary_exn.Utf8 + | Binary_exn.Decode_error.Error _ | Binary_exn.Encode_error.Error _ + | Decode.Step_error _ + (* Lazy_containers errors: these should've already been wrapped by + the decoder or the memory, but we consider them as interpreter + errors nonetheless. *) + | Lazy_map.UnexpectedAccess | Lazy_vector.Bounds | Lazy_vector.SizeOverflow + | Chunked_byte_vector.Bounds | Chunked_byte_vector.SizeOverflow + (* Validator errors: should not happen at all, but some usages of + the validator happens in the parser. *) + | Valid.Invalid _ + (* Table errors. *) + | Table.Type | Table.SizeLimit | Table.OutOfMemory | Table.Bounds + | Table.SizeOverflow + (* Memory errors. *) + | Memory.Type | Memory.SizeLimit | Memory.OutOfMemory | Memory.Bounds + | Memory.SizeOverflow + (* Globals errors. *) + | Global.Type | Global.NotMutable + (* Evaluation errors. *) + | Eval.Link _ | Eval.Trap _ | Eval.Crash _ | Eval.Exhaustion _ | Ixx.Overflow + | Eval.Init_step_error _ | Ixx.DivideByZero | Ixx.InvalidConversion + (* Inputs errors. *) + | Input_buffer.Bounds | Input_buffer.SizeOverflow + | Input_buffer.Cannot_store_an_earlier_message + | Input_buffer.Dequeue_from_empty_queue | Import.Unknown _ -> + true + | _ -> false + +let decode_state_to_string = function + | Decode.Byte_vector_step -> "Byte_vector_step" + | Instr_step -> "Instr_step" + | Instr_block_step -> "Instr_block_step" + | Block_step -> "Block_step" + | Name_step -> "Name_step" + | Func_type_step -> "Func_type_step" + | Import_step -> "Import_step" + | Export_step -> "Export_step" + | Code_step -> "Code_step" + | Elem_step -> "Elem_step" + | Data_step -> "Data_step" + | Module_step -> "Module_step" + +let eval_state_to_string = function + | Eval.Init_step -> "Init_step" + | Map_step -> "Map_step" + | Map_concat_step -> "Map_concat_step" + | Join_step -> "Join_step" + | Section_step -> "Section_step" + +let refine_error exn = + let open Lazy_containers in + let raw_exception = Printexc.to_string exn in + match exn with + (* The locations are removed during encoding, they won't be usable in practice. *) + | Binary_exn.Decode_error.Error (_, explanation) + | Binary_exn.Encode_error.Error (_, explanation) + | Valid.Invalid (_, explanation) + | Eval.Link (_, explanation) + | Eval.Trap (_, explanation) + | Eval.Crash (_, explanation) + | Eval.Exhaustion (_, explanation) + | Import.Unknown (_, explanation) -> + {raw_exception; explanation = Some explanation} + | Values.TypeError _ | Binary_exn.EOS | Binary_exn.Utf8 + | Lazy_map.UnexpectedAccess | Lazy_vector.Bounds | Lazy_vector.SizeOverflow + | Chunked_byte_vector.Bounds | Chunked_byte_vector.SizeOverflow | Table.Type + | Table.SizeLimit | Table.OutOfMemory | Table.Bounds | Table.SizeOverflow + | Memory.Type | Memory.SizeLimit | Memory.OutOfMemory | Memory.Bounds + | Memory.SizeOverflow | Global.Type | Global.NotMutable | Ixx.Overflow + | Ixx.DivideByZero | Ixx.InvalidConversion | Input_buffer.Bounds + | Input_buffer.SizeOverflow | Input_buffer.Cannot_store_an_earlier_message + | Input_buffer.Dequeue_from_empty_queue -> + {raw_exception; explanation = None} + | Decode.Step_error state -> + {raw_exception; explanation = Some (decode_state_to_string state)} + | Eval.Init_step_error state -> + {raw_exception; explanation = Some (eval_state_to_string state)} + | _ -> {raw_exception; explanation = None} + +let encoding = + let open Data_encoding in + let interpreter_error_encoding prefix = + conv + (fun {raw_exception; explanation} -> (raw_exception, explanation)) + (fun (raw_exception, explanation) -> {raw_exception; explanation}) + (obj2 + (req (prefix ^ "_raw_exception") string) + (req (prefix ^ "_explanation") (option string))) + in + union + [ + case + (Tag 0) + ~title:"Decode_error" + (interpreter_error_encoding "decode") + (function Decode_error err -> Some err | _ -> None) + (fun err -> Decode_error err); + case + (Tag 1) + ~title:"Init_error" + (interpreter_error_encoding "init") + (function Init_error err -> Some err | _ -> None) + (fun err -> Init_error err); + case + (Tag 2) + ~title:"Eval_error" + (interpreter_error_encoding "eval") + (function Eval_error err -> Some err | _ -> None) + (fun err -> Eval_error err); + case + (Tag 3) + ~title:"Invalid_state" + (obj1 (req "invalid_state" string)) + (function Invalid_state msg -> Some msg | _ -> None) + (fun msg -> Invalid_state msg); + case + (Tag 4) + ~title:"Unknown_error" + (obj1 (req "unknown_error" string)) + (function Unknown_error exn -> Some exn | _ -> None) + (fun exn -> Unknown_error exn); + ] diff --git a/src/lib_scoru_wasm/wasm_pvm_errors.mli b/src/lib_scoru_wasm/wasm_pvm_errors.mli new file mode 100644 index 0000000000000000000000000000000000000000..f7e0a7f2eb2cf85e60d4748b883385a5c7174e79 --- /dev/null +++ b/src/lib_scoru_wasm/wasm_pvm_errors.mli @@ -0,0 +1,59 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2022 Nomadic Labs *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** Representation of errors of the WASM PVM. *) + +(** Raw exception as printed by `Printexc.to_string`. *) +type raw_exception := string + +(** Embedded message in the exception. *) +type explanation := string + +(** Wrapped exceptions from the interpreter. *) +type interpreter_error = { + raw_exception : raw_exception; + explanation : string option; +} + +type t = + | Decode_error of interpreter_error + (** Wraps exceptions raised during parsing. *) + | Init_error of interpreter_error + (** Wraps exceptions raised during initialization. *) + | Eval_error of interpreter_error + (** Wraps exceptions raised during evaluation. *) + | Invalid_state of explanation + (** Invalid state of the PVM (waiting for input during the parsing for example). *) + | Unknown_error of raw_exception + (** Wraps unexpected exceptions raised by the interpreter. *) + +(** [is_interpreter_error exn] returns true if the exception comes + from the interpreter. *) + +val is_interpreter_error : exn -> bool + +val refine_error : exn -> interpreter_error + +val encoding : t Data_encoding.t diff --git a/src/lib_webassembly/binary/decode.ml b/src/lib_webassembly/binary/decode.ml index 3c2464e3fb2a8954022f7375d87fc38ec35fd060..e12d526fda50e6b5e35cdc48967a41d5c3b85288 100644 --- a/src/lib_webassembly/binary/decode.ml +++ b/src/lib_webassembly/binary/decode.ml @@ -64,6 +64,22 @@ let get_string n s = exception Code = Decode_error.Error +type state = + | Byte_vector_step + | Instr_step + | Instr_block_step + | Block_step + | Name_step + | Func_type_step + | Import_step + | Export_step + | Code_step + | Elem_step + | Data_step + | Module_step + +exception Step_error of state + let string_of_byte b = Printf.sprintf "%02x" b let string_of_multi n = Printf.sprintf "%02lx" n @@ -272,7 +288,7 @@ let byte_vector_step vecs s = let+ () = Ast.add_to_data vecs vector index c in VKRead (vector, Int64.succ index, len) (* Final step, cannot reduce *) - | VKStop _ -> assert false + | VKStop _ -> raise (Step_error Byte_vector_step) (* Types *) @@ -433,7 +449,7 @@ let instr s pos tag = match tag with (* These tags corresponds to resp. block, loop and if, and are now handled directly by the main step loop (see `IKBlock`, `IKLoop` and `IKIf1`. *) - | 0x02 | 0x03 | 0x04 -> assert false + | 0x02 | 0x03 | 0x04 -> raise (Step_error Instr_step) | 0x00 -> Lwt.return unreachable | 0x01 -> Lwt.return nop | 0x05 -> error s pos "misplaced ELSE opcode" @@ -1093,7 +1109,7 @@ let instr_block_step s allocs cont = | _ :: _ :: _ :: _ :: _ -> Stdlib.failwith "More than 3 values popped from the stack" (* Enforces the number of values popped from the stack, which shouldn't fail. *) - | [IKStop _] -> invalid_arg "instr_block" + | [IKStop _] -> raise (Step_error Instr_block_step) | [IKStop lbl; IKBlock (bt, pos); IKNext plbl] -> let* () = end_ s in let e = Source.(block bt lbl @@ region s pos pos) in @@ -1161,12 +1177,13 @@ let instr_block_step s allocs cont = let+ () = add_to_block allocs lbl e in push_rev_values (IKNext lbl :: ks) stack)) (* Stop can only be followed a new block, or being the final state. *) - | IKStop _ :: _ -> invalid_arg "instr_block" + | IKStop _ :: _ -> raise (Step_error Instr_block_step) (* These continuations never reduce directly and are always preceded by an end of block or the accumulation of parsed instructions (`IKNext`). *) - | IKBlock _ :: _ | IKLoop _ :: _ | IKIf1 _ :: _ | IKIf2 _ :: _ -> assert false + | IKBlock _ :: _ | IKLoop _ :: _ | IKIf1 _ :: _ | IKIf2 _ :: _ -> + raise (Step_error Instr_block_step) (* The empty continuation cannot reduce. *) - | [] -> assert false + | [] -> raise (Step_error Instr_block_step) type block_kont = | BlockStart @@ -1191,7 +1208,7 @@ let block_step s allocs = else let+ kont = instr_block_step s allocs kont in BlockParse kont - | BlockStop _ -> assert false + | BlockStop _ -> raise (Step_error Block_step) (** Vector and size continuations *) @@ -1239,7 +1256,8 @@ let name_step s = in let vec = LazyVec {lv with vector = Vector.grow 1l lv.vector} in Lwt.return @@ NKParse (pos, lazy_vec_step d vec, len - offset) - | NKStop _ -> assert false (* final step, cannot reduce. *) + (* final step, cannot reduce. *) + | NKStop _ -> raise (Step_error Name_step) let name s = let open Lwt.Syntax in @@ -1329,7 +1347,8 @@ let func_type_step s = | FKOut (ins, out_vec) -> let+ vt = value_type s in FKOut (ins, lazy_vec_step vt out_vec) - | FKStop _ -> assert false (* cannot reduce *) + | FKStop _ -> raise (Step_error Func_type_step) +(* cannot reduce *) (* let _type_ s = at func_type s *) @@ -1376,7 +1395,8 @@ let import_step s = | ImpKItemName (module_name, nk) -> let+ x = name_step s nk in ImpKItemName (module_name, x) - | ImpKStop _ -> assert false (* Final step, cannot reduce *) + (* Final step, cannot reduce *) + | ImpKStop _ -> raise (Step_error Import_step) (* Table section *) @@ -1427,7 +1447,8 @@ let export_step s = | ExpKName nk -> let+ x = name_step s nk in ExpKName x - | ExpKStop _ -> assert false (* Final step, cannot reduce *) + (* Final step, cannot reduce *) + | ExpKStop _ -> raise (Step_error Export_step) (* Start section *) @@ -1571,7 +1592,8 @@ let code_step s allocs = | CKBody {left; size; locals; const_kont} -> let+ const_kont = block_step s allocs const_kont in CKBody {left; size; locals; const_kont} - | CKStop _ -> assert false (* final step, cannot reduce *) + (* final step, cannot reduce *) + | CKStop _ -> raise (Step_error Code_step) (* Element section *) @@ -1790,7 +1812,8 @@ let elem_step s allocs = | EKInitConst {mode; ref_type; einit_vec; einit_kont = left, k} -> let+ k' = block_step s allocs k in EKInitConst {mode; ref_type; einit_vec; einit_kont = (left, k')} - | EKStop _ -> assert false (* Final step, cannot reduce *) + (* Final step, cannot reduce *) + | EKStop _ -> raise (Step_error Elem_step) (* Data section *) @@ -1844,7 +1867,8 @@ let data_step s allocs = | DKInit {dmode; init_kont} -> let+ init_kont = byte_vector_step allocs s init_kont in DKInit {dmode; init_kont} - | DKStop _ -> assert false (* final step, cannot reduce *) + (* final step, cannot reduce *) + | DKStop _ -> raise (Step_error Data_step) (* DataCount section *) @@ -2312,7 +2336,7 @@ let module_step bytes state = @@ region_ state.stream_name 0 state.stream_pos) in {state with module_kont = MKStop m} |> Lwt.return - | MKStop _ (* Stop cannot reduce. *) -> assert false + | MKStop _ (* Stop cannot reduce. *) -> raise (Step_error Module_step) let initial_decode_kont ~name = let allocation_state = Ast.empty_allocations () in diff --git a/src/lib_webassembly/binary/decode.mli b/src/lib_webassembly/binary/decode.mli index 33b2bce05af76144254d1687984d7286135d49f0..1fc575e5d045731d94def67a2d46f3144a1ae05b 100644 --- a/src/lib_webassembly/binary/decode.mli +++ b/src/lib_webassembly/binary/decode.mli @@ -2,6 +2,24 @@ module Vector = Lazy_vector.Int32Vector exception Code of Source.region * string +(** States representation. *) +type state = + | Byte_vector_step + | Instr_step + | Instr_block_step + | Block_step + | Name_step + | Func_type_step + | Import_step + | Export_step + | Code_step + | Elem_step + | Data_step + | Module_step + +(** Exception raised when the small-step parser evaluate an impossible state. *) +exception Step_error of state + (** Lazy stack using an underlying lazy vector, and a pointer on the head of the stack. *) type 'a lazy_stack = LazyStack of {length : int32; vector : 'a Vector.t} diff --git a/src/lib_webassembly/exec/eval.ml b/src/lib_webassembly/exec/eval.ml index 718277d5c8449d66fdd42109b630d1b343131cbc..1c09c487609ee0608857ef40315281da4c81578e 100644 --- a/src/lib_webassembly/exec/eval.ml +++ b/src/lib_webassembly/exec/eval.ml @@ -23,6 +23,15 @@ exception Crash = Crash.Error (* failure that cannot happen in valid code *) exception Exhaustion = Exhaustion.Error +type init_state = + | Init_step + | Map_step + | Map_concat_step + | Join_step + | Section_step + +exception Init_step_error of init_state + let table_error at = function | Table.Bounds -> "out of bounds table access" | Table.SizeOverflow -> "table size overflow" @@ -781,7 +790,7 @@ and step_resolved module_reg (c : config) frame vs e es : config Lwt.t = ("missing or ill-typed operand on stack (" ^ s1 ^ " : " ^ s2 ^ ")") ) | Refer r, vs -> Lwt.return (Ref r :: vs, []) - | Trapping _, _ -> assert false + | Trapping msg, _ -> Trap.error e.at msg | Returning _, _ -> Crash.error e.at "undefined frame" | Breaking _, _ -> Crash.error e.at "undefined label" | Label (_, _, (vs', [])), vs -> Lwt.return (vs' @ vs, []) @@ -1141,7 +1150,7 @@ let tick_map_completed {map; _} = map_completed map let tick_map_kont v = {tick = None; map = map_kont v} let tick_map_step first_kont kont_completed kont_step = function - | {map; _} when map_completed map -> assert false + | {map; _} when map_completed map -> raise (Init_step_error Map_step) | {tick = None; map} -> let+ x = Vector.get map.offset map.origin in let tick = first_kont x in @@ -1219,8 +1228,8 @@ let join_step = J_Next (tick, acc) | J_Init _ -> (* [num_elements = 0l], so [join_completed] returns [Some], should not be called in this state *) - assert false - | J_Stop _ -> assert false + raise (Init_step_error Join_step) + | J_Stop _ -> raise (Init_step_error Join_step) type ('a, 'b) map_concat_kont = | MC_Map of ('a, 'b Vector.t) map_kont @@ -1241,7 +1250,7 @@ let map_concat_step f = function | Some _ -> (* [map_concat_completed] would have returned [Some], so illegal state to call this function *) - assert false + raise (Init_step_error Map_concat_step) | None -> let+ tick = join_step tick in MC_Join tick) @@ -1310,7 +1319,7 @@ let section_inner_step : | Left x -> let+ y = f x in Right y - | Right _ -> assert false + | Right _ -> raise (Init_step_error Section_step) in function | Func -> lift_either (create_func module_reg self) @@ -1453,7 +1462,7 @@ let init_step ~module_reg ~self host_funcs (m : module_) (exts : extern list) = | IK_Eval (inst, config) -> let+ config = step module_reg config in IK_Eval (inst, config) - | IK_Stop _ -> raise (Invalid_argument "init_step") + | IK_Stop _ -> raise (Init_step_error Init_step) let init ~module_reg ~self host_funcs (m : module_) (exts : extern list) : module_inst Lwt.t = diff --git a/src/lib_webassembly/exec/eval.mli b/src/lib_webassembly/exec/eval.mli index ba3751a9c3fb4919c3d74c00fd9315ca63780bb0..2b12bd28487e2ac38c9b5aff02147756a8f866fd 100644 --- a/src/lib_webassembly/exec/eval.mli +++ b/src/lib_webassembly/exec/eval.mli @@ -9,6 +9,18 @@ exception Crash of Source.region * string exception Exhaustion of Source.region * string +(** Possible states of the small-step initializer, used for error reporting. *) +type init_state = + | Init_step + | Map_step + | Map_concat_step + | Join_step + | Section_step + +(** Exception raised on irreducible states of the small step + initialization. *) +exception Init_step_error of init_state + type frame = {inst : module_key; locals : value ref list} type code = value list * admin_instr list