From d2cb693c5567f2c0ab34b12d352a60fb53730891 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Wed, 11 Mar 2026 15:47:23 +0100 Subject: [PATCH] feat(appd): MountOrchestrator -- EROFS+dm-verity image mount on app launch Add crates/weft-appd/src/mount.rs with MountOrchestrator. On each app launch, supervise() calls MountOrchestrator::mount_if_needed which looks for .app.img, .hash, and .roothash in the app store roots. If all three exist and weft-mount-helper is available (WEFT_MOUNT_HELPER env or /usr/lib/weft/ default paths), it: - creates /tmp/weft-mnt-// - invokes weft-mount-helper mount to set up dm-verity and EROFS mount - sets WEFT_APP_STORE=/tmp/weft-mnt- in the child env so the runtime resolves the package from the mounted read-only image After the process exits, umount() invokes weft-mount-helper umount and removes the temporary directory. Falls back to directory-based install silently if no image is found, mount-helper is absent, or the mount fails. Test: find_image_returns_none_when_absent. --- crates/weft-appd/src/main.rs | 1 + crates/weft-appd/src/mount.rs | 116 ++++++++++++++++++++++++++++++++ crates/weft-appd/src/runtime.rs | 9 +++ 3 files changed, 126 insertions(+) create mode 100644 crates/weft-appd/src/mount.rs diff --git a/crates/weft-appd/src/main.rs b/crates/weft-appd/src/main.rs index 8fcc014..a957faf 100644 --- a/crates/weft-appd/src/main.rs +++ b/crates/weft-appd/src/main.rs @@ -7,6 +7,7 @@ use tokio::sync::Mutex; mod compositor_client; mod ipc; +mod mount; mod runtime; mod ws; diff --git a/crates/weft-appd/src/mount.rs b/crates/weft-appd/src/mount.rs new file mode 100644 index 0000000..6d978b8 --- /dev/null +++ b/crates/weft-appd/src/mount.rs @@ -0,0 +1,116 @@ +use std::path::{Path, PathBuf}; + +fn mount_helper_bin() -> Option { + if let Ok(v) = std::env::var("WEFT_MOUNT_HELPER") { + return Some(v); + } + for candidate in [ + "/usr/lib/weft/weft-mount-helper", + "/usr/local/lib/weft/weft-mount-helper", + ] { + if Path::new(candidate).exists() { + return Some(candidate.to_string()); + } + } + None +} + +pub struct MountOrchestrator { + mountpoint: Option, +} + +impl MountOrchestrator { + pub fn mount_if_needed(app_id: &str, session_id: u64) -> (Self, Option) { + let Some(helper) = mount_helper_bin() else { + return (Self { mountpoint: None }, None); + }; + + let (img, hash_dev, root_hash) = match find_image(app_id) { + Some(t) => t, + None => return (Self { mountpoint: None }, None), + }; + + let base = std::env::temp_dir().join(format!("weft-mnt-{session_id}")); + let mountpoint = base.join(app_id); + + if let Err(e) = std::fs::create_dir_all(&mountpoint) { + tracing::warn!(session_id, %app_id, error=%e, "cannot create mount dir; skipping image mount"); + return (Self { mountpoint: None }, None); + } + + let status = std::process::Command::new(&helper) + .args([ + "mount", + &img.to_string_lossy(), + &hash_dev.to_string_lossy(), + &root_hash, + &mountpoint.to_string_lossy(), + ]) + .status(); + + match status { + Ok(s) if s.success() => { + tracing::info!(session_id, %app_id, path=%mountpoint.display(), "EROFS image mounted"); + ( + Self { + mountpoint: Some(mountpoint), + }, + Some(base), + ) + } + Ok(s) => { + tracing::warn!(session_id, %app_id, status=%s, "mount-helper failed; using directory install"); + let _ = std::fs::remove_dir_all(&base); + (Self { mountpoint: None }, None) + } + Err(e) => { + tracing::warn!(session_id, %app_id, error=%e, "spawn mount-helper failed; using directory install"); + let _ = std::fs::remove_dir_all(&base); + (Self { mountpoint: None }, None) + } + } + } + + pub fn umount(&self) { + let Some(ref mp) = self.mountpoint else { + return; + }; + let Some(helper) = mount_helper_bin() else { + return; + }; + let _ = std::process::Command::new(&helper) + .args(["umount", &mp.to_string_lossy()]) + .status(); + if let Some(parent) = mp.parent() { + let _ = std::fs::remove_dir_all(parent); + } + } +} + +fn find_image(app_id: &str) -> Option<(PathBuf, PathBuf, String)> { + for root in crate::app_store_roots() { + let img = root.join(format!("{app_id}.app.img")); + let hash_dev = root.join(format!("{app_id}.hash")); + let roothash_file = root.join(format!("{app_id}.roothash")); + if img.exists() && hash_dev.exists() && roothash_file.exists() { + let Ok(root_hash) = std::fs::read_to_string(&roothash_file) else { + continue; + }; + return Some((img, hash_dev, root_hash.trim().to_string())); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn find_image_returns_none_when_absent() { + unsafe { std::env::set_var("WEFT_APP_STORE", "/tmp/nonexistent_weft_store_xyz") }; + let result = find_image("com.example.missing"); + unsafe { std::env::remove_var("WEFT_APP_STORE") }; + assert!(result.is_none()); + } +} diff --git a/crates/weft-appd/src/runtime.rs b/crates/weft-appd/src/runtime.rs index 6fb626d..94486e1 100644 --- a/crates/weft-appd/src/runtime.rs +++ b/crates/weft-appd/src/runtime.rs @@ -104,6 +104,9 @@ pub(crate) async fn supervise( } }; + let (mount_orch, store_override) = + crate::mount::MountOrchestrator::mount_if_needed(app_id, session_id); + let mut cmd = if systemd_cgroup_available() { let mut c = tokio::process::Command::new("systemd-run"); c.args([ @@ -133,6 +136,10 @@ pub(crate) async fn supervise( cmd.arg("--ipc-socket").arg(sock); } + if let Some(ref root) = store_override { + cmd.env("WEFT_APP_STORE", root); + } + for (host, guest) in resolve_preopens(app_id) { cmd.arg("--preopen").arg(format!("{host}::{guest}")); } @@ -228,6 +235,8 @@ pub(crate) async fn supervise( .await; } + mount_orch.umount(); + { let mut reg = registry.lock().await; reg.set_state(session_id, AppStateKind::Stopped);