optik icon indicating copy to clipboard operation
optik copied to clipboard

Bug: JSONDecodeError crash when running `hybrid-echidna`

Open rappie opened this issue 3 years ago • 2 comments

Describe the bug It runs echidna for about 2 minutes and then crashes.

To Reproduce Sadly I cannot share my environment. I could narrowing things down with some guidance.

Additional context:

  • Manjaro Linux, Python 3.10.8
  • Echidna 2.0.4
  • Slither 0.9.1
❯ hybrid-echidna . --contract E2E --config echidna-config.yaml
Traceback (most recent call last):
  File "/home/rappie/.local/bin/hybrid-echidna", line 8, in <module>
    sys.exit(main())
  File "/home/rappie/.local/lib/python3.10/site-packages/optik/echidna/__main__.py", line 554, in main
    func(sys.argv[1:])
  File "/home/rappie/.local/lib/python3.10/site-packages/optik/echidna/__main__.py", line 344, in run_hybrid_echidna_with_display
    raise exc
  File "/home/rappie/.local/lib/python3.10/site-packages/optik/echidna/__main__.py", line 321, in run_hybrid_echidna_with_display
    run_hybrid_echidna(args)
  File "/home/rappie/.local/lib/python3.10/site-packages/optik/echidna/__main__.py", line 235, in run_hybrid_echidna
    nb_cov_insts = count_unique_pc(p.stdout)
  File "/home/rappie/.local/lib/python3.10/site-packages/optik/echidna/interface.py", line 523, in count_unique_pc
    data = json.loads(output)
  File "/usr/lib/python3.10/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "/usr/lib/python3.10/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/usr/lib/python3.10/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

rappie avatar Dec 04 '22 15:12 rappie

