blob: fc52546358a0de1e84a4cb357bf8425e0cff8076 [file] [log] [blame]
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
#include "iss_wrapper.h"
#include <cassert>
#include <cstring>
#include <fcntl.h>
#include <ftw.h>
#include <iomanip>
#include <iostream>
#ifdef __MACH__
#include <libproc.h>
#endif
#include <memory>
#include <regex>
#include <signal.h>
#include <sstream>
#include <sys/stat.h>
#include <sys/wait.h>
#include "otbn_trace_checker.h"
// Guard class to safely delete C strings
namespace {
struct CStrDeleter {
void operator()(char *p) const { std::free(p); }
};
} // namespace
typedef std::unique_ptr<char, CStrDeleter> c_str_ptr;
// Guard class to create (and possibly delete) temporary directories.
struct TmpDir {
std::string path;
TmpDir() : path(TmpDir::make_tmp_dir()) {}
~TmpDir() { cleanup(); }
private:
// A wrapper around mkdtemp that respects TMPDIR
static std::string make_tmp_dir() {
const char *tmpdir = getenv("TMPDIR");
if (!tmpdir)
tmpdir = "/tmp";
std::string tmp_template(tmpdir);
tmp_template += "/otbn_XXXXXX";
if (!mkdtemp(&tmp_template.at(0))) {
std::ostringstream oss;
oss << ("Cannot create temporary directory for OTBN simulation "
"with template ")
<< tmp_template << ": " << strerror(errno);
throw std::runtime_error(oss.str());
}
// The backing string for tmp_template will have been populated by mkdtemp.
return tmp_template;
}
// Return true if the OTBN_MODEL_KEEP_TMP environment variable is set to 1.
static bool should_keep_tmp() {
const char *keep_str = getenv("OTBN_MODEL_KEEP_TMP");
if (!keep_str)
return false;
return (strcmp(keep_str, "1") == 0) ? true : false;
}
// Called by nftw when we're deleting the temporary directory
static int ftw_callback(const char *fpath, const struct stat *sb,
int typeflag, struct FTW *ftwbuf) {
// The libc remove() function calls unlink or rmdir as necessary. Ignore
// any failures: we'll check that we managed to delete the directory when
// nftw finishes.
remove(fpath);
// Tell nftw to keep going
return 0;
}
// Recursively delete the temporary directory
void cleanup() {
if (path.empty())
return;
if (TmpDir::should_keep_tmp()) {
std::cerr << "Keeping temporary directory at " << path
<< " because OTBN_MODEL_KEEP_TMP=1.\n";
return;
}
// We're not supposed to keep the directory. Try to delete it and its
// contents. Ignore any failures: we'll just check whether it's gone
// afterwards.
nftw(path.c_str(), TmpDir::ftw_callback, 4, FTW_DEPTH | FTW_PHYS);
// Is there still anything at path? If so, we failed. Print something to
// stderr to tell the user what's going on.
struct stat statbuf;
if (stat(path.c_str(), &statbuf) == 0) {
std::cerr << "ERROR: Failed to delete OTBN temporary directory at "
<< path << ".\n";
}
}
};
// Find the top of the OpenTitan repository
//
// If REPO_TOP is defined, use that. Otherwise, this will only work if we're
// running from a binary inside the git repository. This happens with the
// default paths (which put BUILD_BIN at $REPO_TOP/build-bin). If we can't find
// an enclosing repository, throw a std::runtime_error.
static std::string find_repo_top() {
const char *from_env = getenv("REPO_TOP");
if (from_env)
return std::string(from_env);
// No environment variable. Work from current executable path.
const char *real_self_path;
#ifndef __MACH__
real_self_path = "/proc/self/exe";
#else
char pathbuf[PROC_PIDPATHINFO_MAXSIZE];
if (proc_pidpath(getpid(), pathbuf, sizeof(pathbuf)) <= 0) {
std::ostringstream oss;
oss << "Cannot resolve path: " << strerror(errno);
throw std::runtime_error(oss.str());
}
real_self_path = pathbuf;
#endif
c_str_ptr self_path(realpath(real_self_path, NULL));
if (!self_path) {
std::ostringstream oss;
oss << "Cannot resolve executable path: " << strerror(errno);
throw std::runtime_error(oss.str());
}
// Take a copy of self_path as a std::string and modify it, walking backwards
// over '/' characters and appending .git each time. After the first
// iteration, last_pos is the position of the character before the final
// slash (where the path looks something like "/path/to/check/.git")
std::string path_buf(self_path.get());
struct stat git_dir_stat;
size_t last_pos = std::string::npos;
for (;;) {
size_t last_slash = path_buf.find_last_of('/', last_pos);
// self_path was absolute, so there should always be a '/' at position
// zero.
assert(last_slash != std::string::npos);
if (last_slash == 0) {
// We've got to the slash at the start of an absolute path (and "/.git"
// is probably not the path we want!). Give up.
std::ostringstream oss;
oss << "Cannot find a git top-level directory containing "
<< self_path.get()
<< (". To run the OTBN model outside of the repo, "
"set the $REPO_TOP environment variable.");
throw std::runtime_error(oss.str());
}
// Replace everything after last_slash with ".git". The first time around,
// this will turn "/path/to/elf-file" to "/path/to/.git". After that, it
// will turn "/path/to/check/.git" to "/path/to/.git". Note that last_slash
// is strictly less than the string length (because it's an element index),
// so last_slash + 1 won't fall off the end.
path_buf.replace(last_slash + 1, std::string::npos, ".git");
last_pos = last_slash - 1;
// Does path_buf name a directory? If so, we've found the enclosing git
// directory.
if (stat(path_buf.c_str(), &git_dir_stat) == 0 &&
S_ISDIR(git_dir_stat.st_mode)) {
break;
}
}
// If we get here, path_buf points at a .git directory, and also ends in
// ".git". Resize to trim off the trailing "/.git"
assert(path_buf.size() > 5);
path_buf.resize(path_buf.size() - 5);
return path_buf;
}
// Find the otbn Python model. On failure, throw a std::runtime_error with a
// description of what went wrong.
static std::string find_otbn_model() {
std::string path = find_repo_top() + "/hw/ip/otbn/dv/otbnsim/stepped.py";
c_str_ptr abs_path(realpath(path.c_str(), NULL));
if (!abs_path) {
std::ostringstream oss;
oss << "Cannot find otbnsim.py, at '" << path << "'.\n";
throw std::runtime_error(oss.str());
}
return std::string(abs_path.get());
}
// Read 8 hex characters from str as a uint32_t.
static uint32_t read_hex_32(const char *str) {
char buf[9];
memcpy(buf, str, 8);
buf[8] = '\0';
return strtoul(buf, nullptr, 16);
}
// Read through trace output (in the lines argument) to pick up any write to
// the named CSR register, updating *dest.
static void read_ext_reg(const std::string &reg_name,
const std::vector<std::string> &lines,
uint32_t *dest) {
assert(dest);
// We're interested in lines that show an update to the external register
// called reg_name. These look something like this:
//
// ! otbn.$REG_NAME: 0x00000000
std::regex re("! otbn\\." + reg_name + ": 0x([0-9a-f]{8})");
std::smatch match;
for (const auto &line : lines) {
if (std::regex_match(line, match, re)) {
// Ahah! We have a match. We have captured exactly 8 hex digits, so know
// that we can safely parse them to a uint32_t without risking a parse
// failure or overflow.
assert(match.size() == 2);
*dest = (uint32_t)strtoul(match[1].str().c_str(), nullptr, 16);
}
}
}
// A specialized version of read_ext_reg that updates a boolean flag
// (assuming that the ISS will always signal the register as having
// value 0 or 1). Prints a message to stderr and returns false on
// error.
static bool read_ext_flag(const std::string &reg_name,
const std::vector<std::string> &lines, bool *dest) {
assert(dest);
uint32_t dest32 = *dest ? 1 : 0;
read_ext_reg(reg_name, lines, &dest32);
if (dest32 > 1) {
std::cerr << "ERROR: Unexpected update to " << reg_name << " with value 0x"
<< std::hex << dest32 << std::dec
<< " when we expected a boolean flag.";
return false;
}
*dest = dest32 != 0;
return true;
}
void MirroredRegs::reset() {
status = 0x04;
insn_cnt = 0;
err_bits = 0;
stop_pc = 0;
rnd_req = false;
wipe_start = false;
}
ISSWrapper::ISSWrapper() : tmpdir(new TmpDir()) {
std::string model_path(find_otbn_model());
// We want two pipes: one for writing to the child process, and the other for
// reading from it. We set the O_CLOEXEC flag so that the child process will
// drop all the fds when it execs.
int fds[4];
for (int i = 0; i < 2; ++i) {
// We are using pipe and fcntl instead of pipe2 to support both MacOS and
// Linux
if (pipe(fds + 2 * i)) {
std::ostringstream oss;
oss << "Failed to open pipe " << i << " for ISS: " << strerror(errno);
throw std::runtime_error(oss.str());
}
fcntl(fds[2 * i], F_SETFD, FD_CLOEXEC);
fcntl(fds[2 * i + 1], F_SETFD, FD_CLOEXEC);
}
// fds[0] and fds[2] are the read ends of two pipes, with write ends at
// fds[1] and fds[3], respectively.
//
// We'll attach fds[0] to the child's stdin and fds[3] to the child's stdout.
// That means we write to fds[1] to send data to the child and read from
// fds[2] to get data back.
pid_t pid = fork();
if (pid == -1) {
// Something went wrong.
std::ostringstream oss;
oss << "Failed to fork to create ISS process: " << strerror(errno);
throw std::runtime_error(oss.str());
}
if (pid == 0) {
// We are the child process. Attach stdin/stdout. (No need to close the
// pipe fds: we'll close them as part of the exec.)
close(0);
if (dup2(fds[0], 0) == -1) {
std::cerr << "Failed to set stdin in ISS subprocess: " << strerror(errno)
<< "\n";
abort();
}
close(1);
if (dup2(fds[3], 1) == -1) {
std::cerr << "Failed to set stdout in ISS subprocess: " << strerror(errno)
<< "\n";
abort();
}
// Finally, exec the ISS
execl("/usr/bin/env", "/usr/bin/env", "python3", "-u", model_path.c_str(),
NULL);
}
// We are the parent process and pid is the PID of the child. Close the pipe
// ends that we don't need (because the child is using them)
close(fds[0]);
close(fds[3]);
child_pid = pid;
// Finally, construct FILE* streams for the fds (which will make life easier
// when we actually use them to communicate with the child process)
child_write_file = fdopen(fds[1], "w");
child_read_file = fdopen(fds[2], "r");
// The fdopen calls should have succeeded (because we know the fds are
// valid). Add an assertion to make sure nothing weird happens.
assert(child_write_file);
assert(child_read_file);
}
ISSWrapper::~ISSWrapper() {
// Stop the child process if it's still running. No need to be nice: we'll
// just send a SIGKILL. Also, no need to check whether it's running first: we
// can just fire off the signal and ignore whether it worked or not.
kill(child_pid, SIGKILL);
// Now wait for the child. This should be a very short wait.
waitpid(child_pid, NULL, 0);
// Close the child file handles.
fclose(child_write_file);
fclose(child_read_file);
}
void ISSWrapper::load_d(const std::string &path) {
std::ostringstream oss;
oss << "load_d " << path << "\n";
run_command(oss.str(), nullptr);
}
void ISSWrapper::load_i(const std::string &path) {
std::ostringstream oss;
oss << "load_i " << path << "\n";
run_command(oss.str(), nullptr);
}
void ISSWrapper::add_loop_warp(uint32_t addr, uint32_t from_cnt,
uint32_t to_cnt) {
std::ostringstream oss;
oss << "add_loop_warp 0x" << std::hex << addr << std::dec << " " << from_cnt
<< " " << to_cnt << "\n";
run_command(oss.str(), nullptr);
}
void ISSWrapper::clear_loop_warps() {
run_command("clear_loop_warps\n", nullptr);
}
void ISSWrapper::dump_d(const std::string &path) const {
std::ostringstream oss;
oss << "dump_d " << path << "\n";
run_command(oss.str(), nullptr);
}
void ISSWrapper::start_operation(command_t command) {
std::ostringstream cmd_stream;
cmd_stream << "start_operation ";
switch (command) {
case Execute:
cmd_stream << "Execute\n";
break;
case DmemWipe:
cmd_stream << "DmemWipe\n";
break;
case ImemWipe:
cmd_stream << "ImemWipe\n";
break;
default:
assert(0);
}
run_command(cmd_stream.str(), nullptr);
}
void ISSWrapper::otp_key_cdc_done() {
run_command("otp_key_cdc_done\n", nullptr);
}
void ISSWrapper::edn_rnd_cdc_done() {
run_command("edn_rnd_cdc_done\n", nullptr);
}
void ISSWrapper::edn_urnd_cdc_done() {
run_command("edn_urnd_cdc_done\n", nullptr);
}
void ISSWrapper::edn_flush() { run_command("edn_flush\n", nullptr); }
void ISSWrapper::edn_rnd_step(uint32_t edn_rnd_data, bool fips_err) {
std::ostringstream oss;
oss << "edn_rnd_step " << std::hex << "0x" << edn_rnd_data;
oss << " " << fips_err << "\n";
run_command(oss.str(), nullptr);
}
void ISSWrapper::edn_urnd_step(uint32_t edn_urnd_data) {
std::ostringstream oss;
oss << "edn_urnd_step " << std::hex << "0x" << edn_urnd_data << "\n";
run_command(oss.str(), nullptr);
}
void ISSWrapper::set_keymgr_value(const std::array<uint32_t, 12> &key0_arr,
const std::array<uint32_t, 12> &key1_arr,
bool valid) {
std::ostringstream oss;
oss << "set_keymgr_value 0x" << std::hex << std::setfill('0');
for (int i = 0; i < 12; ++i) {
oss << std::setw(8) << key0_arr[11 - i];
}
oss << " 0x";
for (int i = 0; i < 12; ++i) {
oss << std::setw(8) << key1_arr[11 - i];
}
oss << " " << valid << "\n";
run_command(oss.str(), nullptr);
}
int ISSWrapper::step(bool gen_trace) {
std::vector<std::string> lines;
run_command("step\n", &lines);
if (gen_trace && lines.size()) {
if (!OtbnTraceChecker::get().OnIssTrace(lines)) {
return -1;
}
}
// Try to read STATUS, which is written when execution ends. Execution has
// finished if status_ is either 0 (IDLE) or 0xff (LOCKED)
bool was_stopped = mirrored_.stopped();
read_ext_reg("STATUS", lines, &mirrored_.status);
bool is_stopped = mirrored_.stopped();
bool done = is_stopped && !was_stopped;
// Also try to read INSN_CNT, ERR_BITS and STOP_PC plus some associated
// flags. Some of these flags only get updated around the end of an operation
// but the precise timing is slightly fiddly, so it's easiest to just allow
// updates whenever they arrive.
read_ext_reg("INSN_CNT", lines, &mirrored_.insn_cnt);
read_ext_reg("ERR_BITS", lines, &mirrored_.err_bits);
read_ext_reg("STOP_PC", lines, &mirrored_.stop_pc);
if (!read_ext_flag("RND_REQ", lines, &mirrored_.rnd_req))
return -1;
if (!read_ext_flag("WIPE_START", lines, &mirrored_.wipe_start))
return -1;
return done ? 1 : 0;
}
void ISSWrapper::invalidate_imem() {
run_command("invalidate_imem\n", nullptr);
}
void ISSWrapper::invalidate_dmem() {
run_command("invalidate_dmem\n", nullptr);
}
void ISSWrapper::set_software_errs_fatal(bool new_val) {
std::ostringstream oss;
oss << "set_software_errs_fatal " << new_val << "\n";
run_command(oss.str(), nullptr);
}
void ISSWrapper::initial_secure_wipe() {
run_command("initial_secure_wipe\n", nullptr);
}
uint32_t ISSWrapper::step_crc(const std::array<uint8_t, 6> &item,
uint32_t state) const {
std::vector<std::string> lines;
std::ostringstream oss;
oss << std::hex << "step_crc 0x" << std::setfill('0');
for (size_t i = 0; i < item.size(); ++i) {
oss << std::setw(2) << (int)item[5 - i];
}
oss << " 0x" << std::setw(8) << state << "\n";
run_command(oss.str(), &lines);
read_ext_reg("LOAD_CHECKSUM", lines, &state);
return state;
}
void ISSWrapper::reset(bool gen_trace) {
if (gen_trace)
OtbnTraceChecker::get().Flush();
run_command("reset\n", nullptr);
// Reset all mirrored registers.
mirrored_.reset();
}
void ISSWrapper::send_err_escalation(uint32_t err_val, bool lock_immediately) {
std::ostringstream oss;
oss << "send_err_escalation " << std::hex << "0x" << err_val << " "
<< lock_immediately << "\n";
run_command(oss.str(), nullptr);
}
void ISSWrapper::send_rma_req() {
std::ostringstream oss;
oss << "send_rma_req\n";
run_command(oss.str(), nullptr);
}
void ISSWrapper::get_regs(std::array<uint32_t, 32> *gprs,
std::array<u256_t, 32> *wdrs) {
assert(gprs && wdrs);
std::vector<std::string> lines;
run_command("print_regs\n", &lines);
// A record of which registers we've seen (to check we see each
// register exactly once). GPR i sets bit i. WDR i sets bit 32 + i.
uint64_t seen_mask = 0;
// Lines look like
//
// x3 = 0x12345678
// w10 = 0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
std::regex re("\\s*([wx][0-9]{1,2})\\s*=\\s*0x([0-9a-f]+)");
std::smatch match;
for (const std::string &line : lines) {
if (line == "PRINT_REGS")
continue;
if (!std::regex_match(line, match, re)) {
std::ostringstream oss;
oss << "Invalid line in ISS print_register output (`" << line << "').";
throw std::runtime_error(oss.str());
}
assert(match.size() == 3);
std::string reg_name = match[1].str();
std::string str_value = match[2].str();
assert(reg_name.size() <= 3);
assert(reg_name[0] == 'w' || reg_name[0] == 'x');
bool is_wide = reg_name[0] == 'w';
int reg_idx = atoi(reg_name.c_str() + 1);
assert(reg_idx >= 0);
if (reg_idx >= 32) {
std::ostringstream oss;
oss << "Invalid register name in ISS output (`" << reg_name
<< "'). Line was `" << line << "'.";
throw std::runtime_error(oss.str());
}
unsigned idx_seen = reg_idx + (is_wide ? 32 : 0);
if ((seen_mask >> idx_seen) & 1) {
std::ostringstream oss;
oss << "Duplicate lines writing register " << reg_name << ".";
throw std::runtime_error(oss.str());
}
unsigned num_u32s = is_wide ? 8 : 1;
unsigned expected_value_len = 8 * num_u32s;
if (str_value.size() != expected_value_len) {
std::ostringstream oss;
oss << "Value for register " << reg_name << " has " << str_value.size()
<< " hex characters, but we expected " << expected_value_len << ".";
throw std::runtime_error(oss.str());
}
uint32_t *dst = is_wide ? &(*wdrs)[reg_idx].words[7] : &(*gprs)[reg_idx];
for (unsigned i = 0; i < num_u32s; ++i) {
*dst = read_hex_32(&str_value[8 * i]);
--dst;
}
seen_mask |= ((uint64_t)1 << idx_seen);
}
// Check that we've seen all the registers
if (~seen_mask) {
std::ostringstream oss;
oss << "Some registers were missing from print_register output. Mask: 0x"
<< std::hex << seen_mask << ".";
throw std::runtime_error(oss.str());
}
}
std::vector<uint32_t> ISSWrapper::get_call_stack() {
std::vector<std::string> lines;
run_command("print_call_stack\n", &lines);
std::regex re("\\s*0x([0-9a-f]+)");
std::smatch match;
std::vector<uint32_t> call_stack;
for (const std::string &line : lines) {
if (line == "PRINT_CALL_STACK")
continue;
if (!std::regex_match(line, match, re)) {
std::ostringstream oss;
oss << "Invalid line in ISS print_call_stack output (`" << line << "').";
throw std::runtime_error(oss.str());
}
assert(match.size() == 2);
std::string str_value = match[1];
if (str_value.size() != 8) {
std::ostringstream oss;
oss << "Value from call stack " << str_value << " has "
<< str_value.size() << " hex characters, but we expected 8.";
throw std::runtime_error(oss.str());
}
uint32_t call_stack_entry = read_hex_32(str_value.c_str());
call_stack.push_back(call_stack_entry);
}
return call_stack;
}
std::string ISSWrapper::make_tmp_path(const std::string &relative) const {
return tmpdir->path + "/" + relative;
}
bool ISSWrapper::read_child_response(std::vector<std::string> *dst) const {
char buf[256];
bool continuation = false;
for (;;) {
// fgets reads a line, or fills buf, whichever happens first. It always
// writes the terminating null, so setting the second last position to \0
// beforehand can detect whether we filled buf without needing a call to
// strlen: buf is full if and only if this gets written with something
// other than a null.
buf[sizeof buf - 2] = '\0';
if (!fgets(buf, sizeof buf, child_read_file)) {
// Failed to read from child, or EOF
return false;
}
// If buf is ".\n", and we're not continuing another line, we're done.
if (!continuation && (0 == strcmp(buf, ".\n"))) {
return true;
}
// Have we read an entire line? If not, fgets will have written something
// other than \0 or \n to the second last entry in buf.
char canary = buf[sizeof buf - 2];
bool next_continuation = !(canary == '\0' || canary == '\n');
// We have some informative response from the child. Take a copy if dst is
// not null, stripping any trailing newline.
if (dst) {
if (continuation) {
assert(dst->size());
dst->back() += buf;
} else {
dst->push_back(std::string(buf));
}
// If !next_continuation then we read an entire line. If we didn't get to
// EOF, the last character of dst->back() is a newline. Drop it.
if (!next_continuation && dst->back().back() == '\n') {
dst->back().pop_back();
}
}
// Set the continuation flag if we filled buf without a newline.
continuation = next_continuation;
}
}
void ISSWrapper::run_command(const std::string &cmd,
std::vector<std::string> *dst) const {
assert(cmd.size() > 0);
assert(cmd.back() == '\n');
fputs(cmd.c_str(), child_write_file);
fflush(child_write_file);
if (!read_child_response(dst)) {
std::ostringstream oss;
std::string cmd_line = cmd.substr(0, cmd.size() - 1);
oss << "Failed to run command '" << cmd_line << "': EOF from ISS.";
throw std::runtime_error(oss.str());
}
}