refactor of RealmFS into several components

This commit is contained in:
Bruce Leidl 2019-04-02 15:09:41 -04:00
parent fcbf63db8e
commit b7d4f1e570
8 changed files with 1506 additions and 224 deletions

View File

@ -1,224 +0,0 @@
use std::path::{Path,PathBuf};
use std::fs;
use std::io::Write;
use crate::{ImageHeader,MetaInfo,Mount,Result,util,verity};
const BASE_PATH: &'static str = "/storage/realms/realmfs-images";
const RUN_DIRECTORY: &str = "/run/citadel/images";
const MAX_REALMFS_NAME_LEN: usize = 40;
pub struct RealmFS {
path: PathBuf,
mountpoint: PathBuf,
header: ImageHeader,
metainfo: MetaInfo,
}
impl RealmFS {
/// Locate a RealmFS image by name in the default location using the standard name convention
pub fn load_by_name(name: &str) -> Result<RealmFS> {
if !util::is_valid_name(name, MAX_REALMFS_NAME_LEN) {
bail!("Invalid realmfs name '{}'", name);
}
let path = Path::new(BASE_PATH).join(format!("{}-realmfs.img", name));
if !path.exists() {
bail!("No image found at {}", path.display());
}
RealmFS::load_from_path(path, name)
}
pub fn named_image_exists(name: &str) -> bool {
if !util::is_valid_name(name, MAX_REALMFS_NAME_LEN) {
return false;
}
let path = Path::new(BASE_PATH).join(format!("{}-realmfs.img", name));
path.exists()
}
/// Load RealmFS image from an exact path.
pub fn load_from_path<P: AsRef<Path>>(path: P, name: &str) -> Result<RealmFS> {
let path = path.as_ref().to_owned();
let header = ImageHeader::from_file(&path)?;
if !header.is_magic_valid() {
bail!("Image file {} does not have a valid header", path.display());
}
let metainfo = header.metainfo()?;
let mountpoint = PathBuf::from(format!("{}/{}-realmfs.mountpoint", RUN_DIRECTORY, name));
Ok(RealmFS{
path,
mountpoint,
header,
metainfo,
})
}
pub fn mount_rw(&mut self) -> Result<()> {
// XXX fail if already verity mounted?
// XXX strip dm-verity tree if present?
// XXX just remount if not verity mounted, but currently ro mounted?
self.mount(false)
}
pub fn mount_ro(&mut self) -> Result<()> {
self.mount(true)
}
fn mount(&mut self, read_only: bool) -> Result<()> {
let flags = if read_only {
Some("-oro")
} else {
Some("-orw")
};
if !self.mountpoint.exists() {
fs::create_dir_all(self.mountpoint())?;
}
let loopdev = self.create_loopdev()?;
util::mount(&loopdev.to_string_lossy(), self.mountpoint(), flags)
}
pub fn mount_verity(&self) -> Result<()> {
if self.is_mounted() {
bail!("RealmFS image is already mounted");
}
if !self.is_sealed() {
bail!("Cannot verity mount RealmFS image because it's not sealed");
}
if !self.mountpoint.exists() {
fs::create_dir_all(self.mountpoint())?;
}
let dev = self.setup_verity_device()?;
util::mount(&dev.to_string_lossy(), &self.mountpoint, Some("-oro"))
}
fn setup_verity_device(&self) -> Result<PathBuf> {
// TODO verify signature
if !self.header.has_flag(ImageHeader::FLAG_HASH_TREE) {
self.generate_verity()?;
}
verity::setup_image_device(&self.path, &self.metainfo)
}
pub fn create_loopdev(&self) -> Result<PathBuf> {
let args = format!("--offset 4096 -f --show {}", self.path.display());
let output = util::exec_cmdline_with_output("/sbin/losetup", args)?;
Ok(PathBuf::from(output))
}
pub fn is_mounted(&self) -> bool {
match Mount::is_target_mounted(self.mountpoint()) {
Ok(val) => val,
Err(e) => {
warn!("Error reading /proc/mounts: {}", e);
false
}
}
}
pub fn fork(&self, new_name: &str) -> Result<RealmFS> {
if !util::is_valid_name(new_name, MAX_REALMFS_NAME_LEN) {
bail!("Invalid realmfs name '{}'", new_name);
}
// during install the images have a different base directory
let mut new_path = self.path.clone();
new_path.pop();
new_path.push(format!("{}-realmfs.img", new_name));
if new_path.exists() {
bail!("RealmFS image for name {} already exists", new_name);
}
let args = format!("--reflink=auto {} {}", self.path.display(), new_path.display());
util::exec_cmdline("/usr/bin/cp", args)?;
let header = ImageHeader::new();
header.set_metainfo_bytes(&self.generate_fork_metainfo(new_name));
header.write_header_to(&new_path)?;
let realmfs = RealmFS::load_from_path(&new_path, new_name)?;
// forking unseals since presumably the image is being forked to modify it
realmfs.truncate_verity()?;
Ok(realmfs)
}
fn generate_fork_metainfo(&self, name: &str) -> Vec<u8> {
let mut v = Vec::new();
writeln!(v, "image-type = \"realmfs\"").unwrap();
writeln!(v, "realmfs-name = \"{}\"", name).unwrap();
writeln!(v, "nblocks = {}", self.metainfo.nblocks()).unwrap();
v
}
// Remove verity tree from image file by truncating file to the number of blocks in metainfo
fn truncate_verity(&self) -> Result<()> {
let verity_flag = self.header.has_flag(ImageHeader::FLAG_HASH_TREE);
if verity_flag {
self.header.clear_flag(ImageHeader::FLAG_HASH_TREE);
self.header.write_header_to(&self.path)?;
}
let meta = self.path.metadata()?;
let expected = (self.metainfo.nblocks() + 1) * 4096;
let actual = meta.len() as usize;
if actual > expected {
if !verity_flag {
warn!("RealmFS length was greater than length indicated by metainfo but FLAG_HASH_TREE not set");
}
let f = fs::OpenOptions::new().write(true).open(&self.path)?;
f.set_len(expected as u64)?;
} else if actual < expected {
bail!("RealmFS image {} is shorter than length indicated by metainfo", self.path.display());
}
if verity_flag {
warn!("FLAG_HASH_TREE was set but RealmFS image file length matched metainfo length");
}
Ok(())
}
pub fn is_sealed(&self) -> bool {
!self.metainfo.verity_root().is_empty()
}
fn generate_verity(&self) -> Result<()> {
self.truncate_verity()?;
verity::generate_image_hashtree(&self.path, &self.metainfo)?;
self.header.set_flag(ImageHeader::FLAG_HASH_TREE);
self.header.write_header_to(&self.path)?;
Ok(())
}
pub fn create_overlay(&self, basedir: &Path) -> Result<PathBuf> {
if !self.is_mounted() {
bail!("Cannot create overlay until realmfs is mounted");
}
let workdir = basedir.join("workdir");
let upperdir = basedir.join("upperdir");
let mountpoint = basedir.join("mountpoint");
fs::create_dir_all(&workdir)?;
fs::create_dir_all(&upperdir)?;
fs::create_dir_all(&mountpoint)?;
let args = format!("-t overlay realmfs-overlay -olowerdir={},upperdir={},workdir={} {}",
self.mountpoint.display(),
upperdir.display(),
workdir.display(),
mountpoint.display());
util::exec_cmdline("/usr/bin/mount", args)?;
Ok(mountpoint)
}
pub fn mountpoint(&self) -> &Path {
&self.mountpoint
}
}

