mirror of
https://review.coreboot.org/flashrom.git
synced 2025-04-28 07:23:43 +02:00

In verify_fail_test test that verify works when expected, as well as fails when expected. A verify_region_from_file function is added to support this. BUG=b:235916336 BRANCH=None TEST=None Change-Id: Ibbcc97086466b67cfab4f6c32140bb5f2c456beb Signed-off-by: Evan Benn <evanbenn@chromium.org> Reviewed-on: https://review.coreboot.org/c/flashrom/+/71974 Reviewed-by: Peter Marheine <pmarheine@chromium.org> Tested-by: build bot (Jenkins) <no-reply@coreboot.org> Reviewed-by: Edward O'Callaghan <quasisec@chromium.org>
399 lines
14 KiB
Rust
399 lines
14 KiB
Rust
//
|
|
// 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};
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::convert::TryInto;
|
|
use std::fs::{self, File};
|
|
use std::io::BufRead;
|
|
use std::sync::atomic::AtomicBool;
|
|
|
|
const ELOG_FILE: &str = "/tmp/elog.file";
|
|
const FW_MAIN_B_PATH: &str = "/tmp/FW_MAIN_B.bin";
|
|
|
|
/// 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<HashSet<String>>,
|
|
) -> impl 'n + Iterator<Item = &'t T> {
|
|
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.
|
|
#[allow(clippy::or_fun_call)] // This is used for to_string here and we don't care.
|
|
pub fn generic<'a, TN: Iterator<Item = &'a str>>(
|
|
cmd: &dyn Flashrom,
|
|
fc: FlashChip,
|
|
print_layout: bool,
|
|
output_format: OutputFormat,
|
|
test_names: Option<TN>,
|
|
terminate_flag: Option<&AtomicBool>,
|
|
crossystem: String,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
utils::ac_power_warning();
|
|
|
|
info!("Record crossystem information.\n{}", crossystem);
|
|
|
|
// 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),
|
|
&("WP_Region_List", wp_region_list_test),
|
|
&("Erase_and_Write", erase_write_test),
|
|
&("Fail_to_verify", verify_fail_test),
|
|
&("HWWP_Locks_SWWP", hwwp_locks_swwp_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<HashSet<String>> =
|
|
test_names.map(|names| names.map(|s| s.to_lowercase()).collect());
|
|
let tests = filter_tests(tests, &mut filter_names);
|
|
|
|
let chip_name = cmd
|
|
.name()
|
|
.map(|x| format!("vendor=\"{}\" name=\"{}\"", x.0, x.1))
|
|
.unwrap_or("<Unknown chip>".into());
|
|
|
|
// ------------------------.
|
|
// Run all the tests and collate the findings:
|
|
let results = tester::run_all_tests(fc, cmd, tests, terminate_flag, print_layout);
|
|
|
|
// 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 os_release = sys_info::os_release().unwrap_or("<Unknown OS>".to_string());
|
|
let cros_release = cros_sysinfo::release_description()
|
|
.unwrap_or("<Unknown or not a ChromeOS release>".to_string());
|
|
let system_info = cros_sysinfo::system_info().unwrap_or("<Unknown System>".to_string());
|
|
let bios_info = cros_sysinfo::bios_info().unwrap_or("<Unknown BIOS>".to_string());
|
|
|
|
let meta_data = tester::ReportMetaData {
|
|
chip_name,
|
|
os_release,
|
|
cros_release,
|
|
system_info,
|
|
bios_info,
|
|
};
|
|
tester::collate_all_test_runs(&results, meta_data, output_format);
|
|
Ok(())
|
|
}
|
|
|
|
/// Query the programmer and chip name.
|
|
/// Success means we got something back, which is good enough.
|
|
fn get_device_name_test(env: &mut TestEnv) -> TestResult {
|
|
env.cmd.name()?;
|
|
Ok(())
|
|
}
|
|
|
|
/// List the write-protectable regions of flash.
|
|
/// NOTE: This is not strictly a 'test' as it is allowed to fail on some platforms.
|
|
/// However, we will warn when it does fail.
|
|
fn wp_region_list_test(env: &mut TestEnv) -> TestResult {
|
|
match env.cmd.wp_list() {
|
|
Ok(list_str) => info!("\n{}", list_str),
|
|
Err(e) => warn!("{}", e),
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
/// Verify that enabling hardware and software write protect prevents chip erase.
|
|
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(())
|
|
}
|
|
|
|
/// Verify that enabling hardware write protect prevents disabling software write protect.
|
|
fn hwwp_locks_swwp_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 WP is off.
|
|
// Then enable software WP again for the next test.
|
|
env.wp.set_sw(false)?.set_sw(true)?;
|
|
|
|
// Toggling software WP off should not work when hardware WP is on.
|
|
env.wp.set_hw(true)?;
|
|
if env.wp.set_sw(false).is_ok() {
|
|
return Err("Software WP was reset despite hardware WP being enabled".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Check that the elog contains *something*, as an indication that Coreboot
|
|
/// is actually able to write to the Flash. This only makes sense for chips
|
|
/// running Coreboot, which we assume is just host.
|
|
fn elog_sanity_test(env: &mut TestEnv) -> TestResult {
|
|
if env.chip_type() != FlashChip::HOST {
|
|
info!("Skipping ELOG sanity check for non-host chip");
|
|
return Ok(());
|
|
}
|
|
// flash should be back in the golden state
|
|
env.ensure_golden()?;
|
|
|
|
const ELOG_RW_REGION_NAME: &str = "RW_ELOG";
|
|
env.cmd
|
|
.read_region_into_file(ELOG_FILE.as_ref(), ELOG_RW_REGION_NAME)?;
|
|
|
|
// Just checking for the magic numer
|
|
// TODO: improve this test to read the events
|
|
if fs::metadata(ELOG_FILE)?.len() < 4 {
|
|
return Err("ELOG contained no data".into());
|
|
}
|
|
let data = fs::read(ELOG_FILE)?;
|
|
if u32::from_le_bytes(data[0..4].try_into()?) != 0x474f4c45 {
|
|
return Err("ELOG had bad magic number".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Check that we are running ChromiumOS.
|
|
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())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Verify that software write protect for a range protects only the requested range.
|
|
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 (wp_section_name, start, len) = utils::layout_section(env.layout(), section);
|
|
// Disable hardware WP so we can modify the protected range.
|
|
env.wp.set_hw(false)?;
|
|
// Then enable software WP so the range is enforced and enable hardware
|
|
// WP so that flashrom does not disable software WP during the
|
|
// operation.
|
|
env.wp.set_range((start, len), true)?;
|
|
env.wp.set_hw(true)?;
|
|
|
|
// Check that we cannot write to the protected region.
|
|
if env
|
|
.cmd
|
|
.write_from_file_region(env.random_data_file(), wp_section_name, &env.layout_file)
|
|
.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());
|
|
}
|
|
|
|
// Check that we can write to the non protected region.
|
|
let (non_wp_section_name, _, _) =
|
|
utils::layout_section(env.layout(), section.get_non_overlapping_section());
|
|
env.cmd.write_from_file_region(
|
|
env.random_data_file(),
|
|
non_wp_section_name,
|
|
&env.layout_file,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Check that flashrom 'verify' will fail if the provided data does not match the chip data.
|
|
fn verify_fail_test(env: &mut TestEnv) -> TestResult {
|
|
env.ensure_golden()?;
|
|
// Verify that verify is Ok when the data matches. We verify only a region
|
|
// and not the whole chip because coprocessors or firmware may have written
|
|
// some data in other regions.
|
|
env.cmd
|
|
.read_region_into_file(FW_MAIN_B_PATH.as_ref(), "FW_MAIN_B")?;
|
|
env.cmd
|
|
.verify_region_from_file(FW_MAIN_B_PATH.as_ref(), "FW_MAIN_B")?;
|
|
|
|
// Verify that verify is false when the data does not match
|
|
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<I: IntoIterator<Item = String>>(lines: I) -> HashMap<String, String> {
|
|
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<String, String>, 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);
|
|
}
|