| // Copyright 2021 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! Implements common functions and definitions used throughout the app and library. |
| |
| use std::convert::TryInto; |
| use std::ffi::CString; |
| use std::ffi::OsStr; |
| use std::fs; |
| use std::fs::File; |
| use std::io::prelude::*; |
| use std::io::BufReader; |
| use std::mem::MaybeUninit; |
| use std::os::unix::ffi::OsStrExt; |
| use std::path::PathBuf; |
| use std::process::exit; |
| use std::process::Command; |
| use std::str; |
| use std::time::Duration; |
| |
| use anyhow::Context; |
| use anyhow::Result; |
| use libc::c_ulong; |
| use libc::c_void; |
| use libchromeos::sys::syscall; |
| use log::debug; |
| use log::error; |
| use log::info; |
| use log::warn; |
| use thiserror::Error as ThisError; |
| |
| use crate::cookie::set_hibernate_cookie; |
| use crate::cookie::HibernateCookieValue; |
| use crate::files::HIBERMETA_DIR; |
| use crate::hiberlog::redirect_log; |
| use crate::hiberlog::HiberlogOut; |
| use crate::metrics::MetricsLogger; |
| use crate::metrics::MetricsSample; |
| use crate::mmapbuf::MmapBuffer; |
| |
| const KEYCTL_PATH: &str = "/bin/keyctl"; |
| |
| /// Define the hibernate stages. |
| pub enum HibernateStage { |
| Suspend, |
| Resume, |
| } |
| |
| #[derive(Debug, ThisError)] |
| pub enum HibernateError { |
| /// Cookie error |
| #[error("Cookie error: {0}")] |
| CookieError(String), |
| /// Insufficient Memory available. |
| #[error("Not enough free memory and swap")] |
| InsufficientMemoryAvailableError(), |
| /// Failed to send metrics |
| #[error("Failed to send metrics: {0}")] |
| MetricsSendFailure(String), |
| /// Failed to lock process memory. |
| #[error("Failed to mlockall: {0}")] |
| MlockallError(libchromeos::sys::Error), |
| /// Mmap error. |
| #[error("mmap error: {0}")] |
| MmapError(libchromeos::sys::Error), |
| /// Snapshot device error. |
| #[error("Snapshot device error: {0}")] |
| SnapshotError(String), |
| /// Snapshot ioctl error. |
| #[error("Snapshot ioctl error: {0}: {1}")] |
| SnapshotIoctlError(String, libchromeos::sys::Error), |
| /// Mount not found. |
| #[error("Mount not found")] |
| MountNotFoundError(), |
| /// Swap information not found. |
| #[error("Swap information not found")] |
| SwapInfoNotFoundError(), |
| /// Failed to shut down |
| #[error("Failed to shut down: {0}")] |
| ShutdownError(libchromeos::sys::Error), |
| /// Hibernate volume error |
| #[error("Hibernate volume error")] |
| HibernateVolumeError(), |
| /// Spawned process error |
| #[error("Spawned process error: {0}")] |
| SpawnedProcessError(i32), |
| /// Index out of range error |
| #[error("Index out of range")] |
| IndexOutOfRangeError(), |
| /// Device mapper error |
| #[error("Device mapper error: {0}")] |
| DeviceMapperError(String), |
| /// Merge timeout error |
| #[error("Merge timeout error")] |
| MergeTimeoutError(), |
| /// Update engine busy error |
| #[error("Update engine busy")] |
| UpdateEngineBusyError(), |
| /// Key retrieve error |
| #[error("Unable to retrieve crypto key")] |
| KeyRetrievalError(), |
| /// Syscall stat error |
| #[error("Snapshot stat error: {0}")] |
| SnapshotStatDeviceError(libchromeos::sys::Error), |
| } |
| |
| /// Options taken from the command line affecting hibernate. |
| #[derive(Default)] |
| pub struct HibernateOptions { |
| pub dry_run: bool, |
| pub reboot: bool, |
| } |
| |
| /// Options taken from the command line affecting resume-init. |
| #[derive(Default)] |
| pub struct ResumeInitOptions { |
| pub force: bool, |
| } |
| |
| /// Options taken from the command line affecting resume. |
| #[derive(Default)] |
| pub struct ResumeOptions { |
| pub dry_run: bool, |
| } |
| |
| /// Options taken from the command line affecting abort-resume. |
| pub struct AbortResumeOptions { |
| pub reason: String, |
| } |
| |
| impl Default for AbortResumeOptions { |
| fn default() -> Self { |
| Self { |
| reason: "Manually aborted by hiberman abort-resume".to_string(), |
| } |
| } |
| } |
| |
| /// Get a device id from the path. |
| pub fn get_device_id<P: AsRef<std::path::Path>>(path: P) -> Result<u32> { |
| let path_str_c = CString::new(path.as_ref().as_os_str().as_bytes())?; |
| let mut stats: MaybeUninit<libc::stat> = MaybeUninit::zeroed(); |
| |
| // This is safe because only stats is modified. |
| if syscall!(unsafe { libc::stat(path_str_c.as_ptr(), stats.as_mut_ptr()) }).is_err() { |
| return Err(HibernateError::SnapshotStatDeviceError( |
| libchromeos::sys::Error::last(), |
| )) |
| .context("Failed to stat device"); |
| } |
| |
| // Safe because the syscall just initialized it, and we just verified |
| // the return was successful. |
| unsafe { Ok(stats.assume_init().st_rdev as u32) } |
| } |
| |
| /// Get the page size on this system. |
| pub fn get_page_size() -> usize { |
| // Safe because sysconf() returns a long and has no other side effects. |
| unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize } |
| } |
| |
| /// Get the amount of free memory (in pages) on this system. |
| pub fn get_available_pages() -> usize { |
| // Safe because sysconf() returns a long and has no other side effects. |
| unsafe { libc::sysconf(libc::_SC_AVPHYS_PAGES) as usize } |
| } |
| |
| /// Get the total amount of memory (in pages) on this system. |
| pub fn get_total_memory_pages() -> usize { |
| // Safe because sysconf() returns a long and has no other side effects. |
| let pagecount = unsafe { libc::sysconf(libc::_SC_PHYS_PAGES) as usize }; |
| if pagecount == 0 { |
| warn!( |
| "Failed to get total memory (got {}). Assuming 4GB.", |
| pagecount |
| ); |
| // Just return 4GB worth of pages if the result is unknown, the minimum |
| // we're ever going to see on a hibernating system. |
| let pages_per_mb = 1024 * 1024 / get_page_size(); |
| let pages_per_gb = pages_per_mb * 1024; |
| return pages_per_gb * 4; |
| } |
| |
| pagecount |
| } |
| |
| /// Helper function to get the amount of free physical memory on this system, |
| /// in megabytes. |
| fn get_available_memory_mb() -> u32 { |
| let pagesize = get_page_size() as u64; |
| let pagecount = get_available_pages() as u64; |
| |
| let mb = pagecount * pagesize / (1024 * 1024); |
| mb.try_into().unwrap_or(u32::MAX) |
| } |
| |
| // Helper function to get the amount of free swap on this system. |
| fn get_available_swap_mb() -> Result<u32> { |
| // Look in /proc/meminfo to find swap info. |
| let f = File::open("/proc/meminfo")?; |
| let buf_reader = BufReader::new(f); |
| for line in buf_reader.lines().flatten() { |
| let mut split = line.split_whitespace(); |
| let arg = split.next(); |
| let value = split.next(); |
| if let Some(arg) = arg { |
| if arg == "SwapFree:" { |
| if let Some(value) = value { |
| let swap_free: u32 = value.parse().context("Failed to parse SwapFree value")?; |
| let mb: u32 = swap_free / 1024; |
| return Ok(mb); |
| } |
| } |
| } |
| } |
| Err(HibernateError::SwapInfoNotFoundError()) |
| .context("Failed to find available swap information") |
| } |
| |
| // Preallocate memory that will be needed for the hibernate snapshot. |
| // Currently the kernel is not always able to reclaim memory effectively |
| // when allocating the memory needed for the hibernate snapshot. By |
| // preallocating this memory, we force memory to be swapped into zram and |
| // ensure that we have the free memory needed for the snapshot. |
| pub fn prealloc_mem(metrics_logger: &mut MetricsLogger) -> Result<()> { |
| let available_mb = get_available_memory_mb(); |
| let available_swap = get_available_swap_mb()?; |
| let total_avail = available_mb + available_swap; |
| debug!( |
| "System has {} MB of free memory, {} MB of swap free", |
| available_mb, available_swap |
| ); |
| let memory_pages = get_total_memory_pages(); |
| let hiber_pages = memory_pages / 2; |
| let page_size = get_page_size(); |
| let hiber_size = page_size * hiber_pages; |
| let hiber_mb = hiber_size / (1024 * 1024); |
| let mut extra_mb = (available_mb + available_swap) as isize - hiber_mb as isize; |
| let mut shortfall_mb = 0; |
| if extra_mb < 0 { |
| shortfall_mb = -extra_mb; |
| extra_mb = 0; |
| } |
| metrics_logger.log_metric(MetricsSample { |
| name: "Platform.Hibernate.MemoryAvailable", |
| value: available_mb as isize, |
| min: 0, |
| max: 16384, |
| buckets: 50, |
| }); |
| metrics_logger.log_metric(MetricsSample { |
| name: "Platform.Hibernate.MemoryAndSwapAvailable", |
| value: total_avail as isize, |
| min: 0, |
| max: 32768, |
| buckets: 50, |
| }); |
| metrics_logger.log_metric(MetricsSample { |
| name: "Platform.Hibernate.AdditionalMemoryNeeded", |
| value: shortfall_mb, |
| min: 0, |
| max: 16384, |
| buckets: 50, |
| }); |
| metrics_logger.log_metric(MetricsSample { |
| name: "Platform.Hibernate.ExcessMemoryAvailable", |
| value: extra_mb, |
| min: 0, |
| max: 16384, |
| buckets: 50, |
| }); |
| |
| if hiber_mb > (total_avail).try_into().unwrap() { |
| return Err(HibernateError::InsufficientMemoryAvailableError()) |
| .context("Not enough free memory and swap space for hibernate"); |
| } |
| debug!( |
| "System has {} pages of memory, preallocating {} pages for hibernate", |
| memory_pages, hiber_pages |
| ); |
| |
| let mut buffer = |
| MmapBuffer::new(hiber_size).context("Failed to create buffer for memory allocation")?; |
| let buf = buffer.u8_slice_mut(); |
| let mut i = 0; |
| while i < buf.len() { |
| buf[i] = 0; |
| i += page_size |
| } |
| |
| let available_mb_after = get_available_memory_mb(); |
| let available_swap_after = get_available_swap_mb()?; |
| debug!( |
| "System has {} MB of free memory, {} MB of free swap after giant allocation", |
| available_mb_after, available_swap_after |
| ); |
| |
| drop(buffer); |
| let available_mb_final = get_available_memory_mb(); |
| let available_swap_final = get_available_swap_mb()?; |
| debug!( |
| "System has {} MB of free memory, {} MB of free swap after freeing giant allocation", |
| available_mb_final, available_swap_final |
| ); |
| Ok(()) |
| } |
| |
| /// Look through /proc/mounts to find the block device supporting the |
| /// given directory. The directory must be the root of a mount. |
| pub fn get_device_mounted_at_dir(mount_path: &str) -> Result<String> { |
| // Go look through the mounts to see where the given mount is. |
| let f = File::open("/proc/mounts")?; |
| let buf_reader = BufReader::new(f); |
| for line in buf_reader.lines().flatten() { |
| let mut split = line.split_whitespace(); |
| let blk = split.next(); |
| let path = split.next(); |
| if let Some(path) = path { |
| if path == mount_path { |
| if let Some(blk) = blk { |
| return Ok(blk.to_string()); |
| } |
| } |
| } |
| } |
| |
| Err(HibernateError::MountNotFoundError()) |
| .context(format!("Failed to find mount at {}", mount_path)) |
| } |
| |
| /// Return the path to partition one (stateful) on the root block device. |
| pub fn stateful_block_partition_one() -> Result<String> { |
| let rootdev = path_to_stateful_block()?; |
| let last = rootdev.chars().last(); |
| if let Some(last) = last { |
| if last.is_numeric() { |
| return Ok(format!("{}p1", rootdev)); |
| } |
| } |
| |
| Ok(format!("{}1", rootdev)) |
| } |
| |
| /// Determine the path to the block device containing the stateful partition. |
| /// Farm this out to rootdev to keep the magic in one place. |
| pub fn path_to_stateful_block() -> Result<String> { |
| let output = checked_command_output(Command::new("/usr/bin/rootdev").args(["-d", "-s"])) |
| .context("Cannot get rootdev")?; |
| Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) |
| } |
| |
| /// Determines if the stateful-rw snapshot is active, indicating a resume boot. |
| pub fn is_snapshot_active() -> bool { |
| fs::metadata("/dev/mapper/stateful-rw").is_ok() |
| } |
| |
| pub struct LockedProcessMemory {} |
| |
| impl Drop for LockedProcessMemory { |
| fn drop(&mut self) { |
| unlock_process_memory(); |
| } |
| } |
| |
| /// Lock all present and future memory belonging to this process, preventing it |
| /// from being paged out. Returns a LockedProcessMemory token, which undoes the |
| /// operation when dropped. |
| pub fn lock_process_memory() -> Result<LockedProcessMemory> { |
| // This is safe because mlockall() does not modify memory, it only ensures |
| // it doesn't get swapped out, which maintains Rust's safety guarantees. |
| let rc = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) }; |
| |
| if rc < 0 { |
| return Err(HibernateError::MlockallError( |
| libchromeos::sys::Error::last(), |
| )) |
| .context("Cannot lock process memory"); |
| } |
| |
| Ok(LockedProcessMemory {}) |
| } |
| |
| /// Unlock memory belonging to this process, allowing it to be paged out once |
| /// more. |
| fn unlock_process_memory() { |
| // This is safe because while munlockall() is a foreign function, it has |
| // no immediately observable side effects on program execution. |
| unsafe { |
| libc::munlockall(); |
| }; |
| } |
| |
| /// Log a duration with level info in the form: <action> in X.YYY seconds. |
| pub fn log_duration(action: &str, duration: Duration) { |
| info!( |
| "{} in {}.{:03} seconds", |
| action, |
| duration.as_secs(), |
| duration.subsec_millis() |
| ); |
| } |
| |
| /// Log a duration with an I/O rate at level info in the form: |
| /// <action> in X.YYY seconds, N bytes, A.BB MB/s. |
| pub fn log_io_duration(action: &str, io_bytes: u64, duration: Duration) { |
| let rate = ((io_bytes as f64) / duration.as_secs_f64()) / 1048576.0; |
| info!( |
| "{} in {}.{:03} seconds, {} bytes, {:.3} MB/s", |
| action, |
| duration.as_secs(), |
| duration.subsec_millis(), |
| io_bytes, |
| rate |
| ); |
| } |
| |
| /// Wait for a std::process::Command, and convert the exit status into a Result |
| pub fn checked_command(command: &mut std::process::Command) -> Result<()> { |
| let mut child = command.spawn().context("Failed to spawn child process")?; |
| let exit_status = child.wait().context("Failed to wait for child")?; |
| if exit_status.success() { |
| Ok(()) |
| } else { |
| let code = exit_status.code().unwrap_or(-2); |
| Err(HibernateError::SpawnedProcessError(code)).context(format!( |
| "Command {} failed with code {}", |
| command.get_program().to_string_lossy(), |
| &code |
| )) |
| } |
| } |
| |
| /// Wait for a std::process::Command, convert its exit status into a Result, and |
| /// collect the output on success. |
| pub fn checked_command_output(command: &mut std::process::Command) -> Result<std::process::Output> { |
| let output = command |
| .output() |
| .context("Failed to get output for child process")?; |
| let exit_status = output.status; |
| if exit_status.success() { |
| Ok(output) |
| } else { |
| let code = exit_status.code().unwrap_or(-2); |
| Err(HibernateError::SpawnedProcessError(code)).context(format!( |
| "Command {} failed with code {}", |
| command.get_program().to_string_lossy(), |
| &code |
| )) |
| } |
| } |
| |
| /// Perform emergency bailout procedures (like syncing logs), set the cookie to |
| /// indicate something went very wrong, and reboot the system. |
| pub fn emergency_reboot(reason: &str) { |
| error!("Performing emergency reboot: {}", reason); |
| // Attempt to set the cookie, but do not stop if it fails. |
| if let Err(e) = set_hibernate_cookie::<PathBuf>(None, HibernateCookieValue::EmergencyReboot) { |
| error!("Failed to set cookie to EmergencyReboot: {}", e); |
| } |
| // Redirect the log to in-memory, which flushes out any pending logs if |
| // logging is already directed to a file. |
| redirect_log(HiberlogOut::BufferInMemory); |
| reboot_system().unwrap(); |
| // Exit with a weird error code to avoid going through this path multiple |
| // times. |
| exit(9); |
| } |
| |
| /// Perform an orderly reboot. |
| fn reboot_system() -> Result<()> { |
| error!("Rebooting system!"); |
| checked_command(&mut Command::new("/sbin/reboot")).context("Failed to reboot system") |
| } |
| |
| pub fn mount_filesystem<P: AsRef<OsStr>>( |
| block_device: P, |
| mountpoint: P, |
| fs_type: &str, |
| flags: u64, |
| data: &str, |
| ) -> Result<()> { |
| let bdev_cstr = CString::new(block_device.as_ref().as_bytes())?; |
| let mp_cstr = CString::new(mountpoint.as_ref().as_bytes())?; |
| let fs_cstr = CString::new(fs_type)?; |
| let data_cstr = CString::new(data)?; |
| |
| debug!( |
| "Mounting {} to {}", |
| bdev_cstr.to_string_lossy(), |
| mp_cstr.to_string_lossy() |
| ); |
| |
| // This is safe because mount does not affect memory layout. |
| unsafe { |
| let rc = libc::mount( |
| bdev_cstr.as_ptr(), |
| mp_cstr.as_ptr(), |
| fs_cstr.as_ptr(), |
| flags as c_ulong, |
| data_cstr.as_ptr() as *const c_void, |
| ); |
| |
| if rc < 0 { |
| return Err(libchromeos::sys::Error::last()) |
| .context(format!("Failed to mount {}", bdev_cstr.to_string_lossy())); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| pub fn unmount_filesystem<P: AsRef<OsStr>>(mountpoint: P) -> Result<()> { |
| let mp_cstr = CString::new(mountpoint.as_ref().as_bytes())?; |
| |
| debug!("Unmounting {}", mp_cstr.to_string_lossy()); |
| |
| // This is safe because unmount does not affect memory. |
| unsafe { |
| let rc = libc::umount(mp_cstr.as_ptr()); |
| if rc < 0 { |
| return Err(libchromeos::sys::Error::last()) |
| .context(format!("Failed to unmount {}", mp_cstr.to_string_lossy())); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Add a logon key to the kernel key ring |
| pub fn keyctl_add_key(description: &str, key_data: &[u8]) -> Result<()> { |
| checked_command(Command::new(KEYCTL_PATH).args([ |
| "add", |
| "-x", |
| "logon", |
| description, |
| &hex::encode(key_data), |
| "@s", |
| ])) |
| .context(format!( |
| "Failed to add key '{description}' to the kernel key ring" |
| )) |
| } |
| |
| /// Remove a logon key from the kernel key ring |
| pub fn keyctl_remove_key(description: &str) -> Result<()> { |
| checked_command(Command::new(KEYCTL_PATH).args(["purge", "-s", "logon", description])).context( |
| format!("Failed to remove key '{description}' from the kernel key ring"), |
| ) |
| } |
| |
| /// Provides an API for recording and reading timestamps from disk. |
| pub struct TimestampFile {} |
| |
| impl TimestampFile { |
| /// Record a timestamp to a file. |
| pub fn record_timestamp(name: &str, timestamp: &Duration) -> Result<()> { |
| let path = Self::full_path(name); |
| |
| let mut f = File::options() |
| .create(true) |
| .truncate(true) |
| .write(true) |
| .open(&path)?; |
| |
| f.write_all(timestamp.as_millis().to_string().as_bytes()) |
| .context(format!("Failed to write timestamp to {}", path.display())) |
| } |
| |
| /// Read a timestamp from a file. |
| pub fn read_timestamp(name: &str) -> Result<Duration> { |
| let path = Self::full_path(name); |
| let ts = fs::read_to_string(&path) |
| .context(format!("Failed to read timestamp from {}", path.display()))?; |
| let millis = |
| ts.parse() |
| .context(format!("invalid timestamp in {}: {}", path.display(), ts))?; |
| |
| Ok(Duration::from_millis(millis)) |
| } |
| |
| fn full_path(name: &str) -> PathBuf { |
| PathBuf::from(format!("/{HIBERMETA_DIR}/{name}")) |
| } |
| } |