View File

@ -0,0 +1,365 @@
use std::collections::HashSet;
use std::path::Path;
use crate::{RealmFS, Result, ImageHeader, CommandLine, PublicKey, LoopDevice};
use crate::realmfs::mountpoint::Mountpoint;
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::verity::Verity;
/// Holds the activation status for a RealmFS and provides a thread-safe
/// interface to it.
///
/// If `state` is `None` then the RealmFS is not currently activated.
///
pub struct ActivationState {
state: RwLock<Option<Arc<Activation>>>,
}
impl ActivationState {
pub fn new() -> Self {
let state = RwLock::new(None);
ActivationState { state }
}
/// Load an unknown activation state for `realmfs` by examining
/// the state of the system to determine if the RealmFS is activated.
pub fn load(&self, realmfs: &RealmFS) {
let activation = if realmfs.is_sealed() {
let header = realmfs.header();
let activator = VerityActivator::new(realmfs, header);
activator.activation()
} else {
let activator = LoopActivator::new(realmfs);
activator.activation()
};
*self.state_mut() = activation.map(|a| Arc::new(a))
}
/// If currently activated return the corresponding `Activation` instance
/// otherwise return `None`
pub fn get(&self) -> Option<Arc<Activation>> {
self.state().clone()
}
/// Return `true` if currently activated.
pub fn is_activated(&self) -> bool {
self.state().is_some()
}
/// Activate `realmfs` or if already activated return current `Activation`.
pub fn activate(&self, realmfs: &RealmFS) -> Result<Arc<Activation>> {
let header = realmfs.header();
let mut lock = self.state_mut();
if let Some(ref activation) = *lock {
return Ok(activation.clone());
} else {
let activation = self._activate(realmfs, header)?;
let activation = Arc::new(activation);
*lock = Some(activation.clone());
Ok(activation)
}
}
fn _activate(&self, realmfs: &RealmFS, header: &ImageHeader) -> Result<Activation> {
if realmfs.is_sealed() {
let activator = VerityActivator::new(realmfs, header);
activator.activate()
} else {
let activator = LoopActivator::new(realmfs);
activator.activate()
}
}
/// Deactivate `Activation` only if not in use.
///
/// Returns `true` if state changes from activated to not-activated.
///
pub fn deactivate(&self, active_set: &HashSet<Mountpoint>) -> Result<bool> {
let mut lock = self.state_mut();
if let Some(ref activation) = *lock {
if activation.deactivate(active_set)? {
*lock = None;
return Ok(true);
}
}
Ok(false)
}
/// Return `true` if an `Activation` exists and is currently in-use by some `Realm`
pub fn is_in_use(&self, active_set: &HashSet<Mountpoint>) -> bool {
self.state()
.as_ref()
.map(|a| a.in_use(active_set))
.unwrap_or(false)
}
fn state(&self) -> RwLockReadGuard<Option<Arc<Activation>>> {
self.state.read().unwrap()
}
fn state_mut(&self) -> RwLockWriteGuard<Option<Arc<Activation>>>{
self.state.write().unwrap()
}
}
/// Represents a RealmFS in an activated state. The activation can be one of:
///
/// `Activation::Loop` if the RealmFS is unsealed
/// `Activation::Verity` if the RealmFS is sealed
///
#[derive(Debug)]
pub enum Activation {
///
/// A RealmFS in the unsealed state is activated by creating a /dev/loop
/// device and mounting it twice as both a read-only and read-write tree.
///
Loop {
ro_mountpoint: Mountpoint,
rw_mountpoint: Mountpoint,
device: LoopDevice,
},
///
/// A RealmFS in the sealed state is activated by configuring a dm-verity
/// device and mounting it.
/// `mountpoint` is the filesystem location at which the device is mounted.
/// `device` is a path to a device in /dev/mapper/
///
Verity {
mountpoint: Mountpoint,
device: String,
},
}
impl Activation {
fn new_loop(ro_mountpoint: Mountpoint, rw_mountpoint: Mountpoint, device: LoopDevice) -> Self {
Activation::Loop { ro_mountpoint, rw_mountpoint, device }
}
fn new_verity(mountpoint: Mountpoint, device: String) -> Self {
Activation::Verity{ mountpoint, device }
}
/// Converts an entry read from RealmFS:RUN_DIRECTORY into an `Activation` instance.
///
/// Return an `Activation` corresponding to `mountpoint` if valid activation exists.
///
pub fn for_mountpoint(mountpoint: &Mountpoint) -> Option<Self> {
if mountpoint.tag() == "rw" || mountpoint.tag() == "ro" {
LoopDevice::find_mounted_loop(mountpoint.path()).map(|loopdev| {
let (ro,rw) = Mountpoint::new_loop_pair(mountpoint.realmfs());
Activation::new_loop(ro, rw, loopdev)
})
} else {
let device = Verity::device_name_for_mountpoint(mountpoint);
if Path::new("/dev/mapper").join(&device).exists() {
Some(Activation::new_verity(mountpoint.clone(), device))
} else {
None
}
}
}
/// Deactivate `Activation` only if not in use.
///
/// Returns `true` if state changes from activated to not-activated.
///
pub fn deactivate(&self, active_set: &HashSet<Mountpoint>) -> Result<bool> {
if !self.in_use(active_set) {
self._deactivate()?;
Ok(true)
} else {
Ok(false)
}
}
fn _deactivate(&self) -> Result<()> {
match self {
Activation::Loop { ro_mountpoint, rw_mountpoint, device } => {
ro_mountpoint.deactivate()?;
rw_mountpoint.deactivate()?;
info!("Removing loop device {}", device);
device.detach()
},
Activation::Verity { mountpoint, device } => {
mountpoint.deactivate()?;
Verity::close_device(&device)
},
}
}
/// Return `true` if `mp` is a `Mountpoint` belonging to this `Activation`.
pub fn is_mountpoint(&self, mp: &Mountpoint) -> bool {
match self {
Activation::Loop { ro_mountpoint, rw_mountpoint, ..} => {
mp == ro_mountpoint || mp == rw_mountpoint
},
Activation::Verity { mountpoint, .. } => {
mp == mountpoint
}
}
}
/// Return read-only `Mountpoint` for this `Activation`
pub fn mountpoint(&self) -> &Mountpoint {
match self {
Activation::Loop { ro_mountpoint, ..} => &ro_mountpoint,
Activation::Verity { mountpoint, ..} => &mountpoint,
}
}
/// Return read-write `Mountpoint` if present for this `Activation` type.
pub fn mountpoint_rw(&self) -> Option<&Mountpoint> {
match self {
Activation::Loop { rw_mountpoint, ..} => Some(&rw_mountpoint),
Activation::Verity { .. } => None,
}
}
pub fn device(&self) -> &str{
match self {
Activation::Loop { device, ..} => device.device_str(),
Activation::Verity { device, ..} => &device,
}
}
/// Return `true` if `Activation` is currently in-use by some `Realm`
///
/// `active_set` is a set of mountpoints needed to determine if an activation is
/// in use. This set is obtained by calling `active_mountpoints()` on a `RealmManager`
/// instance.
///
pub fn in_use(&self, active_set: &HashSet<Mountpoint>) -> bool {
match self {
Activation::Loop {ro_mountpoint: ro, rw_mountpoint: rw, ..} => {
active_set.contains(ro) || active_set.contains(rw)
},
Activation::Verity { mountpoint, ..} => {
active_set.contains(mountpoint)
},
}
}
}
struct VerityActivator<'a> {
realmfs: &'a RealmFS,
header: &'a ImageHeader,
}
impl <'a> VerityActivator <'a> {
fn new(realmfs: &'a RealmFS, header: &'a ImageHeader) -> Self {
VerityActivator { realmfs, header }
}
// Determine if `self.realmfs` is already activated by searching for verity mountpoint and
// device name. If found return an `Activation::Verity`
fn activation(&self) -> Option<Activation> {
let mountpoint = self.mountpoint();
if mountpoint.exists() {
let devname = Verity::device_name(&self.realmfs.metainfo());
Some(Activation::new_verity(self.mountpoint(), devname))
} else {
None
}
}
// Perform a verity activation of `self.realmfs` and return an `Activation::Verity`
fn activate(&self) -> Result<Activation> {
info!("Starting verity activation for {}", self.realmfs.name());
let mountpoint = self.mountpoint();
if !mountpoint.exists() {
mountpoint.create_dir()?;
}
let device_name = self.setup_verity_device()?;
info!("verity device created..");
cmd!("/usr/bin/mount", "-oro /dev/mapper/{} {}", device_name, mountpoint)?;
Ok(Activation::new_verity(mountpoint, device_name))
}
fn mountpoint(&self) -> Mountpoint {
Mountpoint::new(self.realmfs.name(), &self.realmfs.metainfo().verity_tag())
}
fn setup_verity_device(&self) -> Result<String> {
if !CommandLine::nosignatures() {
self.verify_signature()?;
}
if !self.header.has_flag(ImageHeader::FLAG_HASH_TREE) {
self.generate_verity()?;
}
Verity::new(self.realmfs.path()).setup(&self.header.metainfo())
}
fn generate_verity(&self) -> Result<()> {
info!("Generating verity hash tree");
Verity::new(self.realmfs.path()).generate_image_hashtree(&self.header.metainfo())?;
info!("Writing header...");
self.header.set_flag(ImageHeader::FLAG_HASH_TREE);
self.header.write_header_to(self.realmfs.path())?;
info!("Done generating verity hash tree");
Ok(())
}
fn verify_signature(&self) -> Result<()> {
let pubkey = self.public_key()?;
if !self.realmfs.header().verify_signature(pubkey) {
bail!("header signature verification failed on realmfs image '{}'", self.realmfs.name());
}
info!("header signature verified on realmfs image '{}'", self.realmfs.name());
Ok(())
}
fn public_key(&self) -> Result<PublicKey> {
let pubkey = if self.realmfs.metainfo().channel() == RealmFS::USER_KEYNAME {
self.realmfs.sealing_keys()?.public_key()
} else {
match self.realmfs.header().public_key()? {
Some(pubkey) => pubkey,
None => bail!("No public key available for channel {}", self.realmfs.metainfo().channel()),
}
};
Ok(pubkey)
}
}
struct LoopActivator<'a> {
realmfs: &'a RealmFS,
}
impl <'a> LoopActivator<'a> {
fn new(realmfs: &'a RealmFS) -> Self {
LoopActivator{ realmfs }
}
// Determine if `self.realmfs` is presently activated by searching for mountpoints. If
// loop activation mountpoints are present return an `Activation::Loop`
fn activation(&self) -> Option<Activation> {
let (ro,rw) = Mountpoint::new_loop_pair(self.realmfs.name());
if ro.exists() && rw.exists() {
Activation::for_mountpoint(&ro)
} else {
None
}
}
// Perform a loop activation of `self.realmfs` and return an `Activation::Loop`
fn activate(&self) -> Result<Activation> {
let (ro,rw) = Mountpoint::new_loop_pair(self.realmfs.name());
ro.create_dir()?;
rw.create_dir()?;
let loopdev = LoopDevice::create(self.realmfs.path(), Some(4096), false)?;
loopdev.mount_pair(rw.path(), ro.path())?;
Ok(Activation::new_loop(ro, rw, loopdev))
}
}

