Files
shorebird-updater/library/src/cache/patch_manager.rs
T
Bryan Oltman 9294c545e8 feat: prepend all log messages with "[shorebird]" (#221)
* feat: prepend all log messages with "[shorebird]"

* update doc

* PR feedback
2024-10-30 11:53:20 -04:00

1210 lines
44 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::{disk_io, signing, PatchInfo};
use anyhow::{bail, Context, Result};
use core::fmt::Debug;
use serde::{Deserialize, Serialize};
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
#[cfg(test)]
use mockall::automock;
#[cfg(test)]
use tempdir::TempDir;
const PATCHES_DIR_NAME: &str = "patches";
const PATCHES_STATE_FILE_NAME: &str = "patches_state.json";
const PATCH_ARTIFACT_FILENAME: &str = "dlc.vmcode";
/// Information about a patch that is persisted to disk.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
struct PatchMetadata {
/// The number of the patch.
number: usize,
/// The size of the patch artifact on disk.
size: u64,
/// The hash of the patch artifact on disk.
hash: String,
/// The signature of `hash`.
signature: Option<String>,
}
/// What gets serialized to disk
#[derive(Debug, Default, Deserialize, Serialize)]
struct PatchesState {
/// The patch we are currently running, if any.
last_booted_patch: Option<PatchMetadata>,
/// The patch that will be run on the next app boot, if any. This may be the same
/// as the last booted patch patch if no new patch has been downloaded.
next_boot_patch: Option<PatchMetadata>,
/// This is given a value when we start booting a patch (record_boot_start_for_patch) and is
/// cleared when:
/// - the patch boots successfully (record_boot_success)
/// - the patch fails to boot (record_boot_failure_for_patch)
/// - the system initializes (on_init, we take this to mean the patch failed to boot)
currently_booting_patch: Option<PatchMetadata>,
/// A list of patch numbers that we have tried and failed to install.
/// We should never attempt to download or install these again for the
/// current release.
known_bad_patches: HashSet<usize>,
}
/// Abstracts the storage of patches on disk.
///
/// The impementation of this (PatchManager) should only be responsible for translating what is on
/// disk into a form that is useful for the updater and vice versa. Some business logic has crept in
/// in the form of validation, and we should consider moving that into a separate module.
#[cfg_attr(test, automock)]
pub trait ManagePatches {
/// Copies the patch file at file_path to the manager's directory structure sets
/// this patch as the next patch to boot.
///
/// The explicit lifetime is required for automock to work with Options.
/// See https://github.com/asomers/mockall/issues/61.
#[allow(clippy::needless_lifetimes)]
fn add_patch<'a>(
&mut self,
number: usize,
file_path: &Path,
hash: &str,
signature: Option<&'a str>,
) -> Result<()>;
/// Returns the patch we most recently successfully booted from (usually the currently running patch),
/// or None if no patch is installed.
fn last_successfully_booted_patch(&self) -> Option<PatchInfo>;
/// The patch we are currently booting, if any. This will only have a value:
/// 1. Between record_boot_start_for_patch and record_boot_success or record_boot_failure_for_patch
/// 2. On init if we attempted to boot a patch but never recorded a successful boot (e.g., because
/// the system crashed).
fn currently_booting_patch(&self) -> Option<PatchInfo>;
/// Returns the next patch to boot, or None if:
/// - no patches have been downloaded
/// - we cannot boot from the patch(es) on disk
fn next_boot_patch(&mut self) -> Option<PatchInfo>;
/// Record that we're booting. If we have a next path, updates the last
/// attempted patch to be the next boot patch.
fn record_boot_start_for_patch(&mut self, patch_number: usize) -> Result<()>;
/// Marks last_attempted_patch as "good", updates last_booted_patch to be the same,
/// and deletes all patch artifacts older than the last_booted_patch.
fn record_boot_success(&mut self) -> Result<()>;
/// Records that the patch with number patch_number failed to boot, and ensures
/// that it will never be returned as the next boot or last booted patch.
fn record_boot_failure_for_patch(&mut self, patch_number: usize) -> Result<()>;
/// Whether we have failed to boot from the patch with `patch_number`.
fn is_known_bad_patch(&self, patch_number: usize) -> bool;
/// Deletes artifacts for the provided patch_number if they exist.
/// If the patch is the next_boot_patch, it is cleared.
fn remove_patch(&mut self, patch_number: usize) -> Result<()>;
/// Resets the patch manager to its initial state, removing all patches. This is
/// intended to be used when a new release version is installed.
fn reset(&mut self) -> Result<()>;
}
// This allows us to use the Debug trait on dyn ManagePatches, which is
// required to have it as a property of UpdaterState.
impl Debug for dyn ManagePatches {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "ManagePatches")
}
}
#[derive(Debug)]
pub struct PatchManager {
/// The base directory used to store patch artifacts and state.
/// The directory structure created within this directory is:
/// patches_state.json
/// patches/
/// <patch_number>/
/// dlc.vmcode
/// <patch_number>/
/// dlc.vmcode
root_dir: PathBuf,
/// Metadata about the patches we have downloaded that is persisted to disk.
patches_state: PatchesState,
/// The key used to sign patch hashes for the current release, if any. If this is
/// not None, all patches must have a signature that can be verified with this key.
patch_public_key: Option<String>,
}
impl PatchManager {
/// Creates a new PatchManager with the given root directory. This directory is
/// assumed to exist. The PatchManager will use this directory to store its
/// state and patch binaries.
pub fn new(root_dir: PathBuf, patch_public_key: Option<&str>) -> Self {
let patches_state = Self::load_patches_state(&root_dir).unwrap_or_default();
Self {
root_dir,
patches_state,
patch_public_key: patch_public_key.map(|s| s.to_owned()),
}
}
fn load_patches_state(root_dir: &Path) -> Option<PatchesState> {
let path = root_dir.join(PATCHES_STATE_FILE_NAME);
match disk_io::read(&path) {
Ok(maybe_state) => maybe_state,
Err(e) => {
shorebird_debug!(
"Failed to load patches state from {}: {}",
path.display(),
e
);
None
}
}
}
fn save_patches_state(&self) -> Result<()> {
let path = self.root_dir.join(PATCHES_STATE_FILE_NAME);
disk_io::write(&self.patches_state, &path)
}
/// The directory where all patch artifacts are stored.
fn patches_dir(&self) -> PathBuf {
self.root_dir.join(PATCHES_DIR_NAME)
}
/// The directory where artifacts for the patch with the given number are stored.
fn patch_dir(&self, patch_number: usize) -> PathBuf {
self.patches_dir().join(patch_number.to_string())
}
/// The path to the runnable patch artifact with the given number. Runnable patch artifact files are
/// named <patch_number>.vmcode
fn patch_artifact_path(&self, patch_number: usize) -> PathBuf {
self.patch_dir(patch_number).join(PATCH_ARTIFACT_FILENAME)
}
fn patch_info_for_number(&self, patch_number: usize) -> PatchInfo {
PatchInfo {
path: self.patch_artifact_path(patch_number),
number: patch_number,
}
}
/// Checks that the patch with the given number:
/// - Has an artifact on disk
/// - That artifact on disk is the same size it was when it was installed
///
/// Returns Ok if the patch is bootable, or an error if it is not.
fn validate_patch_is_bootable(&self, patch: &PatchMetadata) -> Result<()> {
let artifact_path = self.patch_artifact_path(patch.number);
if !Path::exists(&artifact_path) {
bail!(
"Patch {} does not exist at {}",
patch.number,
artifact_path.display()
);
}
let artifact_size_on_disk = std::fs::metadata(&artifact_path)?.len();
if artifact_size_on_disk != patch.size {
bail!(
"Patch {} has size {} on disk, but expected size {}",
patch.number,
artifact_size_on_disk,
patch.size
);
}
if let Some(public_key) = &self.patch_public_key {
// If we have a public key, verify that the patch's hash has a signature.
let signature = patch
.signature
.clone()
.context("Patch signature is missing")?;
// Check that the signature is valid.
let patch_hash = signing::hash_file(&artifact_path)?;
signing::check_signature(&patch_hash, &signature, public_key)?;
} else {
shorebird_info!("No public key provided, skipping signature verification");
}
Ok(())
}
fn delete_patch_artifacts(&mut self, patch_number: usize) -> Result<()> {
let patch_dir = self.patch_dir(patch_number);
if !patch_dir.exists() {
shorebird_debug!("Patch {} not installed, nothing to delete", patch_number);
return Ok(());
}
shorebird_info!("Deleting patch artifacts for patch {}", patch_number);
std::fs::remove_dir_all(&patch_dir)
.map_err(|e| {
shorebird_error!("Failed to delete patch dir {}: {}", patch_dir.display(), e);
e
})
.with_context(|| format!("Failed to delete patch dir {}", &patch_dir.display()))
}
/// Deletes artifacts for the provided bad_patch_number and attempts to set the next_boot_patch to the last
/// successfully booted patch. If the last successfully booted patch is not bootable or has the same number
/// as the patch we're falling back from, we clear it as well.
fn try_fall_back_from_patch(&mut self, bad_patch_number: usize) -> Result<()> {
// Continue even if we fail to delete the patch artifacts. It's more important to not try to
// boot from a bad patch than to delete its artifacts.
// No need to log failure delete_patch_artifacts logs for us.
let _ = self.delete_patch_artifacts(bad_patch_number);
if let Some(ref next_boot_patch) = self.patches_state.next_boot_patch {
// If our next boot patch is bad_patch_number, clear it.
if next_boot_patch.number == bad_patch_number {
self.patches_state.next_boot_patch = None;
}
}
// If we think we can still boot from the last booted patch, set it as the next_boot_patch.
// If something happened to render the last boot patch unbootable, clear it and delete its artifacts.
if let Some(last_boot_patch) = self.patches_state.last_booted_patch.clone() {
if last_boot_patch.number != bad_patch_number
&& self.validate_patch_is_bootable(&last_boot_patch).is_ok()
{
self.patches_state.next_boot_patch = Some(last_boot_patch);
} else {
self.patches_state.last_booted_patch = None;
// No need to log failure delete_patch_artifacts logs for us.
let _ = self.delete_patch_artifacts(last_boot_patch.number);
}
}
self.save_patches_state()
}
/// Deletes all patch artifacts with numbers less than patch_number.
/// We intentionally only delete older patch artifacts. Consider the case:
///
/// 1. We start booting patch 2
/// 2. While booting (i.e., in between boot start and boot success), we download and inflate patch 3
/// 3. We finish booting patch 2
///
/// Deleting all other patch artifacts would delete patch 3, and because we've "seen" patch 3,
/// we would never try to download it again (it would be considered "bad").
fn delete_patch_artifacts_older_than(&mut self, patch_number: usize) -> Result<()> {
for entry in std::fs::read_dir(self.patches_dir())? {
let entry = entry?;
match entry.file_name().to_string_lossy().parse::<usize>() {
Ok(number) if number < patch_number => {
// delete_patch_artifacts logs for us, no need to log here.
let _ = self.delete_patch_artifacts(number);
}
Ok(_) => {}
Err(e) => {
shorebird_error!(
"Failed to parse patch number from patches directory entry, deleting: {}",
e
);
// Attempt to delete the unrecognized directory, but don't stop
// the artifact deletion process if it fails.
let _ = std::fs::remove_dir_all(entry.path());
}
}
}
Ok(())
}
}
impl ManagePatches for PatchManager {
// The explicit lifetime is required for automock to work with Options.
// See https://github.com/asomers/mockall/issues/61.
#[allow(clippy::needless_lifetimes)]
fn add_patch<'a>(
&mut self,
patch_number: usize,
file_path: &Path,
hash: &str,
signature: Option<&'a str>,
) -> Result<()> {
if !file_path.exists() {
bail!("Patch file {} does not exist", file_path.display());
}
let patch_path = self.patch_artifact_path(patch_number);
std::fs::create_dir_all(self.patch_dir(patch_number))
.with_context(|| format!("create_dir_all failed for {}", patch_path.display()))?;
std::fs::rename(file_path, &patch_path)?;
let new_patch = PatchMetadata {
number: patch_number,
size: std::fs::metadata(&patch_path)?.len(),
hash: hash.to_owned(),
signature: signature.map(|s| s.to_owned()),
};
// If a patch was never booted (next_boot_patch != last_booted_patch), we should delete
// it here before setting next_boot_patch to the new patch.
if let (Some(last_boot_patch), Some(next_boot_patch)) = (
self.patches_state.next_boot_patch.clone(),
self.patches_state.last_booted_patch.clone(),
) {
if last_boot_patch.number != next_boot_patch.number {
let _ = self.delete_patch_artifacts(next_boot_patch.number);
}
}
self.patches_state.next_boot_patch = Some(new_patch);
self.save_patches_state()
}
fn last_successfully_booted_patch(&self) -> Option<PatchInfo> {
self.patches_state
.last_booted_patch
.as_ref()
.map(|patch| self.patch_info_for_number(patch.number))
}
fn currently_booting_patch(&self) -> Option<PatchInfo> {
self.patches_state
.currently_booting_patch
.as_ref()
.map(|patch| self.patch_info_for_number(patch.number))
}
fn next_boot_patch(&mut self) -> Option<PatchInfo> {
let next_boot_patch = match self.patches_state.next_boot_patch.clone() {
Some(patch) => patch,
None => return None,
};
if let Err(e) = self.validate_patch_is_bootable(&next_boot_patch) {
shorebird_error!("Patch {} is not bootable: {}", next_boot_patch.number, e);
if let Err(e) = self.try_fall_back_from_patch(next_boot_patch.number) {
shorebird_error!(
"Failed to fall back from next_boot_patch {}: {}",
next_boot_patch.number,
e
);
}
}
self.patches_state
.next_boot_patch
.as_ref()
.map(|patch| self.patch_info_for_number(patch.number))
}
fn record_boot_start_for_patch(&mut self, patch_number: usize) -> Result<()> {
let next_boot_patch = self
.patches_state
.next_boot_patch
.clone()
.context("No next_boot_patch")?;
if next_boot_patch.number != patch_number {
bail!(
"Attempted to record boot success for patch {} but next_boot_patch is {}",
patch_number,
next_boot_patch.number
);
}
self.patches_state.currently_booting_patch = Some(next_boot_patch.clone());
self.save_patches_state()
}
fn record_boot_success(&mut self) -> Result<()> {
let boot_patch = self
.patches_state
.currently_booting_patch
.clone()
.context("No currently_booting_patch")?;
self.patches_state.currently_booting_patch = None;
self.patches_state.last_booted_patch = Some(boot_patch.clone());
if let Err(e) = self.delete_patch_artifacts_older_than(boot_patch.number) {
shorebird_error!(
"Failed to delete patch artifacts older than {}: {}",
boot_patch.number,
e
);
}
self.save_patches_state()
}
fn record_boot_failure_for_patch(&mut self, patch_number: usize) -> Result<()> {
self.patches_state.currently_booting_patch = None;
self.patches_state.known_bad_patches.insert(patch_number);
self.try_fall_back_from_patch(patch_number)
}
fn is_known_bad_patch(&self, patch_number: usize) -> bool {
self.patches_state.known_bad_patches.contains(&patch_number)
}
fn remove_patch(&mut self, patch_number: usize) -> Result<()> {
self.try_fall_back_from_patch(patch_number)
}
fn reset(&mut self) -> Result<()> {
self.patches_state = PatchesState::default();
self.save_patches_state()?;
std::fs::remove_dir_all(self.patches_dir()).with_context(|| {
format!(
"Failed to delete patches dir {}",
self.patches_dir().display()
)
})
}
}
#[cfg(test)]
impl PatchManager {
pub fn manager_for_test(temp_dir: &TempDir) -> PatchManager {
PatchManager::new(temp_dir.path().to_owned(), None)
}
pub fn add_patch_for_test(&mut self, temp_dir: &TempDir, patch_number: usize) -> Result<()> {
self.add_signed_patch_for_test(temp_dir, patch_number, "hash", None)
}
pub fn add_signed_patch_for_test(
&mut self,
temp_dir: &TempDir,
patch_number: usize,
hash: &str,
signature: Option<&str>,
) -> Result<()> {
let file_path = &temp_dir
.path()
.join(format!("patch{}.vmcode", patch_number));
std::fs::write(file_path, patch_number.to_string().repeat(patch_number)).unwrap();
shorebird_info!(
"Adding patch {} with contents {} hash {} at {}",
patch_number,
patch_number.to_string().repeat(patch_number),
hash,
file_path.display()
);
self.add_patch(patch_number, file_path, hash, signature)
}
}
#[cfg(test)]
mod debug_tests {
use tempdir::TempDir;
use super::PatchManager;
#[test]
fn manage_patches_is_debug() {
let temp_dir = TempDir::new("patch_manager").unwrap();
let patch_manager: Box<dyn super::ManagePatches> =
Box::new(PatchManager::manager_for_test(&temp_dir));
assert_eq!(format!("{:?}", patch_manager), "ManagePatches");
}
#[test]
fn patch_manager_is_debug() {
let temp_dir = TempDir::new("patch_manager").unwrap();
let patch_manager = PatchManager::new(temp_dir.path().to_owned(), Some("public_key"));
let expected_str = format!(
"PatchManager {{ root_dir: \"{}\", patches_state: PatchesState {{ last_booted_patch: None, next_boot_patch: None, currently_booting_patch: None, known_bad_patches: {{}} }}, patch_public_key: Some(\"public_key\") }}",
temp_dir.path().display()
);
assert_eq!(format!("{:?}", patch_manager), expected_str);
}
}
#[cfg(test)]
mod add_patch_tests {
use super::*;
use std::path::Path;
use tempdir::TempDir;
#[test]
fn errs_if_file_path_does_not_exist() {
let mut manager = PatchManager::manager_for_test(&TempDir::new("patch_manager").unwrap());
assert!(manager
.add_patch(
1,
Path::new("/path/to/file/that/does/not/exist"),
"hash",
None,
)
.is_err());
}
#[test]
fn adds_patch_successfully() {
let patch_number = 1;
let patch_file_contents = "patch contents";
let temp_dir = TempDir::new("patch_manager").unwrap();
let mut manager = PatchManager::manager_for_test(&temp_dir);
let file_path = &temp_dir.path().join("patch1.vmcode");
std::fs::write(file_path, patch_file_contents).unwrap();
assert!(manager
.add_patch(
patch_number,
Path::new(file_path),
"hash",
Some("my_signature")
)
.is_ok());
assert_eq!(
manager.patches_state.next_boot_patch,
Some(PatchMetadata {
number: patch_number,
size: patch_file_contents.len() as u64,
hash: "hash".to_string(),
signature: Some("my_signature".to_owned())
})
);
assert!(!file_path.exists());
}
}
#[cfg(test)]
mod last_successfully_booted_patch_tests {
use super::*;
use tempdir::TempDir;
#[test]
fn returns_none_if_no_patch_has_been_booted() -> Result<()> {
let temp_dir = TempDir::new("patch_manager").unwrap();
let mut manager = PatchManager::manager_for_test(&temp_dir);
manager.add_patch_for_test(&temp_dir, 1)?;
assert!(manager.last_successfully_booted_patch().is_none());
Ok(())
}
#[test]
fn returns_value_from_patches_state() -> Result<()> {
let temp_dir = TempDir::new("patch_manager").unwrap();
let mut manager = PatchManager::manager_for_test(&temp_dir);
manager.add_patch_for_test(&temp_dir, 1)?;
let expected = PatchInfo {
path: manager.patch_artifact_path(1),
number: 1,
};
manager.patches_state.last_booted_patch = manager.patches_state.next_boot_patch.clone();
assert_eq!(manager.last_successfully_booted_patch(), Some(expected));
Ok(())
}
}
#[cfg(test)]
mod next_boot_patch_tests {
use super::*;
use anyhow::Result;
use tempdir::TempDir;
#[test]
fn returns_none_if_no_next_boot_patch() {
let temp_dir = TempDir::new("patch_manager").unwrap();
let mut manager = PatchManager::manager_for_test(&temp_dir);
assert!(manager.next_boot_patch().is_none());
}
#[test]
fn returns_none_if_next_boot_patch_is_not_bootable() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
manager.add_patch_for_test(&temp_dir, 1)?;
// Write junk to the artifact, this should render the patch unbootable in the eyes
// of the PatchManager.
let artifact_path = manager.patch_artifact_path(1);
std::fs::write(&artifact_path, "junk")?;
assert!(manager.next_boot_patch().is_none());
// Ensure the internal state is cleared.
assert!(manager.patches_state.next_boot_patch.is_none());
// The artifact should have been deleted.
assert!(!&artifact_path.exists());
Ok(())
}
#[test]
fn clears_current_and_next_on_boot_failure_if_they_are_the_same() -> Result<()> {
let patch_file_contents = "patch contents";
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
let file_path = &temp_dir.path().join("patch1.vmcode");
std::fs::write(file_path, patch_file_contents)?;
assert!(manager.add_patch(1, file_path, "hash", None).is_ok());
// Write junk to the artifact, this should render the patch unbootable in the eyes
// of the PatchManager.
let artifact_path = manager.patch_artifact_path(1);
std::fs::write(&artifact_path, "junk")?;
assert!(manager.next_boot_patch().is_none());
// Ensure the internal state is cleared.
assert!(manager.patches_state.next_boot_patch.is_none());
assert!(manager.patches_state.last_booted_patch.is_none());
// The artifact should have been deleted.
assert!(!&artifact_path.exists());
Ok(())
}
#[test]
fn falls_back_to_last_booted_patch_if_still_bootable() -> Result<()> {
let patch_file_contents = "patch contents";
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
let file_path = &temp_dir.path().join("patch1.vmcode");
std::fs::write(file_path, patch_file_contents)?;
// Add patch 1, pretend it booted successfully.
assert!(manager.add_patch(1, file_path, "hash", None).is_ok());
assert!(manager.record_boot_start_for_patch(1).is_ok());
assert!(manager.record_boot_success().is_ok());
assert!(!manager.is_known_bad_patch(1));
// Add patch 2, pretend it failed to boot.
let file_path = &temp_dir.path().join("patch2.vmcode");
std::fs::write(file_path, patch_file_contents)?;
assert!(manager.add_patch(2, file_path, "hash", None).is_ok());
assert!(manager.record_boot_start_for_patch(2).is_ok());
assert!(manager.record_boot_failure_for_patch(2).is_ok());
assert!(manager.is_known_bad_patch(2));
// Verify that we will next attempt to boot from patch 1.
assert_eq!(manager.next_boot_patch().unwrap().number, 1);
Ok(())
}
#[test]
fn does_not_fall_back_to_last_booted_patch_if_corrupted() -> Result<()> {
let patch_file_contents = "patch contents";
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
let file_path = &temp_dir.path().join("patch1.vmcode");
std::fs::write(file_path, patch_file_contents)?;
// Add patch 1, pretend it booted successfully.
assert!(manager.add_patch(1, file_path, "hash", None).is_ok());
assert!(manager.record_boot_start_for_patch(1).is_ok());
assert!(manager.record_boot_success().is_ok());
// Add patch 2, pretend it failed to boot.
let file_path = &temp_dir.path().join("patch2.vmcode");
std::fs::write(file_path, patch_file_contents)?;
assert!(manager.add_patch(2, file_path, "hash", None).is_ok());
assert!(manager.record_boot_start_for_patch(2).is_ok());
assert!(manager.record_boot_failure_for_patch(2).is_ok());
// Write junk to patch 1's artifact. This should prevent us from falling back to it.
let patch_1_artifact_path = manager.patch_artifact_path(1);
std::fs::write(patch_1_artifact_path, "junk")?;
// Verify that we will not attempt to boot from either patch.
assert!(manager.next_boot_patch().is_none());
// Patch 1 should *not* be considered bad, as we successfully booted from it and it only
// became corrupted after that. Downloading it a second time might resolve the issue.
assert!(!manager.is_known_bad_patch(1));
// Patch 2 failed to boot, so it should be considered bad.
assert!(manager.is_known_bad_patch(2));
Ok(())
}
#[test]
fn returns_none_patch_if_first_patch_failed_to_boot() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
// Add a first patch and pretend it failed to boot.
manager.add_patch_for_test(&temp_dir, 1)?;
manager.record_boot_start_for_patch(1)?;
manager.record_boot_failure_for_patch(1)?;
// Because there is no previous patch, we should not attempt to boot any patch.
assert!(manager.next_boot_patch().is_none());
assert!(manager.is_known_bad_patch(1));
Ok(())
}
#[test]
fn returns_last_booted_patch_if_next_patch_failed_to_boot() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
// Add a first patch and pretend it booted successfully.
manager.add_patch_for_test(&temp_dir, 1)?;
manager.record_boot_start_for_patch(1)?;
manager.record_boot_success()?;
// Add a second patch and pretend it failed to boot.
manager.add_patch_for_test(&temp_dir, 2)?;
manager.record_boot_start_for_patch(2)?;
manager.record_boot_failure_for_patch(2)?;
// Verify that we will next attempt to boot from patch 1.
assert_eq!(manager.next_boot_patch().unwrap().number, 1);
assert!(!manager.is_known_bad_patch(1));
assert!(manager.is_known_bad_patch(2));
Ok(())
}
// The constant values below were generated by taking an arbitrary sha256 hash (INFLATED_PATCH_HASH)
// and using openssl to sign it with the private key corresponding to `PUBLIC_KEY`.
// The base64-encoded public key in a DER format. This is required by ring to verify signatures.
// See https://docs.rs/ring/latest/ring/signature/index.html#signing-and-verifying-with-rsa-pkcs1-15-padding
const PUBLIC_KEY: &str = "MIIBCgKCAQEA2wdpEGbuvlPsb9i0qYrfMefJnEw1BHTi8SYZTKrXOvJWmEpPE1hWfbkvYzXu5a96gV1yocF3DMwn04VmRlKhC4AhsD0NL0UNhYhotbKG91Kwi1vAXpHhCdz5gQEBw0K1uB4Jz+zK6WK+31PryYpwLwbyXNqXoY8IAAUQ4STsHYV5w+BMSi8pepWMRd7DR9RHcbNOZlJvdBQ5NxvB4JN4dRMq8cC73ez1P9d7Dfwv3TWY+he9EmuXLT2UivZSlHIrGBa7MFfqyUe2ro0F7Te/B0si12itBbWIqycvqcXjeOPNn6WEpqN7IWjb9LUh162JyYaz5Lb/VeeJX8LKtElccwIDAQAB";
// The message that was signed. In practice, this will be the sha256 hash of an inflated patch artifact.
const INFLATED_PATCH_HASH: &str =
"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b";
// The base64-encoded signature of `INFLATED_PATCH_HASH` created using the private key corresponding
// to `PUBLIC_KEY`.
const SIGNATURE: &str = "ZGccldv01XqHQ76bXuKV/9EQnNK0Q+reQ9bJHVnGfLldF+BLRx0divgPfKP5Df9BJPA3dw1Z1VortfepmMGebP3kS593l5zoktu9MIepxvRAFWNKE5PDTIIvCL/ddTPEHt6NNCeD6HLOMLzbEX3cFZa+lq3UymGi0aqA5DlXirJBGtopojc9nOXZ22n/qHNZIHEkGcqKbSMSK9oC55whKHnlJTbCXdmSyDc65B4PcgseqJom1riVK3XGW1YMrSpuMAU+CDT7HhdESmI1UtH1bYeBITfRhQztdDTfti2vJTf2Y+lYC99CFiISgD7f1m0KUcC+VnEAMZSYtgxSk6AX2A==";
#[test]
fn returns_none_if_public_key_is_invalid() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::new(temp_dir.path().to_path_buf(), Some("not a valid key"));
manager.add_signed_patch_for_test(&temp_dir, 1, INFLATED_PATCH_HASH, Some(SIGNATURE))?;
assert!(manager.next_boot_patch().is_none());
Ok(())
}
#[test]
fn returns_none_if_patch_is_missing_expected_signature() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::new(temp_dir.path().to_path_buf(), Some(PUBLIC_KEY));
manager.add_signed_patch_for_test(&temp_dir, 1, INFLATED_PATCH_HASH, None)?;
assert!(manager.next_boot_patch().is_none());
Ok(())
}
#[test]
fn returns_none_if_patch_has_invalid_signature() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::new(temp_dir.path().to_path_buf(), Some(PUBLIC_KEY));
// Using MESSAGE as a signature because it is valid base64, but not a valid signature.
manager.add_signed_patch_for_test(
&temp_dir,
1,
INFLATED_PATCH_HASH,
Some(INFLATED_PATCH_HASH),
)?;
assert!(manager.next_boot_patch().is_none());
Ok(())
}
#[test]
fn returns_patch_if_patch_has_valid_signature() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::new(temp_dir.path().to_path_buf(), Some(PUBLIC_KEY));
manager.add_signed_patch_for_test(&temp_dir, 1, INFLATED_PATCH_HASH, Some(SIGNATURE))?;
assert!(manager.next_boot_patch().is_some());
let patch = manager.next_boot_patch().unwrap();
assert_eq!(patch.number, 1);
Ok(())
}
#[test]
fn returns_patch_with_arbitrary_signature_if_no_public_key() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
// Create a PatchManager without a public key.
let mut manager = PatchManager::manager_for_test(&temp_dir);
manager.add_signed_patch_for_test(
&temp_dir,
1,
INFLATED_PATCH_HASH,
Some("not a valid signature"),
)?;
assert!(manager.next_boot_patch().is_some());
let patch = manager.next_boot_patch().unwrap();
assert_eq!(patch.number, 1);
Ok(())
}
}
#[cfg(test)]
mod fall_back_tests {
use super::*;
#[test]
fn does_nothing_if_no_patch_exists() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
assert!(manager.patches_state.last_booted_patch.is_none());
assert!(manager.patches_state.next_boot_patch.is_none());
manager.try_fall_back_from_patch(1)?;
assert!(manager.patches_state.last_booted_patch.is_none());
assert!(manager.patches_state.next_boot_patch.is_none());
Ok(())
}
#[test]
fn sets_next_patch_to_latest_patch_if_no_next_patch_exists() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
assert!(manager.patches_state.next_boot_patch.is_none());
manager.patches_state.last_booted_patch = Some(PatchMetadata {
number: 1,
size: 1,
hash: "hash".to_string(),
signature: Some("signature".to_owned()),
});
manager.try_fall_back_from_patch(1)?;
assert_eq!(
manager.patches_state.next_boot_patch,
manager.patches_state.last_booted_patch
);
Ok(())
}
#[test]
fn sets_next_patch_to_latest_patch_if_both_are_present() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
// Download and successfully boot from patch 1
manager.add_patch_for_test(&temp_dir, 1)?;
manager.record_boot_start_for_patch(1)?;
manager.record_boot_success()?;
// Download and fall back from patch 2
manager.add_patch_for_test(&temp_dir, 2)?;
manager.try_fall_back_from_patch(2)?;
assert_eq!(manager.patches_state.last_booted_patch.unwrap().number, 1);
assert_eq!(manager.patches_state.next_boot_patch.unwrap().number, 1);
Ok(())
}
#[test]
fn clears_next_and_last_patches_if_both_fail_validation() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
// Download and successfully boot from patch 1, and then corrupt it on disk.
manager.add_patch_for_test(&temp_dir, 1)?;
manager.record_boot_start_for_patch(1)?;
manager.record_boot_success()?;
let patch_1_path = manager.patch_artifact_path(1);
std::fs::write(patch_1_path, "junkjunkjunk")?;
// Download and fall back from patch 2
manager.add_patch_for_test(&temp_dir, 2)?;
manager.try_fall_back_from_patch(2)?;
// Neither patch should exist.
assert!(manager.patches_state.last_booted_patch.is_none());
assert!(manager.patches_state.next_boot_patch.is_none());
Ok(())
}
#[test]
fn does_not_clear_next_patch_if_changed_since_boot_start() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
// Simulate a situation where we download both patches 1 and 2.
manager.add_patch_for_test(&temp_dir, 1)?;
// Start booting from patch 1.
manager.record_boot_start_for_patch(1)?;
// Download patch 2 before patch 1 finishes booting.
manager.add_patch_for_test(&temp_dir, 2)?;
manager.record_boot_failure_for_patch(1)?;
manager.try_fall_back_from_patch(1)?;
assert!(manager.patches_state.last_booted_patch.is_none());
assert_eq!(
manager
.patches_state
.next_boot_patch
.clone()
.unwrap()
.number,
2
);
assert!(manager.is_known_bad_patch(1));
Ok(())
}
#[test]
fn succeeds_if_deleting_artifacts_fails() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
// Download and successfully boot from patch 1, and then corrupt it on disk.
manager.add_patch_for_test(&temp_dir, 1)?;
manager.record_boot_start_for_patch(1)?;
manager.record_boot_success()?;
// Download patch 2.
manager.add_patch_for_test(&temp_dir, 2)?;
// Remove all data for both patches.
let patch_dir = manager.patch_dir(1);
std::fs::remove_dir_all(patch_dir)?;
let patch_dir = manager.patch_dir(2);
std::fs::remove_dir_all(patch_dir)?;
manager.try_fall_back_from_patch(2)?;
assert!(manager.patches_state.last_booted_patch.is_none());
assert!(manager.patches_state.next_boot_patch.is_none());
Ok(())
}
}
#[cfg(test)]
mod record_boot_success_for_patch_tests {
use super::*;
use anyhow::{Ok, Result};
use tempdir::TempDir;
#[test]
fn errs_if_no_next_boot_patch() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
// This should fail because no patches have been added.
assert!(manager.record_boot_success().is_err());
Ok(())
}
#[test]
fn errs_if_patch_number_does_not_match_next_patch() -> Result<()> {
let patch_number = 1;
let patch_file_contents = "patch contents";
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
let file_path = &temp_dir.path().join("patch1.vmcode");
std::fs::write(file_path, patch_file_contents)?;
assert!(manager
.add_patch(patch_number, file_path, "hash", None)
.is_ok());
assert!(manager.record_boot_success().is_err());
Ok(())
}
#[test]
fn succeeds_when_provided_next_boot_patch_number() -> Result<()> {
let patch_number = 1;
let patch_file_contents = "patch contents";
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
let file_path = &temp_dir.path().join("patch1.vmcode");
std::fs::write(file_path, patch_file_contents)?;
assert!(manager
.add_patch(patch_number, file_path, "hash", None)
.is_ok());
assert!(manager.record_boot_start_for_patch(1).is_ok());
assert!(manager.record_boot_success().is_ok());
Ok(())
}
#[test]
fn deletes_other_patch_artifacts() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
// Download patches 1, 2, and 3 before we start booting from patch 2.
manager.add_patch_for_test(&temp_dir, 1)?;
manager.add_patch_for_test(&temp_dir, 2)?;
manager.add_patch_for_test(&temp_dir, 3)?;
// Start booting from our latest patch.
manager.record_boot_start_for_patch(3)?;
// Download patch 4 while we're booting from patch 3.
manager.add_patch_for_test(&temp_dir, 4)?;
// Record success for patch 3, make sure the artifact still exists.
manager.record_boot_success()?;
// Make sure that recording success for patch 2 deleted artifacts for prior
// patches but not for subsequent patches.
let mut patch_dir_names = std::fs::read_dir(manager.patches_dir())?
.map(|res| res.map(|e| e.path()))
.map(|e| e.unwrap())
.map(|e| e.file_name().unwrap().to_owned())
.collect::<Vec<_>>();
patch_dir_names.sort();
assert_eq!(patch_dir_names, vec!["3", "4"]);
Ok(())
}
#[test]
fn deletes_unrecognized_directories_in_patches_dir() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
// Add a junk directory to the patches directory.
let junk_dir = manager.patches_dir().join("junk");
std::fs::create_dir_all(&junk_dir)?;
manager.add_patch_for_test(&temp_dir, 1)?;
manager.add_patch_for_test(&temp_dir, 2)?;
manager.record_boot_start_for_patch(2)?;
manager.record_boot_success()?;
assert!(!junk_dir.exists());
assert!(!manager.patch_dir(1).exists());
Ok(())
}
}
#[cfg(test)]
mod record_boot_failure_for_patch_tests {
use super::*;
use anyhow::{Ok, Result};
use tempdir::TempDir;
#[test]
fn deletes_failed_patch_artifacts() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
manager.add_patch_for_test(&temp_dir, 1)?;
assert!(manager.record_boot_start_for_patch(1).is_ok());
assert!(manager.record_boot_success().is_ok());
assert!(!manager.is_known_bad_patch(1));
let succeeded_patch_artifact_path = manager.patch_artifact_path(1);
manager.add_patch_for_test(&temp_dir, 2)?;
let failed_patch_artifact_path = manager.patch_artifact_path(2);
// Make sure patch artifacts exist
assert!(failed_patch_artifact_path.exists());
assert!(succeeded_patch_artifact_path.exists());
assert!(manager.record_boot_start_for_patch(2).is_ok());
assert!(manager.record_boot_failure_for_patch(2).is_ok());
assert!(!failed_patch_artifact_path.exists());
assert!(manager.is_known_bad_patch(2));
Ok(())
}
#[test]
fn clears_last_booted_patch_if_it_is_the_failed_patch() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
manager.add_patch_for_test(&temp_dir, 1)?;
let patch_artifact_path = manager.patch_artifact_path(1);
// Pretend we booted from this patch
assert!(manager.record_boot_start_for_patch(1).is_ok());
assert!(manager.record_boot_success().is_ok());
assert_eq!(manager.last_successfully_booted_patch().unwrap().number, 1);
assert_eq!(manager.next_boot_patch().unwrap().number, 1);
assert!(patch_artifact_path.exists());
assert!(!manager.is_known_bad_patch(1));
// Now pretend it failed to boot
assert!(manager.record_boot_start_for_patch(1).is_ok());
assert!(manager.record_boot_failure_for_patch(1).is_ok());
assert!(manager.last_successfully_booted_patch().is_none());
assert!(manager.next_boot_patch().is_none());
assert!(manager.is_known_bad_patch(1));
assert!(!patch_artifact_path.exists());
Ok(())
}
}
#[cfg(test)]
mod reset_tests {
use super::*;
use anyhow::{Ok, Result};
use tempdir::TempDir;
#[test]
fn deletes_patches_dir_and_resets_patches_state() -> Result<()> {
let temp_dir = TempDir::new("patch_manager")?;
let mut manager = PatchManager::manager_for_test(&temp_dir);
manager.add_patch_for_test(&temp_dir, 1)?;
let path_artifacts_dir = manager.patches_dir();
// Make sure the directory and artifact files were created
assert!(path_artifacts_dir.exists());
assert_eq!(std::fs::read_dir(&path_artifacts_dir).unwrap().count(), 1);
assert!(manager.reset().is_ok());
// Make sure the directory and artifact files were deleted
assert!(!path_artifacts_dir.exists());
Ok(())
}
}