RealmFS refactored. Much Simpler.

The concept of an 'unsealed' RealmFS no longer exists so support for this has been
removed. The result is much less complex and easier to understand and maintain.
This commit is contained in:
Bruce Leidl 2020-06-19 10:52:41 -04:00
parent b1f5827096
commit 61d5e10034
23 changed files with 733 additions and 1200 deletions

View File

@ -31,7 +31,6 @@ pub fn help_panel(screen: usize) -> impl View {
.child(help_header("RealmsFS Image Commands"))
.child(DummyView)
.child(help_item("n", "Create new RealmFS as fork of selected image."))
.child(help_item("s", "Seal selected RealmFS image."))
.child(help_item("u", "Open shell to update selected RealmFS image."))
.child(help_item(".", "Toggle display of system RealmFS images."))
.child(DummyView)

View File

@ -219,11 +219,9 @@ impl <'a> RealmInfoRender <'a> {
None => return false,
};
if let Some(activation) = realmfs.activation() {
if activation.is_mountpoint(&mountpoint) {
if realmfs.is_activated() && realmfs.mountpoint() == mountpoint {
return false;
}
};
true
}

View File

@ -11,12 +11,12 @@ use crate::item_list::ItemList;
use crate::realmfs::fork_dialog::ForkDialog;
use crate::notes::NotesDialog;
type ActionCallback = Fn(&RealmFS)+Send+Sync;
type ActionCallback = dyn Fn(&RealmFS)+Send+Sync;
#[derive(Clone)]
pub struct RealmFSAction {
realmfs: RealmFS,
sink: Sender<Box<CbFunc>>,
sink: Sender<Box<dyn CbFunc>>,
callback: Arc<ActionCallback>
}
@ -37,9 +37,7 @@ impl RealmFSAction {
}
EventResult::with_cb(|s| {
let action = RealmFSAction::new(s, Arc::new(|r| {
Self::log_fail("deactivating realmfs", || r.deactivate());
}));
let action = RealmFSAction::new(s, Arc::new(|r| { r.deactivate(); }));
if action.realmfs.is_in_use() {
s.add_layer(Dialog::info("RealmFS is in use and cannot be deactivated").title("Cannot Deactivate"));
@ -65,45 +63,6 @@ impl RealmFSAction {
EventResult::Consumed(None)
}
pub fn seal_realmfs(sealed: bool) -> EventResult {
if sealed {
return EventResult::Consumed(None);
}
EventResult::with_cb(|s| {
let action = RealmFSAction::new(s, Arc::new(|r| {
Self::log_fail("sealing realmfs", || r.seal(None));
}));
if action.realmfs.is_sealed() {
return;
}
if action.realmfs.is_activated() {
s.add_layer(Dialog::info("Cannot seal realmfs because it is currently activated. Deactivate first").title("Cannot Seal"));
return;
}
if !action.realmfs.has_sealing_keys() {
s.add_layer(Dialog::info("Cannot seal realmfs because no keys are available to sign image.").title("Cannot Seal"));
return;
}
let title = "Seal RealmFS?";
let msg = format!("Would you like to seal RealmFS '{}'?", action.realmfs.name());
let dialog = confirm_dialog(title, &msg, move |_| action.run_action());
s.add_layer(dialog);
})
}
pub fn unseal_realmfs(sealed: bool) -> EventResult {
if !sealed {
return EventResult::Consumed(None);
}
let title = "Unseal RealmFS?";
let msg = "Do you want to unseal '$REALMFS'";
Self::confirm_action(title, msg, |r| {
Self::log_fail("unsealing realmfs", || r.unseal());
})
}
pub fn delete_realmfs(user: bool) -> EventResult {
if !user {
return EventResult::Consumed(None);
@ -112,8 +71,7 @@ impl RealmFSAction {
let msg = "Are you sure you want to delete '$REALMFS'?";
let cb = Self::wrap_callback(|r| {
let manager = r.manager();
if let Err(e) = manager.delete_realmfs(r) {
if let Err(e) = r.delete() {
warn!("error deleting realmfs: {}", e);
}
});
@ -210,19 +168,6 @@ impl RealmFSAction {
Arc::new(callback)
}
pub fn confirm_action<F>(title: &'static str, message: &'static str, callback: F) -> EventResult
where F: Fn(&RealmFS), F: 'static + Send+Sync,
{
let callback = Arc::new(callback);
EventResult::with_cb(move |s| {
let action = RealmFSAction::new(s, callback.clone());
let message = message.replace("$REALMFS", action.realmfs.name());
let dialog = confirm_dialog(title, &message, move |_| action.run_action());
s.add_layer(dialog);
})
}
fn new(s: &mut Cursive, callback: Arc<ActionCallback>) -> RealmFSAction {
let realmfs = Self::current_realmfs(s);
let sink = s.cb_sink().clone();

View File

@ -85,8 +85,8 @@ impl ItemListContent<RealmFS> for RealmFSListContent {
}
fn on_event(&mut self, item: Option<&RealmFS>, event: Event) -> EventResult {
let (activated,sealed,user) = item.map(|r| (r.is_activated(), r.is_sealed(), r.is_user_realmfs()))
.unwrap_or((false, false, false));
let (activated,user) = item.map(|r| (r.is_activated(), r.is_user_realmfs()))
.unwrap_or((false, false));
match event {
Event::Key(Key::Enter) => RealmFSAction::activate_realmfs(activated),
@ -96,8 +96,6 @@ impl ItemListContent<RealmFS> for RealmFSListContent {
Event::Char('r') => RealmFSAction::resize_realmfs(),
Event::Char('u') => RealmFSAction::update_realmfs(),
Event::Char('n') => RealmFSAction::fork_realmfs(),
Event::Char('s') => RealmFSAction::seal_realmfs(sealed),
Event::Char('S') => RealmFSAction::unseal_realmfs(sealed),
Event::Char('e') => RealmFSAction::edit_notes(),
Event::Char('.') => {
self.show_system = !self.show_system;
@ -129,17 +127,15 @@ impl <'a> RealmFSInfoRender <'a> {
fn render_realmfs(&mut self) {
let r = self.realmfs;
if r.is_sealed() && r.is_user_realmfs() {
self.heading("Sealed RealmFS");
} else if r.is_sealed() {
self.heading("System RealmFS");
if r.is_user_realmfs() {
self.heading("User RealmFS");
} else {
self.heading("Unsealed RealmFS");
self.heading("System RealmFS");
}
self.print(" ").render_name();
if r.is_sealed() && !r.is_user_realmfs() {
if !r.is_user_realmfs() {
self.print(format!(" (channel={})", r.metainfo().channel()));
}
@ -171,7 +167,7 @@ impl <'a> RealmFSInfoRender <'a> {
match sizes(r) {
Ok((free,allocated)) => {
let size = r.metainfo_nblocks();
let size = r.metainfo().nblocks() + 1;
let used = size - free;
let used_percent = (used as f64 * 100.0) / (size as f64);
@ -203,14 +199,12 @@ impl <'a> RealmFSInfoRender <'a> {
}
fn render_activation(&mut self) {
if !self.realmfs.is_activated() {
return;
}
let activation = match self.realmfs.activation() {
Some(activation) => activation,
None => return,
};
let realms = self.realmfs.manager()
.realms_for_activation(&activation);
let mountpoint = self.realmfs.mountpoint();
let realms = self.realmfs.manager().realms_for_mountpoint(&mountpoint);
if !realms.is_empty() {
self.heading("In Use")
@ -225,25 +219,18 @@ impl <'a> RealmFSInfoRender <'a> {
self.heading("Active").newlines(2);
}
let devname = self.realmfs.mountpoint().verity_device();
self.print(" Device : ")
.dim_style()
.println(activation.device())
.println(devname)
.pop();
let mount = if activation.mountpoint_rw().is_some() { "Mounts" } else { "Mount "};
self.print(format!(" {} : ", mount))
self.print(" Mount : ")
.dim_style()
.print(format!("{}", activation.mountpoint()))
.print(format!("{}", self.realmfs.mountpoint()))
.pop()
.newline();
if let Some(rw) = activation.mountpoint_rw() {
self.print(" ")
.dim_style()
.print(format!("{}", rw))
.pop()
.newline();
}
self.newline();
}

View File

@ -10,11 +10,10 @@ use crate::theme::{ThemeHandler, ThemeChooser};
use crate::terminal::TerminalTools;
use crate::logview::TextContentLogOutput;
use std::sync::{Arc,RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::{mem, io};
use std::mem;
use crate::item_list::ItemList;
use crate::realm::RealmListContent;
use crate::realmfs::RealmFSListContent;
use std::io::Write;
#[derive(Clone)]
pub enum DeferredAction {
@ -325,54 +324,6 @@ impl RealmUI {
}
fn run_realmfs_update(&self, realmfs: &RealmFS) -> Result<()> {
self.with_termtools(|tt| {
tt.apply_base16_by_slug("icy");
tt.set_window_title(format!("Update {}-realmfs.img", realmfs.name()));
tt.clear_screen();
});
let mut update = realmfs.update();
update.setup()?;
if let Some(size) = update.auto_resize_size() {
println!("Resizing image to {} gb", size.size_in_gb());
update.apply_resize(size)?;
realmfs.interactive_update(Some("icy"))
}
println!();
println!("Opening update shell for '{}-realmfs.img'", realmfs.name());
println!();
println!("Exit shell with ctrl-d or 'exit' to return to realm manager");
println!();
update.open_update_shell()?;
if realmfs.is_sealed() {
if self.prompt_user("Apply changes?", true)? {
update.apply_update()
} else {
update.cleanup()
}
} else {
update.apply_update()?;
if !realmfs.is_activated() && self.prompt_user("Seal RealmFS?", true)? {
realmfs.seal(None)?;
}
Ok(())
}
}
fn prompt_user(&self, prompt: &str, default_y: bool) -> Result<bool> {
let yn = if default_y { "(Y/n)" } else { "(y/N)" };
print!("{} {} : ", prompt, yn);
io::stdout().flush()?;
let mut line = String::new();
io::stdin().read_line(&mut line)?;
let yes = match line.trim().chars().next() {
Some(c) => c == 'Y' || c == 'y',
None => default_y,
};
Ok(yes)
}
}

View File

@ -121,7 +121,8 @@ impl UpdateBuilder {
let hashfile = self.config.workdir_path(self.verity_filename());
let outfile = self.config.workdir_path("verity-format.out");
let output = Verity::new(self.image()).generate_initial_hashtree(&hashfile)?;
let verity = Verity::new(self.image())?;
let output = verity.generate_initial_hashtree(&hashfile)?;
fs::write(outfile, output.output())
.context("failed to write veritysetup command output to a file")?;

View File

@ -2,6 +2,7 @@ use clap::App;
use clap::ArgMatches;
use libcitadel::{Result,RealmFS,Logger,LogLevel};
use libcitadel::util::is_euid_root;
use clap::SubCommand;
use clap::AppSettings::*;
use clap::Arg;
@ -45,12 +46,6 @@ is the final absolute size of the image.")
.help("Name of new image to create")
.required(true)))
.subcommand(SubCommand::with_name("seal")
.about("Seal an unsealed RealmFS image")
.arg(Arg::with_name("image")
.help("Path or name of RealmFS image to seal")
.required(true)))
.subcommand(SubCommand::with_name("autoresize")
.about("Increase size of RealmFS image if not enough free space remains")
.arg(Arg::with_name("image")
@ -85,7 +80,6 @@ is the final absolute size of the image.")
("resize", Some(m)) => resize(m),
("autoresize", Some(m)) => autoresize(m),
("fork", Some(m)) => fork(m),
("seal", Some(m)) => seal(m),
("update", Some(m)) => update(m),
("activate", Some(m)) => activate(m),
("deactivate", Some(m)) => deactivate(m),
@ -98,7 +92,6 @@ is the final absolute size of the image.")
}
}
fn realmfs_image(arg_matches: &ArgMatches) -> Result<RealmFS> {
let image = match arg_matches.value_of("image") {
Some(s) => s,
@ -186,28 +179,12 @@ fn fork(arg_matches: &ArgMatches) -> Result<()> {
Ok(())
}
fn seal(arg_matches: &ArgMatches) -> Result<()> {
let img = realmfs_image(arg_matches)?;
let img_arg = arg_matches.value_of("image").unwrap();
if img.is_sealed() {
info!("RealmFS image {} is already sealed", img_arg);
} else if img.is_activated() {
info!("RealmFS image {} cannot be sealed because it is currently activated", img_arg);
} else {
img.seal(None)?;
}
Ok(())
}
fn update(arg_matches: &ArgMatches) -> Result<()> {
if !is_euid_root() {
bail!("RealmFS updates must be run as root");
}
let img = realmfs_image(arg_matches)?;
let mut update = img.update();
update.setup()?;
update.open_update_shell()?;
update.apply_update()?;
update.cleanup()?;
img.interactive_update(Some("icy"))?;
Ok(())
}
@ -215,17 +192,12 @@ fn activate(arg_matches: &ArgMatches) -> Result<()> {
let img = realmfs_image(arg_matches)?;
let img_arg = arg_matches.value_of("image").unwrap();
let activation = if let Some(activation) = img.activation() {
if img.is_activated() {
info!("RealmFS image {} is already activated", img_arg);
activation
} else {
info!("Activating {}", img_arg);
img.activate()?
};
info!("Read-Only mountpoint: {}", activation.mountpoint());
if let Some(rw) = activation.mountpoint_rw() {
info!("Read-Write mountpoint: {}", rw);
img.activate()?;
}
info!("Mountpoint: {}", img.mountpoint());
Ok(())
}
@ -238,7 +210,7 @@ fn deactivate(arg_matches: &ArgMatches) -> Result<()> {
info!("Cannot deactivate RealmFS image {} because it is currently in use", img_arg);
} else {
info!("Deactivating {}", img_arg);
img.deactivate()?;
img.deactivate();
}
Ok(())
}

View File

@ -20,7 +20,7 @@ const METAINFO_OFFSET: usize = 8;
const SIGNATURE_LENGTH: usize = 64;
/// Maximum amount of space in block for metainfo document
const MAX_METAINFO_LEN: usize = (ImageHeader::HEADER_SIZE - (METAINFO_OFFSET + SIGNATURE_LENGTH));
const MAX_METAINFO_LEN: usize = ImageHeader::HEADER_SIZE - (METAINFO_OFFSET + SIGNATURE_LENGTH);
fn is_valid_status_code(code: u8) -> bool {
code <= ImageHeader::STATUS_BAD_META
@ -333,6 +333,12 @@ impl ImageHeader {
self.read_u16(6) as usize
}
pub fn update_metainfo<P: AsRef<Path>>(&self, metainfo_bytes: &[u8], signature: &[u8], path: P) -> Result<()> {
self.set_metainfo_bytes(metainfo_bytes)?;
self.set_signature(signature)?;
self.write_header_to(path)
}
pub fn set_metainfo_bytes(&self, bytes: &[u8]) -> Result<()> {
let metainfo = MetaInfo::parse_bytes(bytes)
.ok_or_else(|| format_err!("Could not parse metainfo bytes as valid metainfo document"))?;
@ -524,8 +530,8 @@ impl MetaInfo {
&self.verity_salt
}
pub fn verity_tag(&self) -> String {
self.verity_root().chars().take(8).collect()
pub fn verity_tag(&self) -> &str {
&self.verity_root()[..8]
}
}

View File

@ -35,7 +35,6 @@ mod realm;
pub mod terminal;
mod system;
pub use crate::config::OsRelease;
pub use crate::blockdev::BlockDev;
pub use crate::cmdline::CommandLine;
@ -43,10 +42,10 @@ pub use crate::header::{ImageHeader,MetaInfo};
pub use crate::partition::Partition;
pub use crate::resource::ResourceImage;
pub use crate::keys::{KeyPair,PublicKey,Signature};
pub use crate::realmfs::{RealmFS,Mountpoint,Activation};
pub use crate::realmfs::{RealmFS,Mountpoint};
pub use crate::keyring::{KeyRing,KernelKey};
pub use crate::exec::{Exec,FileRange};
pub use crate::realmfs::resizer::{ImageResizer,ResizeSize};
pub use crate::realmfs::resizer::ResizeSize;
pub use crate::realm::overlay::RealmOverlay;
pub use crate::realm::realm::Realm;
pub use crate::realm::config::{RealmConfig,OverlayType,GLOBAL_CONFIG};

View File

@ -99,9 +99,6 @@ pub struct RealmConfig {
pub realmfs: Option<String>,
#[serde(rename="realmfs-write")]
pub realmfs_write: Option<bool>,
#[serde(rename="terminal-scheme")]
pub terminal_scheme: Option<String>,
@ -205,7 +202,6 @@ impl RealmConfig {
extra_bindmounts_ro: None,
realm_depends: None,
realmfs: Some(DEFAULT_REALMFS.into()),
realmfs_write: Some(false),
overlay: Some(DEFAULT_OVERLAY.into()),
terminal_scheme: None,
netns: None,
@ -235,7 +231,6 @@ impl RealmConfig {
realm_depends: None,
ephemeral_persistent_dirs: None,
realmfs: None,
realmfs_write: None,
overlay: None,
terminal_scheme: None,
netns: None,
@ -381,11 +376,6 @@ impl RealmConfig {
self.str_value(|c| c.realmfs.as_ref()).unwrap_or(DEFAULT_REALMFS)
}
pub fn realmfs_write(&self) -> bool {
self.bool_value(|c| c.realmfs_write)
}
/// Name of a terminal color scheme to use in this realm.
pub fn terminal_scheme(&self) -> Option<&str> {
self.str_value(|c| c.terminal_scheme.as_ref())

View File

@ -3,7 +3,7 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::{Mountpoint, Activation,Result, Realms, RealmFS, Realm, util};
use crate::{Mountpoint, Result, Realms, RealmFS, Realm, util};
use crate::realmfs::realmfs_set::RealmFSSet;
use super::systemd::Systemd;
@ -125,14 +125,11 @@ impl RealmManager {
self.inner().realms.active(ignore_system)
}
/// Return a list of Realms that are using the `activation`
pub fn realms_for_activation(&self, activation: &Activation) -> Vec<Realm> {
/// Return a list of Realms that are using `mountpoint`
pub fn realms_for_mountpoint(&self, mountpoint: &Mountpoint) -> Vec<Realm> {
self.active_realms(false)
.into_iter()
.filter(|r| {
r.realmfs_mountpoint()
.map_or(false, |mp| activation.is_mountpoint(&mp))
})
.filter(|r| r.has_mountpoint(mountpoint))
.collect()
}
@ -148,30 +145,15 @@ impl RealmManager {
self.inner().realmfs_set.by_name(name)
}
/// Notify `RealmManager` that `mountpoint` has been released by a
/// `Realm`.
/// If mountpoint is no longer in use by another `Realm` deactivate it.
pub fn release_mountpoint(&self, mountpoint: &Mountpoint) {
info!("releasing mountpoint: {}", mountpoint);
if !mountpoint.is_valid() {
warn!("bad mountpoint {} passed to release_mountpoint()", mountpoint);
return;
}
if let Some(realmfs) = self.realmfs_by_name(mountpoint.realmfs()) {
if realmfs.release_mountpoint(mountpoint) {
return;
}
}
if let Some(activation) = Activation::for_mountpoint(mountpoint) {
let active = self.active_mountpoints();
if let Err(e) = activation.deactivate(&active) {
warn!("error on detached deactivation for {}: {}",activation.device(), e);
} else {
info!("Deactivated detached activation for device {}", activation.device());
}
} else {
warn!("No activation found for released mountpoint {}", mountpoint);
if !self.realmfs_mountpoint_in_use(mountpoint) {
mountpoint.deactivate();
}
}
@ -184,6 +166,12 @@ impl RealmManager {
.collect()
}
pub fn realmfs_mountpoint_in_use(&self, mountpoint: &Mountpoint) -> bool {
self.active_realms(false)
.iter()
.any(|r| r.has_mountpoint(mountpoint))
}
pub fn start_boot_realms(&self) -> Result<()> {
if let Some(realm) = self.default_realm() {
if let Err(e) = self.start_realm(&realm) {
@ -387,7 +375,7 @@ impl RealmManager {
if realmfs.is_in_use() {
bail!("Cannot delete realmfs because it is in use");
}
realmfs.deactivate()?;
realmfs.deactivate();
if realmfs.is_activated() {
bail!("Unable to deactive Realmfs, cannot delete");
}

View File

@ -6,12 +6,12 @@ use std::os::unix::fs::MetadataExt;
use super::overlay::RealmOverlay;
use super::config::{RealmConfig,GLOBAL_CONFIG,OverlayType};
use super::config::{RealmConfig,GLOBAL_CONFIG};
use super::realms::Realms;
use super::systemd::Systemd;
use crate::realmfs::{Mountpoint, Activation};
use crate::{symlink, util, Result, RealmFS, CommandLine, RealmManager};
use crate::realmfs::Mountpoint;
use crate::{symlink, util, Result, RealmFS, CommandLine, RealmManager, OverlayType};
const MAX_REALM_NAME_LEN:usize = 128;
@ -138,6 +138,15 @@ impl Realm {
&self.name
}
// Return true if this `Realm` has an active RealmFS mountpoint and
// that mountpoint matches 'mountpoint'
pub fn has_mountpoint(&self, mountpoint: &Mountpoint) -> bool {
self.realmfs_mountpoint()
.map(|mp| &mp == mountpoint)
.unwrap_or(false)
}
// Return a `Mountpoint` instance for the RealmFS used by this `Realm`
pub fn realmfs_mountpoint(&self) -> Option<Mountpoint> {
symlink::read(self.realmfs_mountpoint_symlink())
.map(Into::into)
@ -204,19 +213,17 @@ impl Realm {
///
/// 1) Find the RealmFS for this realm and activate it if not yet activated.
/// 2) If this realm is configured to use an overlay, set it up.
/// 3) If the RealmFS is unsealed, choose between ro/rw mountpoints
/// 4) create 'rootfs' symlink in realm run path pointing to rootfs base
/// 5) create 'realmfs-mountpoint' symlink pointing to realmfs mount
/// 3) create 'rootfs' symlink in realm run path pointing to rootfs base
/// 4) create 'realmfs-mountpoint' symlink pointing to realmfs mount
///
pub fn setup_rootfs(&self) -> Result<PathBuf> {
let realmfs = self.get_named_realmfs(self.config().realmfs())?;
let activation = realmfs.activate()?;
let writeable = self.use_writable_mountpoint(&realmfs);
let mountpoint = self.choose_mountpoint(writeable, &activation)?;
realmfs.activate()?;
let mountpoint = realmfs.mountpoint();
let rootfs = match RealmOverlay::for_realm(self) {
Some(ref overlay) if !writeable => overlay.create(mountpoint.path())?,
Some(ref overlay) => overlay.create(mountpoint.path())?,
_ => mountpoint.path().to_owned(),
};
@ -227,16 +234,6 @@ impl Realm {
Ok(rootfs)
}
fn choose_mountpoint<'a>(&self, writeable: bool, activation: &'a Activation) -> Result<&'a Mountpoint> {
if !writeable {
Ok(activation.mountpoint())
} else if let Some(mountpoint) = activation.mountpoint_rw() {
Ok(mountpoint)
} else {
Err(format_err!("RealmFS activation does not have writable mountpoint as expected"))
}
}
/// Clean up the rootfs created when starting this realm.
///
/// 1) If an overlay was created, remove it.
@ -266,13 +263,6 @@ impl Realm {
}
}
fn use_writable_mountpoint(&self, realmfs: &RealmFS) -> bool {
match realmfs.metainfo().realmfs_owner() {
Some(name) => !realmfs.is_sealed() && name == self.name(),
None => false,
}
}
/// Return named RealmFS instance if it already exists.
///
/// Otherwise, create it as a fork of the 'default' image.
@ -385,15 +375,13 @@ impl Realm {
result
}
/// Return `true` if this realm is configured to use a read-only RealmFS mount.
/// Return `true` if this realm is configured to use a read-only root filesytem.
///
/// Since either type of overlay will produce a writable root filesystem the
/// rootfs is only read-only if no overlay is configured.
///
pub fn readonly_rootfs(&self) -> bool {
if self.config().overlay() != OverlayType::None {
false
} else if CommandLine::sealed() {
true
} else {
!self.config().realmfs_write()
}
self.config().overlay() == OverlayType::None
}
/// Return path to root directory as seen by mount namespace inside the realm container
@ -449,7 +437,6 @@ impl Realm {
/// is the run path of the realm.
pub fn is_current(&self) -> bool {
Realms::read_current_realm_symlink() == Some(self.run_path())
//Realms::current_realm_name().as_ref() == Some(&self.name)
}
/// Return `true` if this realm is the default realm.

View File

@ -1,365 +0,0 @@
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(Arc::new)
}
/// 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());
Self::new_loop(ro, rw, loopdev)
})
} else {
let device = Verity::device_name_for_mountpoint(mountpoint);
if Path::new("/dev/mapper").join(&device).exists() {
Some(Self::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

@ -1,5 +1,4 @@
pub(crate) mod resizer;
mod activator;
mod mountpoint;
mod update;
pub(crate) mod realmfs_set;
@ -8,4 +7,3 @@ mod realmfs;
pub use self::realmfs::RealmFS;
pub use self::mountpoint::Mountpoint;
pub use self::activator::Activation;

View File

@ -1,16 +1,28 @@
use std::fs::{self, DirEntry};
use std::ffi::OsStr;
use std::fmt;
use std::fs::{self, DirEntry};
use std::path::{PathBuf, Path};
use crate::{Result, RealmFS};
use std::ffi::OsStr;
use crate::{Result, RealmFS, CommandLine, ImageHeader};
use crate::verity::Verity;
/// A RealmFS activation mountpoint
/// Represents the path at which a RealmFS is mounted and manages RealmFS activation and
/// deactivation.
///
/// Activation of a RealmFS involves:
///
/// 1. create mountpoint directory
/// 2. create loop and dm-verity device for image file
/// 3. Mount dm-verity device at mountpoint directory
///
/// Deactivation reverses these steps.
///
#[derive(Clone,Eq,PartialEq,Hash,Debug)]
pub struct Mountpoint(PathBuf);
impl Mountpoint {
const MOUNT: &'static str = "/usr/bin/mount";
const UMOUNT: &'static str = "/usr/bin/umount";
/// Read `RealmFS::RUN_DIRECTORY` to collect all current mountpoints
@ -24,13 +36,6 @@ impl Mountpoint {
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:
@ -46,20 +51,107 @@ impl Mountpoint {
self.0.exists()
}
pub fn create_dir(&self) -> Result<()> {
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())?;
pub fn is_mounted(&self) -> bool {
// test for an arbitrary expected directory
self.path().join("etc").exists()
}
fn mount<P: AsRef<Path>>(&self, source: P) -> Result<()> {
cmd!(Self::MOUNT, "-oro {} {}",
source.as_ref().display(),
self.path().display()
)
}
pub fn activate(&self, realmfs: &RealmFS) -> Result<()> {
if self.is_mounted() {
return Ok(())
}
if !self.exists() {
self.create_dir()?;
}
let verity_path = self.verity_device_path();
if verity_path.exists() {
warn!("dm-verity device {:?} already exists which was not expected", verity_path);
} else if let Err(err) = self.setup_verity(realmfs) {
let _ = fs::remove_dir(self.path());
return Err(err);
}
if let Err(err) = self.mount(verity_path) {
self.deactivate();
Err(err)
} else {
Ok(())
}
}
fn setup_verity(&self, realmfs: &RealmFS) -> Result<()> {
if !CommandLine::nosignatures() {
realmfs.verify_signature()?;
}
if !realmfs.header().has_flag(ImageHeader::FLAG_HASH_TREE) {
self.generate_verity(realmfs)?;
}
let verity = Verity::new(realmfs.path())?;
verity.setup()?;
Ok(())
}
fn generate_verity(&self, realmfs: &RealmFS) -> Result<()> {
info!("Generating verity hash tree");
let verity = Verity::new(realmfs.path())?;
verity.generate_image_hashtree()?;
realmfs.header().set_flag(ImageHeader::FLAG_HASH_TREE);
realmfs.header().write_header_to(self.path())?;
info!("Done generating verity hash tree");
Ok(())
}
/// Deactivate this mountpoint by unmounting it and removing the directory.
pub fn deactivate(&self) {
if !self.exists() {
return;
}
info!("Unmounting {} and removing directory", self);
// 1. Unmount directory
if self.is_mounted() {
if let Err(err) = cmd!(Self::UMOUNT, "{}", self) {
warn!("Failed to unmount directory {}: {}", self, err);
}
}
// 2. Remove dm-verity device
let verity = self.verity_device_path();
if verity.exists() {
if let Err(err) = Verity::close_device(self.verity_device().as_str()) {
warn!("Failed to remove dm-verity device {:?}: {}", verity, err);
}
}
// 3. Remove directory
if let Err(err) = fs::remove_dir(self.path()) {
warn!("Failed to remove mountpoint directory {}: {}", self, err);
}
}
fn verity_device_path(&self) -> PathBuf {
Path::new("/dev/mapper")
.join(self.verity_device())
}
// Return the name of the dm-verity device associated with this mountpoint
pub fn verity_device(&self) -> String {
format!("verity-realmfs-{}-{}", self.realmfs(), self.tag())
}
/// Full `&Path` of mountpoint.
pub fn path(&self) -> &Path {

View File

@ -3,46 +3,36 @@ use std::fs;
use std::io::Write;
use std::os::unix::fs::MetadataExt;
use std::path::{Path,PathBuf};
use std::sync::{Arc, Weak, RwLock};
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::{ImageHeader, MetaInfo, Result, KeyRing, KeyPair, util, RealmManager, PublicKey, ResizeSize};
use crate::realmfs::resizer::Superblock;
use std::sync::{Arc, Weak};
use super::activator::Activation;
use crate::realmfs::update::Update;
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.
/// A RealmFS image header includes metadata needed to mount the image with dm-verity to securely
/// securely enforce read-only access to the image. This header is signed with either regular
/// channel keys or with a user-controlled key generated upon installation and stored in the kernel
/// keyring.
///
/// 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.
///
/// RealmFS image files in this directory are named $NAME-realmfs.img so the full path to a RealmFS
/// image with name 'main' would be:
///
/// /storage/realms/realmfs-images/main-realmfs.img
///
#[derive(Clone)]
pub struct RealmFS {
// RealmFS name
@ -51,8 +41,8 @@ pub struct RealmFS {
path: Arc<PathBuf>,
// current RealmFS image file header
header: Arc<ImageHeader>,
activation_state: Arc<ActivationState>,
// mountpoint of the path this realmfs is mounted at when activated
mountpoint: Arc<RwLock<Mountpoint>>,
manager: Weak<RealmManager>,
}
@ -80,45 +70,34 @@ impl RealmFS {
/// Load RealmFS image from an exact path.
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self> {
Self::_load_from_path(path.as_ref(), true)
let header = Self::load_realmfs_header(path.as_ref())?;
let metainfo = header.metainfo();
let name = metainfo.realmfs_name()
.expect("RealmFS does not have a name");
let mountpoint = Mountpoint::new(name, metainfo.verity_tag());
Ok(RealmFS::new(name, path.as_ref(), header, mountpoint))
}
fn _load_from_path(path: &Path, load_activation: bool) -> Result<Self> {
let path = Arc::new(path.to_owned());
let header = Self::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();
fn new(name: &str, path: &Path, header: ImageHeader, mountpoint: Mountpoint) -> Self {
RealmFS {
name: Arc::new(name.to_owned()),
path: Arc::new(path.to_owned()),
header: Arc::new(header),
mountpoint: Arc::new(RwLock::new(mountpoint)),
manager: Weak::new(),
}
Ok(realmfs)
}
pub fn set_manager(&mut self, manager: Arc<RealmManager>) {
pub(super) 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);
}
self.manager.upgrade()
.expect(&format!("No manager set on realmfs {}", self.name))
}
fn with_manager<F>(&self, f: F)
@ -185,6 +164,15 @@ impl RealmFS {
self.path.as_ref()
}
pub fn mountpoint(&self) -> Mountpoint {
let lock = self.mountpoint.read().unwrap();
lock.clone()
}
pub fn delete(&self) -> Result<()> {
self.manager().delete_realmfs(self)
}
/// 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'
@ -235,219 +223,136 @@ impl RealmFS {
self.header().metainfo()
}
// Each time RealmFS header is accessed, verify that the header on disk has not changed.
// If the header changes generate a new mountpoint instance because the verity tag may
// have changed.
fn check_stale_header(&self) -> Result<()> {
if self.header.reload_if_stale(self.path())? {
let mut lock = self.mountpoint.write().unwrap();
*lock = Mountpoint::new(self.name(), self.header.metainfo().verity_tag());
}
Ok(())
}
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),
_ => {},
};
if let Err(err) = self.check_stale_header() {
warn!("error reloading stale image header: {}", err);
}
&self.header
}
/// Return true if this RealmFS is sealed with user signing keys.
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()
self.metainfo().channel() == Self::USER_KEYNAME
}
/// 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)
self.manager().realmfs_mountpoint_in_use(&self.mountpoint())
}
/// 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 deactivate(&self) {
if !self.is_in_use() {
self.mountpoint().deactivate();
}
}
pub fn fork(&self, new_name: &str) -> Result<Self> {
self._fork(new_name, true)
pub fn interactive_update(&self, scheme: Option<&str>) -> Result<()> {
let mut update = Update::create(self)?;
update.run_interactive_update(scheme)
}
/// Create an unsealed copy of this RealmFS image with a new image name.
///
pub fn fork_unsealed(&self, new_name: &str) -> Result<Self> {
Self::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<Self> {
Self::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<Self> {
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<Self> {
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 = Self::_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()
// Return the public key for verifying the signature on this image
fn public_key(&self) -> Result<PublicKey> {
let pubkey = if self.metainfo().channel() == RealmFS::USER_KEYNAME {
self.sealing_keys()?.public_key()
} else {
realmfs.unseal()
match self.header().public_key()? {
Some(pubkey) => pubkey,
None => bail!("No public key available for channel {}", self.metainfo().channel()),
}
};
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)
Ok(pubkey)
}
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 = Self::generate_unsealed_metainfo(self.name(), self.metainfo().nblocks(), None);
self.write_new_metainfo(&bytes, None)?;
if self.has_verity_tree() {
self.truncate_verity()?;
pub(super) fn verify_signature(&self) -> Result<()> {
let pubkey = self.public_key()?;
if !self.header().verify_signature(pubkey) {
bail!("header signature verification failed on realmfs image '{}'", self.name());
}
info!("header signature verified on realmfs image '{}'", self.name());
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_else(|| 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 fork(&self, new_name: &str) -> Result<Self> {
Self::validate_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);
}
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 keys = match self.sealing_keys() {
Ok(keys) => keys,
Err(err) => bail!("Cannot fork realmfs image, no signing keys available: {}", err),
};
info!("forking RealmFS image '{}' to new name '{}'", self.name(), new_name);
let forked = match self.fork_to_path(new_name, &new_path, keys) {
Ok(forked) => forked,
Err(err) => {
if new_path.exists() {
let _ = fs::remove_file(&new_path);
}
let metainfo_bytes = Self::generate_unsealed_metainfo(name, nblocks, owner_realm);
self.write_new_metainfo(&metainfo_bytes, None)
bail!("Failed to fork RealmFS '{}' to '{}': {}", self.name, new_name, err);
}
};
self.with_manager(|m| m.realmfs_added(&forked));
Ok(forked)
}
fn write_new_metainfo(&self, bytes: &[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())
// copy source image file to new name and install updated header
fn fork_to_path(&self, new_name: &str, new_path: &Path, keys: KeyPair) -> Result<Self> {
self.copy_image_file(new_path)?;
let metainfo_bytes = self.fork_metainfo(new_name);
let sig = keys.sign(&metainfo_bytes);
let forked = Self::load_from_path(new_path)?;
forked.header().update_metainfo(&metainfo_bytes, sig.to_bytes(), new_path)?;
Ok(forked)
}
fn generate_unsealed_metainfo(name: &str, nblocks: usize, owner_realm: Option<String>) -> Vec<u8> {
pub(super) fn copy_image_file(&self, to: &Path) -> Result<()> {
if to.exists() {
bail!("Cannot copy image file to {} because it already exists", to.display());
}
cmd!("/usr/bin/cp", "--reflink=auto {} {}", self.path.display(), to.display())?;
Ok(())
}
fn fork_metainfo(&self, new_name: &str) -> Vec<u8> {
// when creating a realmfs fork, only the name will change
let metainfo = self.metainfo();
Self::generate_metainfo(new_name, metainfo.nblocks(), metainfo.verity_salt(), metainfo.verity_root())
}
pub(super) fn generate_metainfo(name: &str, nblocks: usize, verity_salt: &str, verity_root: &str) -> 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 = Self::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> {
pub fn file_nblocks(&self) -> Result<usize> {
let meta = self.path.metadata()?;
let len = meta.len() as usize;
if len % 4096 != 0 {
@ -460,82 +365,6 @@ impl RealmFS {
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 = Self::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_else(|| 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()
}
@ -544,31 +373,22 @@ impl RealmFS {
KeyRing::get_kernel_keypair(Self::USER_KEYNAME)
}
pub fn rotate(&self, new_file: &Path) -> Result<()> {
let backup = |n: usize| Path::new(Self::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)
ResizeSize::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)
let mut update = Update::create(self)?;
update.grow_to(size);
update.resize()
}
pub fn resize_grow_by(&self, size: ResizeSize) -> Result<()> {
ImageResizer::new(self).grow_by(size)
info!("Resizing to an increase of {} blocks", size.nblocks());
let mut update = Update::create(self)?;
update.grow_by(size);
update.resize()
}
pub fn free_size_blocks(&self) -> Result<usize> {
@ -581,23 +401,18 @@ impl RealmFS {
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
/// Activate this RealmFS image if not yet activated.
pub fn activate(&self) -> Result<()> {
self.mountpoint().activate(self)
}
/// 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_or(false, |a| a.is_mountpoint(mountpoint));
if is_ours {
if let Err(e) = self.deactivate() {
warn!("error deactivating mountpoint: {}", e);
/// 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.mountpoint().is_mounted()
}
}
is_ours
}
}

View File

@ -1,7 +1,8 @@
use std::collections::HashMap;
use crate::{RealmFS, RealmManager, Result};
use std::sync::Arc;
use std::fs;
use std::sync::Arc;
use crate::{RealmFS, RealmManager, Result};
pub struct RealmFSSet {
realmfs_map: HashMap<String, RealmFS>,

View File

@ -1,26 +1,20 @@
use std::fs::{File,OpenOptions};
use std::fs::File;
use std::io::{Read,Seek,SeekFrom};
use std::path::Path;
use byteorder::{ByteOrder,LittleEndian};
use crate::{RealmFS,Result,LoopDevice};
use crate::{RealmFS,Result};
const BLOCK_SIZE: usize = 4096;
const BLOCKS_PER_MEG: usize = (1024 * 1024) / BLOCK_SIZE;
const BLOCKS_PER_GIG: usize = 1024 * BLOCKS_PER_MEG;
const E2FSCK: &str = "e2fsck";
const RESIZE2FS: &str = "resize2fs";
// If less than 1gb remaining space
const AUTO_RESIZE_MINIMUM_FREE: ResizeSize = ResizeSize(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,
}
#[derive(Copy,Clone)]
pub struct ResizeSize(usize);
@ -50,89 +44,9 @@ impl ResizeSize {
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 e2fsck {:?}", open_loop);
cmd!(E2FSCK,"{} {} {}","-f","-p",open_loop.device().display())?;
info!("Running resize2fs {:?}", open_loop);
cmd!(RESIZE2FS, "{}", open_loop.device().display())?;
} else {
LoopDevice::with_loop(self.image.path(), Some(4096), false, |loopdev| {
info!("Running e2fsck {:?}", loopdev);
cmd!(E2FSCK,"{} {} {}","-f","-p",loopdev.device().display())?;
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.
/// recommended size.
pub fn auto_resize_size(realmfs: &RealmFS) -> Option<ResizeSize> {
let sb = match Superblock::load(realmfs.path(), 4096) {
Ok(sb) => sb,
@ -145,7 +59,8 @@ impl <'a> ImageResizer<'a> {
sb.free_block_count();
let free_blocks = sb.free_block_count() as usize;
if free_blocks < AUTO_RESIZE_MINIMUM_FREE.nblocks() {
let increase_multiple = realmfs.metainfo_nblocks() / AUTO_RESIZE_INCREASE_SIZE.nblocks();
let metainfo_nblocks = realmfs.metainfo().nblocks() + 1;
let increase_multiple = metainfo_nblocks / AUTO_RESIZE_INCREASE_SIZE.nblocks();
let grow_size = (increase_multiple + 1) * AUTO_RESIZE_INCREASE_SIZE.nblocks();
let mask = grow_size - 1;
let grow_blocks = (free_blocks + mask) & !mask;

View File

@ -1,78 +1,307 @@
use std::fs;
use std::io::{self, Write};
use std::path::{PathBuf, Path};
use std::process::Command;
use crate::{Result, RealmFS };
use crate::realmfs::Mountpoint;
use sodiumoxide::randombytes::randombytes;
use crate::{Result, RealmFS, FileLock, ImageHeader, LoopDevice, ResizeSize, util};
use crate::realm::BridgeAllocator;
use crate::ResizeSize;
use crate::util::is_euid_root;
use crate::terminal::TerminalRestorer;
use crate::verity::Verity;
enum UpdateType {
NotSetup,
Sealed(RealmFS),
Unsealed,
}
const BLOCK_SIZE: usize = 4096;
// The maximum number of backup copies the rotate() method will create
const NUM_BACKUPS: usize = 2;
const E2FSCK: &str = "e2fsck";
const RESIZE2FS: &str = "resize2fs";
/// Manages the process of updating or resizing a `RealmFS` image file.
///
pub struct Update<'a> {
realmfs: &'a RealmFS,
realmfs: &'a RealmFS, // RealmFS being updated
name: String, // name for nspawn instance
target: PathBuf, // Path to the update copy of realmfs image
mountpath: PathBuf, // Path at which update copy is mounted
_lock: FileLock,
resize: Option<ResizeSize>, // If the image needs to be resized, the resize size is stored here
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 }
fn new(realmfs: &'a RealmFS, lock: FileLock) -> Self {
let metainfo = realmfs.metainfo();
let tag = metainfo.verity_tag();
let mountpath = Path::new(RealmFS::RUN_DIRECTORY)
.join(format!("realmfs-{}-{}.update", realmfs.name(), tag));
Update {
realmfs,
name: format!("{}-{}-update", realmfs.name(), tag),
target: realmfs.path().with_extension("update"),
mountpath,
_lock: lock,
resize: ResizeSize::auto_resize_size(realmfs),
network_allocated: false,
}
}
pub fn setup(&mut self) -> Result<()> {
self.update_type = self.create_update_type()?;
pub fn create(realmfs: &'a RealmFS) -> Result<Self> {
let lock = FileLock::nonblocking_acquire(realmfs.path().with_extension("lock"))?
.ok_or(format_err!("Unable to obtain file lock to update realmfs image: {}", realmfs.name()))?;
if !realmfs.has_sealing_keys() {
bail!("Cannot seal realmfs image, no sealing keys available");
}
Ok(Update::new(realmfs, lock))
}
fn name(&self) -> &str {
&self.name
}
fn target(&self) -> &Path {
&self.target
}
fn create_update_copy(&self) -> Result<()> {
if self.target.exists() {
info!("Update file {} already exists, removing it", self.target.display());
fs::remove_file(&self.target)?;
}
self.realmfs.copy_image_file(self.target())?;
self.truncate_verity()?;
self.resize_image_file()?;
Ok(())
}
pub fn auto_resize_size(&self) -> Option<ResizeSize> {
self.target_image().auto_resize_size()
fn setup(&mut self) -> Result<()> {
self.create_update_copy()?;
self.truncate_verity()?;
self.resize_image_file()?;
self.mount_update_image()?;
Ok(())
}
pub fn apply_resize(&self, size: ResizeSize) -> Result<()> {
self.target_image().resize_grow_to(size)
pub fn resize(&mut self) -> Result<()> {
if self.resize.is_none() {
return Ok(())
}
fn target_image(&self) -> &RealmFS {
if let UpdateType::Sealed(ref image) = self.update_type {
image
self.create_update_copy()?;
self.truncate_verity()?;
self.resize_image_file()?;
LoopDevice::with_loop(self.target(), Some(BLOCK_SIZE), false, |loopdev| {
self.resize_device(loopdev)
})
}
fn mount_update_image(&mut self) -> Result<()> {
LoopDevice::with_loop(self.target(), Some(BLOCK_SIZE), false, |loopdev| {
if self.resize.is_some() {
self.resize_device(loopdev)?;
}
if !self.mountpath.exists() {
fs::create_dir_all(&self.mountpath)?;
}
util::mount(loopdev.device_str(), &self.mountpath, Some("-orw,noatime"))?;
Ok(())
})
}
// Return size of image file in blocks based on metainfo `nblocks` field.
// Include header block in count so add one block
fn metainfo_nblock_size(&self) -> usize {
self.realmfs.metainfo().nblocks() + 1
}
fn unmount_update_image(&mut self) {
if self.mountpath.exists() {
if let Err(err) = util::umount(&self.mountpath) {
warn!("Failed to unmount directory {:?}: {}", self.mountpath, err);
}
if let Err(err) = fs::remove_dir(&self.mountpath) {
warn!("Failed to remove mountpoint directory {:?}: {}", self.mountpath, err);
}
}
}
fn resize_device(&self, loopdev: &LoopDevice) -> Result<()> {
info!("Running e2fsck {:?}", loopdev);
cmd!(E2FSCK,"{} {} {}","-f","-p", loopdev.device().display())?;
info!("Running resize2fs {:?}", loopdev);
cmd!(RESIZE2FS, "{}", loopdev.device().display())?;
Ok(())
}
pub fn grow_to(&mut self, size: ResizeSize) {
let target_nblocks = size.nblocks();
let current_nblocks = self.metainfo_nblock_size();
if current_nblocks >= target_nblocks {
info!("RealmFS image is already larger than requested size, doing nothing");
} else {
&self.realmfs
self.set_resize(target_nblocks);
}
}
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 grow_by(&mut self, size: ResizeSize) {
let nblocks = size.nblocks();
self.set_resize(self.metainfo_nblock_size() + nblocks);
}
fn set_resize(&mut self, nblocks: usize) {
self.resize = Some(ResizeSize::blocks(nblocks));
}
fn set_target_len(&self, nblocks: usize) -> Result<()> {
let len = (nblocks * BLOCK_SIZE) as u64;
let f = fs::OpenOptions::new()
.write(true)
.open(&self.target)?;
f.set_len(len)?;
Ok(())
}
// Remove dm-verity hash tree from update copy of image file.
fn truncate_verity(&self) -> Result<()> {
let file_nblocks = self.realmfs.file_nblocks()?;
let metainfo_nblocks = self.metainfo_nblock_size();
if self.realmfs.header().has_flag(ImageHeader::FLAG_HASH_TREE) {
self.set_target_len(metainfo_nblocks)?;
} else if file_nblocks > metainfo_nblocks {
warn!("RealmFS image size was greater than length indicated by metainfo.nblocks but FLAG_HASH_TREE not set");
}
Ok(())
}
// If resize was requested, adjust size of update copy of image file.
fn resize_image_file(&self) -> Result<()> {
let nblocks = match self.resize {
Some(rs) => rs.nblocks() + 1,
None => return Ok(()),
};
if nblocks < self.metainfo_nblock_size() {
bail!("Cannot shrink image")
}
// This is an arbitrary restriction which is probably not needed
if (nblocks - self.metainfo_nblock_size()) > ResizeSize::gigs(8).nblocks() {
bail!("Can only increase size of RealmFS image by a maximum of 8gb at one time");
}
self.set_target_len(nblocks)
}
pub fn cleanup(&mut self) {
if self.mountpath.exists() {
self.unmount_update_image();
}
if self.target().exists() {
if let Err(err) = fs::remove_file(self.target()) {
warn!("Failed to remove update image copy {:?}: {}", self.target(), err);
}
}
pub fn open_update_shell(&mut self) -> Result<()> {
self.run_update_shell("/usr/libexec/configure-host0.sh && exec /bin/bash")
// If an IP address was allocated, free it
if self.network_allocated {
if let Err(err) = BridgeAllocator::default_bridge()
.and_then(|mut allocator| allocator.free_allocation_for(&self.name())) {
warn!("Error releasing address allocation for RealmFS ({}) update: {}", self.realmfs.name(), err);
}
self.network_allocated = false;
}
}
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))?;
fn seal(&mut self) -> Result<()> {
let nblocks = match self.resize {
Some(rs) => rs.nblocks(),
None => self.metainfo_nblock_size() - 1,
};
activation.mountpoint_rw().cloned()
.ok_or_else(|| format_err!("Update image activation does not have a writeable mountpoint"))
let salt = hex::encode(randombytes(32));
let verity = Verity::new(&self.target)?;
let output = verity.generate_image_hashtree_with_salt(&salt, nblocks)?;
// XXX passes metainfo for nblocks
//let output = Verity::new(&self.target).generate_image_hashtree_with_salt(&self.realmfs.metainfo(), &salt)?;
let root_hash = output.root_hash()
.ok_or_else(|| format_err!("no root hash returned from verity format operation"))?;
info!("root hash is {}", output.root_hash().unwrap());
/*
let nblocks = match self.resize {
Some(rs) => rs.nblocks(),
None => self.metainfo_nblock_size() - 1,
};
*/
info!("Signing new image with user realmfs keys");
let metainfo_bytes = RealmFS::generate_metainfo(self.realmfs.name(), nblocks, salt.as_str(), root_hash);
let keys = self.realmfs.sealing_keys().expect("No sealing keys");
let sig = keys.sign(&metainfo_bytes);
let header = ImageHeader::new();
header.set_flag(ImageHeader::FLAG_HASH_TREE);
header.update_metainfo(&metainfo_bytes, sig.to_bytes(), &self.target)
}
fn prompt_user(prompt: &str, default_y: bool) -> Result<bool> {
let yn = if default_y { "(Y/n)" } else { "(y/N)" };
print!("{} {} : ", prompt, yn);
io::stdout().flush()?;
let mut line = String::new();
io::stdin().read_line(&mut line)?;
let yes = match line.trim().chars().next() {
Some(c) => c == 'Y' || c == 'y',
None => default_y,
};
Ok(yes)
}
pub fn run_interactive_update(&mut self, scheme: Option<&str>) -> Result<()> {
if !is_euid_root() {
bail!("RealmFS updates must be run as root");
}
let mut term = TerminalRestorer::new();
if let Some(scheme) = scheme {
term.save_palette();
term.apply_base16_by_slug(scheme);
}
self.setup()?;
println!();
println!("Opening update shell for '{}-realmfs.img'", self.realmfs.name());
println!();
println!("Exit shell with ctrl-d or 'exit' to return to realm manager");
println!();
self.run_update_shell("/usr/libexec/configure-host0.sh && exec /bin/bash")?;
if Self::prompt_user("Apply changes?", true)? {
if let Err(err) = self.apply_update() {
warn!("Failed to apply update changes: {}", err);
}
}
self.cleanup();
Ok(())
}
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();
@ -82,7 +311,7 @@ impl <'a> Update<'a> {
.arg(format!("--setenv=IFCONFIG_GW={}", gw))
.arg("--quiet")
.arg(format!("--machine={}", self.name()))
.arg(format!("--directory={}", mountpoint))
.arg(format!("--directory={}", &self.mountpath.display()))
.arg("--network-zone=clear")
.arg("/bin/bash")
.arg("-c")
@ -92,54 +321,36 @@ impl <'a> Update<'a> {
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(()),
};
fn apply_update(&mut self) -> Result<()> {
self.unmount_update_image();
self.seal()?;
self.rotate()?;
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())
}
fn rotate(&self) -> Result<()> {
let backup = |n: usize|
Path::new(RealmFS::BASE_PATH)
.join(format!("{}-realmfs.img.{}", self.realmfs.name(), n));
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())?;
for i in (1..NUM_BACKUPS).rev() {
let from = backup(i - 1);
if from.exists() {
fs::rename(from, backup(i))?;
}
},
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;
}
fs::rename(self.realmfs.path(), backup(0))?;
fs::rename(self.target(), self.realmfs.path())?;
Ok(())
}
}
impl <'a> Drop for Update<'a> {
fn drop(&mut self) {
self.cleanup();
}
}

View File

@ -91,7 +91,7 @@ impl ResourceImage {
&self.path
}
fn verity(&self) -> Verity {
fn verity(&self) -> Result<Verity> {
Verity::new(self.path())
}
@ -171,12 +171,6 @@ impl ResourceImage {
info!("writing rootfs image to {}", partition.path().display());
cmd_with_output!("/bin/dd", "if={} of={} bs=4096 skip=1", self.path.display(), partition.path().display())?;
/*
let args = format!("if={} of={} bs=4096 skip=1",
self.path.display(), partition.path().display());
util::exec_cmdline_quiet("/bin/dd", args)?;
*/
self.header.set_status(ImageHeader::STATUS_NEW);
self.header.write_partition(partition.path())?;
@ -212,7 +206,8 @@ impl ResourceImage {
if !self.has_verity_hashtree() {
self.generate_verity_hashtree()?;
}
self.verity().setup(&self.metainfo())
let verity = self.verity()?;
verity.setup()
}
pub fn generate_verity_hashtree(&self) -> Result<()> {
@ -223,8 +218,8 @@ impl ResourceImage {
self.decompress()?;
}
info!("Generating dm-verity hash tree for image {}", self.path.display());
// verity::generate_image_hashtree(self.path(), self.metainfo().nblocks(), self.metainfo().verity_salt())?;
self.verity().generate_image_hashtree(&self.metainfo())?;
let verity = self.verity()?;
verity.generate_image_hashtree()?;
self.header.set_flag(ImageHeader::FLAG_HASH_TREE);
self.header.write_header_to(self.path())?;
Ok(())
@ -235,8 +230,8 @@ impl ResourceImage {
self.generate_verity_hashtree()?;
}
info!("Verifying dm-verity hash tree");
self.verity().verify(&self.metainfo())
// verity::verify_image(self.path(), &self.metainfo())
let verity = self.verity()?;
verity.verify()
}
pub fn generate_shasum(&self) -> Result<String> {

View File

@ -5,6 +5,15 @@ use std::path::{Path,PathBuf};
use crate::Result;
///
/// Create a lockfile and acquire an exclusive lock with flock(2)
///
/// The lock can either be acquired by blocking until available or
/// by failing immediately if the lock is already held.
///
/// The lock is released and the lockfile is removed when `FileLock`
/// instance is dropped.
///
pub struct FileLock {
file: File,
path: PathBuf,
@ -12,11 +21,25 @@ pub struct FileLock {
impl FileLock {
pub fn nonblocking_acquire<P: AsRef<Path>>(path: P) -> Result<Option<Self>> {
let file = Self::open_lockfile(path.as_ref())?;
let flock = FileLock {
file,
path: path.as_ref().into(),
};
if flock.lock(false)? {
Ok(Some(flock))
} else {
Ok(None)
}
}
pub fn acquire<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let file = Self::open_lockfile(&path)?;
let flock = FileLock { file, path };
flock.lock()?;
flock.lock(true)?;
Ok(flock)
}
@ -27,6 +50,9 @@ impl FileLock {
}
}
// Make a few attempts just in case we try to open lockfile
// at exact moment another process is releasing and deleting
// file.
for _ in 0..3 {
if let Some(file) = Self::try_create_lockfile(path)? {
return Ok(file);
@ -35,7 +61,7 @@ impl FileLock {
return Ok(file);
}
}
Err(format_err!("unable to acquire lockfile {}", path.display() ))
Err(format_err!("unable to open lockfile {}", path.display() ))
}
fn try_create_lockfile(path: &Path) -> Result<Option<File>> {
@ -55,19 +81,32 @@ impl FileLock {
}
fn unlock(&self) -> Result<()> {
self.flock(libc::LOCK_UN)
}
fn lock(&self) -> Result<()> {
self.flock(libc::LOCK_EX)
}
fn flock(&self, flag: libc::c_int) -> Result<()> {
if unsafe { libc::flock(self.file.as_raw_fd(), flag) } < 0 {
return Err(Error::last_os_error().into());
}
self.flock(libc::LOCK_UN, true)?;
Ok(())
}
fn lock(&self, block: bool) -> Result<bool> {
if block {
self.flock(libc::LOCK_EX, true)
} else {
self.flock(libc::LOCK_EX | libc::LOCK_NB, false)
}
}
fn flock(&self, flag: libc::c_int, block: bool) -> Result<bool> {
if unsafe { libc::flock(self.file.as_raw_fd(), flag) } < 0 {
let errno = Self::last_errno();
if !block && errno == libc::EWOULDBLOCK {
return Ok(false);
}
return Err(Error::from_raw_os_error(errno).into());
}
Ok(true)
}
pub fn last_errno() -> i32 {
unsafe { *libc::__errno_location() }
}
}
impl Drop for FileLock {

View File

@ -209,3 +209,9 @@ pub fn chown_tree(base: &Path, chown_to: (u32,u32), include_base: bool) -> Resul
}
Ok(())
}
pub fn is_euid_root() -> bool {
unsafe {
libc::geteuid() == 0
}
}

View File

@ -3,19 +3,25 @@ use std::collections::HashMap;
use std::fs::{self, OpenOptions,File};
use std::io;
use crate::{Result, MetaInfo, Partition, LoopDevice, Mountpoint};
use crate::{Result, MetaInfo, Partition, LoopDevice, ImageHeader};
use std::sync::Arc;
pub struct Verity {
image: PathBuf,
metainfo: Arc<MetaInfo>,
}
impl Verity {
const VERITYSETUP: &'static str = "/sbin/veritysetup";
pub fn new(image: impl AsRef<Path>) -> Self {
pub fn new(image: impl AsRef<Path>) -> Result<Self> {
let header = ImageHeader::from_file(image.as_ref())?;
let image = image.as_ref().to_path_buf();
Verity { image }
Ok(Verity {
image,
metainfo: header.metainfo(),
})
}
pub fn generate_initial_hashtree(&self, output: impl AsRef<Path>) -> Result<VerityOutput> {
@ -25,15 +31,15 @@ impl Verity {
Ok(VerityOutput::parse(&output))
}
pub fn generate_image_hashtree(&self, metainfo: &MetaInfo) -> Result<VerityOutput> {
let verity_salt = metainfo.verity_salt();
self.generate_image_hashtree_with_salt(metainfo, verity_salt)
pub fn generate_image_hashtree(&self) -> Result<VerityOutput> {
let verity_salt = self.metainfo.verity_salt();
let nblocks = self.metainfo.nblocks();
self.generate_image_hashtree_with_salt(verity_salt, nblocks)
}
pub fn generate_image_hashtree_with_salt(&self, metainfo: &MetaInfo, salt: &str) -> Result<VerityOutput> {
pub fn generate_image_hashtree_with_salt(&self, salt: &str, nblocks: usize) -> Result<VerityOutput> {
let verityfile = self.image.with_extension("verity");
let nblocks = metainfo.nblocks();
// Make sure file size is correct or else verity tree will be appended in wrong place
let meta = self.image.metadata()?;
@ -54,19 +60,20 @@ impl Verity {
Ok(vout)
}
pub fn verify(&self, metainfo: &MetaInfo) -> Result<bool> {
pub fn verify(&self) -> Result<bool> {
LoopDevice::with_loop(self.path(), Some(4096), true, |loopdev| {
cmd_ok!(Self::VERITYSETUP, "--hash-offset={} verify {} {} {}",
metainfo.nblocks() * 4096,
loopdev, loopdev, metainfo.verity_root())
self.metainfo.nblocks() * 4096,
loopdev, loopdev, self.metainfo.verity_root())
})
}
pub fn setup(&self, metainfo: &MetaInfo) -> Result<String> {
pub fn setup(&self) -> Result<String> {
info!("creating loop and dm-verity devices for {:?}", self.path());
LoopDevice::with_loop(self.path(), Some(4096), true, |loopdev| {
let devname = Self::device_name(metainfo);
let devname = self.device_name();
let srcdev = loopdev.to_string();
Self::setup_device(&srcdev, &devname, metainfo)?;
Self::setup_device(&srcdev, &devname, &self.metainfo)?;
Ok(devname)
})
}
@ -82,21 +89,17 @@ impl Verity {
cmd!(Self::VERITYSETUP, "close {}", device_name)
}
pub fn device_name(metainfo: &MetaInfo) -> String {
if metainfo.image_type() == "rootfs" {
fn device_name(&self) -> String {
if self.metainfo.image_type() == "rootfs" {
String::from("rootfs")
} else if metainfo.image_type() == "realmfs" {
let name = metainfo.realmfs_name().unwrap_or("unknown");
format!("verity-realmfs-{}-{}", name, metainfo.verity_tag())
} else if self.metainfo.image_type() == "realmfs" {
let name = self.metainfo.realmfs_name().unwrap_or("unknown");
format!("verity-realmfs-{}-{}", name, self.metainfo.verity_tag())
} else {
format!("verity-{}-{}", metainfo.image_type(), metainfo.verity_tag())
format!("verity-{}-{}", self.metainfo.image_type(), self.metainfo.verity_tag())
}
}
pub fn device_name_for_mountpoint(mountpoint: &Mountpoint) -> String {
format!("verity-realmfs-{}-{}", mountpoint.realmfs(), mountpoint.tag())
}
fn setup_device(srcdev: &str, devname: &str, metainfo: &MetaInfo) -> Result<()> {
let nblocks = metainfo.nblocks();
let verity_root = metainfo.verity_root();