View File

@ -0,0 +1,10 @@
pub(crate) mod resizer;
mod activator;
mod mountpoint;
mod update;
pub(crate) mod realmfs_set;
mod realmfs;
pub use self::realmfs::RealmFS;
pub use self::mountpoint::Mountpoint;
pub use self::activator::Activation;

View File

@ -0,0 +1,129 @@
use std::fs::{self, DirEntry};
use std::fmt;
use std::path::{PathBuf, Path};
use crate::{Result, RealmFS};
use std::ffi::OsStr;
/// A RealmFS activation mountpoint
#[derive(Clone,Eq,PartialEq,Hash,Debug)]
pub struct Mountpoint(PathBuf);
impl Mountpoint {
const UMOUNT: &'static str = "/usr/bin/umount";
/// Read `RealmFS::RUN_DIRECTORY` to collect all current mountpoints
/// and return them.
pub fn all_mountpoints() -> Result<Vec<Mountpoint>> {
let all = fs::read_dir(RealmFS::RUN_DIRECTORY)?
.flat_map(|e| e.ok())
.map(Into::into)
.filter(Mountpoint::is_valid)
.collect();
Ok(all)
}
/// Return a read-only/read-write mountpoint pair.
pub fn new_loop_pair(realmfs: &str) -> (Self,Self) {
let ro = Self::new(realmfs, "ro");
let rw = Self::new(realmfs, "rw");
(ro, rw)
}
/// Build a new `Mountpoint` from the provided realmfs `name` and `tag`.
///
/// The directory name of the mountpoint will have the structure:
///
/// realmfs-$name-$tag.mountpoint
///
pub fn new(name: &str, tag: &str) -> Self {
let filename = format!("realmfs-{}-{}.mountpoint", name, tag);
Mountpoint(Path::new(RealmFS::RUN_DIRECTORY).join(filename))
}
pub fn exists(&self) -> bool {
self.0.exists()
}
pub fn create_dir(&self) -> Result<()> {
fs::create_dir_all(self.path())?;
Ok(())
}
/// Deactivate this mountpoint by unmounting it and removing the directory.
pub fn deactivate(&self) -> Result<()> {
if self.exists() {
info!("Unmounting {} and removing directory", self);
cmd!(Self::UMOUNT, "{}", self)?;
fs::remove_dir(self.path())?;
}
Ok(())
}
/// Full `&Path` of mountpoint.
pub fn path(&self) -> &Path {
self.0.as_path()
}
/// Name of RealmFS extracted from structure of directory filename.
pub fn realmfs(&self) -> &str {
self.field(1)
}
/// Tag field extracted from structure of directory filename.
pub fn tag(&self) -> &str {
self.field(2)
}
fn field(&self, n: usize) -> &str {
Self::filename_fields(self.path())
.and_then(|mut fields| fields.nth(n))
.expect(&format!("Failed to access field {} of mountpoint {}", n, self))
}
/// Return `true` if this instance is a `&Path` in `RealmFS::RUN_DIRECTORY` and
/// the filename has the expected structure.
pub fn is_valid(&self) -> bool {
self.path().starts_with(RealmFS::RUN_DIRECTORY) && self.has_valid_extention() &&
Self::filename_fields(self.path()).map(|it| it.count() == 3).unwrap_or(false)
}
fn has_valid_extention(&self) -> bool {
self.path().extension().map(|e| e == "mountpoint").unwrap_or(false)
}
fn filename_fields(path: &Path) -> Option<impl Iterator<Item=&str>> {
Self::filename(path).map(|name| name.split("-"))
}
fn filename(path: &Path) -> Option<&str> {
path.file_name()
.and_then(OsStr::to_str)
.map(|s| s.trim_end_matches(".mountpoint"))
}
}
impl fmt::Display for Mountpoint {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0.to_str().unwrap())
}
}
impl From<&Path> for Mountpoint {
fn from(p: &Path) -> Self {
Mountpoint(p.to_path_buf())
}
}
impl From<PathBuf> for Mountpoint {
fn from(p: PathBuf) -> Self {
Mountpoint(p)
}
}
impl From<DirEntry> for Mountpoint {
fn from(entry: DirEntry) -> Self {
Mountpoint(entry.path())
}
}