I had the same issue. I managed to fix it by completely uninstalling nix (https://nix-tutorial.gitlabpages.inria.fr/nix-tutorial/installation.html#uninstalling-nix) and downloading the echidna-test v2.0.4 binary from https://github.com/crytic/echidna/releases

SheldonHolmgren avatar Mar 01 '23 14:03 SheldonHolmgren

I see what's going on. In https://github.com/crytic/optik/blob/master/optik/echidna/interface.py#L522 optik assumes that echidna-test --format json will have a json in the second line of the output. But starting from commit 1f59ae189258ab52342331a92208f5b6b8930ea7 the json is in the third line of echidna's output.

The following fix works:

--- a/optik/echidna/interface.py
+++ b/optik/echidna/interface.py
@@ -469,7 +469,7 @@ def extract_cases_from_json_output(output: str) -> List[List[str]]:
     """
     # Sometimes the JSON output starts with a line such as
     # "Loaded total of 500 transactions from /tmp/c4/coverage"
-    if output.startswith("Loaded total of"):
+    while output.startswith("Loaded total of"):
         output = output.split("\n", 1)[1]
     data = json.loads(output)
     if "tests" not in data:
@@ -518,7 +518,7 @@ def count_unique_pc(output: str) -> int:
 
     :param output: echidna's JSON output as a simple string
     """
-    if output.startswith("Loaded total of"):
+    while output.startswith("Loaded total of"):
         output = output.split("\n", 1)[1]
     data = json.loads(output)
     res = 0

But that's not sufficient to make echidna v2.0.5 work. The json format has also changed so the following dirty fix is needed:

--- a/optik/corpus/generator.py
+++ b/optik/corpus/generator.py
@@ -108,10 +108,10 @@ class EchidnaCorpusGenerator(CorpusGenerator):
             with open(os.path.join(corpus_dir, filename), "rb") as f:
                 data = json.loads(f.read())
                 for tx in data:
-                    if tx["_call"]["tag"] == "NoCall":
+                    if tx["call"]["tag"] == "NoCall":
                         continue
                     func_name, args_spec, _ = extract_func_from_call(
-                        tx["_call"]
+                        tx["call"]
                     )
                     func_prototype = func_signature(func_name, args_spec)
                     # Only store one tx for each function
diff --git a/optik/echidna/interface.py b/optik/echidna/interface.py
index ca23622..3f580b8 100644
--- a/optik/echidna/interface.py
+++ b/optik/echidna/interface.py
@@ -168,13 +168,13 @@ def load_tx(tx: Dict, tx_name: str = "") -> AbstractTx:
     # Translate block number/timestamp increments
     block_num_inc = Var(256, f"{tx_name}_block_num_inc")
     block_timestamp_inc = Var(256, f"{tx_name}_block_timestamp_inc")
-    ctx.set(block_num_inc.name, int(tx["_delay"][1], 16), block_num_inc.size)
+    ctx.set(block_num_inc.name, int(tx["delay"][1], 16), block_num_inc.size)
     ctx.set(
-        block_timestamp_inc.name, int(tx["_delay"][0], 16), block_num_inc.size
+        block_timestamp_inc.name, int(tx["delay"][0], 16), block_num_inc.size
     )
 
     # Check if it's a "NoCall" echidna transaction
-    if tx["_call"]["tag"] == "NoCall":
+    if tx["call"]["tag"] == "NoCall":
         return AbstractTx(
             None,
             block_num_inc,
@@ -183,27 +183,27 @@ def load_tx(tx: Dict, tx_name: str = "") -> AbstractTx:
         )
 
     # Translate function call
-    func_name, args_spec, arg_values = extract_func_from_call(tx["_call"])
+    func_name, args_spec, arg_values = extract_func_from_call(tx["call"])
     call_data = function_call(func_name, args_spec, ctx, tx_name, *arg_values)
 
     # Translate message sender
     sender = Var(160, f"{tx_name}_sender")
-    ctx.set(sender.name, int(tx["_src"], 16), sender.size)
+    ctx.set(sender.name, int(tx["src"], 16), sender.size)
 
     # Translate message value
     # Echidna will only send non-zero msg.value to payable funcs
     # so we only make an abstract value in that case
-    if int(tx["_value"], 16) != 0:
+    if int(tx["value"], 16) != 0:
         value = Var(256, f"{tx_name}_value")
-        ctx.set(value.name, int(tx["_value"], 16), value.size)
+        ctx.set(value.name, int(tx["value"], 16), value.size)
     else:
         value = Cst(256, 0)
 
     # Build transaction
     # TODO: make EVMTransaction accept integers as arguments
-    gas_limit = Cst(256, int(tx["_gas'"], 16))
-    gas_price = Cst(256, int(tx["_gasprice'"], 16))
-    recipient = int(tx["_dst"], 16)
+    gas_limit = Cst(256, tx["gas"], 16)
+    gas_price = Cst(256, int(tx["gasprice"], 16))
+    recipient = int(tx["dst"], 16)
     return AbstractTx(
         EVMTransaction(
             sender,  # origin
@@ -335,7 +335,7 @@ def update_tx(tx: Dict, new_model: VarContext, tx_name: str = "") -> Dict:
     tx = tx.copy()  # Copy transaction to avoid in-place modifications
 
     # Update call arguments
-    call = tx["_call"]
+    call = tx["call"]
     args = call["contents"][1]
     for i, arg in enumerate(args):
         update_argument(arg, f"{tx_name}_arg{i}", new_model)
@@ -344,20 +344,20 @@ def update_tx(tx: Dict, new_model: VarContext, tx_name: str = "") -> Dict:
     block_num_inc = f"{tx_name}_block_num_inc"
     block_timestamp_inc = f"{tx_name}_block_timestamp_inc"
     if new_model.contains(block_num_inc):
-        tx["_delay"][1] = hex(new_model.get(block_num_inc))
+        tx["delay"][1] = hex(new_model.get(block_num_inc))
     if new_model.contains(block_timestamp_inc):
-        tx["_delay"][0] = hex(new_model.get(block_timestamp_inc))
+        tx["delay"][0] = hex(new_model.get(block_timestamp_inc))
 
     # Update sender
     sender = f"{tx_name}_sender"
     if new_model.contains(sender):
         # Address so we need to pad it to 40 chars (20bytes)
-        tx["_src"] = f"0x{new_model.get(sender):0{40}x}"
+        tx["src"] = f"0x{new_model.get(sender):0{40}x}"
 
     # Update transaction value
     value = f"{tx_name}_value"
     if new_model.contains(value):
-        tx["_value"] = hex(new_model.get(value))
+        tx["value"] = hex(new_model.get(value))
 
     return tx

I will let someone more knowledgeable identify and implement a proper solution.

SheldonHolmgren avatar Mar 02 '23 15:03 SheldonHolmgren