feat: weft-mount-helper -- setuid helper for EROFS+dm-verity mounts

New crate: weft-mount-helper. A privileged helper binary that sets up
dm-verity devices and mounts EROFS images for app isolation.

Commands:
  mount <img> <hash_dev> <root_hash> <mountpoint>
    - opens a named dm-verity device via veritysetup open
    - mounts the resulting /dev/mapper/<name> as EROFS read-only
    - cleans up the dm device if mount fails
  umount <mountpoint>
    - unmounts the mountpoint
    - closes the dm-verity device via veritysetup close

Device naming: derives a stable name from the mountpoint path, limited
to 31 chars (DM limit), always prefixed weft-. Root check reads
/proc/self/status euid rather than using unsafe libc calls.

Tests: device_name_sanitizes_path, device_name_truncates_long_paths.
This commit is contained in:
Marco Allegretti 2026-03-11 15:43:59 +01:00
parent add4d92945
commit 97ea969075
7 changed files with 403 additions and 5 deletions

203
Cargo.lock generated
View file

@ -164,6 +164,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -430,6 +436,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -664,6 +676,33 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "data-encoding"
version = "2.10.0"
@ -679,6 +718,16 @@ dependencies = [
"uuid",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -809,6 +858,31 @@ dependencies = [
"linux-raw-sys 0.9.4",
]
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"rand_core 0.6.4",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "either"
version = "1.15.0"
@ -875,6 +949,23 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "filetime"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
dependencies = [
"cfg-if",
"libc",
"libredox",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@ -1153,6 +1244,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
version = "1.4.0"
@ -2009,6 +2106,16 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
@ -2339,6 +2446,15 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.44"
@ -2405,6 +2521,15 @@ dependencies = [
"libc",
]
[[package]]
name = "seccompiler"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345a3e4dddf721a478089d4697b83c6c0a8f5bf16086f6c13397e4534eb6e2e5"
dependencies = [
"libc",
]
[[package]]
name = "semver"
version = "1.0.27"
@ -2532,6 +2657,15 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "slab"
version = "0.4.12"
@ -2650,6 +2784,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "sptr"
version = "0.3.2"
@ -2662,6 +2806,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.117"
@ -2713,6 +2863,17 @@ dependencies = [
"winx",
]
[[package]]
name = "tar"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "target-lexicon"
version = "0.13.3"
@ -3821,6 +3982,7 @@ dependencies = [
"toml 0.8.23",
"tracing",
"tracing-subscriber",
"weft-ipc-types",
]
[[package]]
@ -3841,6 +4003,22 @@ dependencies = [
"wayland-backend",
"wayland-scanner",
"wayland-server",
"weft-ipc-types",
]
[[package]]
name = "weft-ipc-types"
version = "0.1.0"
dependencies = [
"rmp-serde",
"serde",
]
[[package]]
name = "weft-mount-helper"
version = "0.1.0"
dependencies = [
"anyhow",
]
[[package]]
@ -3848,8 +4026,14 @@ name = "weft-pack"
version = "0.1.0"
dependencies = [
"anyhow",
"ed25519-dalek",
"hex",
"rand 0.8.5",
"serde",
"sha2",
"tar",
"toml 0.8.23",
"zstd",
]
[[package]]
@ -3857,6 +4041,9 @@ name = "weft-runtime"
version = "0.1.0"
dependencies = [
"anyhow",
"cap-std",
"libc",
"seccompiler",
"tracing",
"tracing-subscriber",
"wasmtime",
@ -4475,6 +4662,16 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix 1.1.4",
]
[[package]]
name = "xcursor"
version = "0.3.10"
@ -4581,6 +4778,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.3"

View file

@ -4,6 +4,7 @@ members = [
"crates/weft-build-meta",
"crates/weft-compositor",
"crates/weft-ipc-types",
"crates/weft-mount-helper",
"crates/weft-pack",
"crates/weft-runtime",
"crates/weft-servo-shell",

View file

@ -83,7 +83,10 @@ impl std::fmt::Display for FrameDecodeError {
Self::TooShort => write!(f, "frame buffer shorter than 4-byte header"),
Self::TooLong(n) => write!(f, "frame length {n} exceeds MAX_FRAME_LEN"),
Self::LengthMismatch { declared, actual } => {
write!(f, "declared length {declared} != actual body length {actual}")
write!(
f,
"declared length {declared} != actual body length {actual}"
)
}
Self::Deserialize(e) => write!(f, "MessagePack deserialize error: {e}"),
}
@ -113,7 +116,11 @@ mod tests {
let frame = frame_encode(&msg).unwrap();
let decoded: AppdToCompositor = frame_decode(&frame).unwrap();
match decoded {
AppdToCompositor::AppSurfaceCreated { app_id, session_id, pid } => {
AppdToCompositor::AppSurfaceCreated {
app_id,
session_id,
pid,
} => {
assert_eq!(app_id, "com.example.app");
assert_eq!(session_id, 42);
assert_eq!(pid, 1234);
@ -184,6 +191,9 @@ mod tests {
frame[2] = bad_len[2];
frame[3] = bad_len[3];
let result: Result<AppdToCompositor, _> = frame_decode(&frame);
assert!(matches!(result, Err(FrameDecodeError::LengthMismatch { .. })));
assert!(matches!(
result,
Err(FrameDecodeError::LengthMismatch { .. })
));
}
}

View file

@ -0,0 +1,12 @@
[package]
name = "weft-mount-helper"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
[[bin]]
name = "weft-mount-helper"
path = "src/main.rs"
[dependencies]
anyhow = "1.0"

View file

@ -0,0 +1,162 @@
use std::path::Path;
use anyhow::Context;
fn main() -> anyhow::Result<()> {
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(String::as_str) {
Some("mount") => {
let img = args.get(2).context(
"usage: weft-mount-helper mount <img> <hash_dev> <root_hash> <mountpoint>",
)?;
let hash_dev = args.get(3).context("missing <hash_dev>")?;
let root_hash = args.get(4).context("missing <root_hash>")?;
let mountpoint = args.get(5).context("missing <mountpoint>")?;
cmd_mount(
Path::new(img),
Path::new(hash_dev),
root_hash,
Path::new(mountpoint),
)?;
}
Some("umount") => {
let mountpoint = args
.get(2)
.context("usage: weft-mount-helper umount <mountpoint>")?;
cmd_umount(Path::new(mountpoint))?;
}
_ => {
eprintln!("usage:");
eprintln!(" weft-mount-helper mount <img> <hash_dev> <root_hash> <mountpoint>");
eprintln!(" weft-mount-helper umount <mountpoint>");
std::process::exit(1);
}
}
Ok(())
}
fn effective_uid() -> Option<u32> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
status
.lines()
.find(|l| l.starts_with("Uid:"))
.and_then(|l| l.split_whitespace().nth(2))
.and_then(|s| s.parse().ok())
}
fn require_root() -> anyhow::Result<()> {
match effective_uid() {
Some(0) => Ok(()),
Some(uid) => anyhow::bail!("weft-mount-helper must run as root (euid={uid})"),
None => {
anyhow::bail!("weft-mount-helper must run as root (could not read /proc/self/status)")
}
}
}
fn device_name(mountpoint: &Path) -> String {
let s = mountpoint.to_string_lossy();
let sanitized: String = s
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
let suffix: String = sanitized
.trim_matches('-')
.chars()
.take(26)
.collect::<String>()
.trim_end_matches('-')
.to_string();
format!("weft-{suffix}")
}
fn cmd_mount(
img: &Path,
hash_dev: &Path,
root_hash: &str,
mountpoint: &Path,
) -> anyhow::Result<()> {
require_root()?;
let dev_name = device_name(mountpoint);
let status = std::process::Command::new("veritysetup")
.args([
"open",
&img.to_string_lossy(),
&dev_name,
&hash_dev.to_string_lossy(),
root_hash,
])
.status()
.context("spawn veritysetup; ensure cryptsetup-bin is installed")?;
if !status.success() {
anyhow::bail!("veritysetup open failed with status {status}");
}
let mapper_dev = format!("/dev/mapper/{dev_name}");
let status = std::process::Command::new("mount")
.args([
"-t",
"erofs",
"-o",
"ro",
&mapper_dev,
&mountpoint.to_string_lossy(),
])
.status()
.context("spawn mount")?;
if !status.success() {
let _ = std::process::Command::new("veritysetup")
.args(["close", &dev_name])
.status();
anyhow::bail!("mount failed with status {status}");
}
eprintln!("mounted: {} -> {}", img.display(), mountpoint.display());
Ok(())
}
fn cmd_umount(mountpoint: &Path) -> anyhow::Result<()> {
require_root()?;
let dev_name = device_name(mountpoint);
let status = std::process::Command::new("umount")
.arg(mountpoint)
.status()
.context("spawn umount")?;
if !status.success() {
anyhow::bail!("umount failed with status {status}");
}
let status = std::process::Command::new("veritysetup")
.args(["close", &dev_name])
.status()
.context("spawn veritysetup close; ensure cryptsetup-bin is installed")?;
if !status.success() {
anyhow::bail!("veritysetup close failed with status {status}");
}
eprintln!("unmounted: {}", mountpoint.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn device_name_sanitizes_path() {
let mp = Path::new("/run/weft/mounts/com.example.myapp");
let name = device_name(mp);
assert!(name.starts_with("weft-"));
assert!(name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'));
assert!(name.len() <= 31);
}
#[test]
fn device_name_truncates_long_paths() {
let mp = Path::new("/run/weft/mounts/com.example.averylongappidthatexceedsthemaximum");
let name = device_name(mp);
assert!(name.len() <= 31);
}
}

View file

@ -127,7 +127,9 @@ fn main() -> anyhow::Result<()> {
eprintln!(" weft-pack install <dir> install package to app store");
eprintln!(" weft-pack uninstall <app_id> remove installed package");
eprintln!(" weft-pack list list installed packages");
eprintln!(" weft-pack build-image <dir> [--out <img>] create EROFS image with mkfs.erofs");
eprintln!(
" weft-pack build-image <dir> [--out <img>] create EROFS image with mkfs.erofs"
);
eprintln!(" weft-pack build-verity <img> [--out <hash>] add dm-verity hash tree");
eprintln!(" weft-pack bundle <dir> [--out <dir>] create .app.tar.zst archive");
eprintln!(" weft-pack unbundle <archive> [--out <dir>] extract .app.tar.zst");
@ -406,7 +408,11 @@ fn build_verity(img: &Path, hash_out: Option<&Path>) -> anyhow::Result<()> {
anyhow::bail!("{} already exists", hash_path.display());
}
let output = std::process::Command::new("veritysetup")
.args(["format", &img.to_string_lossy(), &hash_path.to_string_lossy()])
.args([
"format",
&img.to_string_lossy(),
&hash_path.to_string_lossy(),
])
.output()
.context("spawn veritysetup; ensure cryptsetup-bin is installed")?;
if !output.status.success() {

View file

@ -38,5 +38,9 @@ echo ""
echo "==> cargo test -p weft-pack"
cargo test -p weft-pack -- --test-threads=1 2>&1
echo ""
echo "==> cargo test -p weft-mount-helper"
cargo test -p weft-mount-helper -- --test-threads=1 2>&1
echo ""
echo "ALL DONE"