blob: 0a19fbe8529e16bc40f20c1771acc0138fbe868e [file] [log] [blame]
// Copyright 2021 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.
//! Manages the "valid resume image" cookie.
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
use anyhow::{Context, Result};
use crate::hiberutil::{path_to_stateful_block, HibernateError};
use crate::mmapbuf::MmapBuffer;
/// The hibernate cookie is a flag stored at a known location on disk. The early
/// init scripts use this flag to determine whether or not to mount the stateful
/// partition in snapshot mode for resume, or normal read/write mode for a
/// traditional fresh boot. Normally this sort of cookie would be stored as a
/// regular file in the stateful partition itself. But we can't exactly do that
/// because this is the indicator used to determine _how_ to mount the RW
/// file systems.
///
/// This implementation currently stores the flag as a well-known string inside
/// the leftover space at the end of the sector containing the GPT header. This
/// space is ideal because its location is fixed, it's not manipulated in normal
/// circumstances, and the GPT header format is unlikely to change and start
/// using this space.
struct HibernateCookie {
blockdev: File,
buffer: MmapBuffer,
}
/// Define the size of the region we update.
const COOKIE_READ_SIZE: usize = 0x400;
const COOKIE_WRITE_SIZE: usize = 0x400;
/// Define the magic value the GPT stamps down, which we will use to verify
/// we're writing to an area that we expect. If somehow the world shifted out
/// from under us, this could prevent us from silently corrupting data.
const GPT_MAGIC_OFFSET: usize = 0x200;
const GPT_MAGIC: u64 = 0x5452415020494645; // 'EFI PART'
/// The beginning of the disk starts with a protective MBR, followed by a sector
/// just for the GPT header. The GPT header is quite small and doesn't use its
/// whole sector. Define the offset towards the end of the region where the
/// cookie will be written.
const COOKIE_MAGIC_OFFSET: usize = 0x3E0;
/// Define the magic token we write to indicate a valid hibernate partition.
/// This is both big (as in bigger than a single bit), and points the finger at
/// an obvious culprit, in the case this does end up unintentionally writing
/// over important data. This is made arbitrarily, but intentionally, to be 16
/// bytes.
const COOKIE_VALID_VALUE: &[u8] = b"HibernateCookie!";
/// Define a known "not valid" value as well. This is treated identically to
/// anything else that is invalid, but again could serve as a more useful
/// breadcrumb to someone debugging than 16 vanilla zeroes.
const COOKIE_POISON_VALUE: &[u8] = b"HibernateInvalid";
/// Define the size of the magic token, in bytes.
const COOKIE_SIZE: usize = 16;
impl HibernateCookie {
/// Create a new HibernateCookie structure. This allocates resources but
/// does not attempt to read or write the disk.
pub fn new<P: AsRef<Path>>(path: P) -> Result<HibernateCookie> {
let blockdev = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_DIRECT | libc::O_SYNC)
.open(path)
.context("Failed to open hibernate cookie")?;
let buffer = MmapBuffer::new(COOKIE_READ_SIZE)?;
Ok(HibernateCookie { blockdev, buffer })
}
/// Read the contents of the disk to determine if the cookie is set or not.
/// On success, returns a boolean that is true if the hibernate cookie is
/// set (indicating the on-disk file systems should not be altered).
pub fn is_set(&mut self) -> Result<bool> {
self.blockdev
.seek(SeekFrom::Start(0))
.context("Failed to seek in hibernate cookie")?;
let buffer_slice = self.buffer.u8_slice_mut();
self.blockdev
.read_exact(&mut buffer_slice[..COOKIE_READ_SIZE])
.context("Failed to read hibernate cookie")?;
// Verify there's a GPT header magic where there should be one.
// This would catch cases like writing to the wrong place or the
// GPT layout/location changing. This might need enlightenment for a
// disk with 4kb blocks, this check will let us know that too.
let gpt_sig_offset = GPT_MAGIC_OFFSET;
let gpt_sig_offset_end = gpt_sig_offset + 8;
let mut gpt_sig = [0u8; 8];
let buffer_slice = self.buffer.u8_slice();
gpt_sig.copy_from_slice(&buffer_slice[gpt_sig_offset..gpt_sig_offset_end]);
let gpt_sig = u64::from_le_bytes(gpt_sig);
if gpt_sig != GPT_MAGIC {
return Err(HibernateError::CookieError(format!(
"GPT magic not found: {:x?}",
gpt_sig
)))
.context("Failed to verify GPT magic");
}
let magic_start = COOKIE_MAGIC_OFFSET;
let magic_end = magic_start + COOKIE_SIZE;
let equal = buffer_slice[magic_start..magic_end] == *COOKIE_VALID_VALUE;
Ok(equal)
}
/// Write the hibernate cookie to disk via a fresh read modify write
/// operation. The valid parameter indicates whether to write a valid
/// hibernate cookie (true, indicating on-disk file systems should be
/// altered), or poison value (false, indicating no impending hibernate
/// resume, file systems can be mounted RW).
pub fn write(&mut self, valid: bool) -> Result<()> {
let existing = self.is_set()?;
self.blockdev
.seek(SeekFrom::Start(0))
.context("Failed to seek hibernate cookie")?;
if valid == existing {
return Ok(());
}
let magic_start = COOKIE_MAGIC_OFFSET;
let magic_end = magic_start + COOKIE_SIZE;
let cookie = if valid {
COOKIE_VALID_VALUE
} else {
COOKIE_POISON_VALUE
};
let buffer_slice = self.buffer.u8_slice_mut();
buffer_slice[magic_start..magic_end].copy_from_slice(cookie);
let end = COOKIE_WRITE_SIZE;
self.blockdev
.write_all(&buffer_slice[..end])
.context("Failed to write hibernate cookie")?;
self.blockdev
.flush()
.context("Failed to flush hibernate cookie")?;
self.blockdev
.sync_all()
.context("Failed to sync hibernate cookie")?;
Ok(())
}
}
/// Public function to read the hibernate cookie and return whether or not it is
/// set. The optional path parameter contains the path to the disk to examine.
/// If not supplied, the boot disk will be examined.
pub fn get_hibernate_cookie<P: AsRef<Path>>(path_str: Option<P>) -> Result<bool> {
let mut cookie = open_hibernate_cookie(path_str)?;
cookie.is_set()
}
/// Public function to set the hibernate cookie value. The valid parameter, if
/// true, indicates that upon the next boot file systems should not be altered
/// on disk, since there's a valid resume image. The optional path parameter
/// contains the path to the disk to examine.
pub fn set_hibernate_cookie<P: AsRef<Path>>(path: Option<P>, valid: bool) -> Result<()> {
let mut cookie = open_hibernate_cookie(path)?;
cookie.write(valid)
}
fn open_hibernate_cookie<P: AsRef<Path>>(path_ref: Option<P>) -> Result<HibernateCookie> {
if let Some(path) = path_ref {
HibernateCookie::new(path)
} else {
HibernateCookie::new(path_to_stateful_block()?)
}
}