From 263f7c70f5023b4d2932aa8d61f3eaa616a5d1c9 Mon Sep 17 00:00:00 2001 From: Allison Karlitskaya Date: Mon, 7 Oct 2024 16:15:59 +0200 Subject: [PATCH] Can now import and mount images --- src/bin/cfsctl.rs | 19 +++++- src/bin/mount.rs | 133 +--------------------------------------- src/bin/splitstream.rs | 2 +- src/lib.rs | 1 + src/mount.rs | 134 +++++++++++++++++++++++++++++++++++++++++ src/repository.rs | 35 +++++++---- 6 files changed, 179 insertions(+), 145 deletions(-) create mode 100644 src/mount.rs diff --git a/src/bin/cfsctl.rs b/src/bin/cfsctl.rs index d70ef5c..34632a5 100644 --- a/src/bin/cfsctl.rs +++ b/src/bin/cfsctl.rs @@ -25,11 +25,20 @@ enum Command { Cat { reference: String, }, + /// Imports a composefs image (unsafe!) + ImportImage { + reference: String, + }, /// Stores a tar file as a splitstream in the repository. ImportTar { reference: String, tarfile: Option, }, + /// Mounts a composefs + Mount { + name: String, + mountpoint: String, + }, } fn main() { @@ -49,9 +58,15 @@ fn main() { }, Command::Cat { reference } => { repo.merge_splitstream(&reference, &mut std::io::stdout()).expect("merge"); - } + }, + Command::ImportImage { reference, } => { + repo.import_image(&reference, &mut std::io::stdin()).expect("image-import"); + }, Command::ImportTar { reference, tarfile: _ } => { repo.import_tar(&reference, &mut std::io::stdin()).expect("tar-import"); - } + }, + Command::Mount { name, mountpoint } => { + repo.mount(&name, &mountpoint).expect("mount"); + }, } } diff --git a/src/bin/mount.rs b/src/bin/mount.rs index 40e42d1..bd9a0e6 100644 --- a/src/bin/mount.rs +++ b/src/bin/mount.rs @@ -1,136 +1,7 @@ -use std::os::fd::{ - OwnedFd, - BorrowedFd, - AsFd, - AsRawFd -}; - -use anyhow::Result; -use rustix::mount::{ - FsMountFlags, - FsOpenFlags, - MountAttrFlags, - MoveMountFlags, - UnmountFlags, - fsconfig_create, - fsconfig_set_string, - fsmount, - fsopen, - move_mount, - unmount, -}; - -use composefs_experiments::{ - fsverity, - tmpdir, -}; - -struct FsHandle { - pub fd: OwnedFd, -} - -impl FsHandle { - pub fn open(name: &str) -> Result { - Ok(FsHandle { fd: fsopen(name, FsOpenFlags::FSOPEN_CLOEXEC)? }) - } -} - -impl AsFd for FsHandle { - fn as_fd(&self) -> BorrowedFd { - return self.fd.as_fd(); - } -} - -impl Drop for FsHandle { - fn drop(&mut self) { - let mut buffer = [0u8; 1024]; - loop { - match rustix::io::read(&self.fd, &mut buffer) { - Err(_) => return, // ENODATA, among others? - Ok(0) => return, - Ok(size) => eprintln!("{}", String::from_utf8(buffer[0..size].to_vec()).unwrap()), - } - } - } -} - -struct TmpMount { - pub dir: tmpdir::TempDir, -} - -impl TmpMount { - pub fn mount(fs: BorrowedFd) -> Result { - let tmp = tmpdir::TempDir::new()?; - let mnt = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?; - move_mount(mnt.as_fd(), "", rustix::fs::CWD, &tmp.path, MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH)?; - Ok(TmpMount { dir: tmp }) - } -} - -impl Drop for TmpMount { - fn drop(&mut self) { - unmount(&self.dir.path, UnmountFlags::DETACH) - .expect("umount(MNT_DETACH) failed"); - } -} - -struct MountOptions<'a> { - image: &'a str, - basedir: &'a str, - digest: Option<&'a str>, - verity: bool, -} - -fn proc_self_fd(fd: &A) -> String { - format!("/proc/self/fd/{}", fd.as_fd().as_raw_fd()) -} - -impl<'a> MountOptions<'a> { - pub fn new(image: &'a str, basedir: &'a str) -> MountOptions<'a> { - MountOptions { image, basedir, digest: None, verity: false } - } - - pub fn set_require_verity(&mut self) { - self.verity = true; - } - - pub fn set_digest(&mut self, digest: &'a str) { - self.digest = Some(digest); - } - - fn mount(self, mountpoint: &str) -> Result<()> { - let image = std::fs::File::open(self.image)?; - - if let Some(expected) = self.digest { - let measured: fsverity::Sha256HashValue = fsverity::ioctl::fs_ioc_measure_verity(&image)?; - if expected != hex::encode(measured) { - panic!("expected {:?} measured {:?}", expected, measured); - } - } - - let erofs = FsHandle::open("erofs")?; - fsconfig_set_string(erofs.as_fd(), "source", proc_self_fd(&image))?; - fsconfig_create(erofs.as_fd())?; - - let overlayfs = FsHandle::open("overlay")?; - fsconfig_set_string(overlayfs.as_fd(), "metacopy", "on")?; - fsconfig_set_string(overlayfs.as_fd(), "redirect_dir", "on")?; - - // unfortunately we can't do this via the fd: we need a tmpdir mountpoint - let tmp = TmpMount::mount(erofs.as_fd())?; // NB: must live until the "create" operation - fsconfig_set_string(overlayfs.as_fd(), "lowerdir+", &tmp.dir.path)?; - fsconfig_set_string(overlayfs.as_fd(), "datadir+", self.basedir)?; - fsconfig_create(overlayfs.as_fd())?; - - let mnt = fsmount(overlayfs.as_fd(), FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?; - move_mount(mnt.as_fd(), "", rustix::fs::CWD, mountpoint, MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH)?; - - Ok(()) - } -} - use clap::Parser; +use composefs_experiments::mount::MountOptions; + /// mount a composefs #[derive(Parser, Debug)] #[command(version, about, long_about = None)] diff --git a/src/bin/splitstream.rs b/src/bin/splitstream.rs index ddd9122..17ed0a1 100644 --- a/src/bin/splitstream.rs +++ b/src/bin/splitstream.rs @@ -14,7 +14,7 @@ fn main() { &mut std::io::stdin(), &mut std::io::stdout(), |data: &[u8]| -> Result { - repo.ensure_object(&data) + repo.ensure_object(data) } ).expect("split"); } diff --git a/src/lib.rs b/src/lib.rs index 9aa1fac..37f1b75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ mod util; pub mod repository; pub mod fsverity; +pub mod mount; pub mod splitstream; pub mod tar; pub mod tmpdir; diff --git a/src/mount.rs b/src/mount.rs new file mode 100644 index 0000000..bc35ef3 --- /dev/null +++ b/src/mount.rs @@ -0,0 +1,134 @@ +use std::os::fd::{ + OwnedFd, + BorrowedFd, + AsFd, + AsRawFd +}; + +use anyhow::Result; +use rustix::mount::{ + FsMountFlags, + FsOpenFlags, + MountAttrFlags, + MoveMountFlags, + UnmountFlags, + fsconfig_create, + fsconfig_set_string, + fsmount, + fsopen, + move_mount, + unmount, +}; + +use crate::{ + fsverity, + tmpdir, +}; + +struct FsHandle { + pub fd: OwnedFd, +} + +impl FsHandle { + pub fn open(name: &str) -> Result { + Ok(FsHandle { fd: fsopen(name, FsOpenFlags::FSOPEN_CLOEXEC)? }) + } +} + +impl AsFd for FsHandle { + fn as_fd(&self) -> BorrowedFd { + return self.fd.as_fd(); + } +} + +impl Drop for FsHandle { + fn drop(&mut self) { + let mut buffer = [0u8; 1024]; + loop { + match rustix::io::read(&self.fd, &mut buffer) { + Err(_) => return, // ENODATA, among others? + Ok(0) => return, + Ok(size) => eprintln!("{}", String::from_utf8(buffer[0..size].to_vec()).unwrap()), + } + } + } +} + +struct TmpMount { + pub dir: tmpdir::TempDir, +} + +impl TmpMount { + pub fn mount(fs: BorrowedFd) -> Result { + let tmp = tmpdir::TempDir::new()?; + let mnt = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?; + move_mount(mnt.as_fd(), "", rustix::fs::CWD, &tmp.path, MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH)?; + Ok(TmpMount { dir: tmp }) + } +} + +impl Drop for TmpMount { + fn drop(&mut self) { + unmount(&self.dir.path, UnmountFlags::DETACH) + .expect("umount(MNT_DETACH) failed"); + } +} + +fn proc_self_fd(fd: A) -> String { + format!("/proc/self/fd/{}", fd.as_fd().as_raw_fd()) +} + +pub fn mount_fd(image: F, basedir: &str, mountpoint: &str) -> Result<()> { + let erofs = FsHandle::open("erofs")?; + fsconfig_set_string(erofs.as_fd(), "source", proc_self_fd(&image))?; + fsconfig_create(erofs.as_fd())?; + + let overlayfs = FsHandle::open("overlay")?; + fsconfig_set_string(overlayfs.as_fd(), "metacopy", "on")?; + fsconfig_set_string(overlayfs.as_fd(), "redirect_dir", "on")?; + + // unfortunately we can't do this via the fd: we need a tmpdir mountpoint + let tmp = TmpMount::mount(erofs.as_fd())?; // NB: must live until the "create" operation + fsconfig_set_string(overlayfs.as_fd(), "lowerdir+", &tmp.dir.path)?; + fsconfig_set_string(overlayfs.as_fd(), "datadir+", basedir)?; + fsconfig_create(overlayfs.as_fd())?; + + let mnt = fsmount(overlayfs.as_fd(), FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?; + move_mount(mnt.as_fd(), "", rustix::fs::CWD, mountpoint, MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH)?; + + Ok(()) +} + +pub struct MountOptions<'a> { + image: &'a str, + basedir: &'a str, + digest: Option<&'a str>, + verity: bool, +} + +impl<'a> MountOptions<'a> { + pub fn new(image: &'a str, basedir: &'a str) -> MountOptions<'a> { + MountOptions { image, basedir, digest: None, verity: false } + } + + pub fn set_require_verity(&mut self) { + self.verity = true; + } + + pub fn set_digest(&mut self, digest: &'a str) { + self.digest = Some(digest); + } + + pub fn mount(self, mountpoint: &str) -> Result<()> { + let image = std::fs::File::open(self.image)?; + + if let Some(expected) = self.digest { + let measured: fsverity::Sha256HashValue = fsverity::ioctl::fs_ioc_measure_verity(&image)?; + if expected != hex::encode(measured) { + panic!("expected {:?} measured {:?}", expected, measured); + } + } + + mount_fd(image, self.basedir, mountpoint) + } +} diff --git a/src/repository.rs b/src/repository.rs index 1288e63..c5f2c1a 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -41,6 +41,7 @@ use crate::{ fs_ioc_measure_verity, }, }, + mount::mount_fd, splitstream::splitstream_merge, tar, util::proc_self_fd, @@ -48,6 +49,7 @@ use crate::{ pub struct Repository { repository: OwnedFd, + path: String, } impl Drop for Repository { @@ -58,19 +60,16 @@ impl Drop for Repository { } impl Repository { - pub fn open_fd(repository: OwnedFd) -> Result { - flock(&repository, FlockOperation::LockShared)?; - Ok(Repository { repository }) - } - - pub fn open_path(path: P) -> Result { + pub fn open_path(path: String) -> Result { // O_PATH isn't enough because flock() - Repository::open_fd(open(path, OFlags::RDONLY, Mode::empty())?) + let repository = open(&path, OFlags::RDONLY, Mode::empty())?; + flock(&repository, FlockOperation::LockShared)?; + Ok(Repository { repository, path }) } pub fn open_default() -> Result { - let home = PathBuf::from(std::env::var("HOME").expect("$HOME must be set")); - Repository::open_path(home.join(".var/lib/composefs")) + let home = std::env::var("HOME").expect("$HOME must be set"); + Repository::open_path(format!("{}/.var/lib/composefs", home)) } fn ensure_parent>(&self, path: P) -> Result<()> { @@ -186,11 +185,25 @@ impl Repository { self.link_ref(name, "streams", object_id) } + /// this function is not safe for untrusted users + pub fn import_image(&self, name: &str, image: &mut R) -> Result<()> { + let mut data = vec![]; + image.read_to_end(&mut data)?; + let object_id = self.ensure_object(&data)?; + self.link_ref(name, "images", object_id) + } + + pub fn mount(self, name: &str, mountpoint: &str) -> Result<()> { + let image = self.open_in_category("images", name)?; + let object_path = format!("{}/objects", self.path); + mount_fd(image, &object_path, mountpoint) + } + fn link_ref( &self, name: &str, category: &str, object_id: Sha256HashValue ) -> Result<()> { let object_path = format!("objects/{:02x}/{}", object_id[0], hex::encode(&object_id[1..])); - let category_path = format!("{}/{}", category, hex::encode(&object_id)); + let category_path = format!("{}/{}", category, hex::encode(object_id)); let ref_path = format!("{}/refs/{}", category, name); self.symlink(&ref_path, &category_path)?; @@ -202,7 +215,7 @@ impl Repository { let name = name.as_ref(); let parent = name.parent() .expect("make_link() called for file directly in repo top-level"); - self.ensure_dir(&parent)?; + self.ensure_dir(parent)?; let mut target_path = PathBuf::new(); for _ in parent.iter() {