| // Copyright 2022 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! Creates the emulated pstore and copies it back to RAMOOPS memory on reboot. |
| |
| use std::cmp; |
| use std::fs::{self, File, OpenOptions}; |
| use std::io::{BufRead, BufReader}; |
| use std::os::unix::fs::OpenOptionsExt; |
| use std::path::PathBuf; |
| use std::str::FromStr; |
| |
| use anyhow::{anyhow, bail, Context, Result}; |
| use data_model::{volatile_memory::VolatileMemory, DataInit}; |
| use libsirenia::linux::kmsg; |
| use sys_util::{error, info, MappedRegion, MemoryMapping}; |
| |
| const RAMOOPS_UNBIND: &str = "/sys/devices/platform/ramoops.0/driver/unbind"; |
| const RAMOOPS_BUS_ID: &[u8] = b"ramoops.0"; |
| |
| const RAMOOPS_REGION_HEADER_SIZE: usize = 12; |
| const RAMOOPS_DEFAULT_REGION_SIZE: usize = 0x20000; |
| const PSTORE_CONSOLE_FILENAME: &str = "console-ramoops-0"; |
| const PSTORE_PMSG_FILENAME: &str = "pmsg-ramoops-0"; |
| |
| const HYPERVISOR_DMESG_TAIL_BYTES: usize = 10 * 1024; |
| |
| /// Copy contents of emulated pstore to RAMOOPS memory. |
| pub fn save_pstore(pstore_path: &str, append_dmesg: bool) -> Result<()> { |
| // Unbind the hypervisor ramoops driver so that it doesn't clobber our |
| // writes. If this fails, we continue anyway since the dmesg buffer will |
| // still be preserved as long as the hypervisor does not crash. |
| if let Err(e) = unbind_ramoops() { |
| error!("Error (ignored): {:?}", e); |
| } |
| |
| let pstore_fd = File::open(pstore_path) |
| .with_context(|| format!("Failed to open pstore file: {}", pstore_path))?; |
| let ramoops = mmap_ramoops()?; |
| ramoops |
| .read_to_memory(0, &pstore_fd, ramoops.size()) |
| .context("Failed to copy emulated pstore to ramoops memory")?; |
| |
| if append_dmesg { |
| append_hypervisor_dmesg(&ramoops).context("Failed to append dmesg to ramoops")?; |
| } |
| Ok(()) |
| } |
| |
| fn unbind_ramoops() -> Result<()> { |
| fs::write(RAMOOPS_UNBIND, RAMOOPS_BUS_ID).context("Failed to unbind ramoops driver") |
| } |
| |
| fn get_goog9999_range(line: &str) -> Result<Option<(u64, usize)>> { |
| // We are looking for a line in the following format (with leading spaces): |
| // 769fa000-76af9fff : GOOG9999:00 |
| if let Some((range, name)) = line.split_once(':') { |
| if name.trim() == "GOOG9999:00" { |
| if let Some((begin, end)) = range.trim().split_once('-') { |
| let begin_addr = u64::from_str_radix(begin, 16) |
| .map_err(|_| anyhow!("Invalid begin address: {}", line))?; |
| let end_addr = u64::from_str_radix(end, 16) |
| .map_err(|_| anyhow!("Invalid end address: {}", line))?; |
| let len = (end_addr |
| .checked_sub(begin_addr) |
| .ok_or_else(|| anyhow!("Invalid range: {}", line))? |
| + 1) as usize; |
| return Ok(Some((begin_addr, len))); |
| } |
| } |
| } |
| Ok(None) |
| } |
| |
| fn get_ramoops_location() -> Result<(u64, usize)> { |
| // When the ramoops driver is enabled, and is using the GOOG9999 region, |
| // this info is in /sys/module/ramoops/parameters/mem_{address|size}. |
| // However, if the ramoops driver is disabled, or if it has been overridden |
| // with kernel command line parameters (say, because we are in a VM), this |
| // approach fails. Therefore, we parse /proc/iomem for this info. |
| let iomem = File::open("/proc/iomem").context("Failed to open /proc/iomem")?; |
| for line in BufReader::new(iomem).lines() { |
| let l = line.context("Error reading from /proc/iomem")?; |
| if let Some((addr, len)) = get_goog9999_range(&l)? { |
| info!("pstore: using ramoops {:#x}@{:#x}", len, addr); |
| return Ok((addr, len)); |
| } |
| } |
| bail!("Unable to find mapping for GOOG9999 in /proc/iomem"); |
| } |
| |
| fn mmap_ramoops() -> Result<MemoryMapping> { |
| let (ramoops_addr, ramoops_len) = get_ramoops_location()?; |
| let devmem = OpenOptions::new() |
| .read(true) |
| .write(true) |
| .custom_flags(libc::O_SYNC) |
| .open("/dev/mem") |
| .context("Failed to open /dev/mem")?; |
| MemoryMapping::from_fd_offset(&devmem, ramoops_len, ramoops_addr) |
| .context("Failed to mmap /dev/mem") |
| } |
| |
| fn get_ramoops_region_size(name: &str) -> usize { |
| // Chrome OS sets all regions except dmesg to the same size, so we |
| // use that as the default size here in case of failures. |
| let path = format!("/sys/module/ramoops/parameters/{}_size", name); |
| match fs::read_to_string(&path) { |
| Err(e) => { |
| error!("Error reading {}: {}", path, e); |
| RAMOOPS_DEFAULT_REGION_SIZE |
| } |
| Ok(v) => usize::from_str(v.trim()) |
| .with_context(|| format!("Could not parse {}: {:?}", path, v)) |
| .unwrap_or_else(|e| { |
| error!("Error: {}", e); |
| RAMOOPS_DEFAULT_REGION_SIZE |
| }), |
| } |
| } |
| |
| #[allow(dead_code)] |
| struct RamoopsOffsets { |
| dmesg_size: usize, |
| console_size: usize, |
| console_offset: usize, |
| ftrace_size: usize, |
| ftrace_offset: usize, |
| pmsg_size: usize, |
| pmsg_offset: usize, |
| } |
| |
| impl RamoopsOffsets { |
| fn new(ramoops_size: usize) -> RamoopsOffsets { |
| let console_size = get_ramoops_region_size("console"); |
| let ftrace_size = get_ramoops_region_size("ftrace"); |
| let pmsg_size = get_ramoops_region_size("pmsg"); |
| let dmesg_size = ramoops_size - console_size - ftrace_size - pmsg_size; |
| let console_offset = dmesg_size; |
| let ftrace_offset = console_offset + console_size; |
| let pmsg_offset = ftrace_offset + ftrace_size; |
| info!( |
| "pstore offsets: console={:#x} ftrace={:#x} pmsg={:#x}", |
| console_offset, ftrace_offset, pmsg_offset |
| ); |
| RamoopsOffsets { |
| dmesg_size, |
| console_size, |
| console_offset, |
| ftrace_size, |
| ftrace_offset, |
| pmsg_size, |
| pmsg_offset, |
| } |
| } |
| } |
| |
| // See fs/pstore/ram_core.c in the kernel for the header definition. |
| #[derive(Copy, Clone)] |
| #[repr(C)] |
| struct RamoopsRegionHeader { |
| sig: [u8; 4], // signature, eg. b"DBGC" |
| start: u32, // offset to write next |
| size: u32, // bytes stored |
| } |
| |
| // Safe because PstoreRegionHeader is plain data. |
| unsafe impl DataInit for RamoopsRegionHeader {} |
| |
| /// Copy data from the specified /sys/fs/pstore file to the emulated pstore. |
| fn restore_pstore_region( |
| emulated_pstore: &MemoryMapping, |
| offset: usize, |
| region_size: usize, |
| fname: &str, |
| ) -> Result<()> { |
| // If there is no data in this ramoops region, the file will not exist. |
| let path: PathBuf = ["/sys/fs/pstore", fname].iter().collect(); |
| if !path.is_file() { |
| return Ok(()); |
| } |
| |
| // Write header |
| let flen = path.metadata()?.len() as usize; |
| let data_size: u32 = cmp::min(flen, region_size - RAMOOPS_REGION_HEADER_SIZE) as u32; |
| let header = RamoopsRegionHeader { |
| sig: *b"DBGC", |
| start: data_size, |
| size: data_size, |
| }; |
| emulated_pstore.write_obj(header, offset)?; |
| |
| // Write data |
| let dataf = |
| File::open(&path).with_context(|| format!("Failed to open: {}", path.to_string_lossy()))?; |
| emulated_pstore |
| .read_to_memory( |
| offset + RAMOOPS_REGION_HEADER_SIZE, |
| &dataf, |
| data_size as usize, |
| ) |
| .with_context(|| format!("Failed to write {} to pstore file", fname))?; |
| info!( |
| "pstore: wrote {} bytes to region at {:#x} from {}", |
| data_size, |
| offset, |
| path.to_string_lossy() |
| ); |
| Ok(()) |
| } |
| |
| /// Set up emulated pstore by copying from RAMOOPS memory and /sys/fs/pstore. |
| pub fn restore_pstore(pstore_path: &str) -> Result<()> { |
| // We never read from this file, but mmap requires read permissions. |
| let outputf = OpenOptions::new() |
| .read(true) |
| .write(true) |
| .create(true) |
| .open(pstore_path) |
| .with_context(|| format!("Failed to open pstore file: {}", pstore_path))?; |
| |
| // Use identical size and settings for physical and emulated ramoops. |
| let ramoops = mmap_ramoops()?; |
| outputf |
| .set_len(ramoops.size() as u64) |
| .context("Failed to resize pstore file")?; |
| outputf |
| .sync_all() |
| .context("Failed to sync pstore file after resize")?; |
| let emulated_pstore = |
| MemoryMapping::from_fd(&outputf, ramoops.size()).context("Failed to mmap pstore file")?; |
| |
| let offsets = RamoopsOffsets::new(ramoops.size()); |
| |
| // Copy the dmesg regions as-is from hardware ramoops to pstore file since |
| // they are not being written to. For the rest of the regions, copy from |
| // files in /sys/fs/pstore. |
| ramoops |
| .get_slice(0, offsets.dmesg_size)? |
| .copy_to_volatile_slice(emulated_pstore.get_slice(0, offsets.dmesg_size)?); |
| |
| // For everything except dmesg, use the files in /sys/fs/pstore. |
| // TODO(b/221453622): Handle ftrace buffers. |
| restore_pstore_region( |
| &emulated_pstore, |
| offsets.console_offset, |
| offsets.console_size, |
| PSTORE_CONSOLE_FILENAME, |
| )?; |
| restore_pstore_region( |
| &emulated_pstore, |
| offsets.pmsg_offset, |
| offsets.pmsg_size, |
| PSTORE_PMSG_FILENAME, |
| )?; |
| emulated_pstore |
| .msync() |
| .context("Unable to sync pstore file") |
| } |
| |
| /// Copy the tail of dmesg into the ramoops console buffer. |
| /// |
| /// This causes hypervisor logs to be included in kernel crash reports. |
| pub fn append_hypervisor_dmesg(ramoops: &MemoryMapping) -> Result<()> { |
| let dmesg = kmsg::kmsg_tail(HYPERVISOR_DMESG_TAIL_BYTES)?; |
| // Make the string longer to account for newlines and escaped chars. |
| let mut output = String::with_capacity(HYPERVISOR_DMESG_TAIL_BYTES + 512); |
| output.push_str("\n--------[ hypervisor log ]--------\n"); |
| for line in dmesg { |
| output.push_str(line.as_str()); |
| output.push('\n'); |
| } |
| |
| let offsets = RamoopsOffsets::new(ramoops.size()); |
| let mut header: RamoopsRegionHeader = ramoops.read_obj(offsets.console_offset)?; |
| |
| let data = output.as_bytes(); |
| let console_start_offset = offsets.console_offset + RAMOOPS_REGION_HEADER_SIZE; |
| let copied = ramoops.write_slice(data, console_start_offset + (header.start as usize))?; |
| if copied < data.len() { |
| // wraparound at the end of the buffer |
| let remaining = ramoops.write_slice(&data[copied..], console_start_offset)?; |
| header.start = remaining as u32; |
| } else { |
| header.start += copied as u32; |
| } |
| header.size = cmp::min( |
| offsets.console_size - RAMOOPS_REGION_HEADER_SIZE, |
| (header.size as usize) + data.len(), |
| ) as u32; |
| ramoops.write_obj(header, offsets.console_offset)?; |
| |
| info!("Appended {} bytes to ramoops console log", data.len()); |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| |
| #[test] |
| fn parse_iomem() { |
| assert_eq!( |
| get_goog9999_range(" 769fa000-76af9fff : GOOG9999:00\n").unwrap(), |
| Some((0x769fa000, 1048576)) |
| ); |
| assert_eq!( |
| get_goog9999_range("769fa000-76af9fff:GOOG9999:00").unwrap(), |
| Some((0x769fa000, 1048576)) |
| ); |
| assert_eq!( |
| get_goog9999_range(" 769fa000-76af9fff : GOOG9999:00").unwrap(), |
| Some((0x769fa000, 1048576)) |
| ); |
| assert_eq!( |
| get_goog9999_range(" 769fa000-76af9fff : GOOG9999:00\n").unwrap(), |
| Some((0x769fa000, 1048576)) |
| ); |
| assert_eq!( |
| get_goog9999_range("fe010000-fe010fff : vfio-pci\n").unwrap(), |
| None |
| ); |
| assert_eq!(get_goog9999_range("GOOG9999:00\n").unwrap(), None); |
| assert_eq!(get_goog9999_range(": GOOG9999:00\n").unwrap(), None); |
| assert_eq!( |
| get_goog9999_range("769fa000 : GOOG9999:00\n").unwrap(), |
| None |
| ); |
| assert!(get_goog9999_range("769fa000-76af9fffX : GOOG9999:00\n").is_err()); |
| assert!(get_goog9999_range("769fa000X-76af9fff : GOOG9999:00\n").is_err()); |
| assert!(get_goog9999_range("76af9fff-769fa000 : GOOG9999:00\n").is_err()); |
| } |
| |
| #[test] |
| fn check_ramoops_region_header_size() { |
| assert_eq!( |
| std::mem::size_of::<RamoopsRegionHeader>(), |
| RAMOOPS_REGION_HEADER_SIZE |
| ) |
| } |
| } |