blob: e59bb451f71fef98647d67955729fef6d8eb22ee [file] [log] [blame] [edit]
// Copyright 2022 The ChromiumOS Authors
// 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;
use std::fs::File;
use std::fs::OpenOptions;
use std::os::unix::fs::OpenOptionsExt;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use data_model::volatile_memory::VolatileMemory;
use data_model::DataInit;
use libchromeos::sys::MappedRegion;
use libchromeos::sys::MemoryMapping;
use libchromeos::sys::MemoryMappingBuilder;
use libchromeos::sys_deps::zerocopy;
use libchromeos::sys_deps::zerocopy::FromBytes;
use libsirenia::linux::kmsg;
use libsirenia::sys::get_cbmem_toc;
use log::error;
use log::info;
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_ramoops_location() -> Result<(u64, usize)> {
match get_cbmem_toc()?.iter().find(|&x| x.name == "RAMOOPS") {
None => bail!("RAMOOPS cbmem entry not found"),
Some(e) => {
info!("pstore: using ramoops {:#x}@{:#x}", e.size, e.start);
Ok((e.start, e.size))
}
}
}
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")?;
MemoryMappingBuilder::new(ramoops_len)
.from_file(&devmem)
.offset(ramoops_addr)
.build()
.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, FromBytes)]
#[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.
// TODO: Remove this once crosvm has finished its migration to zerocopy::FromBytes.
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 = MemoryMappingBuilder::new(ramoops.size())
.from_file(&outputf)
.build()
.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")
}
// Append data to the given ramoops region.
//
// region_offset and region_size must correspond to either the console of the
// pmsg ramoops regions. If the data is too large, it will be truncated to fit.
// Returns the number of bytes copied.
fn append_to_ramoops_region(
ramoops: &MemoryMapping,
region_offset: usize,
region_size: usize,
data: &[u8],
) -> Result<usize> {
let mut header: RamoopsRegionHeader = ramoops.read_obj(region_offset)?;
let data_offset = region_offset + RAMOOPS_REGION_HEADER_SIZE;
let max_data_size = region_size - RAMOOPS_REGION_HEADER_SIZE;
let mut copied: usize;
if data.len() >= max_data_size {
copied = ramoops.write_slice(&data[..max_data_size], data_offset)?;
if copied != max_data_size {
bail!("internal error");
}
header.start = 0;
header.size = copied as u32;
} else {
let before_wrap = cmp::min(data.len(), max_data_size - (header.start as usize));
copied =
ramoops.write_slice(&data[..before_wrap], data_offset + (header.start as usize))?;
if copied != before_wrap {
bail!("internal error");
};
if before_wrap == data.len() {
header.start += copied as u32;
} else {
// wraparound at the end of the buffer
let remaining = ramoops.write_slice(&data[before_wrap..], data_offset)?;
header.start = remaining as u32;
copied += remaining;
if copied != data.len() {
bail!("internal error");
};
}
header.size = cmp::min(max_data_size, (header.size as usize) + data.len()) as u32;
}
ramoops.write_obj(header, region_offset)?;
Ok(copied)
}
/// Copy the tail of dmesg into the ramoops console buffer.
///
/// This causes hypervisor logs to be included in kernel crash reports.
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 data = output.as_bytes();
let offsets = RamoopsOffsets::new(ramoops.size());
let copied =
append_to_ramoops_region(ramoops, offsets.console_offset, offsets.console_size, data)?;
info!("Appended {} bytes to ramoops console log", copied);
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn check_ramoops_region_header_size() {
assert_eq!(
std::mem::size_of::<RamoopsRegionHeader>(),
RAMOOPS_REGION_HEADER_SIZE
)
}
fn create_ramoops(sz: usize, data: &[u8]) -> MemoryMapping {
let m = MemoryMappingBuilder::new(sz).build().unwrap();
let h = RamoopsRegionHeader {
sig: *b"DBGC",
start: data.len() as u32,
size: data.len() as u32,
};
m.write_obj(h, 0).unwrap();
m.write_slice(data, RAMOOPS_REGION_HEADER_SIZE).unwrap();
m
}
// The ramoops_append_* tests create a mmap mapping of 40 bytes, and then
// call append_to_ramoops_region() with half that size to simulate the fact
// that the ramoops memory mapping contains multiple regions.
#[test]
fn ramoops_append_nowrap() {
let d1: [u8; 4] = [1, 2, 3, 4];
let m = create_ramoops(40, &d1);
let d2: [u8; 3] = [5, 6, 7];
append_to_ramoops_region(&m, 0, 20, &d2).unwrap();
let mut d3: [u8; 7] = [0; 7];
m.read_slice(&mut d3, RAMOOPS_REGION_HEADER_SIZE).unwrap();
assert_eq!(&d3, &[1, 2, 3, 4, 5, 6, 7]);
let hh: RamoopsRegionHeader = m.read_obj(0).unwrap();
assert_eq!(&hh.sig, b"DBGC");
assert_eq!(hh.start, 7);
assert_eq!(hh.size, 7);
}
#[test]
fn ramoops_append_wrap() {
let d1: [u8; 4] = [1, 2, 3, 4];
let m = create_ramoops(40, &d1);
let d2: [u8; 5] = [5, 6, 7, 8, 9];
append_to_ramoops_region(&m, 0, 20, &d2).unwrap();
let mut d3: [u8; 8] = [0; 8];
m.read_slice(&mut d3, RAMOOPS_REGION_HEADER_SIZE).unwrap();
assert_eq!(&d3, &[9, 2, 3, 4, 5, 6, 7, 8]);
let hh: RamoopsRegionHeader = m.read_obj(0).unwrap();
assert_eq!(&hh.sig, b"DBGC");
assert_eq!(hh.start, 1);
assert_eq!(hh.size, 8);
}
#[test]
fn ramoops_append_trunc() {
let d1: [u8; 4] = [1, 2, 3, 4];
let m = create_ramoops(40, &d1);
let d2: [u8; 10] = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
append_to_ramoops_region(&m, 0, 20, &d2).unwrap();
let mut d3: [u8; 8] = [0; 8];
m.read_slice(&mut d3, RAMOOPS_REGION_HEADER_SIZE).unwrap();
assert_eq!(&d3, &[10, 11, 12, 13, 14, 15, 16, 17]);
let hh: RamoopsRegionHeader = m.read_obj(0).unwrap();
assert_eq!(&hh.sig, b"DBGC");
assert_eq!(hh.start, 0);
assert_eq!(hh.size, 8);
}
}