From 381c982df2511ea86dcdd798cb4960259f6ca022 Mon Sep 17 00:00:00 2001 From: Siemen Dhooghe Date: Wed, 9 Sep 2026 00:34:02 +0200 Subject: [PATCH] [pentest/gdb] Cleanup gdb tests and add rom_ext_imm test The GDB fault simulations can be made faster and more accurate (no missed PCs) using better handling of openocd: - Breakpoint misfire recovery: if jtag/openocd delays happen, do not abort the campaign but retry - Connection check: check gdb health by pinging it and extend openocd restart delays - UART clean: empty UART at every test to ensure no stale data is read out - Switch to hardware breakpoints for observation points Add tags to run a specific test in a larger python unittest. Fix the rom_ext_imm double otp read issue that was bricking gdb. Add the rom_ext_imm test from master. Signed-off-by: Siemen Dhooghe --- sw/device/tests/penetrationtests/BUILD | 13 +- .../penetrationtests/firmware/testdata/BUILD | 41 + sw/device/tests/penetrationtests/pentest.bzl | 33 + sw/host/penetrationtests/python/fi/BUILD | 27 + .../fi_asym_cryptolib_python_gdb_test.py | 953 +++++++++++++++--- .../fi_owner_upgrade_python_gdb_test.py | 67 +- ...ner_upgrade_self_signed_python_gdb_test.py | 67 +- .../fi_rom_ext_imm_skip_python_gdb_test.py | 414 ++++++++ .../gdb_testing/fi_rom_ext_python_gdb_test.py | 83 +- .../fi_rom_ext_rollback_python_gdb_test.py | 83 +- .../fi/gdb_testing/fi_rom_python_gdb_test.py | 184 +++- .../fi_rom_rollback_python_gdb_test.py | 73 +- .../fi_sym_cryptolib_python_gdb_test.py | 425 ++++++-- .../fi_unit_gdb_python_gdb_test.py | 168 ++- .../python/util/gdb_controller.py | 541 ++++++++-- sw/host/penetrationtests/python/util/utils.py | 131 +++ 16 files changed, 2845 insertions(+), 458 deletions(-) create mode 100644 sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_imm_skip_python_gdb_test.py diff --git a/sw/device/tests/penetrationtests/BUILD b/sw/device/tests/penetrationtests/BUILD index 86128170bb0e3..fa7f2bf0a5011 100644 --- a/sw/device/tests/penetrationtests/BUILD +++ b/sw/device/tests/penetrationtests/BUILD @@ -2,7 +2,7 @@ # Licensed under the Apache License, Version 2.0, see LICENSE for details. # SPDX-License-Identifier: Apache-2.0 -load(":pentest.bzl", "pentest_cryptolib_fi_asym", "pentest_cryptolib_fi_gdb_asym", "pentest_cryptolib_fi_gdb_sym", "pentest_cryptolib_fi_sym", "pentest_cryptolib_sca_asym", "pentest_cryptolib_sca_sym", "pentest_fi", "pentest_fi_ibex", "pentest_fi_otbn", "pentest_gdb_unit", "pentest_owner_upgrade_fi_gdb", "pentest_owner_upgrade_self_signed_fi_gdb", "pentest_rom_ext_fi_gdb", "pentest_rom_ext_rollback_fi_gdb", "pentest_rom_fi_gdb", "pentest_rom_rollback_fi_gdb", "pentest_sca") +load(":pentest.bzl", "pentest_cryptolib_fi_asym", "pentest_cryptolib_fi_gdb_asym", "pentest_cryptolib_fi_gdb_sym", "pentest_cryptolib_fi_sym", "pentest_cryptolib_sca_asym", "pentest_cryptolib_sca_sym", "pentest_fi", "pentest_fi_ibex", "pentest_fi_otbn", "pentest_gdb_unit", "pentest_owner_upgrade_fi_gdb", "pentest_owner_upgrade_self_signed_fi_gdb", "pentest_rom_ext_fi_gdb", "pentest_rom_ext_imm_skip_fi_gdb", "pentest_rom_ext_rollback_fi_gdb", "pentest_rom_fi_gdb", "pentest_rom_rollback_fi_gdb", "pentest_sca") load("@ot_python_deps//:requirements.bzl", "requirement") package(default_visibility = ["//visibility:public"]) @@ -526,6 +526,17 @@ pentest_rom_ext_rollback_fi_gdb( test_vectors = [], ) +pentest_rom_ext_imm_skip_fi_gdb( + name = "fi_rom_ext_imm_skip_python_gdb_test", + tags = [ + "manual", + "skip_in_ci", + ], + test_args = "", + test_harness = "//sw/host/penetrationtests/python/fi:fi_rom_ext_imm_skip_python_gdb_test", + test_vectors = [], +) + # This test is to perform unit tests of instruction skipping (also in CI). # The functional test pentest_gdb_unit( diff --git a/sw/device/tests/penetrationtests/firmware/testdata/BUILD b/sw/device/tests/penetrationtests/firmware/testdata/BUILD index e5a148d33b95e..b6230b7553f37 100644 --- a/sw/device/tests/penetrationtests/firmware/testdata/BUILD +++ b/sw/device/tests/penetrationtests/firmware/testdata/BUILD @@ -19,6 +19,7 @@ load( "otp_hex", "otp_image", "otp_json", + "otp_json_immutable_rom_ext", "otp_partition", ) @@ -193,6 +194,46 @@ otp_image( ], ) +otp_json_immutable_rom_ext( + name = "otp_json_rom_ext_imm_skip_fi", + testonly = True, + partitions = [ + otp_partition( + name = "CREATOR_SW_CFG", + items = { + "CREATOR_SW_CFG_RMA_SPIN_EN": otp_hex(CONST.HARDENED_TRUE), + # Number of Ibex cycles to spin: we set this to the highest value + "CREATOR_SW_CFG_RMA_SPIN_CYCLES": "0xffffffff", + # Enable the immutable ROM_EXT + "CREATOR_SW_CFG_IMMUTABLE_ROM_EXT_EN": otp_hex(CONST.HARDENED_TRUE), + # Set the hash to a bogus value + "CREATOR_SW_CFG_IMMUTABLE_ROM_EXT_SHA256_HASH": otp_hex(0x1234), + }, + ), + otp_partition( + name = "SECRET2", + items = { + # We set reproducible bitstreams for the tests + "RMA_TOKEN": "0000000000000005", + "CREATOR_ROOT_KEY_SHARE0": "1111111111111111111111111111111111111111111111111111111111111111", + "CREATOR_ROOT_KEY_SHARE1": "2222222222222222222222222222222222222222222222222222222222222222", + }, + lock = True, + ), + ], + rom_ext = "//sw/device/silicon_creator/rom_ext:rom_ext_dice_x509_slot_virtual", + visibility = ["//visibility:private"], +) + +otp_image( + name = "otp_img_rom_ext_imm_skip_fi", + testonly = True, + src = "//hw/ip/otp_ctrl/data:otp_json_rma", + overlays = STD_OTP_OVERLAYS + [ + ":otp_json_rom_ext_imm_skip_fi", + ], +) + exports_files(["bl0_owner_upgrade.c"]) exports_files(["bl0_self_signed_owner_upgrade.c"]) diff --git a/sw/device/tests/penetrationtests/pentest.bzl b/sw/device/tests/penetrationtests/pentest.bzl index 415c350bd3b09..0a878530ded84 100644 --- a/sw/device/tests/penetrationtests/pentest.bzl +++ b/sw/device/tests/penetrationtests/pentest.bzl @@ -886,6 +886,39 @@ def pentest_rom_ext_rollback_fi_gdb(name, test_vectors, test_args, test_harness, deps = FIRMWARE_DEPS_CRYPTOLIB_FI_ASYM, ) +def pentest_rom_ext_imm_skip_fi_gdb(name, test_vectors, test_args, test_harness, tags): + """A macro for defining a CryptoTest test case. + + Args: + name: the name of the test. + test_vectors: the test vectors to use. + test_args: additional arguments to pass to the test. + test_harness: the test harness to use. + tags: indicate the tags for CI. + """ + opentitan_test( + name = name, + exec_env = { + "//hw/top_earlgrey:fpga_cw340_rom_ext": None, + }, + # Provide a correctly signed binary + srcs = ["//sw/device/tests/penetrationtests/firmware:firmware_cryptolib_fi_asym.c"], + manifest = "//sw/device/silicon_owner:manifest", + fpga = fpga_params( + timeout = "eternal", + # We set an OTP with the RMA lifecycle, ensure ROM_EXT can still boot, but give a bogus ROM_EXT_IMM hash + otp = "//sw/device/tests/penetrationtests/firmware/testdata:otp_img_rom_ext_imm_skip_fi", + data = test_vectors, + tags = tags + ["coverage_broken"], + test_cmd = """ + --bootstrap={firmware} --rom_ext={rom_ext} --rom={rom} + """ + test_args, + test_harness = test_harness, + rom_ext = "//sw/device/silicon_creator/rom_ext:rom_ext_dice_x509_slot_virtual", + ), + deps = FIRMWARE_DEPS_CRYPTOLIB_FI_ASYM, + ) + def pentest_owner_upgrade_fi_gdb(name, test_vectors, test_args, test_harness, tags): """A macro for defining a CryptoTest test case. diff --git a/sw/host/penetrationtests/python/fi/BUILD b/sw/host/penetrationtests/python/fi/BUILD index 1b3b350f4bf16..57d1174cc72b7 100644 --- a/sw/host/penetrationtests/python/fi/BUILD +++ b/sw/host/penetrationtests/python/fi/BUILD @@ -181,6 +181,7 @@ py_binary( "//sw/host/penetrationtests/python/util:dis_parser", "//sw/host/penetrationtests/python/util:gdb_controller", "//sw/host/penetrationtests/python/util:targets", + "//sw/host/penetrationtests/python/util:utils", "@rules_python//python/runfiles", ], ) @@ -200,6 +201,7 @@ py_binary( "//sw/host/penetrationtests/python/util:dis_parser", "//sw/host/penetrationtests/python/util:gdb_controller", "//sw/host/penetrationtests/python/util:targets", + "//sw/host/penetrationtests/python/util:utils", "@rules_python//python/runfiles", ], ) @@ -219,6 +221,7 @@ py_binary( "//sw/host/penetrationtests/python/util:dis_parser", "//sw/host/penetrationtests/python/util:gdb_controller", "//sw/host/penetrationtests/python/util:targets", + "//sw/host/penetrationtests/python/util:utils", "@rules_python//python/runfiles", ], ) @@ -238,6 +241,7 @@ py_binary( "//sw/host/penetrationtests/python/util:dis_parser", "//sw/host/penetrationtests/python/util:gdb_controller", "//sw/host/penetrationtests/python/util:targets", + "//sw/host/penetrationtests/python/util:utils", "@rules_python//python/runfiles", ], ) @@ -258,6 +262,26 @@ py_binary( ], ) +py_binary( + name = "fi_rom_ext_imm_skip_python_gdb_test", + testonly = True, + srcs = ["gdb_testing/fi_rom_ext_imm_skip_python_gdb_test.py"], + data = [ + "//sw/host/opentitantool", + "//third_party/openocd:jtag_cmsis_dap_adapter_cfg", + "//third_party/openocd:openocd_bin", + "//util/openocd/target:lowrisc-earlgrey.cfg", + "@lowrisc_rv32imcb_toolchain//:bin/riscv32-unknown-elf-gdb", + ], + deps = [ + "//sw/host/penetrationtests/python/util:dis_parser", + "//sw/host/penetrationtests/python/util:gdb_controller", + "//sw/host/penetrationtests/python/util:targets", + "//sw/host/penetrationtests/python/util:utils", + "@rules_python//python/runfiles", + ], +) + py_binary( name = "fi_sym_cryptolib_python_gdb_test", testonly = True, @@ -339,6 +363,7 @@ py_binary( "//sw/host/penetrationtests/python/util:dis_parser", "//sw/host/penetrationtests/python/util:gdb_controller", "//sw/host/penetrationtests/python/util:targets", + "//sw/host/penetrationtests/python/util:utils", "@rules_python//python/runfiles", ], ) @@ -358,6 +383,7 @@ py_binary( "//sw/host/penetrationtests/python/util:dis_parser", "//sw/host/penetrationtests/python/util:gdb_controller", "//sw/host/penetrationtests/python/util:targets", + "//sw/host/penetrationtests/python/util:utils", "@rules_python//python/runfiles", ], ) @@ -377,6 +403,7 @@ py_binary( "//sw/host/penetrationtests/python/util:dis_parser", "//sw/host/penetrationtests/python/util:gdb_controller", "//sw/host/penetrationtests/python/util:targets", + "//sw/host/penetrationtests/python/util:utils", "@rules_python//python/runfiles", ], ) diff --git a/sw/host/penetrationtests/python/fi/gdb_testing/fi_asym_cryptolib_python_gdb_test.py b/sw/host/penetrationtests/python/fi/gdb_testing/fi_asym_cryptolib_python_gdb_test.py index 33b60a74cb0e2..ee8d4c37e1f44 100644 --- a/sw/host/penetrationtests/python/fi/gdb_testing/fi_asym_cryptolib_python_gdb_test.py +++ b/sw/host/penetrationtests/python/fi/gdb_testing/fi_asym_cryptolib_python_gdb_test.py @@ -43,6 +43,12 @@ parser = argparse.ArgumentParser() parser.add_argument("--bitstream", type=str) parser.add_argument("--bootstrap", type=str) +parser.add_argument( + "--force-trace", + action="store_true", + help="Force re-running PC tracing even if trace log exists", +) +utils.add_test_selection_args(parser) args, config_args = parser.parse_known_args() @@ -68,36 +74,52 @@ def read_testos_output(): def reset_gdb(gdb): - gdb.close_gdb() - gdb = GDBController( + if gdb and getattr(gdb, "gdb_process", None) and gdb.gdb_process.poll() is None: + try: + gdb.cleanup_skip() + ping = gdb.send_command("p 1", timeout=1.0) + if ping and ("= 1" in ping): + return gdb + except Exception: + pass + if gdb: + try: + gdb.close_gdb() + except Exception: + pass + return GDBController( gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path, ) - return gdb def reset_target_and_gdb(gdb): - gdb.close_gdb() + if gdb: + try: + gdb.close_gdb() + except Exception: + pass target.reset_target() - target.start_openocd(startup_delay=0.2, print_output=False) + time.sleep(0.05) + target.start_openocd(startup_delay=0.3, print_output=False) target.dump_all() trigger_testos_init(print_output=False) - gdb = GDBController( + return GDBController( gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path, ) - return gdb def re_initialize(gdb, print_output=False): - gdb.close_gdb() + # Tier 3: Full FPGA re-initialization (only on unrecoverable lockup) + if gdb: + gdb.close_gdb() target.initialize_target(print_output=print_output) trigger_testos_init(print_output=print_output) target.dump_all() - gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) - return gdb + return GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) class AsymCryptolibFiSim(unittest.TestCase): @@ -137,7 +159,7 @@ def test_p384_sign(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -148,17 +170,13 @@ def test_p384_sign(self): trigger_testos_init() # Connect to GDB - gdb = GDBController( - gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path - ) + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) # We provide the name of the unique marker in the pentest framework function_name = "PENTEST_MARKER_P384_SIGN" # Gives back an array of hits where the function is called trace_address = parser.get_marker_addresses(function_name) - print( - "Start and stop addresses of ", function_name, ": ", trace_address - ) + print("Start and stop addresses of ", function_name, ": ", trace_address) crash_observation_address = parser.get_function_start_address( "ottf_exception_handler" @@ -172,11 +190,11 @@ def test_p384_sign(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the p384 sign from the testOS (we do not read its output) - asymfi.handle_p384_sign( - scalar, pubx[0], puby, message_digest, cfg, trigger - ) + asymfi.handle_p384_sign(scalar, pubx[0], puby, message_digest, cfg, trigger) start_time = time.time() initial_timeout_stopped = False @@ -185,11 +203,35 @@ def test_p384_sign(self): # Run the tracing to get the trace log while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the p384 sign from the testOS (we do not read its output) + asymfi.handle_p384_sign(scalar, pubx[0], puby, message_digest, cfg, trigger) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -263,12 +305,8 @@ def test_p384_sign(self): print("Crash detected, resetting", flush=True) gdb = reset_target_and_gdb(gdb) else: - testos_response_json = json.loads( - testos_response - ) - print( - "Output:", testos_response_json, flush=True - ) + testos_response_json = json.loads(testos_response) + print("Output:", testos_response_json, flush=True) if testos_response_json["status"] == 0: # Record the 'r' value from the signature sign_output[i] = testos_response_json["r"] @@ -278,7 +316,7 @@ def test_p384_sign(self): sign_output[0], sign_output[1], match_threshold_ratio=0.75, - valid_len=48 + valid_len=48, ) or utils.is_majority_zeros( sign_output[i], total_length=48 ): @@ -298,11 +336,14 @@ def test_p384_sign(self): gdb = reset_gdb(gdb) # We do not need to reset the target since it gave an output else: - print( - "No break point found, something went wrong", - flush=True, - ) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print( + "Target did not respond, resetting target", flush=True + ) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -324,9 +365,7 @@ def test_p384_sign(self): finally: print("-" * 80) - print( - f"Total attacks {total_attacks}, successful attacks {successful_faults}" - ) + print(f"Total attacks {total_attacks}, successful attacks {successful_faults}") # Close the OpenOCD and GDB connection at the end if gdb: gdb.close_gdb() @@ -366,7 +405,7 @@ def test_p384_verify(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -397,6 +436,8 @@ def test_p384_verify(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the p384 verify from the testOS (we do not read its output) asymfi.handle_p384_verify( @@ -410,11 +451,37 @@ def test_p384_verify(self): # Run the tracing to get the trace log while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the p384 verify from the testOS (we do not read its output) + asymfi.handle_p384_verify( + pubx, puby, r_bytes, s_bytes, message_digest, cfg, trigger + ) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -491,8 +558,12 @@ def test_p384_verify(self): gdb = reset_gdb(gdb) # We do not need to reset the target since it gave an output else: - print("No break point found, something went wrong", flush=True) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print("Target did not respond, resetting target", flush=True) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -550,7 +621,7 @@ def test_p384_ecdh(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -581,6 +652,8 @@ def test_p384_ecdh(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the p384 verify from the testOS (we do not read its output) asymfi.handle_p384_ecdh(private_key_array[0], public_x, public_y, cfg, trigger) @@ -592,11 +665,35 @@ def test_p384_ecdh(self): # Run the tracing to get the trace log while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the p384 verify from the testOS (we do not read its output) + asymfi.handle_p384_ecdh(private_key_array[0], public_x, public_y, cfg, trigger) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -663,16 +760,12 @@ def test_p384_ecdh(self): testos_response_json = json.loads(testos_response) print("Output:", testos_response_json, flush=True) if testos_response_json["status"] == 0: - ecdh_output[i] = testos_response_json[ - "shared_key" - ] + ecdh_output[i] = testos_response_json["shared_key"] if utils.is_partial_collision( ecdh_output[0], ecdh_output[1], match_threshold_ratio=0.75, - ) or utils.is_majority_zeros( - ecdh_output[i] - ): + ) or utils.is_majority_zeros(ecdh_output[i]): successful_faults += 1 print("-" * 80) print("Successful FI attack!") @@ -684,8 +777,14 @@ def test_p384_ecdh(self): gdb = reset_gdb(gdb) # We do not need to reset the target since it gave an output else: - print("No break point found, something went wrong", flush=True) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print( + "Target did not respond, resetting target", flush=True + ) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -747,7 +846,7 @@ def test_p256_verify(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -778,6 +877,8 @@ def test_p256_verify(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the p256 verify from the testOS (we do not read its output) asymfi.handle_p256_verify( @@ -791,11 +892,37 @@ def test_p256_verify(self): # Run the tracing to get the trace log while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the p256 verify from the testOS (we do not read its output) + asymfi.handle_p256_verify( + pubx, puby, r_bytes, s_bytes, message_digest, cfg, trigger + ) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -872,8 +999,12 @@ def test_p256_verify(self): gdb = reset_gdb(gdb) # We do not need to reset the target since it gave an output else: - print("No break point found, something went wrong", flush=True) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print("Target did not respond, resetting target", flush=True) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -931,7 +1062,7 @@ def test_p256_ecdh(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -962,6 +1093,8 @@ def test_p256_ecdh(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the p256 verify from the testOS (we do not read its output) asymfi.handle_p256_ecdh(private_key_array[0], public_x, public_y, cfg, trigger) @@ -973,11 +1106,35 @@ def test_p256_ecdh(self): # Run the tracing to get the trace log while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the p256 verify from the testOS (we do not read its output) + asymfi.handle_p256_ecdh(private_key_array[0], public_x, public_y, cfg, trigger) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -1044,16 +1201,12 @@ def test_p256_ecdh(self): testos_response_json = json.loads(testos_response) print("Output:", testos_response_json, flush=True) if testos_response_json["status"] == 0: - ecdh_output[i] = testos_response_json[ - "shared_key" - ] + ecdh_output[i] = testos_response_json["shared_key"] if utils.is_partial_collision( ecdh_output[0], ecdh_output[1], match_threshold_ratio=0.75, - ) or utils.is_majority_zeros( - ecdh_output[i] - ): + ) or utils.is_majority_zeros(ecdh_output[i]): successful_faults += 1 print("-" * 80) print("Successful FI attack!") @@ -1065,8 +1218,14 @@ def test_p256_ecdh(self): gdb = reset_gdb(gdb) # We do not need to reset the target since it gave an output else: - print("No break point found, something went wrong", flush=True) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print( + "Target did not respond, resetting target", flush=True + ) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -1130,7 +1289,7 @@ def test_rsa_pkcs1v15_verify(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -1161,6 +1320,8 @@ def test_rsa_pkcs1v15_verify(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the rsa verify from the testOS (we do not read its output) asymfi.handle_rsa_verify( @@ -1182,15 +1343,49 @@ def test_rsa_pkcs1v15_verify(self): total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the rsa verify from the testOS (we do not read its output) + asymfi.handle_rsa_verify( + data, + data_len, + public_exponent, + n, + n_len, + sig, + sig_len, + padding, + hashing, + cfg, + trigger, + ) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -1277,8 +1472,12 @@ def test_rsa_pkcs1v15_verify(self): gdb = reset_gdb(gdb) # We do not need to reset the target since it returned an output else: - print("No break point found, something went wrong", flush=True) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print("Target did not respond, resetting target", flush=True) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -1343,7 +1542,7 @@ def test_rsa_pss_verify(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -1374,6 +1573,8 @@ def test_rsa_pss_verify(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the rsa verify from the testOS (we do not read its output) asymfi.handle_rsa_verify( @@ -1395,15 +1596,49 @@ def test_rsa_pss_verify(self): total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the rsa verify from the testOS (we do not read its output) + asymfi.handle_rsa_verify( + data, + data_len, + public_exponent, + n, + n_len, + sig, + sig_len, + padding, + hashing, + cfg, + trigger, + ) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -1490,8 +1725,12 @@ def test_rsa_pss_verify(self): gdb = reset_gdb(gdb) # We do not need to reset the target since it returned an output else: - print("No break point found, something went wrong", flush=True) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print("Target did not respond, resetting target", flush=True) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -1557,7 +1796,7 @@ def test_rsa_sign(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -1587,6 +1826,8 @@ def test_rsa_sign(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the rsa sign from the testOS (we do not read its output) asymfi.handle_rsa_sign( @@ -1609,11 +1850,46 @@ def test_rsa_sign(self): # Run the tracing to get the trace log while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the rsa sign from the testOS (we do not read its output) + asymfi.handle_rsa_sign( + data_inputs[0], + data_len, + public_exponent, + n, + n_len, + d, + padding, + hashing, + cfg, + trigger, + ) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -1711,6 +1987,15 @@ def test_rsa_sign(self): # Reset GDB by closing and opening again gdb = reset_gdb(gdb) + else: + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print( + "Target did not respond, resetting target", flush=True + ) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( "Error: JSON decoding failed. Invalid response format", @@ -1774,7 +2059,7 @@ def test_x25519_ecdh(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -1785,17 +2070,13 @@ def test_x25519_ecdh(self): trigger_testos_init() # Connect to GDB - gdb = GDBController( - gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path - ) + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) # We provide the name of the unique marker in the pentest framework function_name = "PENTEST_MARKER_X25519" # Gives back an array of hits where the function is called trace_address = parser.get_marker_addresses(function_name) - print( - "Start and stop addresses of ", function_name, ": ", trace_address - ) + print("Start and stop addresses of ", function_name, ": ", trace_address) crash_observation_address = parser.get_function_start_address( "ottf_exception_handler" @@ -1807,11 +2088,11 @@ def test_x25519_ecdh(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the x25519 ecdh from the testOS (we do not read its output) - asymfi.handle_x25519_ecdh( - private_key_array[0], public_x, public_y, cfg, trigger - ) + asymfi.handle_x25519_ecdh(private_key_array[0], public_x, public_y, cfg, trigger) start_time = time.time() initial_timeout_stopped = False @@ -1820,11 +2101,37 @@ def test_x25519_ecdh(self): # Run the tracing to get the trace log while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the x25519 ecdh from the testOS (we do not read its output) + asymfi.handle_x25519_ecdh( + private_key_array[0], public_x, public_y, cfg, trigger + ) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -1901,23 +2208,15 @@ def test_x25519_ecdh(self): print("Crash detected, resetting", flush=True) gdb = reset_target_and_gdb(gdb) else: - testos_response_json = json.loads( - testos_response - ) - print( - "Output:", testos_response_json, flush=True - ) + testos_response_json = json.loads(testos_response) + print("Output:", testos_response_json, flush=True) if testos_response_json["status"] == 0: - ecdh_output[i] = testos_response_json[ - "shared_key" - ] + ecdh_output[i] = testos_response_json["shared_key"] if utils.is_partial_collision( ecdh_output[0], ecdh_output[1], match_threshold_ratio=0.75, - ) or utils.is_majority_zeros( - ecdh_output[i] - ): + ) or utils.is_majority_zeros(ecdh_output[i]): successful_faults += 1 print("-" * 80) print("Successful FI attack!") @@ -1932,6 +2231,15 @@ def test_x25519_ecdh(self): print("-" * 80) # Reset GDB by closing and opening again gdb = reset_gdb(gdb) + else: + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print( + "Target did not respond, resetting target", flush=True + ) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( "Error: JSON decoding failed. Invalid response format", @@ -1952,9 +2260,7 @@ def test_x25519_ecdh(self): finally: print("-" * 80) - print( - f"Total attacks {total_attacks}, successful attacks {successful_faults}" - ) + print(f"Total attacks {total_attacks}, successful attacks {successful_faults}") if gdb: gdb.close_gdb() target.close_openocd() @@ -1966,23 +2272,336 @@ def test_ed25519_verify(self): print("Starting the ed25519 verify test") # Prepare inputs representing an invalid signature. - pubx = [149, 147, 40, 32, 52, 171, 254, 225, 244, 49, 56, 85, 102, 168, 58, 149, 215, - 1, 178, 34, 239, 134, 228, 59, 25, 25, 166, 4, 20, 252, 106, 127] - puby = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0] - r = [179, 233, 109, 11, 158, 166, 221, 245, 31, 170, 150, 231, 193, 167, 36, 93, 15, - 224, 39, 117, 62, 182, 19, 173, 252, 159, 107, 165, 211, 94, 217, 145, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0] - s = [130, 235, 236, 78, 193, 33, 148, 169, 18, 146, 45, 70, 195, 43, 187, 205, 121, 185, - 91, 162, 203, 178, 76, 62, 53, 180, 33, 146, 45, 128, 191, 3, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - message_padded = [81, 67, 237, 157, 177, 57, 129, 5, 154, 9, 80, 122, 120, 229, 232, 245, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + pubx = [ + 149, + 147, + 40, + 32, + 52, + 171, + 254, + 225, + 244, + 49, + 56, + 85, + 102, + 168, + 58, + 149, + 215, + 1, + 178, + 34, + 239, + 134, + 228, + 59, + 25, + 25, + 166, + 4, + 20, + 252, + 106, + 127, + ] + puby = [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + r = [ + 179, + 233, + 109, + 11, + 158, + 166, + 221, + 245, + 31, + 170, + 150, + 231, + 193, + 167, + 36, + 93, + 15, + 224, + 39, + 117, + 62, + 182, + 19, + 173, + 252, + 159, + 107, + 165, + 211, + 94, + 217, + 145, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + s = [ + 130, + 235, + 236, + 78, + 193, + 33, + 148, + 169, + 18, + 146, + 45, + 70, + 195, + 43, + 187, + 205, + 121, + 185, + 91, + 162, + 203, + 178, + 76, + 62, + 53, + 180, + 33, + 146, + 45, + 128, + 191, + 3, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] + message_padded = [ + 81, + 67, + 237, + 157, + 177, + 57, + 129, + 5, + 154, + 9, + 80, + 122, + 120, + 229, + 232, + 245, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ] message_len = 16 cfg = 0 @@ -1998,22 +2617,18 @@ def test_ed25519_verify(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: target.initialize_target() trigger_testos_init() - gdb = GDBController( - gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path - ) + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) function_name = "PENTEST_MARKER_ED25519_VERIFY" trace_address = parser.get_marker_addresses(function_name) - print( - "Start and stop addresses of ", function_name, ": ", trace_address - ) + print("Start and stop addresses of ", function_name, ": ", trace_address) crash_observation_address = parser.get_function_start_address( "ottf_exception_handler" @@ -2024,6 +2639,8 @@ def test_ed25519_verify(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() asymfi.handle_ed25519_verify( pubx, puby, r, s, message_padded, message_len, cfg, trigger @@ -2033,13 +2650,39 @@ def test_ed25519_verify(self): initial_timeout_stopped = False total_timeout_stopped = False + # Run the tracing to get the trace log while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + asymfi.handle_ed25519_verify( + pubx, puby, r, s, message_padded, message_len, cfg, trigger + ) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -2070,9 +2713,7 @@ def test_ed25519_verify(self): for pc, count in pc_count_dict.items(): for i_count in range(min(MAX_SKIPS_PER_LOOP, count)): print("-" * 80) - print( - "Applying instruction skip in ", pc, "occurrence", i_count - ) + print("Applying instruction skip in ", pc, "occurrence", i_count) print("-" * 80) crash_observation = "crash detected" @@ -2114,9 +2755,7 @@ def test_ed25519_verify(self): verification_status = testos_response_json["status"] # Verification passing on invalid payload implies successful FI - if verification_result and ( - verification_status == 0 - ): + if verification_result and (verification_status == 0): successful_faults += 1 print("-" * 80) print("Successful FI attack!") @@ -2124,7 +2763,14 @@ def test_ed25519_verify(self): print(gdb_response) print("Response:", testos_response_json) print("-" * 80) - gdb = reset_gdb(gdb) + gdb = reset_gdb(gdb) + else: + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print("Target did not respond, resetting target", flush=True) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( "Error: JSON decoding failed. Invalid response format", @@ -2145,9 +2791,7 @@ def test_ed25519_verify(self): finally: print("-" * 80) - print( - f"Total attacks {total_attacks}, successful attacks {successful_faults}" - ) + print(f"Total attacks {total_attacks}, successful attacks {successful_faults}") if gdb: gdb.close_gdb() target.close_openocd() @@ -2157,6 +2801,13 @@ def test_ed25519_verify(self): if __name__ == "__main__": + unittest_argv = utils.get_selected_test_argv( + AsymCryptolibFiSim, + requested_name=args.test, + config_args=config_args, + list_tests=args.list_tests, + ) + r = Runfiles.Create() # Get the openocd path. openocd_path = r.Rlocation("lowrisc_opentitan/third_party/openocd/build_openocd/bin/openocd") @@ -2175,7 +2826,7 @@ def test_ed25519_verify(self): if BITSTREAM: bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM) # Get the test result path - log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") + log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") or "/tmp" # Get the firmware path. firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP) # Get the disassembly path. @@ -2206,4 +2857,4 @@ def test_ed25519_verify(self): print("Disassembly is found in ", dis_path, flush=True) - unittest.main(argv=[sys.argv[0]]) + unittest.main(argv=unittest_argv) diff --git a/sw/host/penetrationtests/python/fi/gdb_testing/fi_owner_upgrade_python_gdb_test.py b/sw/host/penetrationtests/python/fi/gdb_testing/fi_owner_upgrade_python_gdb_test.py index ac93f1eb18b39..e01c3f6f1a9c7 100644 --- a/sw/host/penetrationtests/python/fi/gdb_testing/fi_owner_upgrade_python_gdb_test.py +++ b/sw/host/penetrationtests/python/fi/gdb_testing/fi_owner_upgrade_python_gdb_test.py @@ -11,6 +11,7 @@ from sw.host.penetrationtests.python.util import targets from sw.host.penetrationtests.python.util.gdb_controller import GDBController from sw.host.penetrationtests.python.util.dis_parser import DisParser +from sw.host.penetrationtests.python.util import utils from collections import Counter import argparse import unittest @@ -38,8 +39,14 @@ parser = argparse.ArgumentParser() parser.add_argument("--bitstream", type=str) parser.add_argument("--bootstrap", type=str) +parser.add_argument( + "--force-trace", + action="store_true", + help="Force re-running PC tracing even if trace log exists", +) parser.add_argument("--rom_ext", type=str) parser.add_argument("--rom", type=str) +utils.add_test_selection_args(parser) args, config_args = parser.parse_known_args() @@ -74,8 +81,12 @@ def read_uart_output(): def reset_target_and_gdb(gdb, jump_address, print_output=False): - gdb.close_gdb() - target.start_openocd(startup_delay=0.2, print_output=False) + if gdb: + try: + gdb.close_gdb() + except Exception: + pass + target.start_openocd(startup_delay=0.3, print_output=False) gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_ext_elf_path) gdb.reset_target() gdb.send_command(f"set $pc={jump_address}") @@ -105,7 +116,7 @@ def re_initialize(gdb, jump_address, print_output=False): timeout += 1 gdb.close_gdb() response = read_uart_output() - target.start_openocd(print_output=False) + target.start_openocd(startup_delay=0.3, print_output=False) # Connect to GDB gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_ext_elf_path) @@ -132,7 +143,7 @@ def test_owner_upgrade(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -206,21 +217,48 @@ def test_owner_upgrade(self): ], ) gdb.send_command("c", check_response=False) - start_time = time.time() initial_timeout_stopped = False total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(read_uart_output()), flush=True) + gdb = reset_target_and_gdb(gdb, jump_address) + gdb.setup_pc_trace( + pc_trace_file, + trace_start_address, + trace_end_address, + skip_addrs=[ + upsert_register_address, + ], + ) + gdb.send_command("c", check_response=False) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print( + "No initial break point found, can be a misfire, try again", + flush=True, + ) + print("Target UART:", repr(read_uart_output()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -356,7 +394,7 @@ def test_owner_upgrade(self): if BITSTREAM: bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM) # Get the test result path - log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") + log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") or "/tmp" # Get the firmware path. firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP) # Get the rom path. @@ -393,7 +431,14 @@ def test_owner_upgrade(self): rom_ext_parser = DisParser(rom_ext_dis_path) rom_parser = DisParser(rom_dis_path) + unittest_argv = utils.get_selected_test_argv( + FiSimOwnerUpgrade, + requested_name=args.test, + config_args=config_args, + list_tests=args.list_tests, + ) + print("ROM disassembly is found in ", rom_dis_path, flush=True) print("ROM_EXT disassembly is found in ", rom_ext_dis_path, flush=True) - unittest.main(argv=[sys.argv[0]]) + unittest.main(argv=unittest_argv) diff --git a/sw/host/penetrationtests/python/fi/gdb_testing/fi_owner_upgrade_self_signed_python_gdb_test.py b/sw/host/penetrationtests/python/fi/gdb_testing/fi_owner_upgrade_self_signed_python_gdb_test.py index 476c145988dd8..eadbd47e7376d 100644 --- a/sw/host/penetrationtests/python/fi/gdb_testing/fi_owner_upgrade_self_signed_python_gdb_test.py +++ b/sw/host/penetrationtests/python/fi/gdb_testing/fi_owner_upgrade_self_signed_python_gdb_test.py @@ -11,6 +11,7 @@ from sw.host.penetrationtests.python.util import targets from sw.host.penetrationtests.python.util.gdb_controller import GDBController from sw.host.penetrationtests.python.util.dis_parser import DisParser +from sw.host.penetrationtests.python.util import utils from collections import Counter import argparse import unittest @@ -38,8 +39,14 @@ parser = argparse.ArgumentParser() parser.add_argument("--bitstream", type=str) parser.add_argument("--bootstrap", type=str) +parser.add_argument( + "--force-trace", + action="store_true", + help="Force re-running PC tracing even if trace log exists", +) parser.add_argument("--rom_ext", type=str) parser.add_argument("--rom", type=str) +utils.add_test_selection_args(parser) args, config_args = parser.parse_known_args() @@ -74,8 +81,12 @@ def read_uart_output(): def reset_target_and_gdb(gdb, jump_address, print_output=False): - gdb.close_gdb() - target.start_openocd(startup_delay=0.2, print_output=False) + if gdb: + try: + gdb.close_gdb() + except Exception: + pass + target.start_openocd(startup_delay=0.3, print_output=False) gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_ext_elf_path) gdb.reset_target() gdb.send_command(f"set $pc={jump_address}") @@ -105,7 +116,7 @@ def re_initialize(gdb, jump_address, print_output=False): timeout += 1 gdb.close_gdb() response = read_uart_output() - target.start_openocd(print_output=False) + target.start_openocd(startup_delay=0.3, print_output=False) # Connect to GDB gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_ext_elf_path) @@ -134,7 +145,7 @@ def test_owner_upgrade_self_signed(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -208,21 +219,48 @@ def test_owner_upgrade_self_signed(self): ], ) gdb.send_command("c", check_response=False) - start_time = time.time() initial_timeout_stopped = False total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(read_uart_output()), flush=True) + gdb = reset_target_and_gdb(gdb, jump_address) + gdb.setup_pc_trace( + pc_trace_file, + trace_start_address, + trace_end_address, + skip_addrs=[ + upsert_register_address, + ], + ) + gdb.send_command("c", check_response=False) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print( + "No initial break point found, can be a misfire, try again", + flush=True, + ) + print("Target UART:", repr(read_uart_output()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -358,7 +396,7 @@ def test_owner_upgrade_self_signed(self): if BITSTREAM: bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM) # Get the test result path - log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") + log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") or "/tmp" # Get the firmware path. firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP) # Get the rom path. @@ -395,7 +433,14 @@ def test_owner_upgrade_self_signed(self): rom_ext_parser = DisParser(rom_ext_dis_path) rom_parser = DisParser(rom_dis_path) + unittest_argv = utils.get_selected_test_argv( + FiSimOwnerUpgradeSelfSigned, + requested_name=args.test, + config_args=config_args, + list_tests=args.list_tests, + ) + print("ROM disassembly is found in ", rom_dis_path, flush=True) print("ROM_EXT disassembly is found in ", rom_ext_dis_path, flush=True) - unittest.main(argv=[sys.argv[0]]) + unittest.main(argv=unittest_argv) diff --git a/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_imm_skip_python_gdb_test.py b/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_imm_skip_python_gdb_test.py new file mode 100644 index 0000000000000..7576c343b655a --- /dev/null +++ b/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_imm_skip_python_gdb_test.py @@ -0,0 +1,414 @@ +# Copyright lowRISC contributors (OpenTitan project). +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +# What to do when running into errors: +# - If device is busy or seeing "rejected 'gdb' connection, no more connections allowed", +# cut the USB connection, e.g., sudo fuser /dev/ttyUSB0 and kill the PID +# - If the port is busy check sudo lsof -i :3333 and then kill the PID + +from python.runfiles import Runfiles +from sw.host.penetrationtests.python.util import targets +from sw.host.penetrationtests.python.util.gdb_controller import GDBController +from sw.host.penetrationtests.python.util.dis_parser import DisParser +from sw.host.penetrationtests.python.util import utils +from collections import Counter +import argparse +import unittest +import sys +import os +import time +import signal +import serial + +ignored_keys_set = set(["status"]) +opentitantool_path = "" +log_dir = "" +rom_elf_path = "" +rom_parser = None +target = None + +# We set to only apply instruction skips in the first +# MAX_SKIPS_PER_LOOP iterations of a loop +MAX_SKIPS_PER_LOOP = 2 + +# Read in the extra arguments from the opentitan_test. +parser = argparse.ArgumentParser() +parser.add_argument("--bitstream", type=str) +parser.add_argument("--rom", type=str) +parser.add_argument("--otp", type=str) +parser.add_argument("--bootstrap", type=str) +parser.add_argument( + "--force-trace", + action="store_true", + help="Force re-running PC tracing even if trace log exists", +) +parser.add_argument("--rom_ext", type=str) +utils.add_test_selection_args(parser) + +args, config_args = parser.parse_known_args() + +BITSTREAM = args.bitstream +ROM_VMEM = args.rom +OTP_VMEM = args.otp +BOOTSTRAP = args.bootstrap +ROM_EXT = args.rom_ext +ROM = args.rom + +original_stdout = sys.stdout + + +class IterationTimeout: + def __init__(self, seconds, error_message="Iteration timed out"): + self.seconds = seconds + self.error_message = error_message + + def handle_timeout(self, signum, frame): + raise TimeoutError(self.error_message) + + def __enter__(self): + signal.signal(signal.SIGALRM, self.handle_timeout) + signal.alarm(self.seconds) + + def __exit__(self, type, value, traceback): + signal.alarm(0) + + +def read_uart_output(): + # Read the output from the chip + response = target.read_all(max_tries=100) + return response + + +def reset_target_and_gdb(gdb, jump_address, print_output=False): + # Fast path: if GDB session is alive, reset target to halt state and set PC + if gdb and getattr(gdb, "gdb_process", None) and gdb.gdb_process.poll() is None: + try: + gdb.cleanup_skip() + gdb.reset_target(halt=True) + gdb.send_command(f"set $pc={jump_address}") + if gdb.get_program_counter() is not None: + target.dump_all() + return gdb + except Exception: + pass + + if gdb: + try: + gdb.close_gdb() + except Exception: + pass + target.reset_target() + time.sleep(0.05) + target.start_openocd(startup_delay=1.0, print_output=False) + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_elf_path) + gdb.reset_target(halt=True) + gdb.send_command(f"set $pc={jump_address}") + target.dump_all() + return gdb + + +# Only called when we encounter an issue where we want to re-flash everything +def re_initialize(gdb, jump_address, print_output=False): + if gdb: + try: + gdb.close_gdb() + except Exception: + pass + target.close_openocd() + target.clear_bitstream() + target.initialize_target(print_output=print_output) + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_elf_path) + gdb.reset_target(halt=True) + gdb.send_command(f"set $pc={jump_address}") + target.dump_all() + return gdb + + +class RomExtImmSkipFiSim(unittest.TestCase): + def test_rom_ext_imm_skip_fi(self): + print("Starting the rom_ext immutable check skip test") + + # Directory for the trace log files + pc_trace_file = os.path.join(log_dir, "rom_ext_imm_skip_pc_trace.log") + # Directory for the the log of the campaign + campaign_file = os.path.join(log_dir, "rom_ext_imm_skip_test_campaign.log") + + successful_faults = 0 + total_attacks = 0 + + gdb = None + started = False + with open(campaign_file, "w", buffering=1) as campaign: + print(f"Switching terminal output to {campaign_file}", flush=True) + sys.stdout = campaign + try: + # Program the bitstream, flash the target, and set up OpenOCD + target.initialize_target() + + # We set the RMA spin cycles to a long timeout to be able to halt before ROM starts. + # Jump over the spin cycles + jump_address = rom_parser.get_function_start_address("kRomStartRmaSpinSkip") + + # Connect to GDB + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_elf_path) + + # Reset the device and halt it immediately + gdb.reset_target() + gdb.send_command(f"set $pc={jump_address}") + + # Trace rom_verify_immutable_section to capture all instructions + # evaluating the immutable ROM_EXT + + # Functions where we can get GDB to jump over via temporary breakpoints. + upsert_register_address = rom_parser.get_function_start_address("upsert_register") + otp_read32_address = rom_parser.get_function_start_address("otp_read32") + otp_read_address = rom_parser.get_function_start_address("otp_read") + hmac_update_address = rom_parser.get_function_start_address("hmac_sha256_update") + hmac_final_address = rom_parser.get_function_start_address( + "hmac_sha256_final_truncated" + ) + skip_addrs = [ + addr + for addr in [ + upsert_register_address, + otp_read32_address, + otp_read_address, + hmac_update_address, + hmac_final_address, + ] + if addr is not None + ] + print( + f"Trace skip addresses: upsert_register={upsert_register_address}, " + f"otp_read32={otp_read32_address}, otp_read={otp_read_address}, " + f"hmac_sha256_update={hmac_update_address}, " + f"hmac_sha256_final_truncated={hmac_final_address}", + flush=True, + ) + + # We start from rom_verify_immutable_section to trace the immutable check + trace_start_address = rom_parser.get_function_start_address( + "rom_verify_immutable_section" + ) + trace_end_address = rom_parser.get_function_end_address( + "rom_verify_immutable_section" + ) + + print( + "Start and stop addresses for the rom trace: ", + trace_start_address, + trace_end_address, + flush=True, + ) + print("Trace data is logged in ", pc_trace_file, flush=True) + + # Start the tracing + # We set a short timeout to detect whether GDB has connected properly + # and a long timeout for the entire tracing + initial_timeout = 20 + total_timeout = 60 * 60 * 5 + + gdb.setup_pc_trace( + pc_trace_file, + trace_start_address, + trace_end_address, + skip_addrs=skip_addrs, + ) + gdb.send_command("c", check_response=False) + start_time = time.time() + initial_timeout_stopped = False + total_timeout_stopped = False + + # Run the tracing to get the trace log + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(read_uart_output()), flush=True) + gdb = reset_target_and_gdb(gdb, jump_address) + gdb.setup_pc_trace( + pc_trace_file, + trace_start_address, + trace_end_address, + skip_addrs=skip_addrs, + ) + gdb.send_command("c", check_response=False) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(read_uart_output()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) + sys.exit(1) + while time.time() - start_time < total_timeout: + output = gdb.read_output() + if "PC trace complete" in output: + print("\nTrace complete") + total_timeout_stopped = True + break + if not total_timeout_stopped: + print("Final tracing timeout reached") + sys.exit(1) + + # Reset the target, flush the output, and close gdb + gdb = reset_target_and_gdb(gdb, jump_address) + + # Parse and truncate the trace log to get all PCs in a list + pc_list = gdb.parse_pc_trace_file(pc_trace_file) + # Get the unique PCs and annotate their occurence count + pc_count_dict = Counter(pc_list) + if len(pc_count_dict) <= 0: + print("Found no tracing, stopping") + sys.exit(1) + print("Tracing has a total of", len(pc_count_dict), "unique PCs", flush=True) + + # Reset the target, flush the output, and close gdb + gdb = reset_target_and_gdb(gdb, jump_address) + + started = True + for pc, count in pc_count_dict.items(): + for i_count in range(min(MAX_SKIPS_PER_LOOP, count)): + print("-" * 80) + print("Applying instruction skip in ", pc, "occurence", i_count) + print("-" * 80) + + try: + # If we have a timeout, we continue to the next iteration + with IterationTimeout(seconds=60): + gdb.apply_instruction_skip( + pc, rom_parser.parse_next_instruction(pc), i_count + ) + gdb.send_command("c", check_response=False) + + response = read_uart_output() + gdb_response = gdb.read_output() + + if "instruction skip applied" in gdb_response: + total_attacks += 1 + + print("Output:", response, flush=True) + + if "Running" in response: + successful_faults += 1 + print("-" * 80) + print("Successful FI attack!") + print("Location:", pc, "iteration", i_count) + print(gdb_response) + print("Response:", response) + print("-" * 80) + + try: + gdb = reset_target_and_gdb(gdb, jump_address) + except TimeoutError: + print("Timeout, reflashing", flush=True) + gdb = re_initialize(gdb, jump_address) + elif "saved" in response: + # Here we know that something was changed in flash + print("Seeing a flash change, reflashing", flush=True) + gdb = re_initialize(gdb, jump_address) + else: + try: + gdb = reset_target_and_gdb(gdb, jump_address) + except TimeoutError: + print("Timeout, reflashing", flush=True) + gdb = re_initialize(gdb, jump_address) + else: + # Breakpoint not reached (e.g. untaken branch) + gdb = reset_target_and_gdb(gdb, jump_address) + + except (TimeoutError, serial.SerialException) as e: + print("Timeout error, retrying", flush=True) + print(e, flush=True) + signal.alarm(0) + gdb = re_initialize(gdb, jump_address) + + finally: + print("-" * 80) + print(f"Total attacks {total_attacks}, successful attacks {successful_faults}") + # Close the OpenOCD and GDB connection at the end + if gdb: + gdb.close_gdb() + target.close_openocd() + sys.stdout = original_stdout + self.assertEqual(successful_faults, 0) + self.assertEqual(started, True) + + +if __name__ == "__main__": + unittest_argv = utils.get_selected_test_argv( + RomExtImmSkipFiSim, + requested_name=args.test, + config_args=config_args, + list_tests=args.list_tests, + ) + + r = Runfiles.Create() + # Get the openocd path. + openocd_path = r.Rlocation("lowrisc_opentitan/third_party/openocd/build_openocd/bin/openocd") + # Get the openocd config files. + # The config file for jtag + CONFIG_FILE_CHIP = r.Rlocation("openocd/tcl/interface/cmsis-dap.cfg") + # The config for the earlgrey design + CONFIG_FILE_DESIGN = r.Rlocation("lowrisc_opentitan/util/openocd/target/lowrisc-earlgrey.cfg") + # Get the opentitantool path. + opentitantool_path = r.Rlocation("lowrisc_opentitan/sw/host/opentitantool/opentitantool") + # The path for GDB and the default port (set up by OpenOCD) + GDB_PATH = r.Rlocation("lowrisc_rv32imcb_toolchain/bin/riscv32-unknown-elf-gdb") + GDB_PORT = 3333 + # Program the bitstream for FPGAs. + bitstream_path = None + if BITSTREAM: + bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM) + # Load the ROM/OTP memories for FPGAs. + rom_path = None + if ROM_VMEM: + rom_path = r.Rlocation("lowrisc_opentitan/" + ROM_VMEM) + otp_path = None + if OTP_VMEM: + otp_path = r.Rlocation("lowrisc_opentitan/" + OTP_VMEM) + # Get the test result path + log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") or "/tmp" + # Get the firmware path. + firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP) + # Get the rom path. + rom_path = r.Rlocation("lowrisc_opentitan/" + ROM) + # Get the disassembly path. + rom_dis_path = rom_path.replace(".39.scr.vmem", ".dis") + # And the path for the elf. + rom_elf_path = rom_path.replace(".39.scr.vmem", ".elf") + + if "fpga" in BOOTSTRAP: + target_type = "fpga" + else: + target_type = "chip" + + target_cfg = targets.TargetConfig( + target_type=target_type, + interface_type="hyperdebug", + fw_bin=firmware_path, + opentitantool=opentitantool_path, + bitstream=bitstream_path, + tool_args=config_args, + openocd=openocd_path, + openocd_chip_config=CONFIG_FILE_CHIP, + openocd_design_config=CONFIG_FILE_DESIGN, + ) + + target = targets.Target(target_cfg) + rom_parser = DisParser(rom_dis_path) + + unittest.main(argv=unittest_argv) diff --git a/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_python_gdb_test.py b/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_python_gdb_test.py index c8ecf872c9f9d..3b08b1ba9d8f9 100644 --- a/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_python_gdb_test.py +++ b/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_python_gdb_test.py @@ -11,6 +11,7 @@ from sw.host.penetrationtests.python.util import targets from sw.host.penetrationtests.python.util.gdb_controller import GDBController from sw.host.penetrationtests.python.util.dis_parser import DisParser +from sw.host.penetrationtests.python.util import utils from collections import Counter import argparse import unittest @@ -37,8 +38,13 @@ parser = argparse.ArgumentParser() parser.add_argument("--bitstream", type=str) parser.add_argument("--bootstrap", type=str) +parser.add_argument( + "--force-trace", + action="store_true", + help="Force re-running PC tracing even if trace log exists", +) parser.add_argument("--rom_ext", type=str) -parser.add_argument("--rom", type=str) +utils.add_test_selection_args(parser) args, config_args = parser.parse_known_args() @@ -73,10 +79,22 @@ def read_uart_output(): def reset_target_and_gdb(gdb, jump_address, print_output=False): - gdb.close_gdb() - target.start_openocd(startup_delay=0.2, print_output=False) + # Fast path: if OpenOCD direct session is alive, reset target to halt state and set PC + if gdb and getattr(gdb, "use_ocd_direct", False): + try: + gdb.reset_target(halt=True) + gdb.send_command(f"set $pc={jump_address}") + target.dump_all() + gdb.cleanup_skip() + return gdb + except Exception: + pass + + if gdb: + gdb.close_gdb() + target.start_openocd(startup_delay=0.3, print_output=False) gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_ext_elf_path) - gdb.reset_target() + gdb.reset_target(halt=True) gdb.send_command(f"set $pc={jump_address}") target.dump_all() return gdb @@ -84,12 +102,13 @@ def reset_target_and_gdb(gdb, jump_address, print_output=False): # Only called when we encounter an issue where we want to re-flash everything def re_initialize(gdb, jump_address, print_output=False): - gdb.close_gdb() + if gdb: + gdb.close_gdb() target.close_openocd() target.clear_bitstream() target.initialize_target(print_output=print_output) gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_ext_elf_path) - gdb.reset_target() + gdb.reset_target(halt=True) gdb.send_command(f"set $pc={jump_address}") target.dump_all() return gdb @@ -109,7 +128,7 @@ def test_rom_ext_secure_boot(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -154,21 +173,42 @@ def test_rom_ext_secure_boot(self): trace_end_address, ) gdb.send_command("c", check_response=False) - start_time = time.time() initial_timeout_stopped = False total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(read_uart_output()), flush=True) + gdb = reset_target_and_gdb(gdb, jump_address) + gdb.setup_pc_trace( + pc_trace_file, + trace_start_address, + trace_end_address, + ) + gdb.send_command("c", check_response=False) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(read_uart_output()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -240,9 +280,8 @@ def test_rom_ext_secure_boot(self): print("Timeout, reflashing", flush=True) gdb = re_initialize(gdb, jump_address) else: - print("No break point found, something went wrong", flush=True) - # Just to be safe that nothing went into flash, we reflash - gdb = re_initialize(gdb, jump_address) + # Breakpoint not reached (e.g. untaken branch) + gdb = reset_target_and_gdb(gdb, jump_address) except (TimeoutError, serial.SerialException) as e: print("Timeout error, retrying", flush=True) @@ -263,6 +302,13 @@ def test_rom_ext_secure_boot(self): if __name__ == "__main__": + unittest_argv = utils.get_selected_test_argv( + RomExtFiSim, + requested_name=args.test, + config_args=config_args, + list_tests=args.list_tests, + ) + r = Runfiles.Create() # Get the openocd path. openocd_path = r.Rlocation("lowrisc_opentitan/third_party/openocd/build_openocd/bin/openocd") @@ -281,7 +327,7 @@ def test_rom_ext_secure_boot(self): if BITSTREAM: bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM) # Get the test result path - log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") + log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") or "/tmp" # Get the firmware path. firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP) # Get the rom path. @@ -318,7 +364,4 @@ def test_rom_ext_secure_boot(self): rom_ext_parser = DisParser(rom_ext_dis_path) rom_parser = DisParser(rom_dis_path) - print("ROM disassembly is found in ", rom_dis_path, flush=True) - print("ROM_EXT disassembly is found in ", rom_ext_dis_path, flush=True) - - unittest.main(argv=[sys.argv[0]]) + unittest.main(argv=unittest_argv) diff --git a/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_rollback_python_gdb_test.py b/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_rollback_python_gdb_test.py index 5663fa2cdbf6d..301ed50edbabd 100644 --- a/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_rollback_python_gdb_test.py +++ b/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_ext_rollback_python_gdb_test.py @@ -11,6 +11,7 @@ from sw.host.penetrationtests.python.util import targets from sw.host.penetrationtests.python.util.gdb_controller import GDBController from sw.host.penetrationtests.python.util.dis_parser import DisParser +from sw.host.penetrationtests.python.util import utils from collections import Counter import argparse import unittest @@ -37,8 +38,13 @@ parser = argparse.ArgumentParser() parser.add_argument("--bitstream", type=str) parser.add_argument("--bootstrap", type=str) +parser.add_argument( + "--force-trace", + action="store_true", + help="Force re-running PC tracing even if trace log exists", +) parser.add_argument("--rom_ext", type=str) -parser.add_argument("--rom", type=str) +utils.add_test_selection_args(parser) args, config_args = parser.parse_known_args() @@ -73,10 +79,22 @@ def read_uart_output(): def reset_target_and_gdb(gdb, jump_address, print_output=False): - gdb.close_gdb() - target.start_openocd(startup_delay=0.2, print_output=False) + # Fast path: if OpenOCD direct session is alive, reset target to halt state and set PC + if gdb and getattr(gdb, "use_ocd_direct", False): + try: + gdb.reset_target(halt=True) + gdb.send_command(f"set $pc={jump_address}") + target.dump_all() + gdb.cleanup_skip() + return gdb + except Exception: + pass + + if gdb: + gdb.close_gdb() + target.start_openocd(startup_delay=0.3, print_output=False) gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_ext_elf_path) - gdb.reset_target() + gdb.reset_target(halt=True) gdb.send_command(f"set $pc={jump_address}") target.dump_all() return gdb @@ -84,12 +102,13 @@ def reset_target_and_gdb(gdb, jump_address, print_output=False): # Only called when we encounter an issue where we want to re-flash everything def re_initialize(gdb, jump_address, print_output=False): - gdb.close_gdb() + if gdb: + gdb.close_gdb() target.close_openocd() target.clear_bitstream() target.initialize_target(print_output=print_output) gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_ext_elf_path) - gdb.reset_target() + gdb.reset_target(halt=True) gdb.send_command(f"set $pc={jump_address}") target.dump_all() return gdb @@ -109,7 +128,7 @@ def test_rom_ext_rollback(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -152,21 +171,42 @@ def test_rom_ext_rollback(self): trace_end_address, ) gdb.send_command("c", check_response=False) - start_time = time.time() initial_timeout_stopped = False total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(read_uart_output()), flush=True) + gdb = reset_target_and_gdb(gdb, jump_address) + gdb.setup_pc_trace( + pc_trace_file, + trace_start_address, + trace_end_address, + ) + gdb.send_command("c", check_response=False) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(read_uart_output()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -238,9 +278,8 @@ def test_rom_ext_rollback(self): print("Timeout, reflashing", flush=True) gdb = re_initialize(gdb, jump_address) else: - print("No break point found, something went wrong", flush=True) - # Just to be safe that nothing went into flash, we reflash - gdb = re_initialize(gdb, jump_address) + # Breakpoint not reached (e.g. untaken branch) + gdb = reset_target_and_gdb(gdb, jump_address) except (TimeoutError, serial.SerialException) as e: print("Timeout error, retrying", flush=True) @@ -261,6 +300,13 @@ def test_rom_ext_rollback(self): if __name__ == "__main__": + unittest_argv = utils.get_selected_test_argv( + RomExtFiSimRollback, + requested_name=args.test, + config_args=config_args, + list_tests=args.list_tests, + ) + r = Runfiles.Create() # Get the openocd path. openocd_path = r.Rlocation("lowrisc_opentitan/third_party/openocd/build_openocd/bin/openocd") @@ -279,7 +325,7 @@ def test_rom_ext_rollback(self): if BITSTREAM: bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM) # Get the test result path - log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") + log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") or "/tmp" # Get the firmware path. firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP) # Get the rom path. @@ -316,7 +362,4 @@ def test_rom_ext_rollback(self): rom_ext_parser = DisParser(rom_ext_dis_path) rom_parser = DisParser(rom_dis_path) - print("ROM disassembly is found in ", rom_dis_path, flush=True) - print("ROM_EXT disassembly is found in ", rom_ext_dis_path, flush=True) - - unittest.main(argv=[sys.argv[0]]) + unittest.main(argv=unittest_argv) diff --git a/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_python_gdb_test.py b/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_python_gdb_test.py index ce34c2f09fd21..d0fef32c46d05 100644 --- a/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_python_gdb_test.py +++ b/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_python_gdb_test.py @@ -11,6 +11,7 @@ from sw.host.penetrationtests.python.util import targets from sw.host.penetrationtests.python.util.gdb_controller import GDBController from sw.host.penetrationtests.python.util.dis_parser import DisParser +from sw.host.penetrationtests.python.util import utils from collections import Counter import argparse import unittest @@ -33,13 +34,22 @@ # Read in the extra arguments from the opentitan_test. parser = argparse.ArgumentParser() parser.add_argument("--bitstream", type=str) +parser.add_argument("--rom", type=str) +parser.add_argument("--otp", type=str) parser.add_argument("--bootstrap", type=str) +parser.add_argument( + "--force-trace", + action="store_true", + help="Force re-running PC tracing even if trace log exists", +) parser.add_argument("--rom_ext", type=str) -parser.add_argument("--rom", type=str) +utils.add_test_selection_args(parser) args, config_args = parser.parse_known_args() BITSTREAM = args.bitstream +ROM_VMEM = args.rom +OTP_VMEM = args.otp BOOTSTRAP = args.bootstrap ROM_EXT = args.rom_ext ROM = args.rom @@ -70,10 +80,28 @@ def read_uart_output(): def reset_target_and_gdb(gdb, jump_address, print_output=False): - gdb.close_gdb() - target.start_openocd(startup_delay=0.2, print_output=False) + # Fast path: if GDB session is alive, reset target to halt state and set PC + if gdb and getattr(gdb, "gdb_process", None) and gdb.gdb_process.poll() is None: + try: + gdb.cleanup_skip() + gdb.reset_target(halt=True) + gdb.send_command(f"set $pc={jump_address}") + if gdb.get_program_counter() is not None: + target.dump_all() + return gdb + except Exception: + pass + + if gdb: + try: + gdb.close_gdb() + except Exception: + pass + target.reset_target() + time.sleep(0.05) + target.start_openocd(startup_delay=1.0, print_output=False) gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_elf_path) - gdb.reset_target() + gdb.reset_target(halt=True) gdb.send_command(f"set $pc={jump_address}") target.dump_all() return gdb @@ -84,7 +112,8 @@ def test_rom_secure_boot(self): print("Starting the rom secure boot test") # Directory for the trace log files - pc_trace_file = os.path.join(log_dir, "rom_secure_boot_pc_trace.log") + pc_trace_file_1 = os.path.join(log_dir, "rom_secure_boot_pc_trace_1.log") + pc_trace_file_2 = os.path.join(log_dir, "rom_secure_boot_pc_trace_2.log") # Directory for the the log of the campaign campaign_file = os.path.join(log_dir, "rom_secure_boot_test_campaign.log") @@ -93,7 +122,7 @@ def test_rom_secure_boot(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -111,6 +140,8 @@ def test_rom_secure_boot(self): gdb.reset_target() gdb.send_command(f"set $pc={jump_address}") + # Tracing in done in two steps to jump over sc_otbn_cmd_run which makes GDB hang + # Functions where we can get GDB to jump over upsert_register_address = rom_parser.get_function_start_address("upsert_register") @@ -120,16 +151,96 @@ def test_rom_secure_boot(self): "sigverify_ecdsa_p256_key_id_get" ) + # We stop tracing when we execute the p256 verify in the otbn + trace_end_address = rom_parser.get_function_start_address("sc_otbn_cmd_run") + + print( + "Start and stop addresses for the rom for trace 1: ", + trace_start_address, + trace_end_address, + flush=True, + ) + print("First trace data is logged in ", pc_trace_file_1, flush=True) + + # Start the tracing + # We set a short timeout to detect whether GDB has connected properly + # and a long timeout for the entire tracing + initial_timeout = 20 + total_timeout = 60 * 60 * 5 + + gdb.setup_pc_trace( + pc_trace_file_1, + trace_start_address, + trace_end_address, + skip_addrs=[upsert_register_address], + ) + gdb.send_command("c", check_response=False) + start_time = time.time() + initial_timeout_stopped = False + total_timeout_stopped = False + + # Run the tracing to get the trace log + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(read_uart_output()), flush=True) + gdb = reset_target_and_gdb(gdb, jump_address) + gdb.setup_pc_trace( + pc_trace_file_1, + trace_start_address, + trace_end_address, + skip_addrs=[upsert_register_address], + ) + gdb.send_command("c", check_response=False) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(read_uart_output()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) + sys.exit(1) + while time.time() - start_time < total_timeout: + output = gdb.read_output() + if "PC trace complete" in output: + print("\nTrace complete") + total_timeout_stopped = True + break + if not total_timeout_stopped: + print("Final tracing timeout reached") + sys.exit(1) + + # Reset the target, flush the output, and close gdb + gdb = reset_target_and_gdb(gdb, jump_address) + + # We ready the second part of the trace + + # We start from sc_otbn_dmem_read which reads p256 verify's results from otbn + trace_start_address = rom_parser.get_function_start_address("sc_otbn_dmem_read") + # We expect with the test that we end up in shutdown_finalize trace_end_address = rom_parser.get_function_start_address("shutdown_finalize") print( - "Start and stop addresses for the rom: ", + "Start and stop addresses for the rom for trace 2: ", trace_start_address, trace_end_address, flush=True, ) - print("Trace data is logged in ", pc_trace_file, flush=True) + print("Second trace data is logged in ", pc_trace_file_2, flush=True) # Start the tracing # We set a short timeout to detect whether GDB has connected properly @@ -138,27 +249,49 @@ def test_rom_secure_boot(self): total_timeout = 60 * 60 * 5 gdb.setup_pc_trace( - pc_trace_file, + pc_trace_file_2, trace_start_address, trace_end_address, skip_addrs=[upsert_register_address], ) gdb.send_command("c", check_response=False) - start_time = time.time() initial_timeout_stopped = False total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(read_uart_output()), flush=True) + gdb = reset_target_and_gdb(gdb, jump_address) + gdb.setup_pc_trace( + pc_trace_file_2, + trace_start_address, + trace_end_address, + skip_addrs=[upsert_register_address], + ) + gdb.send_command("c", check_response=False) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(read_uart_output()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -171,7 +304,8 @@ def test_rom_secure_boot(self): sys.exit(1) # Parse and truncate the trace log to get all PCs in a list - pc_list = gdb.parse_pc_trace_file(pc_trace_file) + pc_list = gdb.parse_pc_trace_file(pc_trace_file_1) + pc_list.extend(gdb.parse_pc_trace_file(pc_trace_file_2)) # Get the unique PCs and annotate their occurence count pc_count_dict = Counter(pc_list) if len(pc_count_dict) <= 0: @@ -240,6 +374,13 @@ def test_rom_secure_boot(self): if __name__ == "__main__": + unittest_argv = utils.get_selected_test_argv( + RomFiSim, + requested_name=args.test, + config_args=config_args, + list_tests=args.list_tests, + ) + r = Runfiles.Create() # Get the openocd path. openocd_path = r.Rlocation("lowrisc_opentitan/third_party/openocd/build_openocd/bin/openocd") @@ -257,8 +398,15 @@ def test_rom_secure_boot(self): bitstream_path = None if BITSTREAM: bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM) + # Load the ROM/OTP memories for FPGAs. + rom_path = None + if ROM_VMEM: + rom_path = r.Rlocation("lowrisc_opentitan/" + ROM_VMEM) + otp_path = None + if OTP_VMEM: + otp_path = r.Rlocation("lowrisc_opentitan/" + OTP_VMEM) # Get the test result path - log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") + log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") or "/tmp" # Get the firmware path. firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP) # Get the rom path. @@ -310,6 +458,4 @@ def test_rom_secure_boot(self): target = targets.Target(target_cfg) rom_parser = DisParser(rom_dis_path) - print("ROM disassembly is found in ", rom_dis_path, flush=True) - - unittest.main(argv=[sys.argv[0]]) + unittest.main(argv=unittest_argv) diff --git a/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_rollback_python_gdb_test.py b/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_rollback_python_gdb_test.py index e13560b8f0cd9..9ff1940451697 100644 --- a/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_rollback_python_gdb_test.py +++ b/sw/host/penetrationtests/python/fi/gdb_testing/fi_rom_rollback_python_gdb_test.py @@ -11,6 +11,7 @@ from sw.host.penetrationtests.python.util import targets from sw.host.penetrationtests.python.util.gdb_controller import GDBController from sw.host.penetrationtests.python.util.dis_parser import DisParser +from sw.host.penetrationtests.python.util import utils from collections import Counter import argparse import unittest @@ -34,8 +35,13 @@ parser = argparse.ArgumentParser() parser.add_argument("--bitstream", type=str) parser.add_argument("--bootstrap", type=str) +parser.add_argument( + "--force-trace", + action="store_true", + help="Force re-running PC tracing even if trace log exists", +) parser.add_argument("--rom_ext", type=str) -parser.add_argument("--rom", type=str) +utils.add_test_selection_args(parser) args, config_args = parser.parse_known_args() @@ -70,10 +76,22 @@ def read_uart_output(): def reset_target_and_gdb(gdb, jump_address, print_output=False): - gdb.close_gdb() - target.start_openocd(startup_delay=0.2, print_output=False) + # Fast path: if OpenOCD direct session is alive, reset target to halt state and set PC + if gdb and getattr(gdb, "use_ocd_direct", False): + try: + gdb.reset_target(halt=True) + gdb.send_command(f"set $pc={jump_address}") + target.dump_all() + gdb.cleanup_skip() + return gdb + except Exception: + pass + + if gdb: + gdb.close_gdb() + target.start_openocd(startup_delay=0.3, print_output=False) gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=rom_elf_path) - gdb.reset_target() + gdb.reset_target(halt=True) gdb.send_command(f"set $pc={jump_address}") target.dump_all() return gdb @@ -93,7 +111,7 @@ def test_rom_rollback(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -142,21 +160,43 @@ def test_rom_rollback(self): skip_addrs=[upsert_register_address], ) gdb.send_command("c", check_response=False) - start_time = time.time() initial_timeout_stopped = False total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(read_uart_output()), flush=True) + gdb = reset_target_and_gdb(gdb, jump_address) + gdb.setup_pc_trace( + pc_trace_file, + trace_start_address, + trace_end_address, + skip_addrs=[upsert_register_address], + ) + gdb.send_command("c", check_response=False) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(read_uart_output()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -238,6 +278,13 @@ def test_rom_rollback(self): if __name__ == "__main__": + unittest_argv = utils.get_selected_test_argv( + RomFiSimRollback, + requested_name=args.test, + config_args=config_args, + list_tests=args.list_tests, + ) + r = Runfiles.Create() # Get the openocd path. openocd_path = r.Rlocation("lowrisc_opentitan/third_party/openocd/build_openocd/bin/openocd") @@ -256,7 +303,7 @@ def test_rom_rollback(self): if BITSTREAM: bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM) # Get the test result path - log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") + log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") or "/tmp" # Get the firmware path. firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP) # Get the rom path. @@ -286,6 +333,4 @@ def test_rom_rollback(self): target = targets.Target(target_cfg) rom_parser = DisParser(rom_dis_path) - print("ROM disassembly is found in ", rom_dis_path, flush=True) - - unittest.main(argv=[sys.argv[0]]) + unittest.main(argv=unittest_argv) diff --git a/sw/host/penetrationtests/python/fi/gdb_testing/fi_sym_cryptolib_python_gdb_test.py b/sw/host/penetrationtests/python/fi/gdb_testing/fi_sym_cryptolib_python_gdb_test.py index 44b99dbedf886..dc6a02b358419 100644 --- a/sw/host/penetrationtests/python/fi/gdb_testing/fi_sym_cryptolib_python_gdb_test.py +++ b/sw/host/penetrationtests/python/fi/gdb_testing/fi_sym_cryptolib_python_gdb_test.py @@ -40,6 +40,12 @@ parser = argparse.ArgumentParser() parser.add_argument("--bitstream", type=str) parser.add_argument("--bootstrap", type=str) +parser.add_argument( + "--force-trace", + action="store_true", + help="Force re-running PC tracing even if trace log exists", +) +utils.add_test_selection_args(parser) args, config_args = parser.parse_known_args() @@ -65,36 +71,52 @@ def read_testos_output(): def reset_gdb(gdb): - gdb.close_gdb() - gdb = GDBController( + if gdb and getattr(gdb, "gdb_process", None) and gdb.gdb_process.poll() is None: + try: + gdb.cleanup_skip() + ping = gdb.send_command("p 1", timeout=1.0) + if ping and ("= 1" in ping): + return gdb + except Exception: + pass + if gdb: + try: + gdb.close_gdb() + except Exception: + pass + return GDBController( gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path, ) - return gdb def reset_target_and_gdb(gdb): - gdb.close_gdb() + if gdb: + try: + gdb.close_gdb() + except Exception: + pass target.reset_target() - target.start_openocd(startup_delay=0.2, print_output=False) + time.sleep(0.05) + target.start_openocd(startup_delay=0.3, print_output=False) target.dump_all() trigger_testos_init(print_output=False) - gdb = GDBController( + return GDBController( gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path, ) - return gdb def re_initialize(gdb, print_output=False): - gdb.close_gdb() + # Tier 3: Full FPGA re-initialization (only on unrecoverable lockup) + if gdb: + gdb.close_gdb() target.initialize_target(print_output=print_output) trigger_testos_init(print_output=print_output) target.dump_all() - gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) - return gdb + return GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) class SymCryptolibFiSim(unittest.TestCase): @@ -123,7 +145,7 @@ def test_hmac(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -154,6 +176,8 @@ def test_hmac(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the hmac from the testOS (we do not read its output) symfi.handle_hmac(data[0], data_len, key, key_len, hash_mode, mode, cfg, trigger) @@ -163,15 +187,39 @@ def test_hmac(self): total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the hmac from the testOS (we do not read its output) + symfi.handle_hmac( + data[0], data_len, key, key_len, hash_mode, mode, cfg, trigger + ) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -250,7 +298,7 @@ def test_hmac(self): data_out[0], data_out[1], match_threshold_ratio=0.75, - valid_len=32 + valid_len=32, ) ) or utils.is_majority_zeros( data_out[i], total_length=32 @@ -265,8 +313,14 @@ def test_hmac(self): # Reset GDB by closing and opening again gdb = reset_gdb(gdb) else: - print("No break point found, something went wrong", flush=True) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print( + "Target did not respond, resetting target", flush=True + ) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -325,7 +379,7 @@ def test_aes(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -356,6 +410,8 @@ def test_aes(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the hmac from the testOS (we do not read its output) symfi.handle_aes( @@ -367,15 +423,39 @@ def test_aes(self): total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the hmac from the testOS (we do not read its output) + symfi.handle_aes( + data[0], data_len, key, key_len, iv, padding, mode, op_enc, cfg, trigger + ) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -430,8 +510,16 @@ def test_aes(self): # The instruction skip loop symfi.handle_aes( - data[i], data_len, key, key_len, iv, - padding, mode, op_enc, cfg, trigger + data[i], + data_len, + key, + key_len, + iv, + padding, + mode, + op_enc, + cfg, + trigger, ) testos_response = read_testos_output() @@ -455,7 +543,7 @@ def test_aes(self): data_out[0], data_out[1], match_threshold_ratio=0.75, - valid_len=16 + valid_len=16, ) ) or utils.is_majority_zeros( data_out[i], total_length=16 @@ -470,8 +558,14 @@ def test_aes(self): # Reset GDB by closing and opening again gdb = reset_gdb(gdb) else: - print("No break point found, something went wrong", flush=True) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print( + "Target did not respond, resetting target", flush=True + ) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -528,7 +622,7 @@ def test_drbg_generate(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -559,6 +653,8 @@ def test_drbg_generate(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the drbg from the testOS (we do not read its output) symfi.handle_drbg_reseed( @@ -572,15 +668,41 @@ def test_drbg_generate(self): total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the drbg from the testOS (we do not read its output) + symfi.handle_drbg_reseed( + entropy[0], entropy_len, nonce, nonce_len, reseed_interval, mode, 0, 0 + ) + target.read_response() + symfi.handle_drbg_generate([0], 0, data_len, mode, cfg, trigger) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -666,7 +788,7 @@ def test_drbg_generate(self): drbg_out[0], drbg_out[1], match_threshold_ratio=0.75, - valid_len=16 + valid_len=16, ) ) or utils.is_majority_zeros( drbg_out[i], total_length=16 @@ -681,8 +803,14 @@ def test_drbg_generate(self): # Reset GDB by closing and opening again gdb = reset_gdb(gdb) else: - print("No break point found, something went wrong", flush=True) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print( + "Target did not respond, resetting target", flush=True + ) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -739,7 +867,7 @@ def test_drbg_reseed(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -770,6 +898,8 @@ def test_drbg_reseed(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the drbg from the testOS (we do not read its output) symfi.handle_drbg_reseed( @@ -783,15 +913,48 @@ def test_drbg_reseed(self): total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the drbg from the testOS (we do not read its output) + symfi.handle_drbg_reseed( + entropy[0], + entropy_len, + nonce, + nonce_len, + reseed_interval, + mode, + cfg, + trigger, + ) + target.read_response() + symfi.handle_drbg_generate([0], 0, data_len, mode, cfg, trigger) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -866,12 +1029,8 @@ def test_drbg_reseed(self): print("Crash detected, resetting", flush=True) gdb = reset_target_and_gdb(gdb) else: - testos_response_json = json.loads( - testos_response - ) - print( - "Output:", testos_response_json, flush=True - ) + testos_response_json = json.loads(testos_response) + print("Output:", testos_response_json, flush=True) if testos_response_json["status"] == 0: drbg_out[i] = testos_response_json["data"] @@ -880,7 +1039,7 @@ def test_drbg_reseed(self): drbg_out[0], drbg_out[1], match_threshold_ratio=0.75, - valid_len=16 + valid_len=16, ) ) or utils.is_majority_zeros( drbg_out[i], total_length=16 @@ -895,8 +1054,14 @@ def test_drbg_reseed(self): # Reset GDB by closing and opening again gdb = reset_gdb(gdb) else: - print("No break point found, something went wrong", flush=True) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print( + "Target did not respond, resetting target", flush=True + ) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -957,7 +1122,7 @@ def test_gcm(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -968,9 +1133,7 @@ def test_gcm(self): trigger_testos_init() # Connect to GDB - gdb = GDBController( - gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path - ) + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) # We provide the name of the unique marker in the pentest framework function_name = "PENTEST_MARKER_GCM" @@ -1024,6 +1187,8 @@ def test_gcm(self): ], ) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the gcm from the testOS (we do not read its output) symfi.handle_gcm( @@ -1035,15 +1200,63 @@ def test_gcm(self): total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace( + pc_trace_file, + trace_address[0], + trace_address[1], + skip_addrs=[ + ibex_rnd32_read_address, + galois_mul_state_key_address, + hardened_memcpy_address, + hardened_memshred_address, + hardened_memeq_address, + ghash_context_integrity_checksum_address, + hmac_key_integrity_checksum_address, + ghash_process_block_address, + ], + ) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the gcm from the testOS (we do not read its output) + symfi.handle_gcm( + data[0], + data_len, + key, + key_len, + aad, + aad_len, + tag, + tag_len, + iv[0], + cfg, + trigger, + ) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -1129,7 +1342,7 @@ def test_gcm(self): gcm_out[0], gcm_out[1], match_threshold_ratio=0.75, - valid_len=16 + valid_len=16, ) ) or utils.is_majority_zeros( gcm_out[i], total_length=16 @@ -1144,8 +1357,14 @@ def test_gcm(self): # Reset GDB by closing and opening again gdb = reset_gdb(gdb) else: - print("No break point found, something went wrong", flush=True) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print( + "Target did not respond, resetting target", flush=True + ) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -1199,7 +1418,7 @@ def test_cmac(self): gdb = None started = False - with open(campaign_file, "w") as campaign: + with open(campaign_file, "w", buffering=1) as campaign: print(f"Switching terminal output to {campaign_file}", flush=True) sys.stdout = campaign try: @@ -1210,17 +1429,13 @@ def test_cmac(self): trigger_testos_init() # Connect to GDB - gdb = GDBController( - gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path - ) + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) # We provide the name of the unique marker in the pentest framework function_name = "PENTEST_MARKER_CMAC" # Gives back an array of hits where the function is called trace_address = parser.get_marker_addresses(function_name) - print( - "Start and stop addresses of ", function_name, ": ", trace_address - ) + print("Start and stop addresses of ", function_name, ": ", trace_address) crash_observation_address = parser.get_function_start_address( "ottf_exception_handler" @@ -1234,6 +1449,8 @@ def test_cmac(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() # Trigger the cmac from the testOS (we do not read its output) symfi.handle_cmac(data[0], data_len, key, key_len, iv, cfg, trigger) @@ -1243,15 +1460,37 @@ def test_cmac(self): total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + # Trigger the cmac from the testOS (we do not read its output) + symfi.handle_cmac(data[0], data_len, key, key_len, iv, cfg, trigger) + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -1335,23 +1574,17 @@ def test_cmac(self): print("Crash detected, resetting", flush=True) gdb = reset_target_and_gdb(gdb) else: - testos_response_json = json.loads( - testos_response - ) - print( - "Output:", testos_response_json, flush=True - ) + testos_response_json = json.loads(testos_response) + print("Output:", testos_response_json, flush=True) if testos_response_json["status"] == 0: - data_out[i] = tuple( - testos_response_json["data"] - ) + data_out[i] = tuple(testos_response_json["data"]) if ( utils.is_partial_collision( data_out[0], data_out[1], match_threshold_ratio=0.75, - valid_len=16 + valid_len=16, ) ) or utils.is_majority_zeros( data_out[i], total_length=16 @@ -1371,11 +1604,14 @@ def test_cmac(self): # Reset GDB by closing and opening again gdb = reset_gdb(gdb) else: - print( - "No break point found, something went wrong", - flush=True, - ) - gdb = reset_target_and_gdb(gdb) + # Breakpoint not reached (e.g. untaken branch) + if not testos_response: + print( + "Target did not respond, resetting target", flush=True + ) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: print( @@ -1397,9 +1633,7 @@ def test_cmac(self): finally: print("-" * 80) - print( - f"Total attacks {total_attacks}, successful attacks {successful_faults}" - ) + print(f"Total attacks {total_attacks}, successful attacks {successful_faults}") # Close the OpenOCD and GDB connection at the end if gdb: gdb.close_gdb() @@ -1410,6 +1644,13 @@ def test_cmac(self): if __name__ == "__main__": + unittest_argv = utils.get_selected_test_argv( + SymCryptolibFiSim, + requested_name=args.test, + config_args=config_args, + list_tests=args.list_tests, + ) + r = Runfiles.Create() # Get the openocd path. openocd_path = r.Rlocation("lowrisc_opentitan/third_party/openocd/build_openocd/bin/openocd") @@ -1428,7 +1669,7 @@ def test_cmac(self): if BITSTREAM: bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM) # Get the test result path - log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") + log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") or "/tmp" # Get the firmware path. firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP) # Get the disassembly path. @@ -1459,4 +1700,4 @@ def test_cmac(self): print("Disassembly is found in ", dis_path, flush=True) - unittest.main(argv=[sys.argv[0]]) + unittest.main(argv=unittest_argv) diff --git a/sw/host/penetrationtests/python/fi/gdb_testing/fi_unit_gdb_python_gdb_test.py b/sw/host/penetrationtests/python/fi/gdb_testing/fi_unit_gdb_python_gdb_test.py index e023ac4e7876f..7552a70a9605c 100644 --- a/sw/host/penetrationtests/python/fi/gdb_testing/fi_unit_gdb_python_gdb_test.py +++ b/sw/host/penetrationtests/python/fi/gdb_testing/fi_unit_gdb_python_gdb_test.py @@ -15,6 +15,7 @@ from sw.host.penetrationtests.python.util import common_library from sw.host.penetrationtests.python.util.gdb_controller import GDBController from sw.host.penetrationtests.python.util.dis_parser import DisParser +from sw.host.penetrationtests.python.util import utils from collections import Counter import json import argparse @@ -38,6 +39,12 @@ parser = argparse.ArgumentParser() parser.add_argument("--bitstream", type=str) parser.add_argument("--bootstrap", type=str) +parser.add_argument( + "--force-trace", + action="store_true", + help="Force re-running PC tracing even if trace log exists", +) +utils.add_test_selection_args(parser) args, config_args = parser.parse_known_args() @@ -61,36 +68,52 @@ def read_testos_output(): def reset_gdb(gdb): - gdb.close_gdb() - gdb = GDBController( + if gdb and getattr(gdb, "gdb_process", None) and gdb.gdb_process.poll() is None: + try: + gdb.cleanup_skip() + ping = gdb.send_command("p 1", timeout=1.0) + if ping and ("= 1" in ping): + return gdb + except Exception: + pass + if gdb: + try: + gdb.close_gdb() + except Exception: + pass + return GDBController( gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path, ) - return gdb def reset_target_and_gdb(gdb): - gdb.close_gdb() + if gdb: + try: + gdb.close_gdb() + except Exception: + pass target.reset_target() - target.start_openocd(startup_delay=0.2, print_output=False) + time.sleep(0.05) + target.start_openocd(startup_delay=0.3, print_output=False) target.dump_all() trigger_testos_init(print_output=False) - gdb = GDBController( + return GDBController( gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path, ) - return gdb def re_initialize(gdb, print_output=False): - gdb.close_gdb() + # Tier 3: Full FPGA re-initialization (only on unrecoverable lockup) + if gdb: + gdb.close_gdb() target.initialize_target(print_output=print_output) trigger_testos_init(print_output=print_output) target.dump_all() - gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) - return gdb + return GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) class UnitFiSim(unittest.TestCase): @@ -128,6 +151,8 @@ def test_gdb_try(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() gdbfi.handle_gdb_try() @@ -136,15 +161,36 @@ def test_gdb_try(self): total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + gdbfi.handle_gdb_try() + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -208,7 +254,12 @@ def test_gdb_try(self): # Reset GDB by closing and opening again gdb = reset_gdb(gdb) else: - gdb = reset_target_and_gdb(gdb) + # Breakpoint was not hit (e.g. untaken branch): cleanly clean up skip + if not testos_response: + print("Target did not respond, resetting target", flush=True) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: try: @@ -264,6 +315,8 @@ def test_gdb_switch(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() gdbfi.handle_gdb_switch() @@ -272,15 +325,36 @@ def test_gdb_switch(self): total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + gdbfi.handle_gdb_switch() + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -344,7 +418,12 @@ def test_gdb_switch(self): # Reset GDB by closing and opening again gdb = reset_gdb(gdb) else: - gdb = reset_target_and_gdb(gdb) + # Breakpoint was not hit (e.g. untaken branch): cleanly clean up skip + if not testos_response: + print("Target did not respond, resetting target", flush=True) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: try: @@ -400,6 +479,8 @@ def test_gdb_if(self): gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() gdbfi.handle_gdb_if() @@ -408,15 +489,36 @@ def test_gdb_if(self): total_timeout_stopped = False # Run the tracing to get the trace log - # Sometimes the tracing fails due to race conditions, - # we have a quick initial timeout to catch this while time.time() - start_time < initial_timeout: output = gdb.read_output() - if "breakpoint 1, " in output: + if "breakpoint 1, " in output or "Breakpoint 1" in output: initial_timeout_stopped = True break if not initial_timeout_stopped: - print("No initial break point found, can be a misfire, try again") + print( + "Initial break point not hit on first attempt, retrying with reset...", + flush=True, + ) + print("Target UART:", repr(target.read_all()), flush=True) + gdb = reset_target_and_gdb(gdb) + gdb.setup_pc_trace(pc_trace_file, trace_address[0], trace_address[1]) + gdb.send_command("c", check_response=False) + time.sleep(0.1) + target.dump_all() + gdbfi.handle_gdb_if() + start_time = time.time() + while time.time() - start_time < initial_timeout: + output = gdb.read_output() + if "breakpoint 1, " in output or "Breakpoint 1" in output: + initial_timeout_stopped = True + break + if not initial_timeout_stopped: + print("No initial break point found, can be a misfire, try again", flush=True) + print("Target UART:", repr(target.read_all()), flush=True) + if gdb: + gdb.interrupt(timeout=1.0) + print("GDB PC:", gdb.get_program_counter(), flush=True) + print("GDB Backtrace:", gdb.send_command("bt"), flush=True) sys.exit(1) while time.time() - start_time < total_timeout: output = gdb.read_output() @@ -480,7 +582,12 @@ def test_gdb_if(self): # Reset GDB by closing and opening again gdb = reset_gdb(gdb) else: - gdb = reset_target_and_gdb(gdb) + # Breakpoint was not hit (e.g. untaken branch): cleanly clean up skip + if not testos_response: + print("Target did not respond, resetting target", flush=True) + gdb = reset_target_and_gdb(gdb) + else: + gdb = reset_gdb(gdb) except json.JSONDecodeError: try: @@ -504,6 +611,13 @@ def test_gdb_if(self): if __name__ == "__main__": + unittest_argv = utils.get_selected_test_argv( + UnitFiSim, + requested_name=args.test, + config_args=config_args, + list_tests=args.list_tests, + ) + r = Runfiles.Create() # Get the openocd path. openocd_path = r.Rlocation("lowrisc_opentitan/third_party/openocd/build_openocd/bin/openocd") @@ -522,7 +636,7 @@ def test_gdb_if(self): if BITSTREAM: bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM) # Get the test result path - log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") + log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") or "/tmp" # Get the firmware path. firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP) # Get the disassembly path. @@ -551,4 +665,4 @@ def test_gdb_if(self): gdbfi = OTFIUnitGdb(target) parser = DisParser(dis_path) - unittest.main(argv=[sys.argv[0]]) + unittest.main(argv=unittest_argv) diff --git a/sw/host/penetrationtests/python/util/gdb_controller.py b/sw/host/penetrationtests/python/util/gdb_controller.py index 93a42d74d12a4..cf9a5abd1b289 100644 --- a/sw/host/penetrationtests/python/util/gdb_controller.py +++ b/sw/host/penetrationtests/python/util/gdb_controller.py @@ -8,38 +8,189 @@ import time import re import signal +import socket class GDBController: - def __init__(self, gdb_path, gdb_port=3333, remote_host="localhost", elf_file=None): + """Enhanced GDB and OpenOCD controller for high-speed hardware fault injection. + + Provides direct OpenOCD Jim Tcl socket control (:6666) for sub-millisecond + hardware breakpoints and register modification, with automatic fallback to + standard GDB MI pipes (:3333). + """ + + def __init__( + self, + gdb_path, + gdb_port=3333, + remote_host="localhost", + elf_file=None, + ocd_tcl_port=6666, + ): + if isinstance(gdb_port, str) and (not gdb_port.isdigit()): + elf_file = gdb_port + gdb_port = 3333 self.remote_host = remote_host - self.gdb_port = gdb_port + self.gdb_port = int(gdb_port) + self.ocd_tcl_port = int(ocd_tcl_port) self.gdb_path = gdb_path - gdb_command = [ - gdb_path, - "-ex", - f"target remote {remote_host}:{gdb_port}", - ] - if elf_file: - gdb_command.append(elf_file) + self.elf_file = elf_file + self.n_brkp = 1 + self.last_bp_num = 1 + self.last_bp_pc = None + self.cmd_seq = 0 + self._output_buffer = "" + self._skip_hit = False + self._bp_pc_val = None + self._observations = {} + self._ocd_sock = None + self.gdb_process = None + + # Standard GDB connection to OpenOCD gdb_port (3333) + self.use_ocd_direct = False + + if self.use_ocd_direct: + print( + f"[GDBController] Fast OpenOCD active on {remote_host}:{self.ocd_tcl_port}" + ) + gdb_cmd = [gdb_path, "-q", "-ex", "set pagination off", "-ex", "set confirm off"] + if elf_file: + gdb_cmd.extend(["-ex", f"file {elf_file}"]) + self.gdb_process = Popen( + gdb_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=0 + ) + try: + self._ocd_cmd("adapter speed 10000") + self._ocd_cmd("poll_period 1") + self._ocd_cmd("riscv set_mem_access progbuf sysbus") + self._ocd_cmd("catch {rbp all}") + self._ocd_cmd("riscv.tap.0 configure -event halted {}") + except Exception as e: + print(f"[GDBController] OpenOCD direct init warning: {e}") + else: + gdb_command = [ + gdb_path, + "-q", + "-ex", + "set pagination off", + "-ex", + "set confirm off", + ] + if elf_file: + gdb_command.extend(["-ex", f"file {elf_file}"]) + gdb_command.extend(["-ex", f"target remote {remote_host}:{gdb_port}"]) + try: + self.gdb_process = Popen( + gdb_command, stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=0 + ) + + # Wait for GDB startup, symbol loading, and connection to target + init_output = "" + start_time = time.time() + while time.time() - start_time < 10.0: + out = self.read_output(timeout=0.1) + init_output += out + if init_output.strip().endswith("(gdb)"): + break + + time.sleep(0.5) + self.dump_output(timeout=0.1) + + # Start clean + self.send_command("delete breakpoints", timeout=5.0) + + # Configure memory access on Ibex core + try: + self.send_command( + "monitor riscv set_mem_access progbuf sysbus", timeout=2.0 + ) + except Exception: + pass + except Exception: + self.close_gdb() + raise + + def _check_ocd_tcl(self) -> bool: + """Tests whether the OpenOCD Jim Tcl server port is accessible.""" try: - self.gdb_process = Popen(gdb_command, stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=0) - - # Flush the output from GDB - self.dump_output() - # Start clean - self.send_command("delete breakpoints", timeout=10) - # Need to flush again from the breakpoints - self.dump_output() - - # Set number of breakpoints - self.n_brkp = 1 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(0.5) + s.connect((self.remote_host, self.ocd_tcl_port)) + s.sendall(b"version\x1a") + data = s.recv(256) + s.close() + return b"Open On-Chip Debugger" in data except Exception: - self.close_gdb() - raise + return False + + def _get_ocd_sock(self): + """Maintains a persistent socket connection to OpenOCD Jim Tcl.""" + if self._ocd_sock is None: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(2.0) + s.connect((self.remote_host, self.ocd_tcl_port)) + self._ocd_sock = s + return self._ocd_sock + + def _ocd_cmd(self, cmd: str, timeout: float = 3.0) -> str: + """Sends a command to OpenOCD over persistent Jim Tcl socket interface.""" + for attempt in range(2): + try: + s = self._get_ocd_sock() + s.settimeout(timeout) + s.sendall((cmd + "\x1a").encode("utf-8")) + resp = b"" + while True: + data = s.recv(1024) + if not data: + break + resp += data + if b"\x1a" in resp: + break + return resp.decode("utf-8", errors="ignore").replace("\x1a", "") + except Exception: + if self._ocd_sock: + try: + self._ocd_sock.close() + except Exception: + pass + self._ocd_sock = None + if attempt == 1: + raise + + def read_output(self, print_errors=True, timeout=0.05): + """Reads output from GDB or OpenOCD.""" + if self.use_ocd_direct: + output = self._output_buffer + self._output_buffer = "" + if (not self._skip_hit) and (self._bp_pc_val is not None): + try: + hit = self._ocd_cmd("set hit_count").strip() + if hit and int(hit) > 0: + self._skip_hit = True + output += "Breakpoint 1, instruction skip applied\n" + except Exception: + pass + if self._observations: + try: + pc_resp = self._ocd_cmd("reg pc") + for obs_addr, obs_msg in self._observations.items(): + addr_int = ( + int(obs_addr, 16) + if isinstance(obs_addr, str) + else int(obs_addr) + ) + if ( + f"0x{addr_int:x}" in pc_resp.lower() or + f"{addr_int:x}" in pc_resp.lower() + ): + output += f"fisim_result: {obs_msg}\n" + except Exception: + pass + time.sleep(timeout) + return output - def read_output(self, print_errors=False, timeout=0.05): if not self.gdb_process: return "" @@ -50,83 +201,141 @@ def read_output(self, print_errors=False, timeout=0.05): if self.gdb_process.stderr: readable_pipes.append(self.gdb_process.stderr.fileno()) - while True: - try: - current_timeout = timeout if output == "" else 0 - readable, _, _ = select.select(readable_pipes, [], [], current_timeout) - - if not readable: - break - - data_read = False - for fd in readable: - if fd == self.gdb_process.stdout.fileno(): - chunk = os.read(fd, 4096).decode("utf-8", errors="ignore") - if chunk: - output += chunk - data_read = True - elif fd == self.gdb_process.stderr.fileno(): - err_chunk = os.read(fd, 4096).decode("utf-8", errors="ignore") - if err_chunk: - if print_errors: - print(f"GDB Stderr: {err_chunk}") - data_read = True - - if not data_read: - break - - except Exception as e: - print(f"Error reading GDB output: {e}") - break + try: + readable, _, _ = select.select(readable_pipes, [], [], timeout) + + for fd in readable: + if fd == self.gdb_process.stdout.fileno(): + data = os.read(fd, 4096).decode("utf-8", errors="ignore") + output += data + elif fd == self.gdb_process.stderr.fileno(): + err_data = os.read(fd, 4096).decode("utf-8", errors="ignore") + if err_data.strip() and print_errors: + print(f"[GDB Stderr]: {repr(err_data)}") + except Exception as e: + print(f"Error reading GDB output: {e}") return output def dump_output(self, timeout=0.05): - self.read_output(timeout=timeout) + """Flushes pending output.""" + if self.use_ocd_direct: + self._output_buffer = "" + else: + while True: + out = self.read_output(timeout=timeout) + if not out: + break + + def send_command(self, mi_command, timeout=2.0, check_response=True): + """Sends a command to the target debugger.""" + if self.use_ocd_direct: + cmd = mi_command.strip() + if cmd in ("c", "continue"): + self._ocd_cmd("catch {resume}") + return "Continuing.\n" + elif cmd.startswith("delete") or cmd == "d": + self.cleanup_skip() + return "Deleted breakpoints.\n" + elif cmd.startswith("set $pc="): + new_pc = cmd.split("=")[1].strip() + self._ocd_cmd(f"reg pc {new_pc}") + return f"pc: {new_pc}\n" + elif cmd.startswith("monitor "): + mon_body = cmd[8:].strip() + return self._ocd_cmd(mon_body) + elif ( + cmd.startswith("thbreak") or + cmd.startswith("hbreak") or + cmd.startswith("tb ") or + cmd.startswith("b ") + ): + m = re.search(r"0x[0-9a-fA-F]+", cmd) + if m: + addr = int(m.group(0), 16) + return self._ocd_cmd(f"bp 0x{addr:x} 2 hw") + return "" + else: + try: + return self._ocd_cmd(cmd) + except Exception: + return "" - def send_command(self, mi_command, timeout=0.05, check_response=True): if not self.gdb_process or not self.gdb_process.stdin: raise RuntimeError("GDB process not started or stdin not available.") - command_line = mi_command.strip() + "\n" + if check_response: + self.dump_output(timeout=0.01) + self.cmd_seq = getattr(self, "cmd_seq", 0) + 1 + token = f"__SENTINEL_{self.cmd_seq}__" + cmd_body = mi_command.strip() + command_line = f'{cmd_body}\nprintf "{token}\\n"\n' + else: + command_line = mi_command.strip() + "\n" self.gdb_process.stdin.write(command_line.encode("utf-8")) - # After sending the command let's wait for a while till the command is - # processed on the receiving end - time.sleep(0.1) - self.gdb_process.stdin.flush() if check_response: start_time = time.time() response = "" while True: - response += self.read_output() - - if response.strip().endswith("(gdb)") or ( - "^done" in response and response.strip().endswith("=") - ): - break + chunk = self.read_output(timeout=0.05) + if chunk: + response += chunk + if token in response and response.strip().endswith("(gdb)"): + break if time.time() - start_time > timeout: raise TimeoutError( - f"GDB timed out after {timeout}s. Current output: {response}, {mi_command}" + f"GDB timed out after {timeout}s. Output: {repr(response)}, {mi_command}" ) - # To debug you can print this output to see GDB's response - return response + cleaned_response = response.split(token)[0] + return cleaned_response else: return None def reset_target(self, halt=True, reset_delay=0.005): - if halt: - self.send_command("monitor reset halt", check_response=False) + """Resets the target device.""" + if self.use_ocd_direct: + if halt: + self._ocd_cmd("reset halt") + else: + self._ocd_cmd("reset run") + time.sleep(reset_delay) else: - self.send_command("monitor reset run", check_response=False) - time.sleep(reset_delay) - self.dump_output() + if halt: + self.send_command("monitor reset halt", check_response=False) + else: + self.send_command("monitor reset run", check_response=False) + time.sleep(reset_delay) + self.dump_output() def close_gdb(self, timeout=1): + """Gracefully closes debugger and OpenOCD sockets.""" + if self.use_ocd_direct: + try: + self._ocd_cmd("catch {rbp all}") + self._ocd_cmd("riscv.tap.0 configure -event halted {}") + self._ocd_cmd("catch {resume}") + except Exception: + pass + if hasattr(self, "_ocd_sock") and self._ocd_sock: + try: + self._ocd_sock.close() + except Exception: + pass + self._ocd_sock = None + if self.gdb_process: + try: + self.gdb_process.kill() + self.gdb_process.communicate() + except Exception: + pass + self.gdb_process = None + return + if not self.gdb_process or self.gdb_process.poll() is not None: return @@ -141,8 +350,18 @@ def close_gdb(self, timeout=1): self.gdb_process = None def get_program_counter(self): - gdb_command = "p $pc" + """Reads current Program Counter (PC).""" + if self.use_ocd_direct: + try: + resp = self._ocd_cmd("reg pc") + match = re.search(r"0x([0-9a-fA-F]+)", resp) + if match: + return "0x" + match.group(1).strip() + except Exception: + pass + return None + gdb_command = "p $pc" try: response = self.send_command(gdb_command, timeout=0.5) pc_pattern = re.compile(r"0x([0-9a-fA-F]+)") @@ -151,20 +370,27 @@ def get_program_counter(self): return "0x" + match.group(1).strip() if "No symbol " in response or "Undefined command" in response: raise RuntimeError(f"GDB returned an error: {response}") - except Exception: return None - def setup_pc_trace(self, file_name, trace_start_addr, trace_end_addr, skip_addrs=None): + def setup_pc_trace( + self, file_name, trace_start_addr, trace_end_addr, skip_addrs=None + ): + """Configures GDB step-based instruction tracing.""" self.n_brkp = 1 self.send_command(f"set logging file {file_name}") self.send_command("set logging overwrite on") self.send_command("set pagination off") - self.send_command("set logging on") + try: + self.send_command("set logging enabled on") + except Exception: + self.send_command("set logging on") step_logic = "stepi" if skip_addrs: for addr in skip_addrs: + if not addr: + continue step_logic = f""" if $pc == {addr} tbreak *$ra @@ -176,24 +402,27 @@ def setup_pc_trace(self, file_name, trace_start_addr, trace_end_addr, skip_addrs traceloop_definition = f"""\ define traceloop - while 1 - if $pc=={trace_end_addr} - printf "PC trace complete.\\n" - return - end + while $pc != {trace_end_addr} printf "PC: 0x%x\\n", $pc {step_logic} end + printf "PC trace complete.\\n" end """ self.send_command(traceloop_definition) - self.send_command(f"tb *({trace_start_addr})") - commands_definition = "commands 1\ntraceloop\nend" + bp_resp = self.send_command(f"tb *({trace_start_addr})") + m = re.search( + r"(?:Temporary breakpoint|Breakpoint|Hardware assisted breakpoint)\s+(\d+)", + bp_resp or "", + ) + brk_num = int(m.group(1)) if m else self.n_brkp + commands_definition = f"commands {brk_num}\ntraceloop\nend" self.send_command(commands_definition) - self.n_brkp += 1 + self.n_brkp = brk_num + 1 def parse_pc_trace_file(self, file_path): + """Parses program counters recorded during trace.""" pc_list = [] pc_pattern = re.compile(r"PC: (0x[0-9a-fA-F]+)") @@ -210,27 +439,155 @@ def parse_pc_trace_file(self, file_path): return pc_list - def apply_instruction_skip(self, pc_address, next_pc_address, count): - skip_commands = f"commands {self.n_brkp}\n" + def interrupt(self, timeout=2.0): + """Interrupts running target.""" + if self.use_ocd_direct: + self._ocd_cmd("catch {halt}") + time.sleep(0.01) + return + + if not self.gdb_process or self.gdb_process.poll() is not None: + return + self.gdb_process.send_signal(signal.SIGINT) + start_t = time.time() + buf = "" + while time.time() - start_t < timeout: + chunk = self.read_output(timeout=0.05) + if chunk: + buf += chunk + if buf.strip().endswith("(gdb)"): + break + self.dump_output(timeout=0.02) + + def apply_instruction_skip(self, pc_address, next_pc_address, count=1): + """Arms a single instruction skip at pc_address redirecting to next_pc_address.""" + if self.use_ocd_direct: + pc_val = ( + int(pc_address, 16) if isinstance(pc_address, str) else int(pc_address) + ) + next_pc_val = ( + int(next_pc_address, 16) + if isinstance(next_pc_address, str) + else int(next_pc_address) + ) + self.last_bp_pc = pc_address + self._skip_hit = False + + # Ensure target is halted to configure hardware trigger hook + self._ocd_cmd("catch {halt}") + time.sleep(0.01) + self._ocd_cmd("catch {rbp all}") + self._ocd_cmd("riscv set_mem_access progbuf sysbus") + + tcl_script = f""" + set hit_count 0 + riscv.tap.0 configure -event halted {{ + set pc_line [reg pc] + if {{[string match "*0x{pc_val:x}*" $pc_line]}} {{ + reg pc 0x{next_pc_val:x} + catch {{rbp 0x{pc_val:x}}} + set hit_count 1 + catch {{resume}} + }} + }} + """ + self._ocd_cmd(tcl_script) + self._ocd_cmd(f"bp 0x{pc_val:x} 2 hw") + for obs_addr in self._observations.keys(): + obs_val = ( + int(obs_addr, 16) if isinstance(obs_addr, str) else int(obs_addr) + ) + self._ocd_cmd(f"catch {{bp 0x{obs_val:x} 2 hw}}") + self._bp_pc_val = pc_val + self.last_bp_num = self.n_brkp + self.n_brkp += 1 + return + + bp_resp = self.send_command(f"hbreak *({pc_address})") + m = re.search( + r"(?:Temporary breakpoint|Breakpoint|Hardware assisted breakpoint)\s+(\d+)", + bp_resp or "", + ) + brk_num = int(m.group(1)) if m else self.n_brkp + + skip_commands = f"commands {brk_num}\n" + skip_commands += f"delete {brk_num}\n" skip_commands += f"set $pc={next_pc_address}\n" skip_commands += 'printf "instruction skip applied\\n"\n' skip_commands += "c\n" skip_commands += "end" - self.send_command(f"tb *({pc_address})") if count > 1: ignore_amount = count - 1 - self.send_command(f"ignore {self.n_brkp} {ignore_amount}") + self.send_command(f"ignore {brk_num} {ignore_amount}") self.send_command(skip_commands) - self.n_brkp += 1 + self.last_bp_num = brk_num + self.last_bp_pc = pc_address + self.n_brkp = brk_num + 1 def add_observation(self, observations): + """Registers observation breakpoints (e.g. exception handler detection).""" + self._observations.update(observations) + if self.use_ocd_direct: + for addr in observations.keys(): + obs_val = int(addr, 16) if isinstance(addr, str) else int(addr) + self._ocd_cmd(f"catch {{bp 0x{obs_val:x} 2 hw}}") + return + for addr, log_message in observations.items(): - obs_command = f"commands {self.n_brkp}\n" + bp_resp = self.send_command(f"thbreak *({addr})") + m = re.search( + r"(?:Temporary breakpoint|Breakpoint|Hardware assisted breakpoint)\s+(\d+)", + bp_resp or "", + ) + brk_num = int(m.group(1)) if m else self.n_brkp + + obs_command = f"commands {brk_num}\n" obs_command += f'printf "fisim_result: {log_message} \\n"\n' obs_command += "c\n" obs_command += "end" - self.send_command(f"tb *({addr})") self.send_command(obs_command) - self.n_brkp += 1 + self.n_brkp = brk_num + 1 + + def cleanup_skip(self): + """Cleans up armed skip breakpoint and event hook without closing connection.""" + if self.use_ocd_direct: + try: + self._ocd_cmd("catch {rbp all}") + self._ocd_cmd("riscv.tap.0 configure -event halted {}") + except Exception: + pass + self._skip_hit = False + self._bp_pc_val = None + self._observations = {} + else: + try: + self.interrupt(timeout=1.0) + self.send_command("delete breakpoints", timeout=1.0) + except Exception: + pass + self._observations = {} + + def is_skip_hit(self) -> bool: + """Returns True if the armed instruction skip was executed.""" + if self.use_ocd_direct: + if not self._skip_hit and self._bp_pc_val is not None: + try: + hit = self._ocd_cmd("set hit_count").strip() + if hit and int(hit) > 0: + self._skip_hit = True + except Exception: + pass + return self._skip_hit + else: + return "instruction skip applied" in self.read_output() + + def wait_for_skip_applied(self, timeout=0.1) -> bool: + """Waits up to timeout seconds for skip execution.""" + start_t = time.time() + while time.time() - start_t < timeout: + if self.is_skip_hit(): + return True + time.sleep(0.005) + return False diff --git a/sw/host/penetrationtests/python/util/utils.py b/sw/host/penetrationtests/python/util/utils.py index 35def62ae126b..1f783127273ed 100644 --- a/sw/host/penetrationtests/python/util/utils.py +++ b/sw/host/penetrationtests/python/util/utils.py @@ -2,7 +2,9 @@ # Licensed under the Apache License, Version 2.0, see LICENSE for details. # SPDX-License-Identifier: Apache-2.0 +import argparse import struct +import sys def compare_json_data( @@ -105,3 +107,132 @@ def is_partial_collision(out1, out2, match_threshold_ratio=0.75, valid_len=None) match_ratio = matching_bytes / len(out1) return match_ratio >= match_threshold_ratio + + +def add_test_selection_args(parser: argparse.ArgumentParser): + """Adds test selection and listing arguments to the argument parser.""" + parser.add_argument( + "--test", + "--test_name", + "--test-name", + type=str, + default=None, + help="Name of the specific test method to run (e.g. test_p384_verify or p384_verify)", + ) + parser.add_argument( + "--list-tests", + "--list", + dest="list_tests", + action="store_true", + default=False, + help="List all available tests in this suite and exit", + ) + + +def resolve_test_name(requested_name: str, available_tests: list): + """Resolves a user-provided test name against a list of available test methods. + + Supports: + - Exact test method name (e.g. 'test_p384_verify') + - Short name without 'test_' prefix (e.g. 'p384_verify') + - Qualified name (e.g. 'AsymCryptolibFiSim.test_p384_verify') + - Case-insensitive matching + - Unique substring matching + + Returns the resolved test method name, or None if no match or ambiguous. + """ + if not requested_name: + return None + req = requested_name.strip() + if "." in req: + req = req.split(".")[-1] + if req in available_tests: + return req + if f"test_{req}" in available_tests: + return f"test_{req}" + + # Case-insensitive exact or short match + exact_ci = [ + t + for t in available_tests + if t.lower() == req.lower() or + (t.startswith("test_") and t[5:].lower() == req.lower()) + ] + if len(exact_ci) == 1: + return exact_ci[0] + + # Substring match + sub = [t for t in available_tests if req.lower() in t.lower()] + if len(sub) == 1: + return sub[0] + + return None + + +def get_selected_test_argv( + test_class, + requested_name=None, + config_args=None, + list_tests=False, +): + """Resolves unittest argv for running all tests or a single test. + + Args: + test_class: The unittest.TestCase subclass. + requested_name: Optional test name passed via --test flag. + config_args: Optional list of remaining unparsed CLI args. If a positional + arg in config_args matches a test, it is consumed and removed. + list_tests: Whether --list-tests was requested. + + Returns: + List[str] suitable to pass as argv to unittest.main(argv=...). + """ + # Collect available test methods preserving class definition order + available_tests = [ + m + for m in test_class.__dict__.keys() + if m.startswith("test_") and callable(getattr(test_class, m)) + ] + for m in dir(test_class): + if ( + m.startswith("test_") and + callable(getattr(test_class, m)) and + m not in available_tests + ): + available_tests.append(m) + + if list_tests: + print(f"Available tests in {test_class.__name__}:") + for t in available_tests: + short_name = t[5:] if t.startswith("test_") else t + print(f" - {t} (or {short_name})") + sys.exit(0) + + # Check if a positional argument in config_args specifies a test + if not requested_name and config_args is not None: + for arg in list(config_args): + if arg.startswith("-"): + continue + resolved = resolve_test_name(arg, available_tests) + if resolved: + requested_name = resolved + config_args.remove(arg) + break + + if requested_name: + matched = resolve_test_name(requested_name, available_tests) + if not matched: + print(f"Error: Unknown or ambiguous test '{requested_name}'.") + print(f"Available tests in {test_class.__name__}:") + for t in available_tests: + short_name = t[5:] if t.startswith("test_") else t + print(f" - {t} (or {short_name})") + sys.exit(1) + + print( + f"[TEST SELECTION] Running single test: {test_class.__name__}.{matched}\n", + flush=True, + ) + return [sys.argv[0], f"{test_class.__name__}.{matched}"] + + return [sys.argv[0]]