xref: /aosp_15_r20/external/flashrom/util/flashrom_tester/src/tests.rs (revision 0d6140be3aa665ecc836e8907834fcd3e3b018fc)
1 //
2 // Copyright 2019, Google Inc.
3 // All rights reserved.
4 //
5 // Redistribution and use in source and binary forms, with or without
6 // modification, are permitted provided that the following conditions are
7 // met:
8 //
9 //    * Redistributions of source code must retain the above copyright
10 // notice, this list of conditions and the following disclaimer.
11 //    * Redistributions in binary form must reproduce the above
12 // copyright notice, this list of conditions and the following disclaimer
13 // in the documentation and/or other materials provided with the
14 // distribution.
15 //    * Neither the name of Google Inc. nor the names of its
16 // contributors may be used to endorse or promote products derived from
17 // this software without specific prior written permission.
18 //
19 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 //
31 // Alternatively, this software may be distributed under the terms of the
32 // GNU General Public License ("GPL") version 2 as published by the Free
33 // Software Foundation.
34 //
35 
36 use super::cros_sysinfo;
37 use super::tester::{self, OutputFormat, TestCase, TestEnv, TestResult};
38 use super::utils::{self, LayoutNames};
39 use flashrom::{FlashChip, Flashrom};
40 use std::collections::{HashMap, HashSet};
41 use std::convert::TryInto;
42 use std::fs::{self, File};
43 use std::io::BufRead;
44 use std::sync::atomic::AtomicBool;
45 
46 const ELOG_FILE: &str = "/tmp/elog.file";
47 const FW_MAIN_B_PATH: &str = "/tmp/FW_MAIN_B.bin";
48 
49 /// Iterate over tests, yielding only those tests with names matching filter_names.
50 ///
51 /// If filter_names is None, all tests will be run. None is distinct from Some(∅);
52 //  Some(∅) runs no tests.
53 ///
54 /// Name comparisons are performed in lower-case: values in filter_names must be
55 /// converted to lowercase specifically.
56 ///
57 /// When an entry in filter_names matches a test, it is removed from that set.
58 /// This allows the caller to determine if any entries in the original set failed
59 /// to match any test, which may be user error.
filter_tests<'n, 't: 'n, T: TestCase>( tests: &'t [T], filter_names: &'n mut Option<HashSet<String>>, ) -> impl 'n + Iterator<Item = &'t T>60 fn filter_tests<'n, 't: 'n, T: TestCase>(
61     tests: &'t [T],
62     filter_names: &'n mut Option<HashSet<String>>,
63 ) -> impl 'n + Iterator<Item = &'t T> {
64     tests.iter().filter(move |test| match filter_names {
65         // Accept all tests if no names are given
66         None => true,
67         Some(ref mut filter_names) => {
68             // Pop a match to the test name from the filter set, retaining the test
69             // if there was a match.
70             filter_names.remove(&test.get_name().to_lowercase())
71         }
72     })
73 }
74 
75 /// Run tests.
76 ///
77 /// Only returns an Error if there was an internal error; test failures are Ok.
78 ///
79 /// test_names is the case-insensitive names of tests to run; if None, then all
80 /// tests are run. Provided names that don't match any known test will be logged
81 /// as a warning.
82 #[allow(clippy::or_fun_call)] // This is used for to_string here and we don't care.
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>>83 pub fn generic<'a, TN: Iterator<Item = &'a str>>(
84     cmd: &dyn Flashrom,
85     fc: FlashChip,
86     print_layout: bool,
87     output_format: OutputFormat,
88     test_names: Option<TN>,
89     terminate_flag: Option<&AtomicBool>,
90     crossystem: String,
91 ) -> Result<(), Box<dyn std::error::Error>> {
92     utils::ac_power_warning();
93 
94     info!("Record crossystem information.\n{}", crossystem);
95 
96     // Register tests to run:
97     let tests: &[&dyn TestCase] = &[
98         &("Get_device_name", get_device_name_test),
99         &("Coreboot_ELOG_sanity", elog_sanity_test),
100         &("Host_is_ChromeOS", host_is_chrome_test),
101         &("WP_Region_List", wp_region_list_test),
102         &("Erase_and_Write", erase_write_test),
103         &("Fail_to_verify", verify_fail_test),
104         &("HWWP_Locks_SWWP", hwwp_locks_swwp_test),
105         &("Lock_top_quad", partial_lock_test(LayoutNames::TopQuad)),
106         &(
107             "Lock_bottom_quad",
108             partial_lock_test(LayoutNames::BottomQuad),
109         ),
110         &(
111             "Lock_bottom_half",
112             partial_lock_test(LayoutNames::BottomHalf),
113         ),
114         &("Lock_top_half", partial_lock_test(LayoutNames::TopHalf)),
115     ];
116 
117     // Limit the tests to only those requested, unless none are requested
118     // in which case all tests are included.
119     let mut filter_names: Option<HashSet<String>> =
120         test_names.map(|names| names.map(|s| s.to_lowercase()).collect());
121     let tests = filter_tests(tests, &mut filter_names);
122 
123     let chip_name = cmd
124         .name()
125         .map(|x| format!("vendor=\"{}\" name=\"{}\"", x.0, x.1))
126         .unwrap_or("<Unknown chip>".into());
127 
128     // ------------------------.
129     // Run all the tests and collate the findings:
130     let results = tester::run_all_tests(fc, cmd, tests, terminate_flag, print_layout);
131 
132     // Any leftover filtered names were specified to be run but don't exist
133     for leftover in filter_names.iter().flatten() {
134         warn!("No test matches filter name \"{}\"", leftover);
135     }
136 
137     let os_release = sys_info::os_release().unwrap_or("<Unknown OS>".to_string());
138     let cros_release = cros_sysinfo::release_description()
139         .unwrap_or("<Unknown or not a ChromeOS release>".to_string());
140     let system_info = cros_sysinfo::system_info().unwrap_or("<Unknown System>".to_string());
141     let bios_info = cros_sysinfo::bios_info().unwrap_or("<Unknown BIOS>".to_string());
142 
143     let meta_data = tester::ReportMetaData {
144         chip_name,
145         os_release,
146         cros_release,
147         system_info,
148         bios_info,
149     };
150     tester::collate_all_test_runs(&results, meta_data, output_format);
151     Ok(())
152 }
153 
154 /// Query the programmer and chip name.
155 /// Success means we got something back, which is good enough.
get_device_name_test(env: &mut TestEnv) -> TestResult156 fn get_device_name_test(env: &mut TestEnv) -> TestResult {
157     env.cmd.name()?;
158     Ok(())
159 }
160 
161 /// List the write-protectable regions of flash.
162 /// NOTE: This is not strictly a 'test' as it is allowed to fail on some platforms.
163 ///       However, we will warn when it does fail.
wp_region_list_test(env: &mut TestEnv) -> TestResult164 fn wp_region_list_test(env: &mut TestEnv) -> TestResult {
165     match env.cmd.wp_list() {
166         Ok(list_str) => info!("\n{}", list_str),
167         Err(e) => warn!("{}", e),
168     };
169     Ok(())
170 }
171 
172 /// Verify that enabling hardware and software write protect prevents chip erase.
erase_write_test(env: &mut TestEnv) -> TestResult173 fn erase_write_test(env: &mut TestEnv) -> TestResult {
174     if !env.is_golden() {
175         info!("Memory has been modified; reflashing to ensure erasure can be detected");
176         env.ensure_golden()?;
177     }
178 
179     // With write protect enabled erase should fail.
180     env.wp.set_sw(true)?.set_hw(true)?;
181     if env.erase().is_ok() {
182         info!("Flashrom returned Ok but this may be incorrect; verifying");
183         if !env.is_golden() {
184             return Err("Hardware write protect asserted however can still erase!".into());
185         }
186         info!("Erase claimed to succeed but verify is Ok; assume erase failed");
187     }
188 
189     // With write protect disabled erase should succeed.
190     env.wp.set_hw(false)?.set_sw(false)?;
191     env.erase()?;
192     if env.is_golden() {
193         return Err("Successful erase didn't modify memory".into());
194     }
195 
196     Ok(())
197 }
198 
199 /// Verify that enabling hardware write protect prevents disabling software write protect.
hwwp_locks_swwp_test(env: &mut TestEnv) -> TestResult200 fn hwwp_locks_swwp_test(env: &mut TestEnv) -> TestResult {
201     if !env.wp.can_control_hw_wp() {
202         return Err("Lock test requires ability to control hardware write protect".into());
203     }
204 
205     env.wp.set_hw(false)?.set_sw(true)?;
206     // Toggling software WP off should work when hardware WP is off.
207     // Then enable software WP again for the next test.
208     env.wp.set_sw(false)?.set_sw(true)?;
209 
210     // Toggling software WP off should not work when hardware WP is on.
211     env.wp.set_hw(true)?;
212     if env.wp.set_sw(false).is_ok() {
213         return Err("Software WP was reset despite hardware WP being enabled".into());
214     }
215     Ok(())
216 }
217 
218 /// Check that the elog contains *something*, as an indication that Coreboot
219 /// is actually able to write to the Flash. This only makes sense for chips
220 /// running Coreboot, which we assume is just host.
elog_sanity_test(env: &mut TestEnv) -> TestResult221 fn elog_sanity_test(env: &mut TestEnv) -> TestResult {
222     if env.chip_type() != FlashChip::INTERNAL {
223         info!("Skipping ELOG sanity check for non-internal chip");
224         return Ok(());
225     }
226     // flash should be back in the golden state
227     env.ensure_golden()?;
228 
229     const ELOG_RW_REGION_NAME: &str = "RW_ELOG";
230     env.cmd
231         .read_region_into_file(ELOG_FILE.as_ref(), ELOG_RW_REGION_NAME)?;
232 
233     // Just checking for the magic numer
234     // TODO: improve this test to read the events
235     if fs::metadata(ELOG_FILE)?.len() < 4 {
236         return Err("ELOG contained no data".into());
237     }
238     let data = fs::read(ELOG_FILE)?;
239     if u32::from_le_bytes(data[0..4].try_into()?) != 0x474f4c45 {
240         return Err("ELOG had bad magic number".into());
241     }
242     Ok(())
243 }
244 
245 /// Check that we are running ChromiumOS.
host_is_chrome_test(_env: &mut TestEnv) -> TestResult246 fn host_is_chrome_test(_env: &mut TestEnv) -> TestResult {
247     let release_info = if let Ok(f) = File::open("/etc/os-release") {
248         let buf = std::io::BufReader::new(f);
249         parse_os_release(buf.lines().flatten())
250     } else {
251         info!("Unable to read /etc/os-release to probe system information");
252         HashMap::new()
253     };
254 
255     match release_info.get("ID") {
256         Some(id) if id == "chromeos" || id == "chromiumos" => Ok(()),
257         oid => {
258             let id = match oid {
259                 Some(s) => s,
260                 None => "UNKNOWN",
261             };
262             Err(format!(
263                 "Test host os-release \"{}\" should be but is not chromeos",
264                 id
265             )
266             .into())
267         }
268     }
269 }
270 
271 /// Verify that software write protect for a range protects only the requested range.
partial_lock_test(section: LayoutNames) -> impl Fn(&mut TestEnv) -> TestResult272 fn partial_lock_test(section: LayoutNames) -> impl Fn(&mut TestEnv) -> TestResult {
273     move |env: &mut TestEnv| {
274         // Need a clean image for verification
275         env.ensure_golden()?;
276 
277         let (wp_section_name, start, len) = utils::layout_section(env.layout(), section);
278         // Disable hardware WP so we can modify the protected range.
279         env.wp.set_hw(false)?;
280         // Then enable software WP so the range is enforced and enable hardware
281         // WP so that flashrom does not disable software WP during the
282         // operation.
283         env.wp.set_range((start, len), true)?;
284         env.wp.set_hw(true)?;
285 
286         // Check that we cannot write to the protected region.
287         if env
288             .cmd
289             .write_from_file_region(env.random_data_file(), wp_section_name, &env.layout_file)
290             .is_ok()
291         {
292             return Err(
293                 "Section should be locked, should not have been overwritable with random data"
294                     .into(),
295             );
296         }
297         if !env.is_golden() {
298             return Err("Section didn't lock, has been overwritten with random data!".into());
299         }
300 
301         // Check that we can write to the non protected region.
302         let (non_wp_section_name, _, _) =
303             utils::layout_section(env.layout(), section.get_non_overlapping_section());
304         env.cmd.write_from_file_region(
305             env.random_data_file(),
306             non_wp_section_name,
307             &env.layout_file,
308         )?;
309 
310         Ok(())
311     }
312 }
313 
314 /// Check that flashrom 'verify' will fail if the provided data does not match the chip data.
verify_fail_test(env: &mut TestEnv) -> TestResult315 fn verify_fail_test(env: &mut TestEnv) -> TestResult {
316     env.ensure_golden()?;
317     // Verify that verify is Ok when the data matches. We verify only a region
318     // and not the whole chip because coprocessors or firmware may have written
319     // some data in other regions.
320     env.cmd
321         .read_region_into_file(FW_MAIN_B_PATH.as_ref(), "FW_MAIN_B")?;
322     env.cmd
323         .verify_region_from_file(FW_MAIN_B_PATH.as_ref(), "FW_MAIN_B")?;
324 
325     // Verify that verify is false when the data does not match
326     match env.verify(env.random_data_file()) {
327         Ok(_) => Err("Verification says flash is full of random data".into()),
328         Err(_) => Ok(()),
329     }
330 }
331 
332 /// Ad-hoc parsing of os-release(5); mostly according to the spec,
333 /// but ignores quotes and escaping.
parse_os_release<I: IntoIterator<Item = String>>(lines: I) -> HashMap<String, String>334 fn parse_os_release<I: IntoIterator<Item = String>>(lines: I) -> HashMap<String, String> {
335     fn parse_line(line: String) -> Option<(String, String)> {
336         if line.is_empty() || line.starts_with('#') {
337             return None;
338         }
339 
340         let delimiter = match line.find('=') {
341             Some(idx) => idx,
342             None => {
343                 warn!("os-release entry seems malformed: {:?}", line);
344                 return None;
345             }
346         };
347         Some((
348             line[..delimiter].to_owned(),
349             line[delimiter + 1..].to_owned(),
350         ))
351     }
352 
353     lines.into_iter().filter_map(parse_line).collect()
354 }
355 
356 #[test]
test_parse_os_release()357 fn test_parse_os_release() {
358     let lines = [
359         "BUILD_ID=12516.0.0",
360         "# this line is a comment followed by an empty line",
361         "",
362         "ID_LIKE=chromiumos",
363         "ID=chromeos",
364         "VERSION=79",
365         "EMPTY_VALUE=",
366     ];
367     let map = parse_os_release(lines.iter().map(|&s| s.to_owned()));
368 
369     fn get<'a, 'b>(m: &'a HashMap<String, String>, k: &'b str) -> Option<&'a str> {
370         m.get(k).map(|s| s.as_ref())
371     }
372 
373     assert_eq!(get(&map, "ID"), Some("chromeos"));
374     assert_eq!(get(&map, "BUILD_ID"), Some("12516.0.0"));
375     assert_eq!(get(&map, "EMPTY_VALUE"), Some(""));
376     assert_eq!(get(&map, ""), None);
377 }
378 
379 #[test]
test_name_filter()380 fn test_name_filter() {
381     let test_one = ("Test One", |_: &mut TestEnv| Ok(()));
382     let test_two = ("Test Two", |_: &mut TestEnv| Ok(()));
383     let tests: &[&dyn TestCase] = &[&test_one, &test_two];
384 
385     let mut names = None;
386     // All tests pass through
387     assert_eq!(filter_tests(tests, &mut names).count(), 2);
388 
389     names = Some(["test two"].iter().map(|s| s.to_string()).collect());
390     // Filtered out test one
391     assert_eq!(filter_tests(tests, &mut names).count(), 1);
392 
393     names = Some(["test three"].iter().map(|s| s.to_string()).collect());
394     // No tests emitted
395     assert_eq!(filter_tests(tests, &mut names).count(), 0);
396     // Name got left behind because no test matched it
397     assert_eq!(names.unwrap().len(), 1);
398 }
399