View File

@ -0,0 +1,604 @@
use std::ffi::OsStr;
use std::fs;
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::path::{Path,PathBuf};
use sodiumoxide::randombytes::randombytes;
use hex;
use crate::{CommandLine, ImageHeader, MetaInfo, Result, KeyRing, KeyPair, Signature, util, RealmManager};
use super::resizer::{ImageResizer,ResizeSize};
use super::update::Update;
use crate::realmfs::resizer::Superblock;
use std::sync::{Arc, Weak};
use super::activator::Activation;
use super::mountpoint::Mountpoint;
use crate::realmfs::activator::ActivationState;
use crate::verity::Verity;
// Maximum length of a RealmFS name
const MAX_REALMFS_NAME_LEN: usize = 40;
// The maximum number of backup copies the rotate() method will create
const NUM_BACKUPS: usize = 2;
///
/// Representation of a RealmFS disk image file.
///
/// RealmFS images contain the root filesystem for one or more realms. A single RealmFS
/// image may be shared by multiple running realm instances.
///
/// A RealmFS image can be in a state where it includes all the metadata needed to mount the
/// image with dm-verity to securely enforce read-only access to the image. An image in this state
/// is called 'sealed' and it may be signed either with regular channel keys or with a special
/// key generated upon installation and stored in the kernel keyring.
///
/// An image which is not sealed is called 'unsealed'. In this state, the image can be mounted into
/// a realm with write access, but only one realm can write to the image. All other realms
/// use read-only views of the image.
///
/// RealmFS images are normally stored in the directory `BASE_PATH` (/storage/realms/realmfs-images),
/// and images stored in this directory can be loaded by name rather than needing the exact path
/// to the image.
///
#[derive(Clone)]
pub struct RealmFS {
// RealmFS name
name: Arc<String>,
// path to RealmFS image file
path: Arc<PathBuf>,
// current RealmFS image file header
header: Arc<ImageHeader>,
activation_state: Arc<ActivationState>,
manager: Weak<RealmManager>,
}
impl RealmFS {
// Directory where RealmFS images are stored
pub const BASE_PATH: &'static str = "/storage/realms/realmfs-images";
// Directory where RealmFS mountpoints are created
pub const RUN_DIRECTORY: &'static str = "/run/citadel/realmfs";
// Name used to retrieve key by 'description' from kernel key storage
pub const USER_KEYNAME: &'static str = "realmfs-user";
/// Locate a RealmFS image by name in the default location using the standard name convention
pub fn load_by_name(name: &str) -> Result<RealmFS> {
RealmFS::validate_name(name)?;
let path = RealmFS::image_path(name);
if !path.exists() {
bail!("No image found at {}", path.display());
}
RealmFS::load_from_path(path)
}
/// Load RealmFS image from an exact path.
pub fn load_from_path(path: impl AsRef<Path>) -> Result<RealmFS> {
Self::_load_from_path(path.as_ref(), true)
}
fn _load_from_path(path: &Path, load_activation: bool) -> Result<RealmFS> {
let path = Arc::new(path.to_owned());
let header = RealmFS::load_realmfs_header(&path)?;
let name = header.metainfo().realmfs_name()
.expect("RealmFS does not have a name")
.to_owned();
let name = Arc::new(name);
let header = Arc::new(header);
let manager = Weak::new();
let activation_state = Arc::new(ActivationState::new());
let realmfs = RealmFS {
name, path, header, activation_state, manager
};
if load_activation {
realmfs.load_activation();
}
Ok(realmfs)
}
pub fn set_manager(&mut self, manager: Arc<RealmManager>) {
self.manager = Arc::downgrade(&manager);
}
fn load_activation(&self) {
self.activation_state.load(self);
}
pub fn manager(&self) -> Arc<RealmManager> {
if let Some(manager) = self.manager.upgrade() {
manager
} else {
panic!("No manager set on realmfs {}", self.name);
}
}
fn with_manager<F>(&self, f: F)
where F: FnOnce(Arc<RealmManager>)
{
if let Some(manager) = self.manager.upgrade() {
f(manager);
}
}
pub fn is_valid_realmfs_image(path: impl AsRef<Path>) -> bool {
RealmFS::load_realmfs_header(path.as_ref()).is_ok()
}
fn load_realmfs_header(path: &Path) -> Result<ImageHeader> {
let header = ImageHeader::from_file(path)?;
if !header.is_magic_valid() {
bail!("Image file {} does not have a valid header", path.display());
}
let metainfo = header.metainfo();
if metainfo.image_type() != "realmfs" {
bail!("Image file {} is not a realmfs image", path.display());
}
match metainfo.realmfs_name() {
Some(name) => RealmFS::validate_name(name)?,
None => bail!("RealmFS image file {} does not have a 'realmfs-name' field", path.display()),
};
Ok(header)
}
/// Return an Error result if name is not valid.
fn validate_name(name: &str) -> Result<()> {
if RealmFS::is_valid_name(name) {
Ok(())
} else {
Err(format_err!("Invalid realm name '{}'", name))
}
}
/// Return `true` if `name` is a valid name for a RealmFS.
///
/// Valid names:
/// * Are 40 characters or less in length
/// * Have an alphabetic ascii letter as first character
/// * Contain only alphanumeric ascii characters or '-' (dash)
///
pub fn is_valid_name(name: &str) -> bool {
util::is_valid_name(name, MAX_REALMFS_NAME_LEN)
}
pub fn named_image_exists(name: &str) -> bool {
if !util::is_valid_name(name, MAX_REALMFS_NAME_LEN) {
return false;
}
RealmFS::is_valid_realmfs_image(RealmFS::image_path(name))
}
fn image_path(name: &str) -> PathBuf {
Path::new(RealmFS::BASE_PATH).join(format!("{}-realmfs.img", name))
}
/// Return the `Path` to this RealmFS image file.
pub fn path(&self) -> &Path {
self.path.as_ref()
}
/// Return a new `PathBuf` based on the path of the current image by appending
/// the string `ext` as an extension to the filename. If the current filename
/// ends with '.img' then the specified extension is appended to this as '.img.ext'
/// otherwise it replaces any existing extension.
fn path_with_extension(&self, ext: &str) -> PathBuf {
if self.path.extension() == Some(OsStr::new("img")) {
self.path.with_extension(format!("img.{}", ext))
} else {
self.path.with_extension(ext)
}
}
/// Return a new `PathBuf` based on the path of the current image by replacing
/// the image filename with the specified name.
pub fn path_with_filename(&self, filename: impl AsRef<str>) -> PathBuf {
let mut path = (*self.path).clone();
path.pop();
path.push(filename.as_ref());
path
}
/// Return the 'realmfs-name' metainfo field of this image.
pub fn name(&self) -> &str {
self.name.as_str()
}
pub fn notes(&self) -> Option<String> {
let path = self.path_with_extension("notes");
if path.exists() {
return fs::read_to_string(path).ok();
}
None
}
pub fn save_notes(&self, notes: impl AsRef<str>) -> Result<()> {
let path = self.path_with_extension("notes");
let notes = notes.as_ref();
if path.exists() && notes.is_empty() {
fs::remove_file(path)?;
} else {
fs::write(path, notes)?;
}
Ok(())
}
/// Return `MetaInfo` from image header of this RealmFS.
pub fn metainfo(&self) -> Arc<MetaInfo> {
self.header().metainfo()
}
pub fn header(&self) -> &ImageHeader {
match self.header.reload_if_stale(self.path()) {
Ok(true) => self.load_activation(),
Err(e) => warn!("error reloading stale image header: {}", e),
_ => {},
};
&self.header
}
pub fn is_user_realmfs(&self) -> bool {
!self.is_sealed() || self.metainfo().channel() == Self::USER_KEYNAME
}
/// Return `true` if this RealmFS is 'activated'.
///
/// A RealmFS is activated if the device for the image has been created and mounted.
/// Sealed images create dm-verity devices in /dev/mapper and unsealed images create
/// /dev/loop devices.
pub fn is_activated(&self) -> bool {
self.activation_state.is_activated()
}
/// If this RealmFS is activated return `Activation` instance
pub fn activation(&self) -> Option<Arc<Activation>> {
self.activation_state.get()
}
/// Return `true` if RealmFS is activated and some Realm is currently using
/// it. A RealmFS which is in use cannot be deactivated.
pub fn is_in_use(&self) -> bool {
let active = self.manager().active_mountpoints();
self.activation_state.is_in_use(&active)
}
/// Activate this RealmFS image if not yet activated.
pub fn activate(&self) -> Result<Arc<Activation>> {
if CommandLine::sealed() && !self.is_sealed() && !self.is_update_copy() {
bail!("Cannot activate unsealed realmfs '{}' because citadel.sealed is enabled", self.name());
}
self.activation_state.activate(self)
}
/// Deactivate this RealmFS image if currently activated, but not in use.
/// Return `true` if deactivation occurs.
pub fn deactivate(&self) -> Result<bool> {
let active = self.manager().active_mountpoints();
self.activation_state.deactivate(&active)
}
pub fn fork(&self, new_name: &str) -> Result<RealmFS> {
self._fork(new_name, true)
}
/// Create an unsealed copy of this RealmFS image with a new image name.
///
pub fn fork_unsealed(&self, new_name: &str) -> Result<RealmFS> {
RealmFS::validate_name(new_name)?;
info!("forking RealmFS image '{}' to new name '{}'", self.name(), new_name);
let new_path = self.path_with_filename(format!("{}-realmfs.img", new_name));
if new_path.exists() {
bail!("RealmFS image for name {} already exists", new_name);
}
let new_realmfs = self.copy_image(&new_path, new_name, false)?;
self.with_manager(|m| m.realmfs_added(&new_realmfs));
Ok(new_realmfs)
}
fn _fork(&self, new_name: &str, sealed_fork: bool) -> Result<RealmFS> {
RealmFS::validate_name(new_name)?;
info!("forking RealmFS image '{}' to new name '{}'", self.name(), new_name);
let new_path = self.path_with_filename(format!("{}-realmfs.img", new_name));
if new_path.exists() {
bail!("RealmFS image for name {} already exists", new_name);
}
let new_realmfs = self.copy_image(&new_path, new_name, sealed_fork)?;
self.with_manager(|m| m.realmfs_added(&new_realmfs));
Ok(new_realmfs)
}
pub fn update(&self) -> Update {
Update::new(self)
}
fn is_update_copy(&self) -> bool {
self.path().extension() == Some(OsStr::new("update"))
}
pub(crate) fn update_copy(&self) -> Result<RealmFS> {
let path = self.path_with_extension("update");
let name = self.name().to_string() + "-update";
self.copy_image(&path, &name, false)
}
fn copy_image(&self, path: &Path, name: &str, sealed_copy: bool) -> Result<RealmFS> {
if path.exists() {
bail!("Cannot create sealed copy because target path '{}' already exists", path.display());
}
cmd!("/usr/bin/cp", "--reflink=auto {} {}", self.path.display(), path.display())?;
let mut realmfs = RealmFS::_load_from_path(path, false)?;
self.with_manager(|m| realmfs.set_manager(m));
realmfs.name = Arc::new(name.to_owned());
let result = if sealed_copy {
realmfs.write_sealed_copy_header()
} else {
realmfs.unseal()
};
result.map_err(|e|
if let Err(e) = fs::remove_file(path) {
format_err!("failed to remove {} after realmfs fork/copy failed with: {}", path.display(), e)
} else { e })?;
Ok(realmfs)
}
fn write_sealed_copy_header(&self) -> Result<()> {
let keys = match self.sealing_keys() {
Ok(keys) => keys,
Err(err) => bail!("Cannot seal realmfs image, no sealing keys available: {}", err),
};
let metainfo = self.metainfo();
let metainfo_bytes = self.generate_sealed_metainfo(self.name(), metainfo.verity_salt(), metainfo.verity_root());
let sig = keys.sign(&metainfo_bytes);
self.write_new_metainfo(metainfo_bytes, Some(sig))
}
/// Convert to unsealed RealmFS image by removing dm-verity metadata and hash tree
pub fn unseal(&self) -> Result<()> {
let bytes = RealmFS::generate_unsealed_metainfo(self.name(), self.metainfo().nblocks(), None);
self.write_new_metainfo(bytes, None)?;
if self.has_verity_tree() {
self.truncate_verity()?;
}
Ok(())
}
pub fn set_owner_realm(&self, owner_realm: &str) -> Result<()> {
if self.is_sealed() {
bail!("Cannot set owner realm because RealmFS is sealed");
}
if let Some(activation) = self.activation() {
let rw_mountpoint = activation.mountpoint_rw()
.ok_or(format_err!("unsealed activation expected"))?;
if self.manager().active_mountpoints().contains(rw_mountpoint) {
bail!("Cannot set owner realm because RW mountpoint is in use (by current owner?)");
}
}
let nblocks = self.metainfo().nblocks();
self.update_unsealed_metainfo(self.name(), nblocks, Some(owner_realm.to_owned()))
}
pub fn update_unsealed_metainfo(&self, name: &str, nblocks: usize, owner_realm: Option<String>) -> Result<()> {
if self.is_sealed() {
bail!("Cannot update metainfo on sealed realmfs image");
}
let metainfo_bytes = RealmFS::generate_unsealed_metainfo(name, nblocks, owner_realm);
self.write_new_metainfo(metainfo_bytes, None)
}
fn write_new_metainfo(&self, bytes: Vec<u8>, sig: Option<Signature>) -> Result<()> {
self.header.set_metainfo_bytes(&bytes)?;
if let Some(sig) = sig {
self.header.set_signature(sig.to_bytes())?;
}
self.header.write_header_to(self.path())
}
fn generate_unsealed_metainfo(name: &str, nblocks: usize, owner_realm: Option<String>) -> Vec<u8> {
let mut v = Vec::new();
writeln!(v, "image-type = \"realmfs\"").unwrap();
writeln!(v, "realmfs-name = \"{}\"", name).unwrap();
writeln!(v, "nblocks = {}", nblocks).unwrap();
if let Some(owner) = owner_realm {
writeln!(v, "realmfs-owner = \"{}\"", owner).unwrap();
}
v
}
fn generate_sealed_metainfo(&self, name: &str, verity_salt: &str, verity_root: &str) -> Vec<u8> {
let mut v = RealmFS::generate_unsealed_metainfo(name, self.metainfo().nblocks(), None);
writeln!(v, "channel = \"{}\"", Self::USER_KEYNAME).unwrap();
writeln!(v, "verity-salt = \"{}\"", verity_salt).unwrap();
writeln!(v, "verity-root = \"{}\"", verity_root).unwrap();
v
}
// Remove verity tree from image file by truncating file to the number of blocks in metainfo
fn truncate_verity(&self) -> Result<()> {
let file_nblocks = self.file_nblocks()?;
let expected = self.metainfo_nblocks();
if self.has_verity_tree() {
let f = fs::OpenOptions::new().write(true).open(self.path())?;
let lock = self.header();
lock.clear_flag(ImageHeader::FLAG_HASH_TREE);
lock.write_header(&f)?;
debug!("Removing appended dm-verity hash tree by truncating image from {} blocks to {} blocks", file_nblocks, expected);
f.set_len((expected * 4096) as u64)?;
} else if file_nblocks > expected {
warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set");
}
Ok(())
}
// Return the length in blocks of the actual image file on disk
fn file_nblocks(&self) -> Result<usize> {
let meta = self.path.metadata()?;
let len = meta.len() as usize;
if len % 4096 != 0 {
bail!("realmfs image file '{}' has size which is not a multiple of block size", self.path.display());
}
let nblocks = len / 4096;
if nblocks < (self.metainfo().nblocks() + 1) {
bail!("realmfs image file '{}' has shorter length than nblocks field of image header", self.path.display());
}
Ok(nblocks)
}
fn has_verity_tree(&self) -> bool {
self.header().has_flag(ImageHeader::FLAG_HASH_TREE)
}
pub fn is_sealed(&self) -> bool {
!self.metainfo().verity_root().is_empty()
}
pub fn seal(&self, new_name: Option<&str>) -> Result<()> {
if self.is_sealed() {
info!("RealmFS {} is already sealed. Doing nothing.", self.name());
return Ok(())
}
let keys = match self.sealing_keys() {
Ok(keys) => keys,
Err(err) => bail!("Cannot seal realmfs image, no sealing keys available: {}", err),
};
if self.is_activated() {
bail!("Cannot seal RealmFS because it is currently activated");
}
if self.has_verity_tree() {
warn!("unsealed RealmFS already has a verity hash tree, removing it");
self.truncate_verity()?;
}
let tmp = self.path_with_extension("sealing");
if tmp.exists() {
info!("Temporary copy of realmfs image {} already exists, removing it.", self.name());
fs::remove_file(&tmp)?;
}
info!("Creating temporary copy of realmfs image");
cmd!("/usr/bin/cp", "--reflink=auto {} {}", self.path.display(), tmp.display())?;
let name = new_name.unwrap_or_else(|| self.name());
let mut realmfs = RealmFS::load_from_path(&tmp)?;
realmfs.set_manager(self.manager());
let finish = || {
realmfs.generate_sealing_verity(&keys, name)?;
verbose!("Rename {} to {}", self.path().display(), self.path_with_extension("old").display());
fs::rename(self.path(), self.path_with_extension("old"))?;
verbose!("Rename {} to {}", realmfs.path().display(), self.path().display());
fs::rename(realmfs.path(), self.path())?;
Ok(())
};
if let Err(err) = finish() {
if tmp.exists() {
let _ = fs::remove_file(tmp);
}
return Err(err);
}
Ok(())
}
fn generate_sealing_verity(&self, keys: &KeyPair, name: &str) -> Result<()> {
info!("Generating verity hash tree for sealed realmfs ({})", self.path().display());
let salt = hex::encode(randombytes(32));
let output = Verity::new(self.path()).generate_image_hashtree_with_salt(&self.metainfo(), &salt)?;
let root_hash = output.root_hash()
.ok_or(format_err!("no root hash returned from verity format operation"))?;
info!("root hash is {}", output.root_hash().unwrap());
info!("Signing new image with user realmfs keys");
let metainfo_bytes = self.generate_sealed_metainfo(name, &salt, &root_hash);
let sig = keys.sign(&metainfo_bytes);
self.header().set_flag(ImageHeader::FLAG_HASH_TREE);
self.write_new_metainfo(metainfo_bytes, Some(sig))
}
pub fn has_sealing_keys(&self) -> bool {
self.sealing_keys().is_ok()
}
pub fn sealing_keys(&self) -> Result<KeyPair> {
KeyRing::get_kernel_keypair(Self::USER_KEYNAME)
}
pub fn rotate(&self, new_file: &Path) -> Result<()> {
let backup = |n: usize| Path::new(RealmFS::BASE_PATH).join(format!("{}-realmfs.img.{}", self.name(), n));
for i in (1..NUM_BACKUPS).rev() {
let from = backup(i - 1);
if from.exists() {
fs::rename(from, backup(i))?;
}
}
fs::rename(self.path(), backup(0))?;
fs::rename(new_file, self.path())?;
Ok(())
}
pub fn auto_resize_size(&self) -> Option<ResizeSize> {
ImageResizer::auto_resize_size(self)
}
pub fn resize_grow_to(&self, size: ResizeSize) -> Result<()> {
info!("Resizing to {} blocks", size.nblocks());
ImageResizer::new(self).grow_to(size)
}
pub fn resize_grow_by(&self, size: ResizeSize) -> Result<()> {
ImageResizer::new(self).grow_by(size)
}
pub fn free_size_blocks(&self) -> Result<usize> {
let sb = Superblock::load(self.path(), 4096)?;
Ok(sb.free_block_count() as usize)
}
pub fn allocated_size_blocks(&self) -> Result<usize> {
let meta = self.path().metadata()?;
Ok(meta.blocks() as usize / 8)
}
/// Size of image file in blocks (including header block) based on metainfo `nblocks` field.
pub fn metainfo_nblocks(&self) -> usize {
self.metainfo().nblocks() + 1
}
/// Return `true` if mountpoint belongs to current `Activation` state of
/// this `RealmFS`
pub fn release_mountpoint(&self, mountpoint: &Mountpoint) -> bool {
let is_ours = self.activation()
.map(|a| a.is_mountpoint(mountpoint))
.unwrap_or(false);
if is_ours {
if let Err(e) = self.deactivate() {
warn!("error deactivating mountpoint: {}", e);
}
}
is_ours
}
}

