authfs: support remote output directory
This change contains 3 major groups:
- authfs/{aidl, fd_server}: new AIDL API and the service implementation
- authfs/src: implement FUSE APIs for creating directory and file, by
interact with the new service API as a client
- authfs/tests, tests/: test coverage
A few notable changes that might help reviewing:
- Now that both AuthFs and FdService struct is no longer immutable (in
order to allow writable directory), their BTreeMap are now guarded by
Arc<Mutex<_>>.
* AuthFs::insert_new_inode and FdService::insert_new_fd are designed
specifically to allow querying then mutating the map, which isn't
trivial.
- File and directory modes from the user program / VFS are currently
ignored (just not to grow the change size).
- Some shuffling of test paths to make it easy to clean up in tearDown.
Bug: 203251769
Test: AuthFsHostTest
Change-Id: I50f3f1ba8a3ebd969cf0f25a8feab2ec8cb1a2dc
diff --git a/authfs/src/file.rs b/authfs/src/file.rs
index 404e3a5..bbe5e6c 100644
--- a/authfs/src/file.rs
+++ b/authfs/src/file.rs
@@ -1,5 +1,7 @@
+mod remote_dir;
mod remote_file;
+pub use remote_dir::RemoteDirEditor;
pub use remote_file::{RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader};
use binder::unstable_api::{new_spibinder, AIBinder};
@@ -8,9 +10,10 @@
use crate::common::CHUNK_SIZE;
use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::IVirtFdService;
-use authfs_aidl_interface::binder::Strong;
+use authfs_aidl_interface::binder::{Status, Strong};
pub type VirtFdService = Strong<dyn IVirtFdService>;
+pub type VirtFdServiceStatus = Status;
pub type ChunkBuffer = [u8; CHUNK_SIZE as usize];
diff --git a/authfs/src/file/remote_dir.rs b/authfs/src/file/remote_dir.rs
new file mode 100644
index 0000000..2e1bc33
--- /dev/null
+++ b/authfs/src/file/remote_dir.rs
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+use std::collections::HashMap;
+use std::io;
+use std::path::{Path, PathBuf};
+
+use super::remote_file::RemoteFileEditor;
+use super::{VirtFdService, VirtFdServiceStatus};
+use crate::fsverity::VerifiedFileEditor;
+use crate::fusefs::Inode;
+
+const MAX_ENTRIES: u16 = 100; // Arbitrary limit
+
+/// A remote directory backed by a remote directory FD, where the provider/fd_server is not
+/// trusted.
+///
+/// The directory is assumed empty initially without the trust to the storage. Functionally, when
+/// the backing storage is not clean, the fd_server can fail to create a file or directory when
+/// there is name collision. From RemoteDirEditor's perspective of security, the creation failure
+/// is just one of possible errors that can happen, and what matters is RemoteDirEditor maintains
+/// the integrity itself.
+///
+/// When new files are created through RemoteDirEditor, the file integrity are maintained within the
+/// VM. Similarly, integrity (namely the list of entries) of the directory, or new directories
+/// created within such a directory, are also maintained within the VM. A compromised fd_server or
+/// malicious client can't affect the view to the files and directories within such a directory in
+/// the VM.
+pub struct RemoteDirEditor {
+ service: VirtFdService,
+ remote_dir_fd: i32,
+
+ /// Mapping of entry names to the corresponding inode number. The actual file/directory is
+ /// stored in the global pool in fusefs.
+ entries: HashMap<PathBuf, Inode>,
+}
+
+impl RemoteDirEditor {
+ pub fn new(service: VirtFdService, remote_dir_fd: i32) -> Self {
+ RemoteDirEditor { service, remote_dir_fd, entries: HashMap::new() }
+ }
+
+ /// Returns the number of entries created.
+ pub fn number_of_entries(&self) -> u16 {
+ self.entries.len() as u16 // limited to MAX_ENTRIES
+ }
+
+ /// Creates a remote file at the current directory. If succeed, the returned remote FD is
+ /// stored in `entries` as the inode number.
+ pub fn create_file(
+ &mut self,
+ basename: &Path,
+ ) -> io::Result<(Inode, VerifiedFileEditor<RemoteFileEditor>)> {
+ self.validate_argument(basename)?;
+
+ let basename_str =
+ basename.to_str().ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
+ let new_fd = self
+ .service
+ .createFileInDirectory(self.remote_dir_fd, basename_str)
+ .map_err(into_io_error)?;
+ let new_inode = new_fd as Inode;
+
+ let new_remote_file =
+ VerifiedFileEditor::new(RemoteFileEditor::new(self.service.clone(), new_fd));
+ self.entries.insert(basename.to_path_buf(), new_inode);
+ Ok((new_inode, new_remote_file))
+ }
+
+ /// Creates a remote directory at the current directory. If succeed, the returned remote FD is
+ /// stored in `entries` as the inode number.
+ pub fn mkdir(&mut self, basename: &Path) -> io::Result<(Inode, RemoteDirEditor)> {
+ self.validate_argument(basename)?;
+
+ let basename_str =
+ basename.to_str().ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
+ let new_fd = self
+ .service
+ .createDirectoryInDirectory(self.remote_dir_fd, basename_str)
+ .map_err(into_io_error)?;
+ let new_inode = new_fd as Inode;
+
+ let new_remote_dir = RemoteDirEditor::new(self.service.clone(), new_fd);
+ self.entries.insert(basename.to_path_buf(), new_inode);
+ Ok((new_inode, new_remote_dir))
+ }
+
+ /// Returns the inode number of a file or directory named `name` previously created through
+ /// `RemoteDirEditor`.
+ pub fn find_inode(&self, name: &Path) -> Option<Inode> {
+ self.entries.get(name).copied()
+ }
+
+ fn validate_argument(&self, basename: &Path) -> io::Result<()> {
+ // Kernel should only give us a basename.
+ debug_assert!(basename.parent().is_none());
+ if self.entries.contains_key(basename) {
+ Err(io::Error::from_raw_os_error(libc::EEXIST))
+ } else if self.entries.len() >= MAX_ENTRIES.into() {
+ Err(io::Error::from_raw_os_error(libc::EMLINK))
+ } else {
+ Ok(())
+ }
+ }
+}
+
+fn into_io_error(e: VirtFdServiceStatus) -> io::Error {
+ let maybe_errno = e.service_specific_error();
+ if maybe_errno > 0 {
+ io::Error::from_raw_os_error(maybe_errno)
+ } else {
+ io::Error::new(io::ErrorKind::Other, e.get_description())
+ }
+}
diff --git a/authfs/src/fusefs.rs b/authfs/src/fusefs.rs
index d985581..0fa827f 100644
--- a/authfs/src/fusefs.rs
+++ b/authfs/src/fusefs.rs
@@ -15,16 +15,17 @@
*/
use anyhow::Result;
-use log::{debug, warn};
-use std::collections::BTreeMap;
+use log::{debug, error, warn};
+use std::collections::{btree_map, BTreeMap};
use std::convert::TryFrom;
-use std::ffi::CStr;
+use std::ffi::{CStr, OsStr};
use std::fs::OpenOptions;
use std::io;
use std::mem::MaybeUninit;
use std::option::Option;
-use std::os::unix::io::AsRawFd;
+use std::os::unix::{ffi::OsStrExt, io::AsRawFd};
use std::path::Path;
+use std::sync::{Arc, Mutex};
use std::time::Duration;
use fuse::filesystem::{
@@ -35,7 +36,8 @@
use crate::common::{divide_roundup, ChunkedSizeIter, CHUNK_SIZE};
use crate::file::{
- RandomWrite, ReadByChunk, RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader,
+ RandomWrite, ReadByChunk, RemoteDirEditor, RemoteFileEditor, RemoteFileReader,
+ RemoteMerkleTreeReader,
};
use crate::fsverity::{VerifiedFileEditor, VerifiedFileReader};
@@ -57,6 +59,9 @@
/// A file type that is initially empty, and the content is stored on a remote server. File
/// integrity is guaranteed with private Merkle tree.
VerifiedNew { editor: VerifiedFileEditor<RemoteFileEditor> },
+ /// A directory type that is initially empty. One can create new file (`VerifiedNew`) and new
+ /// directory (`VerifiedNewDirectory` itself) with integrity guaranteed within the VM.
+ VerifiedNewDirectory { dir: RemoteDirEditor },
}
struct AuthFs {
@@ -65,7 +70,7 @@
/// For further optimization to minimize the search cost, since Inode is integer, we may
/// consider storing them in a Vec if we can guarantee that the numbers are small and
/// consecutive.
- file_pool: BTreeMap<Inode, FileConfig>,
+ file_pool: Arc<Mutex<BTreeMap<Inode, FileConfig>>>,
/// Maximum bytes in the write transaction to the FUSE device. This limits the maximum size to
/// a read request (including FUSE protocol overhead).
@@ -73,19 +78,43 @@
}
impl AuthFs {
- pub fn new(file_pool: BTreeMap<Inode, FileConfig>, max_write: u32) -> AuthFs {
+ pub fn new(file_pool: Arc<Mutex<BTreeMap<Inode, FileConfig>>>, max_write: u32) -> AuthFs {
AuthFs { file_pool, max_write }
}
- /// Handles the file associated with `inode` if found. This function returns whatever the
- /// handler returns.
- fn handle_file<F, R>(&self, inode: &Inode, handler: F) -> io::Result<R>
+ /// Handles the file associated with `inode` if found. This function returns whatever
+ /// `handle_fn` returns.
+ fn handle_file<F, R>(&self, inode: &Inode, handle_fn: F) -> io::Result<R>
where
F: FnOnce(&FileConfig) -> io::Result<R>,
{
+ let file_pool = self.file_pool.lock().unwrap();
let config =
- self.file_pool.get(inode).ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
- handler(config)
+ file_pool.get(inode).ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ handle_fn(config)
+ }
+
+ /// Inserts a new inode and corresponding `FileConfig` created by `create_fn` to the file pool,
+ /// then returns the new inode number.
+ fn insert_new_inode<F>(&self, inode: &Inode, create_fn: F) -> io::Result<Inode>
+ where
+ F: FnOnce(&mut FileConfig) -> io::Result<(Inode, FileConfig)>,
+ {
+ let mut file_pool = self.file_pool.lock().unwrap();
+ let mut config =
+ file_pool.get_mut(inode).ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ let (new_inode, new_file_config) = create_fn(&mut config)?;
+ if let btree_map::Entry::Vacant(entry) = file_pool.entry(new_inode) {
+ entry.insert(new_file_config);
+ Ok(new_inode)
+ } else {
+ // We can't assume fd_server is trusted, so the returned FD may collide with existing
+ // one, even when we are creating a new file. Do not override an existing FD. In terms
+ // of security, it is better to "leak" the file created earlier, than returning an
+ // existing inode as a new file.
+ error!("Inode {} already exists, do not override", new_inode);
+ Err(io::Error::from_raw_os_error(libc::EIO))
+ }
}
}
@@ -105,25 +134,30 @@
}
}
-enum FileMode {
+#[allow(clippy::enum_variant_names)]
+enum AccessMode {
ReadOnly,
ReadWrite,
}
-fn create_stat(ino: libc::ino_t, file_size: u64, file_mode: FileMode) -> io::Result<libc::stat64> {
+fn create_stat(
+ ino: libc::ino_t,
+ file_size: u64,
+ access_mode: AccessMode,
+) -> io::Result<libc::stat64> {
+ // SAFETY: stat64 is a plan C struct without pointer.
let mut st = unsafe { MaybeUninit::<libc::stat64>::zeroed().assume_init() };
st.st_ino = ino;
- st.st_mode = match file_mode {
+ st.st_mode = match access_mode {
// Until needed, let's just grant the owner access.
- FileMode::ReadOnly => libc::S_IFREG | libc::S_IRUSR,
- FileMode::ReadWrite => libc::S_IFREG | libc::S_IRUSR | libc::S_IWUSR,
+ // TODO(205169366): Implement mode properly.
+ AccessMode::ReadOnly => libc::S_IFREG | libc::S_IRUSR,
+ AccessMode::ReadWrite => libc::S_IFREG | libc::S_IRUSR | libc::S_IWUSR,
};
- st.st_dev = 0;
st.st_nlink = 1;
st.st_uid = 0;
st.st_gid = 0;
- st.st_rdev = 0;
st.st_size = libc::off64_t::try_from(file_size)
.map_err(|_| io::Error::from_raw_os_error(libc::EFBIG))?;
st.st_blksize = blk_size();
@@ -133,6 +167,30 @@
Ok(st)
}
+fn create_dir_stat(ino: libc::ino_t, file_number: u16) -> io::Result<libc::stat64> {
+ // SAFETY: stat64 is a plan C struct without pointer.
+ let mut st = unsafe { MaybeUninit::<libc::stat64>::zeroed().assume_init() };
+
+ st.st_ino = ino;
+ // TODO(205169366): Implement mode properly.
+ st.st_mode = libc::S_IFDIR
+ | libc::S_IXUSR
+ | libc::S_IWUSR
+ | libc::S_IRUSR
+ | libc::S_IXGRP
+ | libc::S_IXOTH;
+
+ // 2 extra for . and ..
+ st.st_nlink = file_number
+ .checked_add(2)
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::EOVERFLOW))?
+ .into();
+
+ st.st_uid = 0;
+ st.st_gid = 0;
+ Ok(st)
+}
+
fn offset_to_chunk_index(offset: u64) -> u64 {
offset / CHUNK_SIZE
}
@@ -196,30 +254,62 @@
Ok(FsOptions::WRITEBACK_CACHE)
}
- fn lookup(&self, _ctx: Context, _parent: Inode, name: &CStr) -> io::Result<Entry> {
- // Only accept file name that looks like an integrer. Files in the pool are simply exposed
- // by their inode number. Also, there is currently no directory structure.
- let num = name.to_str().map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?;
- // Normally, `lookup` is required to increase a reference count for the inode (while
- // `forget` will decrease it). It is not necessary here since the files are configured to
- // be static.
- let inode = num.parse::<Inode>().map_err(|_| io::Error::from_raw_os_error(libc::ENOENT))?;
- let st = self.handle_file(&inode, |config| match config {
- FileConfig::UnverifiedReadonly { file_size, .. }
- | FileConfig::VerifiedReadonly { file_size, .. } => {
- create_stat(inode, *file_size, FileMode::ReadOnly)
- }
- FileConfig::VerifiedNew { editor } => {
- create_stat(inode, editor.size(), FileMode::ReadWrite)
- }
- })?;
- Ok(Entry {
- inode,
- generation: 0,
- attr: st,
- entry_timeout: DEFAULT_METADATA_TIMEOUT,
- attr_timeout: DEFAULT_METADATA_TIMEOUT,
- })
+ fn lookup(&self, _ctx: Context, parent: Inode, name: &CStr) -> io::Result<Entry> {
+ // TODO(victorhsieh): convert root directory (inode == 1) to a readonly directory. Right
+ // now, it's the (global) inode pool, so all inodes can be accessed from root.
+ if parent == 1 {
+ // Only accept file name that looks like an integrer. Files in the pool are simply
+ // exposed by their inode number. Also, there is currently no directory structure.
+ let num = name.to_str().map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?;
+ // Normally, `lookup` is required to increase a reference count for the inode (while
+ // `forget` will decrease it). It is not necessary here since the files are configured
+ // to be static.
+ let inode =
+ num.parse::<Inode>().map_err(|_| io::Error::from_raw_os_error(libc::ENOENT))?;
+ let st = self.handle_file(&inode, |config| match config {
+ FileConfig::UnverifiedReadonly { file_size, .. }
+ | FileConfig::VerifiedReadonly { file_size, .. } => {
+ create_stat(inode, *file_size, AccessMode::ReadOnly)
+ }
+ FileConfig::VerifiedNew { editor } => {
+ create_stat(inode, editor.size(), AccessMode::ReadWrite)
+ }
+ FileConfig::VerifiedNewDirectory { dir } => {
+ create_dir_stat(inode, dir.number_of_entries())
+ }
+ })?;
+ Ok(Entry {
+ inode,
+ generation: 0,
+ attr: st,
+ entry_timeout: DEFAULT_METADATA_TIMEOUT,
+ attr_timeout: DEFAULT_METADATA_TIMEOUT,
+ })
+ } else {
+ let inode = self.handle_file(&parent, |config| match config {
+ FileConfig::VerifiedNewDirectory { dir } => {
+ let path: &Path = cstr_to_path(name);
+ dir.find_inode(path).ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))
+ }
+ _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+ })?;
+ let st = self.handle_file(&inode, |config| match config {
+ FileConfig::VerifiedNew { editor } => {
+ create_stat(inode, editor.size(), AccessMode::ReadWrite)
+ }
+ FileConfig::VerifiedNewDirectory { dir } => {
+ create_dir_stat(inode, dir.number_of_entries())
+ }
+ _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
+ })?;
+ Ok(Entry {
+ inode,
+ generation: 0,
+ attr: st,
+ entry_timeout: DEFAULT_METADATA_TIMEOUT,
+ attr_timeout: DEFAULT_METADATA_TIMEOUT,
+ })
+ }
}
fn getattr(
@@ -233,10 +323,13 @@
match config {
FileConfig::UnverifiedReadonly { file_size, .. }
| FileConfig::VerifiedReadonly { file_size, .. } => {
- create_stat(inode, *file_size, FileMode::ReadOnly)?
+ create_stat(inode, *file_size, AccessMode::ReadOnly)?
}
FileConfig::VerifiedNew { editor } => {
- create_stat(inode, editor.size(), FileMode::ReadWrite)?
+ create_stat(inode, editor.size(), AccessMode::ReadWrite)?
+ }
+ FileConfig::VerifiedNewDirectory { dir } => {
+ create_dir_stat(inode, dir.number_of_entries())?
}
},
DEFAULT_METADATA_TIMEOUT,
@@ -261,13 +354,54 @@
// No need to check access modes since all the modes are allowed to the
// read-writable file.
}
+ FileConfig::VerifiedNewDirectory { .. } => {
+ // TODO(victorhsieh): implement when needed.
+ return Err(io::Error::from_raw_os_error(libc::ENOSYS));
+ }
}
- // Always cache the file content. There is currently no need to support direct I/O or avoid
- // the cache buffer. Memory mapping is only possible with cache enabled.
+ // Always cache the file content. There is currently no need to support direct I/O or
+ // avoid the cache buffer. Memory mapping is only possible with cache enabled.
Ok((None, fuse::sys::OpenOptions::KEEP_CACHE))
})
}
+ fn create(
+ &self,
+ _ctx: Context,
+ parent: Self::Inode,
+ name: &CStr,
+ _mode: u32,
+ _flags: u32,
+ _umask: u32,
+ ) -> io::Result<(Entry, Option<Self::Handle>, fuse::sys::OpenOptions)> {
+ // TODO(205169366): Implement mode properly.
+ // TODO(205172873): handle O_TRUNC and O_EXCL properly.
+ let new_inode = self.insert_new_inode(&parent, |config| match config {
+ FileConfig::VerifiedNewDirectory { dir } => {
+ let basename: &Path = cstr_to_path(name);
+ if dir.find_inode(basename).is_some() {
+ return Err(io::Error::from_raw_os_error(libc::EEXIST));
+ }
+ let (new_inode, new_file) = dir.create_file(basename)?;
+ Ok((new_inode, FileConfig::VerifiedNew { editor: new_file }))
+ }
+ _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
+ })?;
+
+ Ok((
+ Entry {
+ inode: new_inode,
+ generation: 0,
+ attr: create_stat(new_inode, /* file_size */ 0, AccessMode::ReadWrite)?,
+ entry_timeout: DEFAULT_METADATA_TIMEOUT,
+ attr_timeout: DEFAULT_METADATA_TIMEOUT,
+ },
+ // See also `open`.
+ /* handle */ None,
+ fuse::sys::OpenOptions::KEEP_CACHE,
+ ))
+ }
+
fn read<W: io::Write + ZeroCopyWriter>(
&self,
_ctx: Context,
@@ -292,6 +426,7 @@
// request a read even if the file is open with O_WRONLY.
read_chunks(w, editor, editor.size(), offset, size)
}
+ _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
}
})
}
@@ -330,7 +465,7 @@
match config {
FileConfig::VerifiedNew { editor } => {
// Initialize the default stat.
- let mut new_attr = create_stat(inode, editor.size(), FileMode::ReadWrite)?;
+ let mut new_attr = create_stat(inode, editor.size(), AccessMode::ReadWrite)?;
// `valid` indicates what fields in `attr` are valid. Update to return correctly.
if valid.contains(SetattrValid::SIZE) {
// st_size is i64, but the cast should be safe since kernel should not give a
@@ -407,6 +542,36 @@
}
})
}
+
+ fn mkdir(
+ &self,
+ _ctx: Context,
+ parent: Self::Inode,
+ name: &CStr,
+ _mode: u32,
+ _umask: u32,
+ ) -> io::Result<Entry> {
+ // TODO(205169366): Implement mode properly.
+ let new_inode = self.insert_new_inode(&parent, |config| match config {
+ FileConfig::VerifiedNewDirectory { dir } => {
+ let basename: &Path = cstr_to_path(name);
+ if dir.find_inode(basename).is_some() {
+ return Err(io::Error::from_raw_os_error(libc::EEXIST));
+ }
+ let (new_inode, new_dir) = dir.mkdir(basename)?;
+ Ok((new_inode, FileConfig::VerifiedNewDirectory { dir: new_dir }))
+ }
+ _ => Err(io::Error::from_raw_os_error(libc::EBADF)),
+ })?;
+
+ Ok(Entry {
+ inode: new_inode,
+ generation: 0,
+ attr: create_dir_stat(new_inode, /* file_number */ 0)?,
+ entry_timeout: DEFAULT_METADATA_TIMEOUT,
+ attr_timeout: DEFAULT_METADATA_TIMEOUT,
+ })
+ }
}
/// Mount and start the FUSE instance. This requires CAP_SYS_ADMIN.
@@ -442,6 +607,10 @@
dev_fuse,
max_write,
max_read,
- AuthFs::new(file_pool, max_write),
+ AuthFs::new(Arc::new(Mutex::new(file_pool)), max_write),
)
}
+
+fn cstr_to_path(cstr: &CStr) -> &Path {
+ OsStr::from_bytes(cstr.to_bytes()).as_ref()
+}
diff --git a/authfs/src/main.rs b/authfs/src/main.rs
index a6956e2..0add77f 100644
--- a/authfs/src/main.rs
+++ b/authfs/src/main.rs
@@ -42,7 +42,7 @@
mod fusefs;
use auth::FakeAuthenticator;
-use file::{RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader};
+use file::{RemoteDirEditor, RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader};
use fsverity::{VerifiedFileEditor, VerifiedFileReader};
use fusefs::{FileConfig, Inode};
@@ -81,6 +81,15 @@
#[structopt(long, parse(try_from_str = parse_remote_new_rw_file_option))]
remote_new_rw_file: Vec<OptionRemoteRwFile>,
+ /// A new directory that is assumed empty in the backing filesystem. New files created in this
+ /// directory are integrity-protected in the same way as --remote-new-verified-file. Can be
+ /// multiple.
+ ///
+ /// For example, `--remote-new-verified-dir 12:34` tells the filesystem to associate entry 12
+ /// with a remote dir FD 34.
+ #[structopt(long, parse(try_from_str = parse_remote_new_rw_dir_option))]
+ remote_new_rw_dir: Vec<OptionRemoteRwDir>,
+
/// Enable debugging features.
#[structopt(long)]
debug: bool,
@@ -111,6 +120,13 @@
remote_id: i32,
}
+struct OptionRemoteRwDir {
+ ino: Inode,
+
+ /// ID to refer to the remote dir.
+ remote_id: i32,
+}
+
fn parse_remote_ro_file_option(option: &str) -> Result<OptionRemoteRoFile> {
let strs: Vec<&str> = option.split(':').collect();
if strs.len() != 3 {
@@ -145,6 +161,17 @@
})
}
+fn parse_remote_new_rw_dir_option(option: &str) -> Result<OptionRemoteRwDir> {
+ let strs: Vec<&str> = option.split(':').collect();
+ if strs.len() != 2 {
+ bail!("Invalid option: {}", option);
+ }
+ Ok(OptionRemoteRwDir {
+ ino: strs[0].parse::<Inode>().unwrap(),
+ remote_id: strs[1].parse::<i32>().unwrap(),
+ })
+}
+
fn new_config_remote_verified_file(
service: file::VirtFdService,
remote_id: i32,
@@ -182,6 +209,14 @@
Ok(FileConfig::VerifiedNew { editor: VerifiedFileEditor::new(remote_file) })
}
+fn new_config_remote_new_verified_dir(
+ service: file::VirtFdService,
+ remote_id: i32,
+) -> Result<FileConfig> {
+ let dir = RemoteDirEditor::new(service, remote_id);
+ Ok(FileConfig::VerifiedNewDirectory { dir })
+}
+
fn prepare_file_pool(args: &Args) -> Result<BTreeMap<Inode, FileConfig>> {
let mut file_pool = BTreeMap::new();
@@ -216,6 +251,13 @@
);
}
+ for config in &args.remote_new_rw_dir {
+ file_pool.insert(
+ config.ino,
+ new_config_remote_new_verified_dir(service.clone(), config.remote_id)?,
+ );
+ }
+
Ok(file_pool)
}