blob: 4b4c53b286bc3d7466ceda6fd48add93a1e9c79e [file]
# Licensed under the Apache-2.0 license
# SPDX-License-Identifier: Apache-2.0
"""Utility to invoke the caliptra emulator and pipe the output through the detokenizer."""
import argparse
import logging
import os
import subprocess
import sys
import tempfile
import threading
import time
import pathlib
from pathlib import Path
from pw_tokenizer import detokenize
_LOG = logging.getLogger(__name__)
_LOG.setLevel(logging.INFO)
# Environment variable naming the SSH host (user@host or host alias) for the
# VCK190 FPGA board used by the "fpga" interface.
FPGA_HOST = "VCK190_FPGA_HOST"
def scan_output_for_result(lines):
"""Scan detokenized output lines for a PASS/FAIL sentinel.
Returns 0 on a line containing "PASS", 1 on a line containing "FAIL",
or None if no sentinel has appeared yet.
"""
for line in lines:
if "PASS" in line:
return 0
if "FAIL" in line:
return 1
return None
try:
import caliptra.emulator_cptra_rom # type: ignore
import caliptra.emulator_cptra_firmware # type: ignore
import caliptra.emulator_mcu_rom # type: ignore
import caliptra.emulator_exe # type: ignore
from python.runfiles import runfiles # type: ignore
r = runfiles.Create()
_CPTRA_ROM = r.Rlocation(*caliptra.emulator_cptra_rom.RLOCATION)
_CPTRA_FIRMWARE = r.Rlocation(*caliptra.emulator_cptra_firmware.RLOCATION)
_MCU_ROM = r.Rlocation(*caliptra.emulator_mcu_rom.RLOCATION)
_EMULATOR = r.Rlocation(*caliptra.emulator_exe.RLOCATION)
except ImportError as e:
_LOG.fatal("runfiles could not open resources: %r", e)
def _parse_args():
"""Parse and return command line arguments."""
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--interface",
type=str,
help="interface type",
)
parser.add_argument(
"--elf",
type=pathlib.Path,
help="elf file ",
)
parser.add_argument(
"--bin",
type=pathlib.Path,
help="bin file",
)
parser.add_argument(
"--manifest",
type=pathlib.Path,
help="authorization manifest",
)
parser.add_argument(
"--vendor-pk-hash",
type=str,
help="SHA384 of public keys",
)
return parser.parse_args()
def _detokenizer(image: Path, tokenized_file: Path, finished: threading.Event):
try:
detokenizer = detokenize.Detokenizer(image)
line_buffer = ""
with open(tokenized_file, "r", buffering=1) as f:
while not finished.is_set():
try:
chunk = f.readline()
if chunk:
# qemu may not write a complete line, so buffer
# the chunks until there is a complete line to
# pass to the detokenizer.
line_buffer += chunk
# Use a while loop, as there could also potentially
# be multiple lines printed in-between iterations.
while "\n" in line_buffer:
newline_pos = line_buffer.find("\n") + 1
complete_line = line_buffer[:newline_pos]
if not complete_line.endswith("\r\n"):
complete_line = complete_line.replace("\n", "\r\n")
detokenizer.detokenize_text_to_file(
complete_line, sys.stdout.buffer
)
sys.stdout.flush()
line_buffer = line_buffer[newline_pos:]
except BlockingIOError:
# If writing to stdout too fast, it's sometimes possible
# to get BlockingIOError due to the stdout buffer being
# full, so sleep and try again.
time.sleep(0.1)
# detokenize any remaining data in the buffer.
if line_buffer:
detokenizer.detokenize_text_to_file(complete_line, sys.stdout.buffer)
sys.stdout.flush()
except OSError as e:
print(f"Exception opening file {e}", file=sys.stderr)
def load_and_run(
image: Path,
interface: str,
manifest: str,
vendor_pk_hash: str,
elf: Path | None = None,
) -> list[str]:
"""Prepare arguments to load an image into a board and spawn a console."""
if interface == "emulator":
cmd = [
_EMULATOR,
f"--rom={_MCU_ROM}",
f"--firmware={image}",
f"--caliptra-rom={_CPTRA_ROM}",
f"--caliptra-firmware={_CPTRA_FIRMWARE}",
"--i3c-port=65534",
"--rom-offset=0x80000000",
"--rom-size=0x8000",
"--dccm-offset=0x50000000",
"--dccm-size=0x4000",
"--sram-offset=0x40000000",
"--sram-size=0x80000",
"--pic-offset=0x60000000",
"--i3c-offset=0x20004000",
"--i3c-size=0x1000",
"--mci-offset=0x21000000",
"--mci-size=0xe00000",
"--mbox-offset=0x30020000",
"--mbox-size=0x28",
"--soc-offset=0x30030000",
"--soc-size=0x5e0",
"--otp-offset=0x70000000",
"--otp-size=0x140",
"--lc-offset=0x70000400",
"--lc-size=0x8c",
]
if manifest and str(manifest) != "None":
cmd.append(f"--soc-manifest={manifest}")
if vendor_pk_hash and str(vendor_pk_hash) != "None":
cmd.append(f"--vendor-pk-hash={vendor_pk_hash}")
return cmd
elif interface == "fpga":
host = os.environ.get(FPGA_HOST)
if not host:
_LOG.fatal(
"%s is not set; cannot reach the VCK190 board", FPGA_HOST
)
sys.exit(1)
remote_bin = f"/tmp/{Path(image).name}"
subprocess.run(["scp", str(image), f"{host}:{remote_bin}"], check=True)
# Loads the image into the MCU ROM backdoor SRAM, deasserts
# cptra_ss_rst_b, and streams the debug FIFO back over stdout.
# See hw/fpga/README.md's "JTAG debug" section and
# hw/fpga/kernel-modules/mcu_rom_backdoor.c for the mechanism.
cmd = [
"ssh",
host,
"sudo",
"caliptra-mcu-sw/hw/fpga/launch_openocd.sh",
"load-and-run",
remote_bin,
]
_LOG.info("Invoking fpga runner: %s", cmd)
proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
# Reuse the same Detokenizer mechanism the emulator's tokenized
# console path uses (see _detokenizer() above), rather than
# introducing a second detokenization code path.
detokenizer = detokenize.Detokenizer(elf)
text = detokenizer.detokenize_text(proc.stdout)
result = scan_output_for_result(text.splitlines())
if result is None:
_LOG.fatal("Device produced no PASS/FAIL sentinel")
sys.exit(1)
sys.exit(result)
else:
raise Exception("unknown mechanism", mechanism)
def simple_console(cmd: list[str]):
"""Invoke for a simple (non-tokenized) console."""
_LOG.info("Invoking mcu emulator: %s", cmd)
return subprocess.run(cmd, check=False).returncode
def tokenized_console(cmd: list[str]):
"""Invoke for a tokenized console."""
_LOG.info("Invoking mcu emulator: %s", cmd)
with tempfile.NamedTemporaryFile() as f:
with subprocess.Popen(
args=cmd,
stdout=f,
) as proc:
# Capturing the sub process stdout or stderr and then writing to
# stdout can cause deadlocks (see
# https://docs.python.org/3/library/subprocess.html#subprocess.Popen.stderr)
# due to a write buffer (child process) filling up the pipe
# buffer before the parent process can consume it.
# To work around this, write to a temp file, and have the
# detokenizer poll and detokenize the temp file.
finished_event = threading.Event()
stdout_thread = threading.Thread(
target=_detokenizer,
args=(Path(args.elf), Path(f.name), finished_event),
daemon=True,
)
stdout_thread.start()
out, err = proc.communicate()
finished_event.set()
if out:
print(out)
if err:
print(err)
return_code = proc.returncode
stdout_thread.join()
return return_code
def _main(args) -> int:
cmd = load_and_run(
args.bin,
args.interface,
args.manifest,
args.vendor_pk_hash,
elf=args.elf,
)
# TODO(cfrantz): add support for the tokenized console.
return_code = simple_console(cmd)
sys.exit(return_code)
if __name__ == "__main__":
logging.basicConfig()
_main(_parse_args())