View File

@ -0,0 +1,70 @@
use std::collections::HashMap;
use crate::{RealmFS, RealmManager, Result};
use std::sync::Arc;
use std::fs;
pub struct RealmFSSet {
realmfs_map: HashMap<String, RealmFS>,
}
impl RealmFSSet {
pub fn load() -> Result<Self> {
let mut realmfs_map = HashMap::new();
for realmfs in Self::load_all()? {
let name = realmfs.name().to_string();
realmfs_map.insert(name, realmfs);
}
Ok( RealmFSSet { realmfs_map })
}
fn load_all() -> Result<Vec<RealmFS>> {
let mut v = Vec::new();
for entry in fs::read_dir(RealmFS::BASE_PATH)? {
if let Some(realmfs) = Self::entry_to_realmfs(entry?) {
v.push(realmfs)
}
}
Ok(v)
}
fn entry_to_realmfs(entry: fs::DirEntry) -> Option<RealmFS> {
if let Ok(filename) = entry.file_name().into_string() {
if filename.ends_with("-realmfs.img") {
let name = filename.trim_end_matches("-realmfs.img");
if RealmFS::is_valid_name(name) && RealmFS::named_image_exists(name) {
return RealmFS::load_by_name(name).ok();
}
}
}
None
}
pub fn set_manager(&mut self, manager: Arc<RealmManager>) {
self.realmfs_map.iter_mut().for_each(|(_,v)| v.set_manager(manager.clone()))
}
pub fn by_name(&self, name: &str) -> Option<RealmFS> {
self.realmfs_map.get(name).cloned()
}
pub fn add(&mut self, realmfs: &RealmFS) {
if !self.realmfs_map.contains_key(realmfs.name()) {
self.realmfs_map.insert(realmfs.name().to_string(), realmfs.clone());
}
}
pub fn remove(&mut self, name: &str) -> Option<RealmFS> {
self.realmfs_map.remove(name)
}
pub fn name_exists(&self, name: &str) -> bool {
self.realmfs_map.contains_key(name)
}
pub fn realmfs_list(&self) -> Vec<RealmFS> {
let mut v = self.realmfs_map.values().cloned().collect::<Vec<RealmFS>>();
v.sort_unstable_by(|a,b| a.name().cmp(&b.name()));
v
}
}

