| // 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. |
| |
| //! Implement hibernate suspend functionality |
| |
| use std::ffi::CString; |
| use std::mem::MaybeUninit; |
| use std::time::Instant; |
| |
| use anyhow::{Context, Result}; |
| use libc::{reboot, RB_POWER_OFF}; |
| use log::{debug, error, info}; |
| use sys_util::syscall; |
| |
| use crate::cookie::set_hibernate_cookie; |
| use crate::crypto::{CryptoMode, CryptoWriter}; |
| use crate::diskfile::{BouncedDiskFile, DiskFile}; |
| use crate::files::{ |
| create_hibernate_dir, does_hiberfile_exist, preallocate_header_file, preallocate_hiberfile, |
| preallocate_log_file, preallocate_metadata_file, HIBERNATE_DIR, |
| }; |
| use crate::hiberlog::{flush_log, redirect_log, replay_logs, reset_log, HiberlogFile, HiberlogOut}; |
| use crate::hibermeta::{HibernateMetadata, META_FLAG_ENCRYPTED, META_FLAG_VALID, META_TAG_SIZE}; |
| use crate::hiberutil::HibernateOptions; |
| use crate::hiberutil::{ |
| get_page_size, is_lvm_system, lock_process_memory, path_to_stateful_block, prealloc_mem, |
| HibernateError, BUFFER_PAGES, |
| }; |
| use crate::imagemover::ImageMover; |
| use crate::keyman::HibernateKeyManager; |
| use crate::snapdev::{FrozenUserspaceTicket, SnapshotDevice, SnapshotMode}; |
| use crate::splitter::ImageSplitter; |
| use crate::sysfs::Swappiness; |
| |
| /// Define the swappiness value we'll set during hibernation. |
| const SUSPEND_SWAPPINESS: i32 = 100; |
| /// Define how low stateful free space must be as a percentage before we clean |
| /// up the hiberfile after each hibernate. |
| const LOW_DISK_FREE_THRESHOLD_PERCENT: u64 = 10; |
| |
| /// The SuspendConductor weaves a delicate baton to guide us through the |
| /// symphony of hibernation. |
| pub struct SuspendConductor { |
| options: HibernateOptions, |
| metadata: HibernateMetadata, |
| } |
| |
| impl SuspendConductor { |
| /// Create a new SuspendConductor in preparation for imminent hibernation. |
| pub fn new() -> Result<Self> { |
| Ok(SuspendConductor { |
| options: Default::default(), |
| metadata: HibernateMetadata::new()?, |
| }) |
| } |
| |
| /// Public entry point that hibernates the system, and returns either upon |
| /// failure to hibernate or after the system has resumed from a successful |
| /// hibernation. |
| pub fn hibernate(&mut self, options: HibernateOptions) -> Result<()> { |
| info!("Beginning hibernate"); |
| let start = Instant::now(); |
| create_hibernate_dir()?; |
| let is_lvm = is_lvm_system()?; |
| let files_exist = does_hiberfile_exist(); |
| let should_zero = is_lvm && !files_exist; |
| let header_file = preallocate_header_file(should_zero)?; |
| let hiber_file = preallocate_hiberfile(should_zero)?; |
| let meta_file = preallocate_metadata_file(should_zero)?; |
| |
| // The resume log file needs to be preallocated now before the |
| // snapshot is taken, though it's not used here. |
| preallocate_log_file(HiberlogFile::Resume, should_zero)?; |
| let mut log_file = preallocate_log_file(HiberlogFile::Suspend, should_zero)?; |
| let duration = start.elapsed(); |
| info!( |
| "Set up {}hibernate files on {}LVM system in {}.{:03} seconds", |
| if files_exist { "" } else { "new " }, |
| if is_lvm { "" } else { "non-" }, |
| duration.as_secs(), |
| duration.subsec_millis() |
| ); |
| |
| self.options = options; |
| // Don't allow the logfile to log as it creates a deadlock. |
| log_file.set_logging(false); |
| let fs_stats = Self::get_fs_stats()?; |
| let _locked_memory = lock_process_memory()?; |
| let mut swappiness = Swappiness::new()?; |
| swappiness.set_swappiness(SUSPEND_SWAPPINESS)?; |
| let mut key_manager = HibernateKeyManager::new(); |
| // Set up the hibernate metadata encryption keys. This was populated |
| // at login time by a previous instance of this process. |
| if self.options.test_keys { |
| key_manager.use_test_keys()?; |
| } else { |
| key_manager.load_public_key()?; |
| } |
| |
| // Now that the public key is loaded, derive a metadata encryption key. |
| key_manager.install_new_metadata_key(&mut self.metadata)?; |
| |
| // Stop logging to syslog, and divert instead to a file since the |
| // logging daemon's about to be frozen. |
| redirect_log(HiberlogOut::File(Box::new(log_file))); |
| debug!("Syncing filesystems"); |
| // This is safe because sync() does not modify memory. |
| unsafe { |
| libc::sync(); |
| } |
| |
| prealloc_mem().context("Failed to preallocate memory for hibernate")?; |
| |
| let result = self.suspend_system(header_file, hiber_file, meta_file); |
| // Now send any remaining logs and future logs to syslog. |
| redirect_log(HiberlogOut::Syslog); |
| // Replay logs first because they happened earlier. |
| replay_logs( |
| result.is_ok() && !self.options.dry_run, |
| !self.options.dry_run, |
| ); |
| self.delete_data_if_disk_full(fs_stats); |
| result |
| } |
| |
| /// Inner helper function to actually take the snapshot, save it to disk, |
| /// and shut down. Returns upon a failure to hibernate, or after a |
| /// successful hibernation has resumed. |
| fn suspend_system( |
| &mut self, |
| header_file: DiskFile, |
| hiber_file: DiskFile, |
| meta_file: BouncedDiskFile, |
| ) -> Result<()> { |
| let mut snap_dev = SnapshotDevice::new(SnapshotMode::Read)?; |
| info!("Freezing userspace"); |
| let frozen_userspace = snap_dev.freeze_userspace()?; |
| self.snapshot_and_save(header_file, hiber_file, meta_file, frozen_userspace) |
| } |
| |
| /// Snapshot the system, write the result to disk, and power down. Returns |
| /// upon failure to hibernate, or after a hibernated system has successfully |
| /// resumed. |
| fn snapshot_and_save( |
| &mut self, |
| header_file: DiskFile, |
| hiber_file: DiskFile, |
| mut meta_file: BouncedDiskFile, |
| mut frozen_userspace: FrozenUserspaceTicket, |
| ) -> Result<()> { |
| let block_path = path_to_stateful_block()?; |
| let dry_run = self.options.dry_run; |
| let snap_dev = frozen_userspace.as_mut(); |
| let platform_mode = self.options.force_platform_mode; |
| snap_dev.set_platform_mode(platform_mode)?; |
| // This is where the suspend path and resume path fork. On success, |
| // both halves of these conditions execute, just at different times. |
| if snap_dev.atomic_snapshot()? { |
| // Suspend path. Everything after this point is invisible to the |
| // hibernated kernel. |
| self.write_image(header_file, hiber_file, snap_dev)?; |
| meta_file.rewind()?; |
| self.metadata.write_to_disk(&mut meta_file)?; |
| drop(meta_file); |
| // Set the hibernate cookie so the next boot knows to start in RO mode. |
| info!("Setting hibernate cookie at {}", block_path); |
| set_hibernate_cookie(Some(&block_path), true)?; |
| if dry_run { |
| info!("Not powering off due to dry run"); |
| } else if platform_mode { |
| info!("Entering S4"); |
| } else { |
| info!("Powering off"); |
| } |
| |
| // Flush out the hibernate log, and instead keep logs in memory. |
| // Any logs beyond here are lost upon powerdown. |
| flush_log(); |
| redirect_log(HiberlogOut::BufferInMemory); |
| // Power the thing down. |
| if !dry_run { |
| if platform_mode { |
| snap_dev.power_off()?; |
| } else { |
| Self::power_off()?; |
| } |
| |
| error!("Returned from power off"); |
| } |
| |
| // Unset the hibernate cookie. |
| info!("Unsetting hibernate cookie at {}", block_path); |
| set_hibernate_cookie(Some(&block_path), false)?; |
| } else { |
| // This is the resume path. First, forcefully reset the logger, which is some |
| // stale partial state that the suspend path ultimately flushed and closed. |
| // Keep logs in memory for now. |
| reset_log(); |
| redirect_log(HiberlogOut::BufferInMemory); |
| info!("Resumed from hibernate"); |
| } |
| |
| Ok(()) |
| } |
| |
| /// Save the snapshot image to disk. |
| fn write_image( |
| &mut self, |
| mut header_file: DiskFile, |
| mut hiber_file: DiskFile, |
| snap_dev: &mut SnapshotDevice, |
| ) -> Result<()> { |
| let image_size = snap_dev.get_image_size()?; |
| let page_size = get_page_size(); |
| let mode = if self.options.unencrypted { |
| CryptoMode::Unencrypted |
| } else { |
| CryptoMode::Encrypt |
| }; |
| let mut encryptor = CryptoWriter::new( |
| &mut hiber_file, |
| &self.metadata.data_key, |
| &self.metadata.data_iv, |
| mode, |
| page_size * BUFFER_PAGES, |
| )?; |
| |
| if !self.options.unencrypted { |
| self.metadata.flags |= META_FLAG_ENCRYPTED; |
| debug!("Added encryption"); |
| } |
| |
| debug!("Hibernate image is {} bytes", image_size); |
| let mut splitter = ImageSplitter::new(&mut header_file, &mut encryptor, &mut self.metadata); |
| let mut writer = ImageMover::new( |
| &mut snap_dev.file, |
| &mut splitter, |
| image_size, |
| page_size, |
| page_size * BUFFER_PAGES, |
| )?; |
| writer |
| .move_all() |
| .context("Failed to write out main image")?; |
| |
| info!("Wrote {} MB", image_size / 1024 / 1024); |
| |
| self.metadata.data_tag = encryptor.get_tag()?; |
| |
| assert!(self.metadata.data_tag != [0u8; META_TAG_SIZE]); |
| |
| self.metadata.image_size = image_size as u64; |
| self.metadata.flags |= META_FLAG_VALID; |
| Ok(()) |
| } |
| |
| /// Clean up the hibernate files, releasing that space back to other usermode apps. |
| fn delete_data_if_disk_full(&mut self, fs_stats: libc::statvfs) { |
| let free_percent = fs_stats.f_bfree * 100 / fs_stats.f_blocks; |
| if free_percent < LOW_DISK_FREE_THRESHOLD_PERCENT { |
| debug!("Freeing hiberdata: FS is only {}% free", free_percent); |
| // TODO: Unlink hiberfile and metadata. |
| } else { |
| debug!("Not freeing hiberfile: FS is {}% free", free_percent); |
| } |
| } |
| |
| /// Utility function to get the current stateful file system usage. |
| fn get_fs_stats() -> Result<libc::statvfs> { |
| let path = CString::new(HIBERNATE_DIR).unwrap(); |
| let mut stats: MaybeUninit<libc::statvfs> = MaybeUninit::zeroed(); |
| |
| // This is safe because only stats is modified. |
| syscall!(unsafe { libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) })?; |
| |
| // Safe because the syscall just initialized it, and we just verified |
| // the return was successful. |
| unsafe { Ok(stats.assume_init()) } |
| } |
| |
| /// Utility function to power the system down immediately. |
| fn power_off() -> Result<()> { |
| // This is safe because the system either ceases to exist, or does |
| // nothing to memory. |
| unsafe { |
| // On success, we shouldn't be executing, so the return code can be |
| // ignored because we already know it's a failure. |
| let _ = reboot(RB_POWER_OFF); |
| Err(HibernateError::ShutdownError(sys_util::Error::last())) |
| .context("Failed to shut down") |
| } |
| } |
| } |