// 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. //! Programmable flash device that supports the minimum interface that OVMF //! requires. This is purpose-built to allow OVMF to store UEFI variables in //! the same way that it stores them on QEMU. //! //! For that reason it's heavily based on [QEMU's pflash implementation], while //! taking even more shortcuts, chief among them being the complete lack of CFI //! tables, which systems would normally use to learn how to use the device. //! //! In addition to full-width reads, we only support single byte writes, //! block erases, and status requests, which OVMF uses to probe the device to //! determine if it is pflash. //! //! Note that without SMM support in crosvm (which it doesn't yet have) this //! device is directly accessible to potentially malicious kernels. With SMM //! and the appropriate changes to this device this could be made more secure //! by ensuring only the BIOS is able to touch the pflash. //! //! [QEMU's pflash implementation]: https://github.com/qemu/qemu/blob/master/hw/block/pflash_cfi01.c use std::path::PathBuf; use anyhow::bail; use base::error; use base::VolatileSlice; use disk::DiskFile; use serde::Deserialize; use serde::Serialize; use crate::pci::CrosvmDeviceId; use crate::BusAccessInfo; use crate::BusDevice; use crate::DeviceId; use crate::Suspendable; const COMMAND_WRITE_BYTE: u8 = 0x10; const COMMAND_BLOCK_ERASE: u8 = 0x20; const COMMAND_CLEAR_STATUS: u8 = 0x50; const COMMAND_READ_STATUS: u8 = 0x70; const COMMAND_BLOCK_ERASE_CONFIRM: u8 = 0xd0; const COMMAND_READ_ARRAY: u8 = 0xff; const STATUS_READY: u8 = 0x80; fn pflash_parameters_default_block_size() -> u32 { // 4K 4 * (1 << 10) } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct PflashParameters { pub path: PathBuf, #[serde(default = "pflash_parameters_default_block_size")] pub block_size: u32, } #[derive(Clone, Copy, Debug, Deserialize, Serialize)] enum State { ReadArray, ReadStatus, BlockErase(u64), Write(u64), } pub struct Pflash { image: Box, image_size: u64, block_size: u32, state: State, status: u8, } impl Pflash { pub fn new(image: Box, block_size: u32) -> anyhow::Result { if !block_size.is_power_of_two() { bail!("Block size {} is not a power of 2", block_size); } let image_size = image.get_len()?; if image_size % block_size as u64 != 0 { bail!( "Disk size {} is not a multiple of block size {}", image_size, block_size ); } Ok(Pflash { image, image_size, block_size, state: State::ReadArray, status: STATUS_READY, }) } } impl BusDevice for Pflash { fn device_id(&self) -> DeviceId { CrosvmDeviceId::Pflash.into() } fn debug_label(&self) -> String { "pflash".to_owned() } fn read(&mut self, info: BusAccessInfo, data: &mut [u8]) { let offset = info.offset; match &self.state { State::ReadArray => { if offset + data.len() as u64 >= self.image_size { error!("pflash read request beyond disk"); return; } if let Err(e) = self .image .read_exact_at_volatile(VolatileSlice::new(data), offset) { error!("pflash failed to read: {}", e); } } State::ReadStatus => { self.state = State::ReadArray; for d in data { *d = self.status; } } _ => { error!( "pflash received unexpected read in state {:?}, recovering to ReadArray mode", self.state ); self.state = State::ReadArray; } } } fn write(&mut self, info: BusAccessInfo, data: &[u8]) { if data.len() > 1 { error!("pflash write request for >1 byte, ignoring"); return; } let data = data[0]; let offset = info.offset; match self.state { State::Write(expected_offset) => { self.state = State::ReadArray; self.status = STATUS_READY; if offset != expected_offset { error!("pflash received write for offset {} that doesn't match offset from WRITE_BYTE command {}", offset, expected_offset); return; } if offset >= self.image_size { error!( "pflash offset {} greater than image size {}", offset, self.image_size ); return; } if let Err(e) = self .image .write_all_at_volatile(VolatileSlice::new(&mut [data]), offset) { error!("failed to write to pflash: {}", e); } } State::BlockErase(expected_offset) => { self.state = State::ReadArray; self.status = STATUS_READY; if data != COMMAND_BLOCK_ERASE_CONFIRM { error!("pflash write data {} after BLOCK_ERASE command, wanted COMMAND_BLOCK_ERASE_CONFIRM", data); return; } if offset != expected_offset { error!("pflash offset {} for BLOCK_ERASE_CONFIRM command does not match the one for BLOCK_ERASE {}", offset, expected_offset); return; } if offset >= self.image_size { error!( "pflash block erase attempt offset {} beyond image size {}", offset, self.image_size ); return; } if offset % self.block_size as u64 != 0 { error!( "pflash block erase offset {} not on block boundary with block size {}", offset, self.block_size ); return; } if let Err(e) = self.image.write_all_at_volatile( VolatileSlice::new(&mut [0xff].repeat(self.block_size.try_into().unwrap())), offset, ) { error!("pflash failed to erase block: {}", e); } } _ => { // If we're not expecting anything else then assume this is a // command to transition states. let command = data; match command { COMMAND_READ_ARRAY => { self.state = State::ReadArray; self.status = STATUS_READY; } COMMAND_READ_STATUS => self.state = State::ReadStatus, COMMAND_CLEAR_STATUS => { self.state = State::ReadArray; self.status = 0; } COMMAND_WRITE_BYTE => self.state = State::Write(offset), COMMAND_BLOCK_ERASE => self.state = State::BlockErase(offset), _ => { error!("received unexpected/unsupported pflash command {}, ignoring and returning to read mode", command); self.state = State::ReadArray } } } } } } impl Suspendable for Pflash { fn snapshot(&mut self) -> anyhow::Result { Ok(serde_json::to_value((self.status, self.state))?) } fn restore(&mut self, data: serde_json::Value) -> anyhow::Result<()> { let (status, state) = serde_json::from_value(data)?; self.status = status; self.state = state; Ok(()) } fn sleep(&mut self) -> anyhow::Result<()> { // TODO(schuffelen): Flush the disk after lifting flush() from AsyncDisk to DiskFile Ok(()) } fn wake(&mut self) -> anyhow::Result<()> { Ok(()) } } #[cfg(test)] mod tests { use base::FileReadWriteAtVolatile; use tempfile::tempfile; use super::*; const IMAGE_SIZE: usize = 4 * (1 << 20); // 4M const BLOCK_SIZE: u32 = 4 * (1 << 10); // 4K fn empty_image() -> Box { let f = Box::new(tempfile().unwrap()); f.write_all_at_volatile(VolatileSlice::new(&mut [0xff].repeat(IMAGE_SIZE)), 0) .unwrap(); f } fn new(f: Box) -> Pflash { Pflash::new(f, BLOCK_SIZE).unwrap() } fn off(offset: u64) -> BusAccessInfo { BusAccessInfo { offset, address: 0, id: 0, } } #[test] fn read() { let f = empty_image(); let mut want = [0xde, 0xad, 0xbe, 0xef]; let offset = 0x1000; f.write_all_at_volatile(VolatileSlice::new(&mut want), offset) .unwrap(); let mut pflash = new(f); let mut got = [0u8; 4]; pflash.read(off(offset), &mut got[..]); assert_eq!(want, got); } #[test] fn write() { let f = empty_image(); let want = [0xdeu8]; let offset = 0x1000; let mut pflash = new(f); pflash.write(off(offset), &[COMMAND_WRITE_BYTE]); pflash.write(off(offset), &want); // Make sure the data reads back correctly over the bus... pflash.write(off(0), &[COMMAND_READ_ARRAY]); let mut got = [0u8; 1]; pflash.read(off(offset), &mut got); assert_eq!(want, got); // And from the backing file itself... pflash .image .read_exact_at_volatile(VolatileSlice::new(&mut got), offset) .unwrap(); assert_eq!(want, got); // And when we recreate the device. let mut pflash = new(pflash.image); pflash.read(off(offset), &mut got); assert_eq!(want, got); // Finally make sure our status is ready. let mut got = [0u8; 4]; pflash.write(off(offset), &[COMMAND_READ_STATUS]); pflash.read(off(offset), &mut got); let want = [STATUS_READY; 4]; assert_eq!(want, got); } #[test] fn erase() { let f = empty_image(); let mut data = [0xde, 0xad, 0xbe, 0xef]; let offset = 0x1000; f.write_all_at_volatile(VolatileSlice::new(&mut data), offset) .unwrap(); f.write_all_at_volatile(VolatileSlice::new(&mut data), offset * 2) .unwrap(); let mut pflash = new(f); pflash.write(off(offset), &[COMMAND_BLOCK_ERASE]); pflash.write(off(offset), &[COMMAND_BLOCK_ERASE_CONFIRM]); pflash.write(off(0), &[COMMAND_READ_ARRAY]); let mut got = [0u8; 4]; pflash.read(off(offset), &mut got); let want = [0xffu8; 4]; assert_eq!(want, got); let want = data; pflash.read(off(offset * 2), &mut got); assert_eq!(want, got); // Make sure our status is ready. pflash.write(off(offset), &[COMMAND_READ_STATUS]); pflash.read(off(offset), &mut got); let want = [STATUS_READY; 4]; assert_eq!(want, got); } #[test] fn status() { let f = empty_image(); let mut data = [0xde, 0xad, 0xbe, 0xff]; let offset = 0x0; f.write_all_at_volatile(VolatileSlice::new(&mut data), offset) .unwrap(); let mut pflash = new(f); // Make sure we start off in the "ready" status. pflash.write(off(offset), &[COMMAND_READ_STATUS]); let mut got = [0u8; 4]; pflash.read(off(offset), &mut got); let want = [STATUS_READY; 4]; assert_eq!(want, got); // Make sure we can clear the status properly. pflash.write(off(offset), &[COMMAND_CLEAR_STATUS]); pflash.write(off(offset), &[COMMAND_READ_STATUS]); pflash.read(off(offset), &mut got); let want = [0; 4]; assert_eq!(want, got); // We implicitly jump back into READ_ARRAY mode after reading the, // status but for OVMF's probe we require that this doesn't actually // affect the cleared status. pflash.read(off(offset), &mut got); pflash.write(off(offset), &[COMMAND_READ_STATUS]); pflash.read(off(offset), &mut got); let want = [0; 4]; assert_eq!(want, got); } #[test] fn overwrite() { let f = empty_image(); let data = [0]; let offset = off((16 * IMAGE_SIZE).try_into().unwrap()); // Ensure a write past the pflash device doesn't grow the backing file. let mut pflash = new(f); let old_size = pflash.image.get_len().unwrap(); assert_eq!(old_size, IMAGE_SIZE as u64); pflash.write(offset, &[COMMAND_WRITE_BYTE]); pflash.write(offset, &data); let new_size = pflash.image.get_len().unwrap(); assert_eq!(new_size, IMAGE_SIZE as u64); } }