use btlib::{ blocktree::{Blocktree, ModeAuthorizer}, crypto::{ tpm::{TpmCredStore, TpmCreds}, CredStore, }, }; use fuse_backend_rs::{ api::server::Server, transport::{Error, FuseSession}, }; use log::error; use std::{ ffi::{c_char, CString}, fs::{self, File}, io, os::fd::FromRawFd, os::{raw::c_int, unix::ffi::OsStrExt}, path::{Path, PathBuf}, str::FromStr, sync::mpsc::Sender, }; use tss_esapi::{ tcti_ldr::{TabrmdConfig, TctiNameConf}, Context, }; const DEFAULT_TABRMD: &str = "bus_type=session"; const MOUNT_OPTIONS: &str = "default_permissions"; const FSNAME: &str = "btfuse"; const FSTYPE: &str = "bt"; trait PathExt { fn try_create_dir(&self) -> io::Result<()>; } impl> PathExt for T { fn try_create_dir(&self) -> io::Result<()> { match fs::create_dir(self) { Ok(_) => Ok(()), Err(err) => match err.kind() { io::ErrorKind::AlreadyExists => Ok(()), _ => Err(err), }, } } } #[link(name = "fuse3")] extern "C" { /// Opens a channel to the kernel. fn fuse_open_channel(mountpoint: *const c_char, options: *const c_char) -> c_int; } /// Calls into libfuse3 to mount this file system at the given path. The file descriptor to use /// to communicate with the kernel is returned. fn mount_at>(mnt_point: P) -> File { let mountpoint = CString::new(mnt_point.as_ref().as_os_str().as_bytes()).unwrap(); let options = CString::new(MOUNT_OPTIONS).unwrap(); let raw_fd = unsafe { fuse_open_channel(mountpoint.as_ptr(), options.as_ptr()) }; unsafe { File::from_raw_fd(raw_fd) } } /// A fuse daemon process. struct FuseDaemon<'a> { /// The path of the directory to store all file system information in. path: &'a Path, /// The configuration string to use to connect to a Tabrmd instance. tabrmd_config: &'a str, /// An optional [Sender] which is called when this daemon has finished starting up. started_signal: Option>, } impl<'a> FuseDaemon<'a> { fn new(path: &'a Path, tabrmd_config: &'a str) -> FuseDaemon<'_> { FuseDaemon { path, tabrmd_config, started_signal: None, } } fn tpm_state_path>(path: P) -> PathBuf { path.as_ref().join("tpm_state") } fn mnt_path>(path: P) -> PathBuf { path.as_ref().join("mnt") } fn server>(&self, mnt_path: P) -> Server> { let empty = fs::read_dir(&mnt_path) .expect("failed to read mountdir") .next() .is_none(); let context = Context::new(TctiNameConf::Tabrmd( TabrmdConfig::from_str(self.tabrmd_config).expect("failed to parse Tabrmd config"), )) .expect("failed to connect to Tabrmd"); let cred_store = TpmCredStore::new(context, Self::tpm_state_path(self.path)) .expect("failed to create TpmCredStore"); let node_creds = cred_store .node_creds() .expect("failed to retrieve node creds"); let bt_path = self.path.join("bt"); bt_path .try_create_dir() .expect("failed to create blocktree directory"); let fs = if empty { Blocktree::new_empty(bt_path, 0, node_creds, ModeAuthorizer {}) } else { Blocktree::new_existing(bt_path, node_creds, ModeAuthorizer {}) } .expect("failed to create blocktree"); Server::new(fs) } fn fuse_session>(&self, mnt_path: P) -> FuseSession { self.path .try_create_dir() .expect("failed to create main directory"); let mut session = FuseSession::new(mnt_path.as_ref(), FSNAME, FSTYPE, false) .expect("failed to create FUSE session"); session.set_fuse_file(mount_at(mnt_path)); session } fn start(&self) { let mnt_path = Self::mnt_path(self.path); mnt_path .try_create_dir() .expect("failed to create mount directory"); let server = self.server(&mnt_path); let session = self.fuse_session(&mnt_path); drop(mnt_path); let mut channel = session .new_channel() .expect("failed to create FUSE channel"); if let Some(tx) = self.started_signal.as_ref() { tx.send(()).expect("failed to send started signal"); } loop { match channel.get_request() { Ok(Some((reader, writer))) => { if let Err(err) = server.handle_message(reader, writer.into(), None, None) { error!("error while handling FUSE message: {err}"); } } Ok(None) => break, Err(err) => { match err { // Occurs when the file system is unmounted. Error::SessionFailure(_) => break, _ => error!("{err}"), } } } } } } fn main() { env_logger::init(); let main_dir = std::env::args().nth(1).expect("no mount point given"); let main_dir = PathBuf::from_str(main_dir.as_str()).expect("failed to convert mount point to PathBuf"); let tabrmd_string = std::env::var("BT_TABRMD").ok(); let tabrmd_str = tabrmd_string .as_ref() .map_or(DEFAULT_TABRMD, |s| s.as_str()); let daemon = FuseDaemon::new(&main_dir, tabrmd_str); daemon.start(); } #[cfg(test)] mod test { use std::{ ffi::{OsStr, OsString}, fs::{ create_dir, hard_link, metadata, read, read_dir, remove_dir, remove_file, set_permissions, write, Permissions, ReadDir, }, os::unix::fs::PermissionsExt, sync::mpsc::{channel, Receiver}, thread::JoinHandle, time::Duration, }; use btlib::{crypto::Creds, log::BuilderExt, Epoch, Principaled}; use swtpm_harness::SwtpmHarness; use tempdir::TempDir; use super::*; /// Unmounts the file system at the given path. fn unmount>(mnt_path: P) { const PROG: &str = "fusermount"; let mnt_path = mnt_path .as_ref() .as_os_str() .to_str() .expect("failed to convert mnt_path to `str`"); let code = std::process::Command::new(PROG) .args(["-u", mnt_path]) .status() .expect("waiting for exit status failed") .code() .expect("code returned None"); if code != 0 { panic!("{PROG} exited with a non-zero status: {code}"); } } fn file_names(read_dir: ReadDir) -> impl Iterator { read_dir.map(|entry| entry.unwrap().file_name()) } struct TestCase { temp_path_rx: Receiver, mnt_path: Option, handle: Option>, } impl TestCase { const ROOT_PASSWD: &str = "Gnurlingwheel"; fn new() -> TestCase { let (tx, rx) = channel(); let (started_tx, started_rx) = channel(); let handle = std::thread::spawn(move || { let dir = TempDir::new("btfuse").expect("failed to create TempDir"); tx.send(dir.path().to_owned()).expect("send failed"); let swtpm = SwtpmHarness::new().expect("failed to start swtpm"); { let context = swtpm.context().expect("failed to create TPM context"); let cred_store = TpmCredStore::new(context, FuseDaemon::tpm_state_path(dir.path())) .expect("failed to create TpmCredStore"); let root_creds = cred_store .gen_root_creds(Self::ROOT_PASSWD) .expect("failed to gen root creds"); let mut node_creds = cred_store.node_creds().expect("failed to get node creds"); let expires = Epoch::now() + Duration::from_secs(3600); let writecap = root_creds .issue_writecap(node_creds.principal(), vec![], expires) .expect("failed to issue writecap to node creds"); cred_store .assign_node_writecap(&mut node_creds, writecap) .expect("failed to assign writecap"); } let mut daemon = FuseDaemon::new(dir.path(), swtpm.tabrmd_config()); daemon.started_signal = Some(started_tx); daemon.start() }); started_rx .recv_timeout(Duration::from_secs(1)) .expect("failed to received started signal from `FuseDaemon`"); println!("sent started signal"); TestCase { temp_path_rx: rx, mnt_path: None, handle: Some(handle), } } fn mnt_path(&mut self) -> &PathBuf { self.mnt_path.get_or_insert_with(|| { let temp_path = self .temp_path_rx .recv_timeout(Duration::from_secs(1)) .expect("receive failed"); FuseDaemon::mnt_path(temp_path) }) } fn wait(&mut self) { if let Some(handle) = self.handle.take() { handle.join().expect("join failed"); } } fn unmount_and_wait(&mut self) { // If `handle` has already been taken that means `wait` has already been called. If // this thread called `wait` and subsequently became unblocked, we know that the FUSE // thread halted, which only happens when the file system is unmounted. Hence // we don't have to unmount it, nor wait for the FUSE thread. if self.handle.is_none() { return; } let mnt_path = self.mnt_path(); unmount(mnt_path); self.wait(); } } impl Drop for TestCase { fn drop(&mut self) { self.unmount_and_wait(); } } /// Creates a new file system and mounts it at `/tmp/btfuse./mnt` so it can be /// tested manually. //#[test] #[allow(dead_code)] fn manual_test() { std::env::set_var("RUST_LOG", "debug"); env_logger::Builder::from_default_env().btformat().init(); let mut case = TestCase::new(); case.wait(); } /// Tests if the file system can be mount then unmounted successfully. #[test] fn mount_then_unmount() { let _ = TestCase::new(); } #[test] fn write_read() { const EXPECTED: &[u8] = b"The paths to failure are uncountable, yet to success there is but one."; let mut case = TestCase::new(); let file_path = case.mnt_path().join("file"); write(&file_path, EXPECTED).expect("write failed"); let actual = read(&file_path).expect("read failed"); assert_eq!(EXPECTED, actual); } #[test] fn create_file_then_readdir() { const DATA: &[u8] = b"Au revoir Shoshanna!"; let file_name = OsStr::new("landa_dialog.txt"); let expected = [file_name]; let mut case = TestCase::new(); let mnt_path = case.mnt_path(); let file_path = mnt_path.join(file_name); write(&file_path, DATA).expect("write failed"); let first = file_names(read_dir(&mnt_path).expect("read_dir failed")); assert!(first.eq(expected)); let second = file_names(read_dir(&mnt_path).expect("read_dir failed")); assert!(second.eq(expected)); } #[test] fn create_then_delete_file() { const DATA: &[u8] = b"The universe is hostile, so impersonal. Devour to survive"; let file_name = OsStr::new("tool_lyrics.txt"); let mut case = TestCase::new(); let mnt_path = case.mnt_path(); let file_path = mnt_path.join(file_name); write(&file_path, DATA).expect("write failed"); remove_file(&file_path).expect("remove_file failed"); let expected: [&OsStr; 0] = []; assert!(file_names(read_dir(&mnt_path).expect("read_dir failed")).eq(expected)) } #[test] fn hard_link_then_remove() { const EXPECTED: &[u8] = b"And the lives we've reclaimed"; let name1 = OsStr::new("refugee_lyrics.txt"); let name2 = OsStr::new("rise_against_lyrics.txt"); let mut case = TestCase::new(); let mnt_path = case.mnt_path(); let path1 = mnt_path.join(name1); let path2 = mnt_path.join(name2); write(&path1, EXPECTED).expect("write failed"); hard_link(&path1, &path2).expect("hard_link failed"); remove_file(&path1).expect("remove_file failed"); let actual = read(&path2).expect("read failed"); assert_eq!(EXPECTED, actual); } #[test] fn hard_link_then_remove_both() { const EXPECTED: &[u8] = b"And the lives we've reclaimed"; let name1 = OsStr::new("refugee_lyrics.txt"); let name2 = OsStr::new("rise_against_lyrics.txt"); let mut case = TestCase::new(); let mnt_path = case.mnt_path(); let path1 = mnt_path.join(name1); let path2 = mnt_path.join(name2); write(&path1, EXPECTED).expect("write failed"); hard_link(&path1, &path2).expect("hard_link failed"); remove_file(&path1).expect("remove_file on path1 failed"); remove_file(&path2).expect("remove_file on path2 failed"); let expected: [&OsStr; 0] = []; assert!(file_names(read_dir(&mnt_path).expect("read_dir failed")).eq(expected)); } #[test] fn set_mode_bits() { const EXPECTED: u32 = libc::S_IFREG | 0o777; let mut case = TestCase::new(); let file_path = case.mnt_path().join("bagobits"); write(&file_path, []).expect("write failed"); let original = metadata(&file_path) .expect("metadata failed") .permissions() .mode(); assert_ne!(EXPECTED, original); set_permissions(&file_path, Permissions::from_mode(EXPECTED)) .expect("set_permissions failed"); let actual = metadata(&file_path) .expect("metadata failed") .permissions() .mode(); assert_eq!(EXPECTED, actual); } #[test] fn create_directory() { const EXPECTED: &str = "etc"; let mut case = TestCase::new(); let mnt_path = case.mnt_path(); let dir_path = mnt_path.join(EXPECTED); create_dir(&dir_path).expect("create_dir failed"); let actual = file_names(read_dir(mnt_path).expect("read_dir failed")); assert!(actual.eq([EXPECTED])); } #[test] fn create_file_under_new_directory() { const DIR_NAME: &str = "etc"; const FILE_NAME: &str = "file"; let mut case = TestCase::new(); let mnt_path = case.mnt_path(); let dir_path = mnt_path.join(DIR_NAME); let file_path = dir_path.join(FILE_NAME); create_dir(&dir_path).expect("create_dir failed"); write(&file_path, []).expect("write failed"); let actual = file_names(read_dir(dir_path).expect("read_dir failed")); assert!(actual.eq([FILE_NAME])); } #[test] fn create_then_remove_directory() { const DIR_NAME: &str = "etc"; let mut case = TestCase::new(); let mnt_path = case.mnt_path(); let dir_path = mnt_path.join(DIR_NAME); create_dir(&dir_path).expect("create_dir failed"); remove_dir(&dir_path).expect("remove_dir failed"); let actual = file_names(read_dir(&mnt_path).expect("read_dir failed")); const EMPTY: [&str; 0] = [""; 0]; assert!(actual.eq(EMPTY)); } #[test] fn read_only_dir_cant_create_subdir() { const DIR_NAME: &str = "etc"; let mut case = TestCase::new(); let dir_path = case.mnt_path().join(DIR_NAME); create_dir(&dir_path).expect("create_dir failed"); set_permissions(&dir_path, Permissions::from_mode(libc::S_IFDIR | 0o444)) .expect("set_permissions failed"); let result = create_dir(dir_path.join("sub")); let err = result.err().expect("create_dir returned `Ok`"); let os_err = err.raw_os_error().expect("raw_os_error was empty"); assert_eq!(os_err, libc::EACCES); } #[test] fn read_only_dir_cant_remove_subdir() { const DIR_NAME: &str = "etc"; let mut case = TestCase::new(); let dir_path = case.mnt_path().join(DIR_NAME); let sub_path = dir_path.join("sub"); create_dir(&dir_path).expect("create_dir failed"); create_dir(&sub_path).expect("create_dir failed"); set_permissions(&dir_path, Permissions::from_mode(libc::S_IFDIR | 0o444)) .expect("set_permissions failed"); let result = remove_dir(&sub_path); let err = result.err().expect("remove_dir returned `Ok`"); let os_err = err.raw_os_error().expect("raw_os_error was empty"); assert_eq!(os_err, libc::EACCES); } }