From 0f510a7458e0efe95534667bba122b4ab67b26c1 Mon Sep 17 00:00:00 2001 From: Edward O'Callaghan Date: Tue, 18 Feb 2020 14:38:08 +1100 Subject: util/flashrom_tester: Upstream E2E testing framework The following is a E2E tester for a specific chip/chipset combo. The tester itself is completely self-contained and allows the user to specify which tests they wish to preform. Supported tests include: - chip-name - read - write - erase - wp-locking Change-Id: Ic2905a76cad90b1546b9328d668bf8abbf8aed44 Signed-off-by: Edward O'Callaghan Reviewed-on: https://review.coreboot.org/c/flashrom/+/38951 Tested-by: build bot (Jenkins) Reviewed-by: David Hendricks --- util/flashrom_tester/src/cros_sysinfo.rs | 80 ++++ util/flashrom_tester/src/lib.rs | 46 +++ util/flashrom_tester/src/logger.rs | 172 +++++++++ util/flashrom_tester/src/main.rs | 143 +++++++ util/flashrom_tester/src/rand_util.rs | 81 ++++ util/flashrom_tester/src/tester.rs | 636 +++++++++++++++++++++++++++++++ util/flashrom_tester/src/tests.rs | 385 +++++++++++++++++++ util/flashrom_tester/src/types.rs | 53 +++ util/flashrom_tester/src/utils.rs | 298 +++++++++++++++ 9 files changed, 1894 insertions(+) create mode 100644 util/flashrom_tester/src/cros_sysinfo.rs create mode 100644 util/flashrom_tester/src/lib.rs create mode 100644 util/flashrom_tester/src/logger.rs create mode 100644 util/flashrom_tester/src/main.rs create mode 100644 util/flashrom_tester/src/rand_util.rs create mode 100644 util/flashrom_tester/src/tester.rs create mode 100644 util/flashrom_tester/src/tests.rs create mode 100644 util/flashrom_tester/src/types.rs create mode 100644 util/flashrom_tester/src/utils.rs (limited to 'util/flashrom_tester/src') diff --git a/util/flashrom_tester/src/cros_sysinfo.rs b/util/flashrom_tester/src/cros_sysinfo.rs new file mode 100644 index 00000000..ddb08026 --- /dev/null +++ b/util/flashrom_tester/src/cros_sysinfo.rs @@ -0,0 +1,80 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use std::ffi::OsStr; +use std::fmt::Debug; +use std::io::Result as IoResult; +use std::process::{Command, Stdio}; + +use super::utils; + +fn dmidecode_dispatch>(args: &[S]) -> IoResult { + let output = Command::new("/usr/sbin/dmidecode") + .args(args) + .stdin(Stdio::null()) + .output()?; + + if !output.status.success() { + return Err(utils::translate_command_error(&output)); + } + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) +} + +pub fn system_info() -> IoResult { + dmidecode_dispatch(&["-q", "-t1"]) +} + +pub fn bios_info() -> IoResult { + dmidecode_dispatch(&["-q", "-t0"]) +} + +pub fn eventlog_list() -> Result { + mosys_dispatch(&["eventlog", "list"]) +} + +fn mosys_dispatch + Debug>(args: &[S]) -> IoResult { + info!("mosys_dispatch() running: /usr/sbin/mosys {:?}", args); + + let output = Command::new("/usr/sbin/mosys") + .args(args) + .stdin(Stdio::null()) + .output()?; + if !output.status.success() { + return Err(utils::translate_command_error(&output)); + } + + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + Ok(stdout) +} diff --git a/util/flashrom_tester/src/lib.rs b/util/flashrom_tester/src/lib.rs new file mode 100644 index 00000000..d8f1cb6a --- /dev/null +++ b/util/flashrom_tester/src/lib.rs @@ -0,0 +1,46 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +#[macro_use] +extern crate log; + +#[macro_use] +pub mod types; + +pub mod cros_sysinfo; +pub mod rand_util; +pub mod tester; +pub mod tests; +pub mod utils; diff --git a/util/flashrom_tester/src/logger.rs b/util/flashrom_tester/src/logger.rs new file mode 100644 index 00000000..e1c00f5d --- /dev/null +++ b/util/flashrom_tester/src/logger.rs @@ -0,0 +1,172 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use flashrom_tester::types; +use std::io::Write; +use std::path::PathBuf; +use std::sync::Mutex; + +struct Logger { + level: log::LevelFilter, + target: LogTarget, +} + +enum LogTarget +where + W: Write, +{ + Terminal, + Write(Mutex), +} + +impl log::Log for Logger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= self.level + } + + fn log(&self, record: &log::Record) { + fn log_internal(mut w: W, record: &log::Record) -> std::io::Result<()> { + let now = chrono::Local::now(); + write!(w, "{}{} ", types::MAGENTA, now.format("%Y-%m-%dT%H:%M:%S"))?; + write!( + w, + "{}[ {} ]{} ", + types::YELLOW, + record.level(), + types::RESET + )?; + writeln!(w, "{}", record.args()) + } + + // Write errors deliberately ignored + let _ = match self.target { + LogTarget::Terminal => { + let stdout = std::io::stdout(); + let mut lock = stdout.lock(); + log_internal(&mut lock, record) + } + LogTarget::Write(ref mutex) => { + let mut lock = mutex.lock().unwrap(); + log_internal(&mut *lock, record) + } + }; + } + + fn flush(&self) { + // Flush errors deliberately ignored + let _ = match self.target { + LogTarget::Terminal => std::io::stdout().flush(), + LogTarget::Write(ref w) => w.lock().unwrap().flush(), + }; + } +} + +pub fn init(to_file: Option, debug: bool) { + let mut logger = Logger { + level: log::LevelFilter::Info, + target: LogTarget::Terminal, + }; + + if debug { + logger.level = log::LevelFilter::Debug; + } + if let Some(path) = to_file { + logger.target = LogTarget::Write(Mutex::new( + std::fs::File::create(path).expect("Unable to open log file for writing"), + )); + } + + log::set_max_level(logger.level); + log::set_boxed_logger(Box::new(logger)).unwrap(); +} + +#[cfg(test)] +mod tests { + use super::{LogTarget, Logger}; + use log::{Level, LevelFilter, Log, Record}; + use std::sync::Mutex; + + fn run_records(records: &[Record]) -> String { + let mut buf = Vec::::new(); + { + let lock = Mutex::new(&mut buf); + let logger = Logger { + level: LevelFilter::Info, + target: LogTarget::Write(lock), + }; + + for record in records { + if logger.enabled(record.metadata()) { + logger.log(&record); + } + } + } + String::from_utf8(buf).unwrap() + } + + /// Log messages have the expected format + #[test] + fn format() { + let buf = run_records(&[Record::builder() + .args(format_args!("Test message at INFO")) + .level(Level::Info) + .build()]); + + assert_eq!(&buf[..5], "\x1b[35m"); + // Time is difficult to test, assume it's formatted okay + assert_eq!( + &buf[24..], + " \x1b[33m[ INFO ]\x1b[0m Test message at INFO\n" + ); + } + + #[test] + fn level_filter() { + let buf = run_records(&[ + Record::builder() + .args(format_args!("Test message at DEBUG")) + .level(Level::Debug) + .build(), + Record::builder() + .args(format_args!("Hello, world!")) + .level(Level::Error) + .build(), + ]); + + // There is one line because the Debug record wasn't written. + println!("{}", buf); + assert_eq!(buf.lines().count(), 1); + } +} diff --git a/util/flashrom_tester/src/main.rs b/util/flashrom_tester/src/main.rs new file mode 100644 index 00000000..1cc525e6 --- /dev/null +++ b/util/flashrom_tester/src/main.rs @@ -0,0 +1,143 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +#[macro_use] +extern crate log; + +mod logger; + +use clap::{App, Arg}; +use flashrom::FlashChip; +use flashrom_tester::{tester, tests}; +use std::path::PathBuf; + +pub mod built_info { + include!(concat!(env!("OUT_DIR"), "/built.rs")); +} + +fn main() { + let matches = App::new("flashrom_tester") + .long_version(&*format!( + "{}-{}\n\ + Target: {}\n\ + Profile: {}\n\ + Features: {:?}\n\ + Build time: {}\n\ + Compiler: {}", + built_info::PKG_VERSION, + option_env!("VCSID").unwrap_or(""), + built_info::TARGET, + built_info::PROFILE, + built_info::FEATURES, + built_info::BUILT_TIME_UTC, + built_info::RUSTC_VERSION, + )) + .arg(Arg::with_name("flashrom_binary").required(true)) + .arg( + Arg::with_name("ccd_target_type") + .required(true) + .possible_values(&["host", "ec", "servo"]), + ) + .arg( + Arg::with_name("print-layout") + .short("l") + .long("print-layout") + .help("Print the layout file's contents before running tests"), + ) + .arg( + Arg::with_name("log-file") + .short("o") + .long("log-file") + .takes_value(true) + .help("Write logs to a file rather than stdout"), + ) + .arg( + Arg::with_name("log_debug") + .short("d") + .long("debug") + .help("Write detailed logs, for debugging"), + ) + .arg( + Arg::with_name("output-format") + .short("f") + .long("output-format") + .help("Set the test report format") + .takes_value(true) + .case_insensitive(true) + .possible_values(&["pretty", "json"]) + .default_value("pretty"), + ) + .arg( + Arg::with_name("test_name") + .multiple(true) + .help("Names of individual tests to run (run all if unspecified)"), + ) + .get_matches(); + + logger::init( + matches.value_of_os("log-file").map(PathBuf::from), + matches.is_present("log_debug"), + ); + debug!("Args parsed and logging initialized OK"); + + let flashrom_path = matches + .value_of("flashrom_binary") + .expect("flashrom_binary should be required"); + let ccd_type = FlashChip::from( + matches + .value_of("ccd_target_type") + .expect("ccd_target_type should be required"), + ) + .expect("ccd_target_type should admit only known types"); + + let print_layout = matches.is_present("print-layout"); + let output_format = matches + .value_of("output-format") + .expect("output-format should have a default value") + .parse::() + .expect("output-format is not a parseable OutputFormat"); + let test_names = matches.values_of("test_name"); + + if let Err(e) = tests::generic( + flashrom_path, + ccd_type, + print_layout, + output_format, + test_names, + ) { + eprintln!("Failed to run tests: {:?}", e); + std::process::exit(1); + } +} diff --git a/util/flashrom_tester/src/rand_util.rs b/util/flashrom_tester/src/rand_util.rs new file mode 100644 index 00000000..51411d0d --- /dev/null +++ b/util/flashrom_tester/src/rand_util.rs @@ -0,0 +1,81 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use std::fs::File; +use std::io::prelude::*; +use std::io::BufWriter; + +use rand::prelude::*; + +pub fn gen_rand_testdata(path: &str, size: usize) -> std::io::Result<()> { + let mut buf = BufWriter::new(File::create(path)?); + + let mut a: Vec = Vec::with_capacity(size); + // Pad out array to be filled in by Rng::fill(). + a.resize(size, 0b0); + thread_rng().fill(a.as_mut_slice()); + + buf.write_all(a.as_slice())?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gen_rand_testdata() { + use super::gen_rand_testdata; + + let path0 = "/tmp/idk_test00"; + let path1 = "/tmp/idk_test01"; + let sz = 1024; + + gen_rand_testdata(path0, sz).unwrap(); + gen_rand_testdata(path1, sz).unwrap(); + + let mut buf0 = Vec::new(); + let mut buf1 = Vec::new(); + + let mut f = File::open(path0).unwrap(); + let mut g = File::open(path1).unwrap(); + + f.read_to_end(&mut buf0).unwrap(); + g.read_to_end(&mut buf1).unwrap(); + + assert_ne!(buf0, buf1); + } +} diff --git a/util/flashrom_tester/src/tester.rs b/util/flashrom_tester/src/tester.rs new file mode 100644 index 00000000..fbef2016 --- /dev/null +++ b/util/flashrom_tester/src/tester.rs @@ -0,0 +1,636 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use super::rand_util; +use super::types; +use super::utils::{self, LayoutSizes}; +use flashrom::{FlashChip, Flashrom, FlashromCmd}; +use serde_json::json; +use std::mem::MaybeUninit; +use std::sync::Mutex; + +// type-signature comes from the return type of lib.rs workers. +type TestError = Box; +pub type TestResult = Result<(), TestError>; + +pub struct TestEnv<'a> { + chip_type: FlashChip, + /// Flashrom instantiation information. + /// + /// Where possible, prefer to use methods on the TestEnv rather than delegating + /// to the raw flashrom functions. + pub cmd: &'a FlashromCmd, + layout: LayoutSizes, + + pub wp: WriteProtectState<'a, 'static>, + /// The path to a file containing the flash contents at test start. + // TODO(pmarheine) migrate this to a PathBuf for clarity + original_flash_contents: String, + /// The path to a file containing flash-sized random data + // TODO(pmarheine) make this a PathBuf too + random_data: String, +} + +impl<'a> TestEnv<'a> { + pub fn create(chip_type: FlashChip, cmd: &'a FlashromCmd) -> Result { + let rom_sz = cmd.get_size()?; + let out = TestEnv { + chip_type: chip_type, + cmd: cmd, + layout: utils::get_layout_sizes(rom_sz)?, + wp: WriteProtectState::from_hardware(cmd)?, + original_flash_contents: "/tmp/flashrom_tester_golden.bin".into(), + random_data: "/tmp/random_content.bin".into(), + }; + + info!("Stashing golden image for verification/recovery on completion"); + flashrom::read(&out.cmd, &out.original_flash_contents)?; + flashrom::verify(&out.cmd, &out.original_flash_contents)?; + + info!("Generating random flash-sized data"); + rand_util::gen_rand_testdata(&out.random_data, rom_sz as usize) + .map_err(|io_err| format!("I/O error writing random data file: {:#}", io_err))?; + + Ok(out) + } + + pub fn run_test(&mut self, test: T) -> TestResult { + let use_dut_control = self.chip_type == FlashChip::SERVO; + if use_dut_control && flashrom::dut_ctrl_toggle_wp(false).is_err() { + error!("failed to dispatch dut_ctrl_toggle_wp()!"); + } + + let name = test.get_name(); + info!("Beginning test: {}", name); + let out = test.run(self); + info!("Completed test: {}; result {:?}", name, out); + + if use_dut_control && flashrom::dut_ctrl_toggle_wp(true).is_err() { + error!("failed to dispatch dut_ctrl_toggle_wp()!"); + } + out + } + + pub fn chip_type(&self) -> FlashChip { + // This field is not public because it should be immutable to tests, + // so this getter enforces that it is copied. + self.chip_type + } + + /// Return the path to a file that contains random data and is the same size + /// as the flash chip. + pub fn random_data_file(&self) -> &str { + &self.random_data + } + + pub fn layout(&self) -> &LayoutSizes { + &self.layout + } + + /// Return true if the current Flash contents are the same as the golden image + /// that was present at the start of testing. + pub fn is_golden(&self) -> bool { + flashrom::verify(&self.cmd, &self.original_flash_contents).is_ok() + } + + /// Do whatever is necessary to make the current Flash contents the same as they + /// were at the start of testing. + pub fn ensure_golden(&mut self) -> Result<(), String> { + self.wp.set_hw(false)?.set_sw(false)?; + flashrom::write(&self.cmd, &self.original_flash_contents) + } + + /// Attempt to erase the flash. + pub fn erase(&self) -> Result<(), String> { + flashrom::erase(self.cmd) + } + + /// Verify that the current Flash contents are the same as the file at the given + /// path. + /// + /// Returns Err if they are not the same. + pub fn verify(&self, contents_path: &str) -> Result<(), String> { + flashrom::verify(self.cmd, contents_path) + } +} + +impl Drop for TestEnv<'_> { + fn drop(&mut self) { + info!("Verifying flash remains unmodified"); + if !self.is_golden() { + warn!("ROM seems to be in a different state at finish; restoring original"); + if let Err(e) = self.ensure_golden() { + error!("Failed to write back golden image: {:?}", e); + } + } + } +} + +/// RAII handle for setting write protect in either hardware or software. +/// +/// Given an instance, the state of either write protect can be modified by calling +/// `set` or `push`. When it goes out of scope, the write protects will be returned +/// to the state they had then it was created. +/// +/// The lifetime `'p` on this struct is the parent state it derives from; `'static` +/// implies it is derived from hardware, while anything else is part of a stack +/// created by `push`ing states. An initial state is always static, and the stack +/// forms a lifetime chain `'static -> 'p -> 'p1 -> ... -> 'pn`. +pub struct WriteProtectState<'a, 'p> { + /// The parent state this derives from. + /// + /// If it's a root (gotten via `from_hardware`), then this is Hardware and the + /// liveness flag will be reset on drop. + initial: InitialState<'p>, + // Tuples are (hardware, software) + current: (bool, bool), + cmd: &'a FlashromCmd, +} + +enum InitialState<'p> { + Hardware(bool, bool), + Previous(&'p WriteProtectState<'p, 'p>), +} + +impl InitialState<'_> { + fn get_target(&self) -> (bool, bool) { + match self { + InitialState::Hardware(hw, sw) => (*hw, *sw), + InitialState::Previous(s) => s.current, + } + } +} + +impl<'a> WriteProtectState<'a, 'static> { + /// Initialize a state from the current state of the hardware. + /// + /// Panics if there is already a live state derived from hardware. In such a situation the + /// new state must be derived from the live one, or the live one must be dropped first. + pub fn from_hardware(cmd: &'a FlashromCmd) -> Result { + let mut lock = Self::get_liveness_lock() + .lock() + .expect("Somebody panicked during WriteProtectState init from hardware"); + if *lock { + drop(lock); // Don't poison the lock + panic!("Attempted to create a new WriteProtectState when one is already live"); + } + + let hw = Self::get_hw(cmd)?; + let sw = Self::get_sw(cmd)?; + info!("Initial hardware write protect: HW={} SW={}", hw, sw); + + *lock = true; + Ok(WriteProtectState { + initial: InitialState::Hardware(hw, sw), + current: (hw, sw), + cmd, + }) + } + + /// Get the actual hardware write protect state. + fn get_hw(cmd: &FlashromCmd) -> Result { + if cmd.fc.can_control_hw_wp() { + super::utils::get_hardware_wp() + } else { + Ok(false) + } + } + + /// Get the actual software write protect state. + fn get_sw(cmd: &FlashromCmd) -> Result { + flashrom::wp_status(cmd, true) + } +} + +impl<'a, 'p> WriteProtectState<'a, 'p> { + /// Return true if the current programmer supports setting the hardware + /// write protect. + /// + /// If false, calls to set_hw() will do nothing. + pub fn can_control_hw_wp(&self) -> bool { + self.cmd.fc.can_control_hw_wp() + } + + /// Set the software write protect. + pub fn set_sw(&mut self, enable: bool) -> Result<&mut Self, String> { + info!("request={}, current={}", enable, self.current.1); + if self.current.1 != enable { + flashrom::wp_toggle(self.cmd, /* en= */ enable)?; + self.current.1 = enable; + } + Ok(self) + } + + /// Set the hardware write protect. + pub fn set_hw(&mut self, enable: bool) -> Result<&mut Self, String> { + if self.current.0 != enable { + if self.can_control_hw_wp() { + super::utils::toggle_hw_wp(/* dis= */ !enable)?; + self.current.0 = enable; + } else if enable { + info!( + "Ignoring attempt to enable hardware WP with {:?} programmer", + self.cmd.fc + ); + } + } + Ok(self) + } + + /// Stack a new write protect state on top of the current one. + /// + /// This is useful if you need to temporarily make a change to write protection: + /// + /// ```no_run + /// # fn main() -> Result<(), String> { + /// # let cmd: flashrom::FlashromCmd = unimplemented!(); + /// let wp = flashrom_tester::tester::WriteProtectState::from_hardware(&cmd)?; + /// { + /// let mut wp = wp.push(); + /// wp.set_sw(false)?; + /// // Do something with software write protect disabled + /// } + /// // Now software write protect returns to its original state, even if + /// // set_sw() failed. + /// # Ok(()) + /// # } + /// ``` + /// + /// This returns a new state which restores the original when it is dropped- the new state + /// refers to the old, so the compiler enforces that states are disposed of in the reverse + /// order of their creation and correctly restore the original state. + pub fn push<'p1>(&'p1 self) -> WriteProtectState<'a, 'p1> { + WriteProtectState { + initial: InitialState::Previous(self), + current: self.current, + cmd: self.cmd, + } + } + + fn get_liveness_lock() -> &'static Mutex { + static INIT: std::sync::Once = std::sync::Once::new(); + /// Value becomes true when there is a live WriteProtectState derived `from_hardware`, + /// blocking duplicate initialization. + /// + /// This is required because hardware access is not synchronized; it's possible to leave the + /// hardware in an unintended state by creating a state handle from it, modifying the state, + /// creating another handle from the hardware then dropping the first handle- then on drop + /// of the second handle it will restore the state to the modified one rather than the initial. + /// + /// This flag ensures that a duplicate root state cannot be created. + /// + /// This is a Mutex rather than AtomicBool because acquiring the flag needs to perform + /// several operations that may themselves fail- acquisitions must be fully synchronized. + static mut LIVE_FROM_HARDWARE: MaybeUninit> = MaybeUninit::uninit(); + + unsafe { + INIT.call_once(|| { + LIVE_FROM_HARDWARE.as_mut_ptr().write(Mutex::new(false)); + }); + &*LIVE_FROM_HARDWARE.as_ptr() + } + } + + /// Reset the hardware to what it was when this state was created, reporting errors. + /// + /// This behaves exactly like allowing a state to go out of scope, but it can return + /// errors from that process rather than panicking. + pub fn close(mut self) -> Result<(), String> { + unsafe { + let out = self.drop_internal(); + // We just ran drop, don't do it again + std::mem::forget(self); + out + } + } + + /// Internal Drop impl. + /// + /// This is unsafe because it effectively consumes self when clearing the + /// liveness lock. Callers must be able to guarantee that self will be forgotten + /// if the state was constructed from hardware in order to uphold the liveness + /// invariant (that only a single state constructed from hardware exists at any + /// time). + unsafe fn drop_internal(&mut self) -> Result<(), String> { + let lock = match self.initial { + InitialState::Hardware(_, _) => Some( + Self::get_liveness_lock() + .lock() + .expect("Somebody panicked during WriteProtectState drop from hardware"), + ), + _ => None, + }; + let (hw, sw) = self.initial.get_target(); + + fn enable_str(enable: bool) -> &'static str { + if enable { + "en" + } else { + "dis" + } + } + + // Toggle both protects back to their initial states. + // Software first because we can't change it once hardware is enabled. + if sw != self.current.1 { + // Is the hw wp currently enabled? + if self.current.0 { + super::utils::toggle_hw_wp(/* dis= */ true).map_err(|e| { + format!( + "Failed to {}able hardware write protect: {}", + enable_str(false), + e + ) + })?; + } + flashrom::wp_toggle(self.cmd, /* en= */ sw).map_err(|e| { + format!( + "Failed to {}able software write protect: {}", + enable_str(sw), + e + ) + })?; + } + + assert!( + self.cmd.fc.can_control_hw_wp() || (!self.current.0 && !hw), + "HW WP must be disabled if it cannot be controlled" + ); + if hw != self.current.0 { + super::utils::toggle_hw_wp(/* dis= */ !hw).map_err(|e| { + format!( + "Failed to {}able hardware write protect: {}", + enable_str(hw), + e + ) + })?; + } + + if let Some(mut lock) = lock { + // Initial state was constructed via from_hardware, now we can clear the liveness + // lock since reset is complete. + *lock = false; + } + Ok(()) + } +} + +impl<'a, 'p> Drop for WriteProtectState<'a, 'p> { + /// Sets both write protects to the state they had when this state was created. + /// + /// Panics on error because there is no mechanism to report errors in Drop. + fn drop(&mut self) { + unsafe { self.drop_internal() }.expect("Error while dropping WriteProtectState") + } +} + +pub trait TestCase { + fn get_name(&self) -> &str; + fn expected_result(&self) -> TestConclusion; + fn run(&self, env: &mut TestEnv) -> TestResult; +} + +impl, F: Fn(&mut TestEnv) -> TestResult> TestCase for (S, F) { + fn get_name(&self) -> &str { + self.0.as_ref() + } + + fn expected_result(&self) -> TestConclusion { + TestConclusion::Pass + } + + fn run(&self, env: &mut TestEnv) -> TestResult { + (self.1)(env) + } +} + +impl TestCase for &T { + fn get_name(&self) -> &str { + (*self).get_name() + } + + fn expected_result(&self) -> TestConclusion { + (*self).expected_result() + } + + fn run(&self, env: &mut TestEnv) -> TestResult { + (*self).run(env) + } +} + +#[allow(dead_code)] +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum TestConclusion { + Pass, + Fail, + UnexpectedPass, + UnexpectedFail, +} + +pub struct ReportMetaData { + pub chip_name: String, + pub os_release: String, + pub system_info: String, + pub bios_info: String, +} + +fn decode_test_result(res: TestResult, con: TestConclusion) -> (TestConclusion, Option) { + use TestConclusion::*; + + match (res, con) { + (Ok(_), Fail) => (UnexpectedPass, None), + (Err(e), Pass) => (UnexpectedFail, Some(e)), + _ => (Pass, None), + } +} + +pub fn run_all_tests( + chip: FlashChip, + cmd: &FlashromCmd, + ts: TS, +) -> Vec<(String, (TestConclusion, Option))> +where + T: TestCase + Copy, + TS: IntoIterator, +{ + let mut env = TestEnv::create(chip, cmd).expect("Failed to set up test environment"); + + let mut results = Vec::new(); + for t in ts { + let result = decode_test_result(env.run_test(t), t.expected_result()); + results.push((t.get_name().into(), result)); + } + results +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum OutputFormat { + Pretty, + Json, +} + +impl std::str::FromStr for OutputFormat { + type Err = (); + + fn from_str(s: &str) -> Result { + use OutputFormat::*; + + if s.eq_ignore_ascii_case("pretty") { + Ok(Pretty) + } else if s.eq_ignore_ascii_case("json") { + Ok(Json) + } else { + Err(()) + } + } +} + +pub fn collate_all_test_runs( + truns: &[(String, (TestConclusion, Option))], + meta_data: ReportMetaData, + format: OutputFormat, +) { + match format { + OutputFormat::Pretty => { + println!(); + println!(" ============================="); + println!(" ===== AVL qual RESULTS ===="); + println!(" ============================="); + println!(); + println!(" %---------------------------%"); + println!(" os release: {}", meta_data.os_release); + println!(" chip name: {}", meta_data.chip_name); + println!(" system info: \n{}", meta_data.system_info); + println!(" bios info: \n{}", meta_data.bios_info); + println!(" %---------------------------%"); + println!(); + + for trun in truns.iter() { + let (name, (result, error)) = trun; + if *result != TestConclusion::Pass { + println!( + " {} {}", + style!(format!(" <+> {} test:", name), types::BOLD), + style_dbg!(result, types::RED) + ); + match error { + None => {} + Some(e) => info!(" - {} failure details:\n{}", name, e.to_string()), + }; + } else { + println!( + " {} {}", + style!(format!(" <+> {} test:", name), types::BOLD), + style_dbg!(result, types::GREEN) + ); + } + } + println!(); + } + OutputFormat::Json => { + use serde_json::{Map, Value}; + + let mut all_pass = true; + let mut tests = Map::::new(); + for (name, (result, error)) in truns { + let passed = *result == TestConclusion::Pass; + all_pass &= passed; + + let error = match error { + Some(e) => Value::String(format!("{:#?}", e)), + None => Value::Null, + }; + + assert!( + !tests.contains_key(name), + "Found multiple tests named {:?}", + name + ); + tests.insert( + name.into(), + json!({ + "pass": passed, + "error": error, + }), + ); + } + + let json = json!({ + "pass": all_pass, + "metadata": { + "os_release": meta_data.os_release, + "chip_name": meta_data.chip_name, + "system_info": meta_data.system_info, + "bios_info": meta_data.bios_info, + }, + "tests": tests, + }); + println!("{:#}", json); + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn decode_test_result() { + use super::decode_test_result; + use super::TestConclusion::*; + + let (result, err) = decode_test_result(Ok(()), Pass); + assert_eq!(result, Pass); + assert!(err.is_none()); + + let (result, err) = decode_test_result(Ok(()), Fail); + assert_eq!(result, UnexpectedPass); + assert!(err.is_none()); + + let (result, err) = decode_test_result(Err("broken".into()), Pass); + assert_eq!(result, UnexpectedFail); + assert!(err.is_some()); + + let (result, err) = decode_test_result(Err("broken".into()), Fail); + assert_eq!(result, Pass); + assert!(err.is_none()); + } + + #[test] + fn output_format_round_trip() { + use super::OutputFormat::{self, *}; + + assert_eq!(format!("{:?}", Pretty).parse::(), Ok(Pretty)); + assert_eq!(format!("{:?}", Json).parse::(), Ok(Json)); + } +} diff --git a/util/flashrom_tester/src/tests.rs b/util/flashrom_tester/src/tests.rs new file mode 100644 index 00000000..dd756893 --- /dev/null +++ b/util/flashrom_tester/src/tests.rs @@ -0,0 +1,385 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use super::cros_sysinfo; +use super::tester::{self, OutputFormat, TestCase, TestEnv, TestResult}; +use super::utils::{self, LayoutNames}; +use flashrom::{FlashChip, Flashrom, FlashromCmd}; +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::io::{BufRead, Write}; + +const LAYOUT_FILE: &'static str = "/tmp/layout.file"; + +/// Iterate over tests, yielding only those tests with names matching filter_names. +/// +/// If filter_names is None, all tests will be run. None is distinct from Some(∅); +// Some(∅) runs no tests. +/// +/// Name comparisons are performed in lower-case: values in filter_names must be +/// converted to lowercase specifically. +/// +/// When an entry in filter_names matches a test, it is removed from that set. +/// This allows the caller to determine if any entries in the original set failed +/// to match any test, which may be user error. +fn filter_tests<'n, 't: 'n, T: TestCase>( + tests: &'t [T], + filter_names: &'n mut Option>, +) -> impl 'n + Iterator { + tests.iter().filter(move |test| match filter_names { + // Accept all tests if no names are given + None => true, + Some(ref mut filter_names) => { + // Pop a match to the test name from the filter set, retaining the test + // if there was a match. + filter_names.remove(&test.get_name().to_lowercase()) + } + }) +} + +/// Run tests. +/// +/// Only returns an Error if there was an internal error; test failures are Ok. +/// +/// test_names is the case-insensitive names of tests to run; if None, then all +/// tests are run. Provided names that don't match any known test will be logged +/// as a warning. +pub fn generic<'a, TN: Iterator>( + path: &str, + fc: FlashChip, + print_layout: bool, + output_format: OutputFormat, + test_names: Option, +) -> Result<(), Box> { + let p = path.to_string(); + let cmd = FlashromCmd { path: p, fc }; + + utils::ac_power_warning(); + + info!("Calculate ROM partition sizes & Create the layout file."); + let rom_sz: i64 = cmd.get_size()?; + let layout_sizes = utils::get_layout_sizes(rom_sz)?; + { + let mut f = File::create(LAYOUT_FILE)?; + let mut buf: Vec = vec![]; + utils::construct_layout_file(&mut buf, &layout_sizes)?; + + f.write_all(&buf)?; + if print_layout { + info!( + "Dumping layout file as requested:\n{}", + String::from_utf8_lossy(&buf) + ); + } + } + + info!( + "Record crossystem information.\n{}", + utils::collect_crosssystem()? + ); + + // Register tests to run: + let tests: &[&dyn TestCase] = &[ + &("Get_device_name", get_device_name_test), + &("Coreboot_ELOG_sanity", elog_sanity_test), + &("Host_is_ChromeOS", host_is_chrome_test), + &("Toggle_WP", wp_toggle_test), + &("Erase_and_Write", erase_write_test), + &("Fail_to_verify", verify_fail_test), + &("Lock", lock_test), + &("Lock_top_quad", partial_lock_test(LayoutNames::TopQuad)), + &( + "Lock_bottom_quad", + partial_lock_test(LayoutNames::BottomQuad), + ), + &( + "Lock_bottom_half", + partial_lock_test(LayoutNames::BottomHalf), + ), + &("Lock_top_half", partial_lock_test(LayoutNames::TopHalf)), + ]; + + // Limit the tests to only those requested, unless none are requested + // in which case all tests are included. + let mut filter_names: Option> = if let Some(names) = test_names { + Some(names.map(|s| s.to_lowercase()).collect()) + } else { + None + }; + let tests = filter_tests(tests, &mut filter_names); + + // ------------------------. + // Run all the tests and collate the findings: + let results = tester::run_all_tests(fc, &cmd, tests); + + // Any leftover filtered names were specified to be run but don't exist + for leftover in filter_names.iter().flatten() { + warn!("No test matches filter name \"{}\"", leftover); + } + + let chip_name = flashrom::name(&cmd) + .map(|x| format!("vendor=\"{}\" name=\"{}\"", x.0, x.1)) + .unwrap_or("".into()); + let os_rel = sys_info::os_release().unwrap_or("".to_string()); + let system_info = cros_sysinfo::system_info().unwrap_or("".to_string()); + let bios_info = cros_sysinfo::bios_info().unwrap_or("".to_string()); + + let meta_data = tester::ReportMetaData { + chip_name: chip_name, + os_release: os_rel, + system_info: system_info, + bios_info: bios_info, + }; + tester::collate_all_test_runs(&results, meta_data, output_format); + Ok(()) +} + +fn get_device_name_test(env: &mut TestEnv) -> TestResult { + // Success means we got something back, which is good enough. + flashrom::name(env.cmd)?; + Ok(()) +} + +fn wp_toggle_test(env: &mut TestEnv) -> TestResult { + // NOTE: This is not strictly a 'test' as it is allowed to fail on some platforms. + // However, we will warn when it does fail. + // List the write-protected regions of flash. + match flashrom::wp_list(env.cmd) { + Ok(list_str) => info!("\n{}", list_str), + Err(e) => warn!("{}", e), + }; + // Fails if unable to set either one + env.wp.set_hw(false)?; + env.wp.set_sw(false)?; + Ok(()) +} + +fn erase_write_test(env: &mut TestEnv) -> TestResult { + if !env.is_golden() { + info!("Memory has been modified; reflashing to ensure erasure can be detected"); + env.ensure_golden()?; + } + + // With write protect enabled erase should fail. + env.wp.set_sw(true)?.set_hw(true)?; + if env.erase().is_ok() { + info!("Flashrom returned Ok but this may be incorrect; verifying"); + if !env.is_golden() { + return Err("Hardware write protect asserted however can still erase!".into()); + } + info!("Erase claimed to succeed but verify is Ok; assume erase failed"); + } + + // With write protect disabled erase should succeed. + env.wp.set_hw(false)?.set_sw(false)?; + env.erase()?; + if env.is_golden() { + return Err("Successful erase didn't modify memory".into()); + } + + Ok(()) +} + +fn lock_test(env: &mut TestEnv) -> TestResult { + if !env.wp.can_control_hw_wp() { + return Err("Lock test requires ability to control hardware write protect".into()); + } + + env.wp.set_hw(false)?.set_sw(true)?; + // Toggling software WP off should work when hardware is off. + // Then enable again for another go. + env.wp.push().set_sw(false)?; + + env.wp.set_hw(true)?; + // Clearing should fail when hardware is enabled + if env.wp.set_sw(false).is_ok() { + return Err("Software WP was reset despite hardware WP being enabled".into()); + } + Ok(()) +} + +fn elog_sanity_test(env: &mut TestEnv) -> TestResult { + // Check that the elog contains *something*, as an indication that Coreboot + // is actually able to write to the Flash. Because this invokes mosys on the + // host, it doesn't make sense to run for other chips. + if env.chip_type() != FlashChip::HOST { + info!("Skipping ELOG sanity check for non-host chip"); + return Ok(()); + } + // mosys reads the flash, it should be back in the golden state + env.ensure_golden()?; + // Output is one event per line, drop empty lines in the interest of being defensive. + let event_count = cros_sysinfo::eventlog_list()? + .lines() + .filter(|l| !l.is_empty()) + .count(); + + if event_count == 0 { + Err("ELOG contained no events".into()) + } else { + Ok(()) + } +} + +fn host_is_chrome_test(_env: &mut TestEnv) -> TestResult { + let release_info = if let Ok(f) = File::open("/etc/os-release") { + let buf = std::io::BufReader::new(f); + parse_os_release(buf.lines().flatten()) + } else { + info!("Unable to read /etc/os-release to probe system information"); + HashMap::new() + }; + + match release_info.get("ID") { + Some(id) if id == "chromeos" || id == "chromiumos" => Ok(()), + oid => { + let id = match oid { + Some(s) => s, + None => "UNKNOWN", + }; + Err(format!( + "Test host os-release \"{}\" should be but is not chromeos", + id + ) + .into()) + } + } +} + +fn partial_lock_test(section: LayoutNames) -> impl Fn(&mut TestEnv) -> TestResult { + move |env: &mut TestEnv| { + // Need a clean image for verification + env.ensure_golden()?; + + let (name, start, len) = utils::layout_section(env.layout(), section); + // Disable software WP so we can do range protection, but hardware WP + // must remain enabled for (most) range protection to do anything. + env.wp.set_hw(false)?.set_sw(false)?; + flashrom::wp_range(env.cmd, (start, len), true)?; + env.wp.set_hw(true)?; + + let rws = flashrom::ROMWriteSpecifics { + layout_file: Some(LAYOUT_FILE), + write_file: Some(env.random_data_file()), + name_file: Some(name), + }; + if flashrom::write_file_with_layout(env.cmd, &rws).is_ok() { + return Err( + "Section should be locked, should not have been overwritable with random data" + .into(), + ); + } + if !env.is_golden() { + return Err("Section didn't lock, has been overwritten with random data!".into()); + } + Ok(()) + } +} + +fn verify_fail_test(env: &mut TestEnv) -> TestResult { + // Comparing the flash contents to random data says they're not the same. + match env.verify(env.random_data_file()) { + Ok(_) => Err("Verification says flash is full of random data".into()), + Err(_) => Ok(()), + } +} + +/// Ad-hoc parsing of os-release(5); mostly according to the spec, +/// but ignores quotes and escaping. +fn parse_os_release>(lines: I) -> HashMap { + fn parse_line(line: String) -> Option<(String, String)> { + if line.is_empty() || line.starts_with('#') { + return None; + } + + let delimiter = match line.find('=') { + Some(idx) => idx, + None => { + warn!("os-release entry seems malformed: {:?}", line); + return None; + } + }; + Some(( + line[..delimiter].to_owned(), + line[delimiter + 1..].to_owned(), + )) + } + + lines.into_iter().filter_map(parse_line).collect() +} + +#[test] +fn test_parse_os_release() { + let lines = [ + "BUILD_ID=12516.0.0", + "# this line is a comment followed by an empty line", + "", + "ID_LIKE=chromiumos", + "ID=chromeos", + "VERSION=79", + "EMPTY_VALUE=", + ]; + let map = parse_os_release(lines.iter().map(|&s| s.to_owned())); + + fn get<'a, 'b>(m: &'a HashMap, k: &'b str) -> Option<&'a str> { + m.get(k).map(|s| s.as_ref()) + } + + assert_eq!(get(&map, "ID"), Some("chromeos")); + assert_eq!(get(&map, "BUILD_ID"), Some("12516.0.0")); + assert_eq!(get(&map, "EMPTY_VALUE"), Some("")); + assert_eq!(get(&map, ""), None); +} + +#[test] +fn test_name_filter() { + let test_one = ("Test One", |_: &mut TestEnv| Ok(())); + let test_two = ("Test Two", |_: &mut TestEnv| Ok(())); + let tests: &[&dyn TestCase] = &[&test_one, &test_two]; + + let mut names = None; + // All tests pass through + assert_eq!(filter_tests(tests, &mut names).count(), 2); + + names = Some(["test two"].iter().map(|s| s.to_string()).collect()); + // Filtered out test one + assert_eq!(filter_tests(tests, &mut names).count(), 1); + + names = Some(["test three"].iter().map(|s| s.to_string()).collect()); + // No tests emitted + assert_eq!(filter_tests(tests, &mut names).count(), 0); + // Name got left behind because no test matched it + assert_eq!(names.unwrap().len(), 1); +} diff --git a/util/flashrom_tester/src/types.rs b/util/flashrom_tester/src/types.rs new file mode 100644 index 00000000..b22ded2b --- /dev/null +++ b/util/flashrom_tester/src/types.rs @@ -0,0 +1,53 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +pub const BOLD: &str = "\x1b[1m"; + +pub const RESET: &str = "\x1b[0m"; +pub const MAGENTA: &str = "\x1b[35m"; +pub const YELLOW: &str = "\x1b[33m"; +pub const GREEN: &str = "\x1b[92m"; +pub const RED: &str = "\x1b[31m"; + +macro_rules! style_dbg { + ($s: expr, $c: expr) => { + format!("{}{:?}{}", $c, $s, types::RESET) + }; +} +macro_rules! style { + ($s: expr, $c: expr) => { + format!("{}{}{}", $c, $s, types::RESET) + }; +} diff --git a/util/flashrom_tester/src/utils.rs b/util/flashrom_tester/src/utils.rs new file mode 100644 index 00000000..d17480bc --- /dev/null +++ b/util/flashrom_tester/src/utils.rs @@ -0,0 +1,298 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use std::io::prelude::*; +use std::process::Command; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum LayoutNames { + TopQuad, + TopHalf, + BottomHalf, + BottomQuad, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LayoutSizes { + half_sz: i64, + quad_sz: i64, + rom_top: i64, + bottom_half_top: i64, + bottom_quad_top: i64, + top_quad_bottom: i64, +} + +pub fn get_layout_sizes(rom_sz: i64) -> Result { + if rom_sz <= 0 { + return Err("invalid rom size provided".into()); + } + if rom_sz & (rom_sz - 1) != 0 { + return Err("invalid rom size, not a power of 2".into()); + } + Ok(LayoutSizes { + half_sz: rom_sz / 2, + quad_sz: rom_sz / 4, + rom_top: rom_sz - 1, + bottom_half_top: (rom_sz / 2) - 1, + bottom_quad_top: (rom_sz / 4) - 1, + top_quad_bottom: (rom_sz / 4) * 3, + }) +} + +pub fn layout_section(ls: &LayoutSizes, ln: LayoutNames) -> (&'static str, i64, i64) { + match ln { + LayoutNames::TopQuad => ("TOP_QUAD", ls.top_quad_bottom, ls.quad_sz), + LayoutNames::TopHalf => ("TOP_HALF", ls.half_sz, ls.half_sz), + LayoutNames::BottomHalf => ("BOTTOM_HALF", 0, ls.half_sz), + LayoutNames::BottomQuad => ("BOTTOM_QUAD", 0, ls.quad_sz), + } +} + +pub fn construct_layout_file(mut target: F, ls: &LayoutSizes) -> std::io::Result<()> { + writeln!(target, "000000:{:x} BOTTOM_QUAD", ls.bottom_quad_top)?; + writeln!(target, "000000:{:x} BOTTOM_HALF", ls.bottom_half_top)?; + writeln!(target, "{:x}:{:x} TOP_HALF", ls.half_sz, ls.rom_top)?; + writeln!(target, "{:x}:{:x} TOP_QUAD", ls.top_quad_bottom, ls.rom_top) +} + +pub fn toggle_hw_wp(dis: bool) -> Result<(), String> { + // The easist way to toggle the harware write-protect is + // to {dis}connect the battery (and/or open the WP screw). + let s = if dis { "dis" } else { "" }; + info!("Prompt for hardware WP {}able", s); + eprintln!(" > {}connect the battery (and/or open the WP screw)", s); + pause(); + let wp = get_hardware_wp()?; + if wp && dis { + eprintln!("Hardware write protect is still ENABLED!"); + return toggle_hw_wp(dis); + } + if !wp && !dis { + eprintln!("Hardware write protect is still DISABLED!"); + return toggle_hw_wp(dis); + } + Ok(()) +} + +pub fn ac_power_warning() { + info!("*****************************"); + info!("AC power *must be* connected!"); + info!("*****************************"); + pause(); +} + +fn pause() { + let mut stdout = std::io::stdout(); + // We want the cursor to stay at the end of the line, so we print without a newline + // and flush manually. + stdout.write(b"Press any key to continue...").unwrap(); + stdout.flush().unwrap(); + std::io::stdin().read(&mut [0]).unwrap(); +} + +pub fn get_hardware_wp() -> std::result::Result { + let (_, wp) = parse_crosssystem(&collect_crosssystem()?)?; + Ok(wp) +} + +pub fn collect_crosssystem() -> Result { + let cmd = match Command::new("crossystem").output() { + Ok(x) => x, + Err(e) => return Err(format!("Failed to run crossystem: {}", e)), + }; + + if !cmd.status.success() { + return Err(translate_command_error(&cmd).to_string()); + }; + + Ok(String::from_utf8_lossy(&cmd.stdout).into_owned()) +} + +fn parse_crosssystem(s: &str) -> Result<(Vec<&str>, bool), &'static str> { + // grep -v 'fwid +=' | grep -v 'hwid +=' + let sysinfo = s + .split_terminator("\n") + .filter(|s| !s.contains("fwid +=") && !s.contains("hwid +=")); + + let state_line = match sysinfo.clone().filter(|s| s.starts_with("wpsw_cur")).next() { + None => return Err("No wpsw_cur in system info"), + Some(line) => line, + }; + let wp_s_val = state_line + .trim_start_matches("wpsw_cur") + .trim_start_matches(' ') + .trim_start_matches('=') + .trim_start_matches(' ') + .get(..1) + .unwrap() + .parse::(); + + match wp_s_val { + Ok(v) => { + if v == 1 { + return Ok((sysinfo.collect(), true)); + } else if v == 0 { + return Ok((sysinfo.collect(), false)); + } else { + return Err("Unknown state value"); + } + } + Err(_) => return Err("Cannot parse state value"), + } +} + +pub fn translate_command_error(output: &std::process::Output) -> std::io::Error { + use std::io::{Error, ErrorKind}; + // There is two cases on failure; + // i. ) A bad exit code, + // ii.) A SIG killed us. + match output.status.code() { + Some(code) => { + let e = format!( + "{}\nExited with error code: {}", + String::from_utf8_lossy(&output.stderr), + code + ); + Error::new(ErrorKind::Other, e) + } + None => Error::new( + ErrorKind::Other, + "Process terminated by a signal".to_string(), + ), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn construct_layout_file() { + use super::{construct_layout_file, get_layout_sizes}; + + let mut buf = Vec::new(); + construct_layout_file( + &mut buf, + &get_layout_sizes(0x10000).expect("64k is a valid chip size"), + ) + .expect("no I/O errors expected"); + + assert_eq!( + &buf[..], + &b"000000:3fff BOTTOM_QUAD\n\ + 000000:7fff BOTTOM_HALF\n\ + 8000:ffff TOP_HALF\n\ + c000:ffff TOP_QUAD\n"[..] + ); + } + + #[test] + fn get_layout_sizes() { + use super::get_layout_sizes; + + assert_eq!( + get_layout_sizes(-128).err(), + Some("invalid rom size provided".into()) + ); + + assert_eq!( + get_layout_sizes(3 << 20).err(), + Some("invalid rom size, not a power of 2".into()) + ); + + assert_eq!( + get_layout_sizes(64 << 10).unwrap(), + LayoutSizes { + half_sz: 0x8000, + quad_sz: 0x4000, + rom_top: 0xFFFF, + bottom_half_top: 0x7FFF, + bottom_quad_top: 0x3FFF, + top_quad_bottom: 0xC000, + } + ); + } + + #[test] + fn parse_crosssystem() { + use super::parse_crosssystem; + + assert_eq!( + parse_crosssystem("This is not the tool you are looking for").err(), + Some("No wpsw_cur in system info") + ); + + assert_eq!( + parse_crosssystem("wpsw_cur = ERROR").err(), + Some("Cannot parse state value") + ); + + assert_eq!( + parse_crosssystem("wpsw_cur = 3").err(), + Some("Unknown state value") + ); + + assert_eq!( + parse_crosssystem("wpsw_cur = 0"), + Ok((vec!["wpsw_cur = 0"], false)) + ); + + assert_eq!( + parse_crosssystem("wpsw_cur = 1"), + Ok((vec!["wpsw_cur = 1"], true)) + ); + + assert_eq!( + parse_crosssystem("wpsw_cur=1"), + Ok((vec!["wpsw_cur=1"], true)) + ); + + assert_eq!( + parse_crosssystem( + "fwid += 123wpsw_cur\n\ + hwid += aaaaa\n\ + wpsw_boot = 0 # [RO/int]\n\ + wpsw_cur = 1 # [RO/int]\n" + ), + Ok(( + vec![ + "wpsw_boot = 0 # [RO/int]", + "wpsw_cur = 1 # [RO/int]" + ], + true + )) + ); + } +} -- cgit v1.2.3