View File

@ -0,0 +1,183 @@
use std::fs::{File,OpenOptions};
use std::io::{Read,Seek,SeekFrom};
use std::path::Path;
use byteorder::{ByteOrder,LittleEndian};
use crate::{RealmFS,Result,LoopDevice};
const BLOCK_SIZE: usize = 4096;
const BLOCKS_PER_MEG: usize = (1024 * 1024) / BLOCK_SIZE;
const BLOCKS_PER_GIG: usize = 1024 * BLOCKS_PER_MEG;
const RESIZE2FS: &str = "resize2fs";
// If less than 1gb remaining space
const AUTO_RESIZE_MINIMUM_FREE: ResizeSize = ResizeSize(1 * BLOCKS_PER_GIG);
// ... add 4gb to size of image
const AUTO_RESIZE_INCREASE_SIZE: ResizeSize = ResizeSize(4 * BLOCKS_PER_GIG);
pub struct ImageResizer<'a> {
image: &'a RealmFS,
}
pub struct ResizeSize(usize);
impl ResizeSize {
pub fn gigs(n: usize) -> ResizeSize {
ResizeSize(BLOCKS_PER_GIG * n)
}
pub fn megs(n: usize) -> ResizeSize {
ResizeSize(BLOCKS_PER_MEG * n)
}
pub fn blocks(n: usize) -> ResizeSize {
ResizeSize(n)
}
pub fn nblocks(&self) -> usize {
self.0
}
pub fn size_in_gb(&self) -> usize {
self.0 / BLOCKS_PER_GIG
}
pub fn size_in_mb(&self) -> usize {
self.0 / BLOCKS_PER_MEG
}
}
impl <'a> ImageResizer<'a> {
pub fn new(image: &'a RealmFS) -> ImageResizer<'a> {
ImageResizer { image }
}
pub fn grow_to(&mut self, size: ResizeSize) -> Result<()> {
let target_nblocks = size.nblocks();
let current_nblocks = self.image.metainfo_nblocks();
if current_nblocks >= target_nblocks {
info!("RealmFS image is already larger than requested size, doing nothing");
} else {
let size = ResizeSize::blocks(target_nblocks - current_nblocks);
self.grow_by(size)?;
}
Ok(())
}
pub fn grow_by(&mut self, size: ResizeSize) -> Result<()> {
let nblocks = size.nblocks();
let new_nblocks = self.image.metainfo_nblocks() + nblocks;
if self.image.is_sealed() {
bail!("Cannot resize sealed image '{}'. unseal first", self.image.name());
}
self.resize(new_nblocks)
}
fn resize(&self, new_nblocks: usize) -> Result<()> {
if new_nblocks < self.image.metainfo_nblocks() {
bail!("Cannot shrink image")
}
if (new_nblocks - self.image.metainfo_nblocks()) > ResizeSize::gigs(8).nblocks() {
bail!("Can only increase size of RealmFS image by a maximum of 8gb at one time");
}
ImageResizer::resize_image_file(self.image.path(), new_nblocks)?;
if let Some(open_loop) = self.notify_open_loops()? {
info!("Running resize2fs {:?}", open_loop);
cmd!(RESIZE2FS, "{}", open_loop.device().display())?;
} else {
LoopDevice::with_loop(self.image.path(), Some(4096), false, |loopdev| {
info!("Running resize2fs {:?}", loopdev);
cmd!(RESIZE2FS, "{}", loopdev.device().display())?;
Ok(())
})?;
}
let owner = self.image.metainfo().realmfs_owner().map(|s| s.to_owned());
self.image.update_unsealed_metainfo(self.image.name(), new_nblocks - 1, owner)?;
Ok(())
}
fn resize_image_file(file: &Path, nblocks: usize) -> Result<()> {
let len = nblocks * BLOCK_SIZE;
info!("Resizing image file to {}", len);
OpenOptions::new()
.write(true)
.open(file)?
.set_len(len as u64)?;
Ok(())
}
fn notify_open_loops(&self) -> Result<Option<LoopDevice>> {
let mut open_loop = None;
for loopdev in LoopDevice::find_devices_for(self.image.path())? {
loopdev.resize()
.unwrap_or_else(|err| warn!("Error running losetup -c {:?}: {}", loopdev, err));
open_loop = Some(loopdev);
}
Ok(open_loop)
}
/// If the RealmFS needs to be resized to a larger size, returns the
/// recommended size. Pass this value to `ImageResizer.grow_to()` to
/// complete the resize.
pub fn auto_resize_size(realmfs: &RealmFS) -> Option<ResizeSize> {
let sb = match Superblock::load(realmfs.path(), 4096) {
Ok(sb) => sb,
Err(e) => {
warn!("Error reading superblock from {}: {}", realmfs.path().display(), e);
return None;
},
};
sb.free_block_count();
let free_blocks = sb.free_block_count() as usize;
if free_blocks < AUTO_RESIZE_MINIMUM_FREE.nblocks() {
let mask = AUTO_RESIZE_INCREASE_SIZE.nblocks() - 1;
let grow_blocks = (free_blocks + mask) & !mask;
Some(ResizeSize::blocks(grow_blocks))
} else {
None
}
}
}
const SUPERBLOCK_SIZE: usize = 1024;
pub struct Superblock([u8; SUPERBLOCK_SIZE]);
impl Superblock {
fn new() -> Superblock {
Superblock([0u8; SUPERBLOCK_SIZE])
}
pub fn load(path: impl AsRef<Path>, offset: u64) -> Result<Superblock> {
let mut sb = Superblock::new();
let mut file = File::open(path.as_ref())?;
file.seek(SeekFrom::Start(1024 + offset))?;
file.read_exact(&mut sb.0)?;
Ok(sb)
}
pub fn free_block_count(&self) -> u64 {
self.split_u64(0x0C, 0x158)
}
fn u32(&self, offset: usize) -> u32 {
LittleEndian::read_u32(self.at(offset))
}
fn split_u64(&self, offset_lo: usize, offset_hi: usize) -> u64 {
let lo = self.u32(offset_lo) as u64;
let hi = self.u32(offset_hi) as u64;
(hi << 32) | lo
}
fn at(&self, offset: usize) -> &[u8] {
&self.0[offset..]
}
}

View File

@ -0,0 +1,145 @@
use std::fs;
use std::process::Command;
use crate::{Result, RealmFS };
use crate::realmfs::Mountpoint;
use crate::realm::BridgeAllocator;
use crate::ResizeSize;
enum UpdateType {
NotSetup,
Sealed(RealmFS),
Unsealed,
}
pub struct Update<'a> {
realmfs: &'a RealmFS,
network_allocated: bool,
update_type: UpdateType,
}
impl <'a> Update<'a> {
pub fn new(realmfs: &'a RealmFS) -> Self {
Update { realmfs, network_allocated: false, update_type: UpdateType::NotSetup }
}
pub fn setup(&mut self) -> Result<()> {
self.update_type = self.create_update_type()?;
Ok(())
}
pub fn auto_resize_size(&self) -> Option<ResizeSize> {
self.target_image().auto_resize_size()
}
pub fn apply_resize(&self, size: ResizeSize) -> Result<()> {
self.target_image().resize_grow_to(size)
}
fn target_image(&self) -> &RealmFS {
if let UpdateType::Sealed(ref image) = self.update_type {
image
} else {
&self.realmfs
}
}
fn create_update_type(&self) -> Result<UpdateType> {
if self.realmfs.is_sealed() {
let update_image = self.realmfs.update_copy()?;
Ok(UpdateType::Sealed(update_image))
} else {
Ok(UpdateType::Unsealed)
}
}
pub fn open_update_shell(&mut self) -> Result<()> {
self.run_update_shell("/usr/libexec/configure-host0.sh && exec /bin/bash")
}
fn mountpoint(&self) -> Result<Mountpoint> {
let target = self.target_image();
let activation = target.activate()
.map_err(|e| format_err!("failed to activate update image: {}", e))?;
activation.mountpoint_rw().cloned()
.ok_or(format_err!("Update image activation does not have a writeable mountpoint"))
}
pub fn run_update_shell(&mut self, command: &str) -> Result<()> {
let mountpoint = self.mountpoint().map_err(|e| {
let _ = self.cleanup();
format_err!("Could not run update shell: {}", e)
})?;
let mut alloc = BridgeAllocator::default_bridge()?;
let addr = alloc.allocate_address_for(&self.name())?;
let gw = alloc.gateway();
self.network_allocated = true;
Command::new("/usr/bin/systemd-nspawn")
.arg(format!("--setenv=IFCONFIG_IP={}", addr))
.arg(format!("--setenv=IFCONFIG_GW={}", gw))
.arg("--quiet")
.arg(format!("--machine={}", self.name()))
.arg(format!("--directory={}", mountpoint))
.arg(format!("--network-zone=clear"))
.arg("/bin/bash")
.arg("-c")
.arg(command)
.status()
.map_err(|e| {
let _ = self.cleanup();
e
})?;
self.deactivate_update()?;
Ok(())
}
fn deactivate_update(&self) -> Result<()> {
match self.update_type {
UpdateType::Sealed(ref update_image) => update_image.deactivate()?,
UpdateType::Unsealed => self.realmfs.deactivate()?,
UpdateType::NotSetup => return Ok(()),
};
Ok(())
}
pub fn apply_update(&mut self) -> Result<()> {
match self.update_type {
UpdateType::Sealed(ref update_image) => {
update_image.seal(Some(self.realmfs.name()))?;
fs::rename(update_image.path(), self.realmfs.path())?;
self.cleanup()
},
UpdateType::Unsealed => self.cleanup(),
UpdateType::NotSetup => Ok(()),
}
}
fn name(&self) -> String {
format!("{}-update", self.realmfs.name())
}
pub fn cleanup(&mut self) -> Result<()> {
match self.update_type {
UpdateType::Sealed(ref update_image) => {
update_image.deactivate()?;
if update_image.path().exists() {
fs::remove_file(update_image.path())?;
}
},
UpdateType::Unsealed => {
self.realmfs.deactivate()?;
}
_ => {},
}
self.update_type = UpdateType::NotSetup;
if self.network_allocated {
BridgeAllocator::default_bridge()?
.free_allocation_for(&self.name())?;
self.network_allocated = false;
}
Ok(())
}
}