Merge changes I49b222b9,I96bdda45,I7a75ef4e,I50f3f1ba,Ica4a9a21
* changes:
Assign inode values properly in AuthFS
Various renames in authfs
Remove unnecessary local ID in authfs
authfs: support remote output directory
Workaround a quirk in the OpenOptions unix impl
diff --git a/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl b/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
index d8f481a..58ccfc3 100644
--- a/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
+++ b/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
@@ -17,8 +17,8 @@
package com.android.virt.fs;
/**
- * A service that works like a file server, where the files and directories are identified by "FD"
- * as the unique identifier.
+ * A service that works like a file server, where the files and directories are identified by
+ * "remote FD" that may be pre-exchanged or created on request.
*
* When a binder error is returned and it is a service specific error, the error code is an errno
* value which is an int.
@@ -30,28 +30,45 @@
const int MAX_REQUESTING_DATA = 16384;
/**
- * Returns the content of the given file ID, from the offset, for the amount of requested size
+ * Returns the content of the given remote FD, from the offset, for the amount of requested size
* or until EOF.
*/
- byte[] readFile(int id, long offset, int size);
+ byte[] readFile(int fd, long offset, int size);
/**
- * Returns the content of fs-verity compatible Merkle tree of the given file ID, from the
+ * Returns the content of fs-verity compatible Merkle tree of the given remote FD, from the
* offset, for the amount of requested size or until EOF.
*/
- byte[] readFsverityMerkleTree(int id, long offset, int size);
+ byte[] readFsverityMerkleTree(int fd, long offset, int size);
- /** Returns the fs-verity signature of the given file ID. */
- byte[] readFsveritySignature(int id);
+ /** Returns the fs-verity signature of the given remote FD. */
+ byte[] readFsveritySignature(int fd);
/**
- * Writes the buffer to the given file ID from the file's offset. Returns the number of bytes
+ * Writes the buffer to the given remote FD from the file's offset. Returns the number of bytes
* written.
*/
- int writeFile(int id, in byte[] buf, long offset);
+ int writeFile(int fd, in byte[] buf, long offset);
- /** Resizes the file backed by the given file ID to the new size. */
- void resize(int id, long size);
+ /** Resizes the file backed by the given remote FD to the new size. */
+ void resize(int fd, long size);
- long getFileSize(int id);
+ /** Returns the file size. */
+ long getFileSize(int fd);
+
+ /**
+ * Create a file given the remote directory FD.
+ *
+ * @param basename The file name to create. Must not contain directory separator.
+ * @return file A remote FD that represents the new created file.
+ */
+ int createFileInDirectory(int fd, String basename);
+
+ /**
+ * Create a directory inside the given remote directory FD.
+ *
+ * @param basename The directory name to create. Must not contain directory separator.
+ * @return file FD that represents the new created directory.
+ */
+ int createDirectoryInDirectory(int id, String basename);
}
diff --git a/authfs/fd_server/src/aidl.rs b/authfs/fd_server/src/aidl.rs
index 48547e7..0c41eac 100644
--- a/authfs/fd_server/src/aidl.rs
+++ b/authfs/fd_server/src/aidl.rs
@@ -16,23 +16,27 @@
use anyhow::Result;
use log::error;
-use nix::errno::Errno;
+use nix::{
+ dir::Dir, errno::Errno, fcntl::openat, fcntl::OFlag, sys::stat::mkdirat, sys::stat::Mode,
+};
use std::cmp::min;
-use std::collections::BTreeMap;
+use std::collections::{btree_map, BTreeMap};
use std::convert::TryInto;
use std::fs::File;
use std::io;
use std::os::unix::fs::FileExt;
-use std::os::unix::io::AsRawFd;
+use std::os::unix::io::{AsRawFd, FromRawFd};
+use std::path::MAIN_SEPARATOR;
+use std::sync::{Arc, Mutex};
use crate::fsverity;
use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::{
BnVirtFdService, IVirtFdService, MAX_REQUESTING_DATA,
};
use authfs_aidl_interface::binder::{
- BinderFeatures, Interface, Result as BinderResult, Status, StatusCode, Strong,
+ BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, StatusCode, Strong,
};
-use binder_common::new_binder_service_specific_error;
+use binder_common::{new_binder_exception, new_binder_service_specific_error};
fn validate_and_cast_offset(offset: i64) -> Result<u64, Status> {
offset.try_into().map_err(|_| new_errno_error(Errno::EINVAL))
@@ -64,26 +68,53 @@
/// A readable/writable file to serve by this server. This backing file should just be a
/// regular file and does not have any specific property.
ReadWrite(File),
+
+ /// A writable directory to serve by this server.
+ OutputDir(Dir),
}
pub struct FdService {
- /// A pool of opened files, may be readonly or read-writable.
- fd_pool: BTreeMap<i32, FdConfig>,
+ /// A pool of opened files and directories, which can be looked up by the FD number.
+ fd_pool: Arc<Mutex<BTreeMap<i32, FdConfig>>>,
}
impl FdService {
pub fn new_binder(fd_pool: BTreeMap<i32, FdConfig>) -> Strong<dyn IVirtFdService> {
- BnVirtFdService::new_binder(FdService { fd_pool }, BinderFeatures::default())
+ BnVirtFdService::new_binder(
+ FdService { fd_pool: Arc::new(Mutex::new(fd_pool)) },
+ BinderFeatures::default(),
+ )
}
- /// Handles the requesting file `id` with `handler` if it is in the FD pool. This function
- /// returns whatever the handler returns.
- fn handle_fd<F, R>(&self, id: i32, handler: F) -> BinderResult<R>
+ /// Handles the requesting file `id` with `handle_fn` if it is in the FD pool. This function
+ /// returns whatever `handle_fn` returns.
+ fn handle_fd<F, R>(&self, id: i32, handle_fn: F) -> BinderResult<R>
where
F: FnOnce(&FdConfig) -> BinderResult<R>,
{
- let fd_config = self.fd_pool.get(&id).ok_or_else(|| new_errno_error(Errno::EBADF))?;
- handler(fd_config)
+ let fd_pool = self.fd_pool.lock().unwrap();
+ let fd_config = fd_pool.get(&id).ok_or_else(|| new_errno_error(Errno::EBADF))?;
+ handle_fn(fd_config)
+ }
+
+ /// Inserts a new FD and corresponding `FdConfig` created by `create_fn` to the FD pool, then
+ /// returns the new FD number.
+ fn insert_new_fd<F>(&self, fd: i32, create_fn: F) -> BinderResult<i32>
+ where
+ F: FnOnce(&mut FdConfig) -> BinderResult<(i32, FdConfig)>,
+ {
+ let mut fd_pool = self.fd_pool.lock().unwrap();
+ let mut fd_config = fd_pool.get_mut(&fd).ok_or_else(|| new_errno_error(Errno::EBADF))?;
+ let (new_fd, new_fd_config) = create_fn(&mut fd_config)?;
+ if let btree_map::Entry::Vacant(entry) = fd_pool.entry(new_fd) {
+ entry.insert(new_fd_config);
+ Ok(new_fd)
+ } else {
+ Err(new_binder_exception(
+ ExceptionCode::ILLEGAL_STATE,
+ format!("The newly created FD {} is already in the pool unexpectedly", new_fd),
+ ))
+ }
}
}
@@ -101,6 +132,7 @@
new_errno_error(Errno::EIO)
})
}
+ FdConfig::OutputDir(_) => Err(new_errno_error(Errno::EISDIR)),
})
}
@@ -133,6 +165,7 @@
// use.
Err(new_errno_error(Errno::ENOSYS))
}
+ FdConfig::OutputDir(_) => Err(new_errno_error(Errno::EISDIR)),
})
}
@@ -162,6 +195,7 @@
// There is no signature for a writable file.
Err(new_errno_error(Errno::ENOSYS))
}
+ FdConfig::OutputDir(_) => Err(new_errno_error(Errno::EISDIR)),
})
}
@@ -179,6 +213,7 @@
new_errno_error(Errno::EIO)
})? as i32)
}
+ FdConfig::OutputDir(_) => Err(new_errno_error(Errno::EISDIR)),
})
}
@@ -194,6 +229,7 @@
new_errno_error(Errno::EIO)
})
}
+ FdConfig::OutputDir(_) => Err(new_errno_error(Errno::EISDIR)),
})
}
@@ -218,6 +254,50 @@
// for a writable file.
Err(new_errno_error(Errno::ENOSYS))
}
+ FdConfig::OutputDir(_) => Err(new_errno_error(Errno::EISDIR)),
+ })
+ }
+
+ fn createFileInDirectory(&self, fd: i32, basename: &str) -> BinderResult<i32> {
+ if basename.contains(MAIN_SEPARATOR) {
+ return Err(new_errno_error(Errno::EINVAL));
+ }
+ self.insert_new_fd(fd, |config| match config {
+ FdConfig::OutputDir(dir) => {
+ let new_fd = openat(
+ dir.as_raw_fd(),
+ basename,
+ // TODO(205172873): handle the case when the file already exist, e.g. truncate
+ // or fail, and possibly allow the client to specify. For now, always truncate.
+ OFlag::O_CREAT | OFlag::O_RDWR | OFlag::O_TRUNC,
+ Mode::S_IRUSR | Mode::S_IWUSR,
+ )
+ .map_err(new_errno_error)?;
+ // SAFETY: new_fd is just created and not an error.
+ let new_file = unsafe { File::from_raw_fd(new_fd) };
+ Ok((new_fd, FdConfig::ReadWrite(new_file)))
+ }
+ _ => Err(new_errno_error(Errno::ENOTDIR)),
+ })
+ }
+
+ fn createDirectoryInDirectory(&self, dir_fd: i32, basename: &str) -> BinderResult<i32> {
+ if basename.contains(MAIN_SEPARATOR) {
+ return Err(new_errno_error(Errno::EINVAL));
+ }
+ self.insert_new_fd(dir_fd, |config| match config {
+ FdConfig::OutputDir(_) => {
+ mkdirat(dir_fd, basename, Mode::S_IRWXU).map_err(new_errno_error)?;
+ let new_dir = Dir::openat(
+ dir_fd,
+ basename,
+ OFlag::O_DIRECTORY | OFlag::O_RDONLY,
+ Mode::empty(),
+ )
+ .map_err(new_errno_error)?;
+ Ok((new_dir.as_raw_fd(), FdConfig::OutputDir(new_dir)))
+ }
+ _ => Err(new_errno_error(Errno::ENOTDIR)),
})
}
}
diff --git a/authfs/fd_server/src/main.rs b/authfs/fd_server/src/main.rs
index 3413ce6..bbcd49f 100644
--- a/authfs/fd_server/src/main.rs
+++ b/authfs/fd_server/src/main.rs
@@ -28,6 +28,7 @@
use anyhow::{bail, Result};
use binder_common::rpc_server::run_rpc_server;
use log::debug;
+use nix::dir::Dir;
use std::collections::BTreeMap;
use std::fs::File;
use std::os::unix::io::FromRawFd;
@@ -77,6 +78,12 @@
Ok((fd, FdConfig::ReadWrite(file)))
}
+fn parse_arg_rw_dirs(arg: &str) -> Result<(i32, FdConfig)> {
+ let fd = arg.parse::<i32>()?;
+
+ Ok((fd, FdConfig::OutputDir(Dir::from_fd(fd)?)))
+}
+
struct Args {
fd_pool: BTreeMap<i32, FdConfig>,
ready_fd: Option<File>,
@@ -93,6 +100,10 @@
.long("rw-fds")
.multiple(true)
.number_of_values(1))
+ .arg(clap::Arg::with_name("rw-dirs")
+ .long("rw-dirs")
+ .multiple(true)
+ .number_of_values(1))
.arg(clap::Arg::with_name("ready-fd")
.long("ready-fd")
.takes_value(true))
@@ -111,6 +122,12 @@
fd_pool.insert(fd, config);
}
}
+ if let Some(args) = matches.values_of("rw-dirs") {
+ for arg in args {
+ let (fd, config) = parse_arg_rw_dirs(arg)?;
+ fd_pool.insert(fd, config);
+ }
+ }
let ready_fd = if let Some(arg) = matches.value_of("ready-fd") {
let fd = arg.parse::<i32>()?;
Some(fd_to_file(fd)?)
diff --git a/authfs/service/src/authfs.rs b/authfs/service/src/authfs.rs
index 6d87243..1b05749 100644
--- a/authfs/service/src/authfs.rs
+++ b/authfs/service/src/authfs.rs
@@ -132,11 +132,11 @@
// TODO(b/185178698): Many input files need to be signed and verified.
// or can we use debug cert for now, which is better than nothing?
args.push(OsString::from("--remote-ro-file-unverified"));
- args.push(OsString::from(format!("{}:{}", conf.fd, conf.fd)));
+ args.push(OsString::from(conf.fd.to_string()));
}
for conf in out_fds {
args.push(OsString::from("--remote-new-rw-file"));
- args.push(OsString::from(format!("{}:{}", conf.fd, conf.fd)));
+ args.push(OsString::from(conf.fd.to_string()));
}
if debuggable {
args.push(OsString::from("--debug"));
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..64ccc41 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, HashMap};
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::path::Path;
+use std::os::unix::{ffi::OsStrExt, io::AsRawFd};
+use std::path::{Path, PathBuf};
+use std::sync::Mutex;
use std::time::Duration;
use fuse::filesystem::{
@@ -35,17 +36,19 @@
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};
-const DEFAULT_METADATA_TIMEOUT: std::time::Duration = Duration::from_secs(5);
-
pub type Inode = u64;
type Handle = u64;
-/// `FileConfig` defines the file type supported by AuthFS.
-pub enum FileConfig {
+const DEFAULT_METADATA_TIMEOUT: Duration = Duration::from_secs(5);
+const ROOT_INODE: Inode = 1;
+
+/// `AuthFsEntry` defines the filesystem entry type supported by AuthFS.
+pub enum AuthFsEntry {
/// A file type that is verified against fs-verity signature (thus read-only). The file is
/// served from a remote server.
VerifiedReadonly {
@@ -57,35 +60,74 @@
/// 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 },
}
+// AuthFS needs to be `Sync` to be accepted by fuse::worker::start_message_loop as a `FileSystem`.
struct AuthFs {
- /// Store `FileConfig`s using the `Inode` number as the search index.
- ///
- /// 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>,
+ /// Table for `Inode` to `AuthFsEntry` lookup. This needs to be `Sync` to be used in
+ /// `fuse::worker::start_message_loop`.
+ inode_table: Mutex<BTreeMap<Inode, AuthFsEntry>>,
- /// Maximum bytes in the write transaction to the FUSE device. This limits the maximum size to
- /// a read request (including FUSE protocol overhead).
+ /// Root directory entry table for path to `Inode` lookup. The root directory content should
+ /// remain constant throughout the filesystem's lifetime.
+ root_entries: HashMap<PathBuf, Inode>,
+
+ /// Maximum bytes in the write transaction to the FUSE device. This limits the maximum buffer
+ /// size in a read request (including FUSE protocol overhead) that the filesystem writes to.
max_write: u32,
}
impl AuthFs {
- pub fn new(file_pool: BTreeMap<Inode, FileConfig>, max_write: u32) -> AuthFs {
- AuthFs { file_pool, max_write }
+ pub fn new(root_entries_by_path: HashMap<PathBuf, AuthFsEntry>, max_write: u32) -> AuthFs {
+ let mut next_inode = ROOT_INODE + 1;
+ let mut inode_table = BTreeMap::new();
+ let mut root_entries = HashMap::new();
+
+ root_entries_by_path.into_iter().for_each(|(path_buf, entry)| {
+ next_inode += 1;
+ root_entries.insert(path_buf, next_inode);
+ inode_table.insert(next_inode, entry);
+ });
+
+ AuthFs { inode_table: Mutex::new(inode_table), root_entries, 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_inode<F, R>(&self, inode: &Inode, handle_fn: F) -> io::Result<R>
where
- F: FnOnce(&FileConfig) -> io::Result<R>,
+ F: FnOnce(&AuthFsEntry) -> io::Result<R>,
{
+ let inode_table = self.inode_table.lock().unwrap();
let config =
- self.file_pool.get(inode).ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
- handler(config)
+ inode_table.get(inode).ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ handle_fn(config)
+ }
+
+ /// Inserts a new inode and corresponding `AuthFsEntry` created by `create_fn` to the inode
+ /// table, then returns the new inode number.
+ fn insert_new_inode<F>(&self, inode: &Inode, create_fn: F) -> io::Result<Inode>
+ where
+ F: FnOnce(&mut AuthFsEntry) -> io::Result<(Inode, AuthFsEntry)>,
+ {
+ let mut inode_table = self.inode_table.lock().unwrap();
+ let mut config =
+ inode_table.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) = inode_table.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 +147,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 +180,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 +267,59 @@
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> {
+ if parent == ROOT_INODE {
+ let inode = *self
+ .root_entries
+ .get(cstr_to_path(name))
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ // Normally, `lookup` is required to increase a reference count for the inode (while
+ // `forget` will decrease it). It is not yet necessary until we start to support
+ // deletion (only for `VerifiedNewDirectory`).
+ let st = self.handle_inode(&inode, |config| match config {
+ AuthFsEntry::UnverifiedReadonly { file_size, .. }
+ | AuthFsEntry::VerifiedReadonly { file_size, .. } => {
+ create_stat(inode, *file_size, AccessMode::ReadOnly)
+ }
+ AuthFsEntry::VerifiedNew { editor } => {
+ create_stat(inode, editor.size(), AccessMode::ReadWrite)
+ }
+ AuthFsEntry::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_inode(&parent, |config| match config {
+ AuthFsEntry::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_inode(&inode, |config| match config {
+ AuthFsEntry::VerifiedNew { editor } => {
+ create_stat(inode, editor.size(), AccessMode::ReadWrite)
+ }
+ AuthFsEntry::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(
@@ -228,15 +328,18 @@
inode: Inode,
_handle: Option<Handle>,
) -> io::Result<(libc::stat64, Duration)> {
- self.handle_file(&inode, |config| {
+ self.handle_inode(&inode, |config| {
Ok((
match config {
- FileConfig::UnverifiedReadonly { file_size, .. }
- | FileConfig::VerifiedReadonly { file_size, .. } => {
- create_stat(inode, *file_size, FileMode::ReadOnly)?
+ AuthFsEntry::UnverifiedReadonly { file_size, .. }
+ | AuthFsEntry::VerifiedReadonly { file_size, .. } => {
+ create_stat(inode, *file_size, AccessMode::ReadOnly)?
}
- FileConfig::VerifiedNew { editor } => {
- create_stat(inode, editor.size(), FileMode::ReadWrite)?
+ AuthFsEntry::VerifiedNew { editor } => {
+ create_stat(inode, editor.size(), AccessMode::ReadWrite)?
+ }
+ AuthFsEntry::VerifiedNewDirectory { dir } => {
+ create_dir_stat(inode, dir.number_of_entries())?
}
},
DEFAULT_METADATA_TIMEOUT,
@@ -252,22 +355,63 @@
) -> io::Result<(Option<Self::Handle>, fuse::sys::OpenOptions)> {
// Since file handle is not really used in later operations (which use Inode directly),
// return None as the handle.
- self.handle_file(&inode, |config| {
+ self.handle_inode(&inode, |config| {
match config {
- FileConfig::VerifiedReadonly { .. } | FileConfig::UnverifiedReadonly { .. } => {
+ AuthFsEntry::VerifiedReadonly { .. } | AuthFsEntry::UnverifiedReadonly { .. } => {
check_access_mode(flags, libc::O_RDONLY)?;
}
- FileConfig::VerifiedNew { .. } => {
+ AuthFsEntry::VerifiedNew { .. } => {
// No need to check access modes since all the modes are allowed to the
// read-writable file.
}
+ AuthFsEntry::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 {
+ AuthFsEntry::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, AuthFsEntry::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,
@@ -279,19 +423,20 @@
_lock_owner: Option<u64>,
_flags: u32,
) -> io::Result<usize> {
- self.handle_file(&inode, |config| {
+ self.handle_inode(&inode, |config| {
match config {
- FileConfig::VerifiedReadonly { reader, file_size } => {
+ AuthFsEntry::VerifiedReadonly { reader, file_size } => {
read_chunks(w, reader, *file_size, offset, size)
}
- FileConfig::UnverifiedReadonly { reader, file_size } => {
+ AuthFsEntry::UnverifiedReadonly { reader, file_size } => {
read_chunks(w, reader, *file_size, offset, size)
}
- FileConfig::VerifiedNew { editor } => {
+ AuthFsEntry::VerifiedNew { editor } => {
// Note that with FsOptions::WRITEBACK_CACHE, it's possible for the kernel to
// 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)),
}
})
}
@@ -308,8 +453,8 @@
_delayed_write: bool,
_flags: u32,
) -> io::Result<usize> {
- self.handle_file(&inode, |config| match config {
- FileConfig::VerifiedNew { editor } => {
+ self.handle_inode(&inode, |config| match config {
+ AuthFsEntry::VerifiedNew { editor } => {
let mut buf = vec![0; size as usize];
r.read_exact(&mut buf)?;
editor.write_at(&buf, offset)
@@ -326,11 +471,11 @@
_handle: Option<Handle>,
valid: SetattrValid,
) -> io::Result<(libc::stat64, Duration)> {
- self.handle_file(&inode, |config| {
+ self.handle_inode(&inode, |config| {
match config {
- FileConfig::VerifiedNew { editor } => {
+ AuthFsEntry::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
@@ -381,9 +526,9 @@
name: &CStr,
size: u32,
) -> io::Result<GetxattrReply> {
- self.handle_file(&inode, |config| {
+ self.handle_inode(&inode, |config| {
match config {
- FileConfig::VerifiedNew { editor } => {
+ AuthFsEntry::VerifiedNew { editor } => {
// FUSE ioctl is limited, thus we can't implement fs-verity ioctls without a kernel
// change (see b/196635431). Until it's possible, use xattr to expose what we need
// as an authfs specific API.
@@ -407,11 +552,41 @@
}
})
}
+
+ 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 {
+ AuthFsEntry::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, AuthFsEntry::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.
pub fn loop_forever(
- file_pool: BTreeMap<Inode, FileConfig>,
+ root_entries: HashMap<PathBuf, AuthFsEntry>,
mountpoint: &Path,
extra_options: &Option<String>,
) -> Result<(), fuse::Error> {
@@ -442,6 +617,10 @@
dev_fuse,
max_write,
max_read,
- AuthFs::new(file_pool, max_write),
+ AuthFs::new(root_entries, 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..f6a2a56 100644
--- a/authfs/src/main.rs
+++ b/authfs/src/main.rs
@@ -29,7 +29,7 @@
use anyhow::{bail, Context, Result};
use log::error;
-use std::collections::BTreeMap;
+use std::collections::HashMap;
use std::convert::TryInto;
use std::path::PathBuf;
use structopt::StructOpt;
@@ -42,9 +42,9 @@
mod fusefs;
use auth::FakeAuthenticator;
-use file::{RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader};
+use file::{RemoteDirEditor, RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader};
use fsverity::{VerifiedFileEditor, VerifiedFileReader};
-use fusefs::{FileConfig, Inode};
+use fusefs::AuthFsEntry;
#[derive(StructOpt)]
struct Args {
@@ -62,24 +62,33 @@
/// A read-only remote file with integrity check. Can be multiple.
///
- /// For example, `--remote-verified-file 5:10:/path/to/cert` tells the filesystem to associate
- /// entry 5 with a remote file 10, and need to be verified against the /path/to/cert.
+ /// For example, `--remote-ro-file 5:/path/to/cert` tells the filesystem to associate the
+ /// file $MOUNTPOINT/5 with a remote FD 5, and need to be verified against the /path/to/cert.
#[structopt(long, parse(try_from_str = parse_remote_ro_file_option))]
remote_ro_file: Vec<OptionRemoteRoFile>,
/// A read-only remote file without integrity check. Can be multiple.
///
- /// For example, `--remote-unverified-file 5:10` tells the filesystem to associate entry 5
- /// with a remote file 10.
- #[structopt(long, parse(try_from_str = parse_remote_ro_file_unverified_option))]
- remote_ro_file_unverified: Vec<OptionRemoteRoFileUnverified>,
+ /// For example, `--remote-ro-file-unverified 5` tells the filesystem to associate the file
+ /// $MOUNTPOINT/5 with a remote FD 5.
+ #[structopt(long)]
+ remote_ro_file_unverified: Vec<i32>,
/// A new read-writable remote file with integrity check. Can be multiple.
///
- /// For example, `--remote-new-verified-file 12:34` tells the filesystem to associate entry 12
- /// with a remote file 34.
- #[structopt(long, parse(try_from_str = parse_remote_new_rw_file_option))]
- remote_new_rw_file: Vec<OptionRemoteRwFile>,
+ /// For example, `--remote-new-rw-file 5` tells the filesystem to associate the file
+ /// $MOUNTPOINT/5 with a remote FD 5.
+ #[structopt(long)]
+ remote_new_rw_file: Vec<i32>,
+
+ /// 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-rw-dir 5` tells the filesystem to associate $MOUNTPOINT/5
+ /// with a remote dir FD 5.
+ #[structopt(long)]
+ remote_new_rw_dir: Vec<i32>,
/// Enable debugging features.
#[structopt(long)]
@@ -87,136 +96,119 @@
}
struct OptionRemoteRoFile {
- ino: Inode,
-
/// ID to refer to the remote file.
- remote_id: i32,
+ remote_fd: i32,
/// Certificate to verify the authenticity of the file's fs-verity signature.
/// TODO(170494765): Implement PKCS#7 signature verification.
_certificate_path: PathBuf,
}
-struct OptionRemoteRoFileUnverified {
- ino: Inode,
-
- /// ID to refer to the remote file.
- remote_id: i32,
-}
-
-struct OptionRemoteRwFile {
- ino: Inode,
-
- /// ID to refer to the remote file.
- remote_id: i32,
-}
-
fn parse_remote_ro_file_option(option: &str) -> Result<OptionRemoteRoFile> {
let strs: Vec<&str> = option.split(':').collect();
- if strs.len() != 3 {
+ if strs.len() != 2 {
bail!("Invalid option: {}", option);
}
Ok(OptionRemoteRoFile {
- ino: strs[0].parse::<Inode>()?,
- remote_id: strs[1].parse::<i32>()?,
- _certificate_path: PathBuf::from(strs[2]),
+ remote_fd: strs[0].parse::<i32>()?,
+ _certificate_path: PathBuf::from(strs[1]),
})
}
-fn parse_remote_ro_file_unverified_option(option: &str) -> Result<OptionRemoteRoFileUnverified> {
- let strs: Vec<&str> = option.split(':').collect();
- if strs.len() != 2 {
- bail!("Invalid option: {}", option);
- }
- Ok(OptionRemoteRoFileUnverified {
- ino: strs[0].parse::<Inode>()?,
- remote_id: strs[1].parse::<i32>()?,
- })
-}
-
-fn parse_remote_new_rw_file_option(option: &str) -> Result<OptionRemoteRwFile> {
- let strs: Vec<&str> = option.split(':').collect();
- if strs.len() != 2 {
- bail!("Invalid option: {}", option);
- }
- Ok(OptionRemoteRwFile {
- ino: strs[0].parse::<Inode>().unwrap(),
- remote_id: strs[1].parse::<i32>().unwrap(),
- })
-}
-
-fn new_config_remote_verified_file(
+fn new_remote_verified_file_entry(
service: file::VirtFdService,
- remote_id: i32,
+ remote_fd: i32,
file_size: u64,
-) -> Result<FileConfig> {
- let signature = service.readFsveritySignature(remote_id).context("Failed to read signature")?;
+) -> Result<AuthFsEntry> {
+ let signature = service.readFsveritySignature(remote_fd).context("Failed to read signature")?;
let authenticator = FakeAuthenticator::always_succeed();
- Ok(FileConfig::VerifiedReadonly {
+ Ok(AuthFsEntry::VerifiedReadonly {
reader: VerifiedFileReader::new(
&authenticator,
- RemoteFileReader::new(service.clone(), remote_id),
+ RemoteFileReader::new(service.clone(), remote_fd),
file_size,
signature,
- RemoteMerkleTreeReader::new(service.clone(), remote_id),
+ RemoteMerkleTreeReader::new(service.clone(), remote_fd),
)?,
file_size,
})
}
-fn new_config_remote_unverified_file(
+fn new_remote_unverified_file_entry(
service: file::VirtFdService,
- remote_id: i32,
+ remote_fd: i32,
file_size: u64,
-) -> Result<FileConfig> {
- let reader = RemoteFileReader::new(service, remote_id);
- Ok(FileConfig::UnverifiedReadonly { reader, file_size })
+) -> Result<AuthFsEntry> {
+ let reader = RemoteFileReader::new(service, remote_fd);
+ Ok(AuthFsEntry::UnverifiedReadonly { reader, file_size })
}
-fn new_config_remote_new_verified_file(
+fn new_remote_new_verified_file_entry(
service: file::VirtFdService,
- remote_id: i32,
-) -> Result<FileConfig> {
- let remote_file = RemoteFileEditor::new(service, remote_id);
- Ok(FileConfig::VerifiedNew { editor: VerifiedFileEditor::new(remote_file) })
+ remote_fd: i32,
+) -> Result<AuthFsEntry> {
+ let remote_file = RemoteFileEditor::new(service, remote_fd);
+ Ok(AuthFsEntry::VerifiedNew { editor: VerifiedFileEditor::new(remote_file) })
}
-fn prepare_file_pool(args: &Args) -> Result<BTreeMap<Inode, FileConfig>> {
- let mut file_pool = BTreeMap::new();
+fn new_remote_new_verified_dir_entry(
+ service: file::VirtFdService,
+ remote_fd: i32,
+) -> Result<AuthFsEntry> {
+ let dir = RemoteDirEditor::new(service, remote_fd);
+ Ok(AuthFsEntry::VerifiedNewDirectory { dir })
+}
+
+fn prepare_root_dir_entries(args: &Args) -> Result<HashMap<PathBuf, AuthFsEntry>> {
+ let mut root_entries = HashMap::new();
let service = file::get_rpc_binder_service(args.cid)?;
for config in &args.remote_ro_file {
- file_pool.insert(
- config.ino,
- new_config_remote_verified_file(
+ root_entries.insert(
+ remote_fd_to_path_buf(config.remote_fd),
+ new_remote_verified_file_entry(
service.clone(),
- config.remote_id,
- service.getFileSize(config.remote_id)?.try_into()?,
+ config.remote_fd,
+ service.getFileSize(config.remote_fd)?.try_into()?,
)?,
);
}
- for config in &args.remote_ro_file_unverified {
- file_pool.insert(
- config.ino,
- new_config_remote_unverified_file(
+ for remote_fd in &args.remote_ro_file_unverified {
+ let remote_fd = *remote_fd;
+ root_entries.insert(
+ remote_fd_to_path_buf(remote_fd),
+ new_remote_unverified_file_entry(
service.clone(),
- config.remote_id,
- service.getFileSize(config.remote_id)?.try_into()?,
+ remote_fd,
+ service.getFileSize(remote_fd)?.try_into()?,
)?,
);
}
- for config in &args.remote_new_rw_file {
- file_pool.insert(
- config.ino,
- new_config_remote_new_verified_file(service.clone(), config.remote_id)?,
+ for remote_fd in &args.remote_new_rw_file {
+ let remote_fd = *remote_fd;
+ root_entries.insert(
+ remote_fd_to_path_buf(remote_fd),
+ new_remote_new_verified_file_entry(service.clone(), remote_fd)?,
);
}
- Ok(file_pool)
+ for remote_fd in &args.remote_new_rw_dir {
+ let remote_fd = *remote_fd;
+ root_entries.insert(
+ remote_fd_to_path_buf(remote_fd),
+ new_remote_new_verified_dir_entry(service.clone(), remote_fd)?,
+ );
+ }
+
+ Ok(root_entries)
+}
+
+fn remote_fd_to_path_buf(fd: i32) -> PathBuf {
+ PathBuf::from(fd.to_string())
}
fn try_main() -> Result<()> {
@@ -227,8 +219,8 @@
android_logger::Config::default().with_tag("authfs").with_min_level(log_level),
);
- let file_pool = prepare_file_pool(&args)?;
- fusefs::loop_forever(file_pool, &args.mount_point, &args.extra_options)?;
+ let root_entries = prepare_root_dir_entries(&args)?;
+ fusefs::loop_forever(root_entries, &args.mount_point, &args.extra_options)?;
bail!("Unexpected exit after the handler loop")
}
diff --git a/authfs/tests/AndroidTest.xml b/authfs/tests/AndroidTest.xml
index 9deab5b..643e2b4 100644
--- a/authfs/tests/AndroidTest.xml
+++ b/authfs/tests/AndroidTest.xml
@@ -23,7 +23,7 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="throw-if-cmd-fail" value="true" />
- <!-- Prepare test directory. -->
+ <!-- Prepare test directories. -->
<option name="run-command" value="mkdir -p /data/local/tmp/authfs/mnt" />
<option name="teardown-command" value="rm -rf /data/local/tmp/authfs" />
</target_preparer>
@@ -33,7 +33,7 @@
<option name="abort-on-push-failure" value="true" />
<!-- Test executable -->
- <option name="push-file" key="open_then_run" value="/data/local/tmp/authfs/open_then_run" />
+ <option name="push-file" key="open_then_run" value="/data/local/tmp/open_then_run" />
<!-- Test data files -->
<option name="push-file" key="cert.der" value="/data/local/tmp/authfs/cert.der" />
diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
index 3ed8748..8a13ef3 100644
--- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java
@@ -34,8 +34,8 @@
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.AfterClassWithInfo;
import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
-import com.android.tradefed.util.AbiUtils;
import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
import org.junit.After;
import org.junit.AssumptionViolatedException;
@@ -54,11 +54,14 @@
/** Test directory on Android where data are located */
private static final String TEST_DIR = "/data/local/tmp/authfs";
- /** Mount point of authfs on Microdroid during the test */
- private static final String MOUNT_DIR = "/data/local/tmp";
+ /** Output directory where the test can generate output on Android */
+ private static final String TEST_OUTPUT_DIR = "/data/local/tmp/authfs/output_dir";
/** Path to open_then_run on Android */
- private static final String OPEN_THEN_RUN_BIN = TEST_DIR + "/open_then_run";
+ private static final String OPEN_THEN_RUN_BIN = "/data/local/tmp/open_then_run";
+
+ /** Mount point of authfs on Microdroid during the test */
+ private static final String MOUNT_DIR = "/data/local/tmp";
/** Path to fd_server on Android */
private static final String FD_SERVER_BIN = "/apex/com.android.virt/bin/fd_server";
@@ -79,7 +82,6 @@
private static boolean sAssumptionFailed;
private ExecutorService mThreadPool = Executors.newCachedThreadPool();
- private String mArch;
@BeforeClassWithInfo
public static void beforeClassWithDevice(TestInformation testInfo)
@@ -141,23 +143,22 @@
}
@Before
- public void setUp() {
+ public void setUp() throws Exception {
assumeFalse(sAssumptionFailed);
- mArch = AbiUtils.getArchForAbi(getAbi().getName());
+ sAndroid.run("mkdir " + TEST_OUTPUT_DIR);
}
@After
- public void tearDown() throws DeviceNotAvailableException {
+ public void tearDown() throws Exception {
sAndroid.tryRun("killall fd_server");
- sAndroid.tryRun("rm -f " + TEST_DIR + "/output");
+ sAndroid.run("rm -rf " + TEST_OUTPUT_DIR);
tryRunOnMicrodroid("killall authfs");
tryRunOnMicrodroid("umount " + MOUNT_DIR);
}
@Test
- public void testReadWithFsverityVerification_RemoteFile()
- throws DeviceNotAvailableException, InterruptedException {
+ public void testReadWithFsverityVerification_RemoteFile() throws Exception {
// Setup
runFdServerOnAndroid(
"--open-ro 3:input.4m --open-ro 4:input.4m.merkle_dump --open-ro 5:input.4m.fsv_sig"
@@ -165,25 +166,24 @@
"--ro-fds 3:4:5 --ro-fds 6");
runAuthFsOnMicrodroid(
- "--remote-ro-file-unverified 10:6 --remote-ro-file 11:3:cert.der --cid "
+ "--remote-ro-file-unverified 6 --remote-ro-file 3:cert.der --cid "
+ VMADDR_CID_HOST);
// Action
- String actualHashUnverified4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
- String actualHash4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/11");
+ String actualHashUnverified4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/6");
+ String actualHash4m = computeFileHashOnMicrodroid(MOUNT_DIR + "/3");
// Verify
String expectedHash4m = computeFileHashOnAndroid(TEST_DIR + "/input.4m");
- assertEquals("Inconsistent hash from /authfs/10: ", expectedHash4m, actualHashUnverified4m);
- assertEquals("Inconsistent hash from /authfs/11: ", expectedHash4m, actualHash4m);
+ assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4m, actualHashUnverified4m);
+ assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4m, actualHash4m);
}
// Separate the test from the above simply because exec in shell does not allow open too many
// files.
@Test
- public void testReadWithFsverityVerification_RemoteSmallerFile()
- throws DeviceNotAvailableException, InterruptedException {
+ public void testReadWithFsverityVerification_RemoteSmallerFile() throws Exception {
// Setup
runFdServerOnAndroid(
"--open-ro 3:input.4k --open-ro 4:input.4k.merkle_dump --open-ro"
@@ -191,46 +191,43 @@
+ " --open-ro 8:input.4k1.fsv_sig",
"--ro-fds 3:4:5 --ro-fds 6:7:8");
runAuthFsOnMicrodroid(
- "--remote-ro-file 10:3:cert.der --remote-ro-file 11:6:cert.der --cid "
- + VMADDR_CID_HOST);
+ "--remote-ro-file 3:cert.der --remote-ro-file 6:cert.der --cid " + VMADDR_CID_HOST);
// Action
- String actualHash4k = computeFileHashOnMicrodroid(MOUNT_DIR + "/10");
- String actualHash4k1 = computeFileHashOnMicrodroid(MOUNT_DIR + "/11");
+ String actualHash4k = computeFileHashOnMicrodroid(MOUNT_DIR + "/3");
+ String actualHash4k1 = computeFileHashOnMicrodroid(MOUNT_DIR + "/6");
// Verify
String expectedHash4k = computeFileHashOnAndroid(TEST_DIR + "/input.4k");
String expectedHash4k1 = computeFileHashOnAndroid(TEST_DIR + "/input.4k1");
- assertEquals("Inconsistent hash from /authfs/10: ", expectedHash4k, actualHash4k);
- assertEquals("Inconsistent hash from /authfs/11: ", expectedHash4k1, actualHash4k1);
+ assertEquals("Inconsistent hash from /authfs/3: ", expectedHash4k, actualHash4k);
+ assertEquals("Inconsistent hash from /authfs/6: ", expectedHash4k1, actualHash4k1);
}
@Test
- public void testReadWithFsverityVerification_TamperedMerkleTree()
- throws DeviceNotAvailableException, InterruptedException {
+ public void testReadWithFsverityVerification_TamperedMerkleTree() throws Exception {
// Setup
runFdServerOnAndroid(
"--open-ro 3:input.4m --open-ro 4:input.4m.merkle_dump.bad "
+ "--open-ro 5:input.4m.fsv_sig",
"--ro-fds 3:4:5");
- runAuthFsOnMicrodroid("--remote-ro-file 10:3:cert.der --cid " + VMADDR_CID_HOST);
+ runAuthFsOnMicrodroid("--remote-ro-file 3:cert.der --cid " + VMADDR_CID_HOST);
// Verify
- assertFalse(copyFileOnMicrodroid(MOUNT_DIR + "/10", "/dev/null"));
+ assertFalse(copyFileOnMicrodroid(MOUNT_DIR + "/3", "/dev/null"));
}
@Test
- public void testWriteThroughCorrectly()
- throws DeviceNotAvailableException, InterruptedException {
+ public void testWriteThroughCorrectly() throws Exception {
// Setup
- runFdServerOnAndroid("--open-rw 3:output", "--rw-fds 3");
- runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid " + VMADDR_CID_HOST);
+ runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3");
+ runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST);
// Action
String srcPath = "/system/bin/linker64";
- String destPath = MOUNT_DIR + "/20";
- String backendPath = TEST_DIR + "/output";
+ String destPath = MOUNT_DIR + "/3";
+ String backendPath = TEST_OUTPUT_DIR + "/out.file";
assertTrue(copyFileOnMicrodroid(srcPath, destPath));
// Verify
@@ -239,15 +236,14 @@
}
@Test
- public void testWriteFailedIfDetectsTampering()
- throws DeviceNotAvailableException, InterruptedException {
+ public void testWriteFailedIfDetectsTampering() throws Exception {
// Setup
- runFdServerOnAndroid("--open-rw 3:output", "--rw-fds 3");
- runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid " + VMADDR_CID_HOST);
+ runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3");
+ runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST);
String srcPath = "/system/bin/linker64";
- String destPath = MOUNT_DIR + "/20";
- String backendPath = TEST_DIR + "/output";
+ String destPath = MOUNT_DIR + "/3";
+ String backendPath = TEST_OUTPUT_DIR + "/out.file";
assertTrue(copyFileOnMicrodroid(srcPath, destPath));
// Action
@@ -258,28 +254,32 @@
// Write to a block partially requires a read back to calculate the new hash. It should fail
// when the content is inconsistent to the known hash. Use direct I/O to avoid simply
// writing to the filesystem cache.
- assertEquals(
- tryRunOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=1024 direct"),
- null);
+ assertFalse(
+ writeZerosAtFileOffsetOnMicrodroid(
+ destPath, /* offset */ 0, /* number */ 1024, /* writeThrough */ true));
// A full 4K write does not require to read back, so write can succeed even if the backing
// block has already been tampered.
- runOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=4096 skip=4096");
+ assertTrue(
+ writeZerosAtFileOffsetOnMicrodroid(
+ destPath, /* offset */ 4096, /* number */ 4096, /* writeThrough */ false));
// Otherwise, a partial write with correct backing file should still succeed.
- runOnMicrodroid("dd if=/dev/zero of=" + destPath + " bs=1 count=1024 skip=8192");
+ assertTrue(
+ writeZerosAtFileOffsetOnMicrodroid(
+ destPath, /* offset */ 8192, /* number */ 1024, /* writeThrough */ false));
}
@Test
- public void testFileResize() throws DeviceNotAvailableException, InterruptedException {
+ public void testFileResize() throws Exception {
// Setup
- runFdServerOnAndroid("--open-rw 3:output", "--rw-fds 3");
- runAuthFsOnMicrodroid("--remote-new-rw-file 20:3 --cid " + VMADDR_CID_HOST);
- String outputPath = MOUNT_DIR + "/20";
- String backendPath = TEST_DIR + "/output";
+ runFdServerOnAndroid("--open-rw 3:" + TEST_OUTPUT_DIR + "/out.file", "--rw-fds 3");
+ runAuthFsOnMicrodroid("--remote-new-rw-file 3 --cid " + VMADDR_CID_HOST);
+ String outputPath = MOUNT_DIR + "/3";
+ String backendPath = TEST_OUTPUT_DIR + "/out.file";
// Action & Verify
- runOnMicrodroid("yes $'\\x01' | tr -d '\\n' | dd bs=1 count=10000 of=" + outputPath);
+ createFileWithOnesOnMicrodroid(outputPath, 10000);
assertEquals(getFileSizeInBytesOnMicrodroid(outputPath), 10000);
expectBackingFileConsistency(
outputPath,
@@ -301,6 +301,112 @@
"e53130831c13dabff71d5d1797e3aaa467b4b7d32b3b8782c4ff03d76976f2aa");
}
+ @Test
+ public void testOutputDirectory_WriteNewFiles() throws Exception {
+ // Setup
+ String androidOutputDir = TEST_OUTPUT_DIR + "/dir";
+ String authfsOutputDir = MOUNT_DIR + "/3";
+ sAndroid.run("mkdir " + androidOutputDir);
+ runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
+ runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
+
+ // Action & Verify
+ // Can create a new file to write.
+ String expectedAndroidPath = androidOutputDir + "/file";
+ String authfsPath = authfsOutputDir + "/file";
+ createFileWithOnesOnMicrodroid(authfsPath, 10000);
+ assertEquals(getFileSizeInBytesOnMicrodroid(authfsPath), 10000);
+ expectBackingFileConsistency(
+ authfsPath,
+ expectedAndroidPath,
+ "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
+
+ // Regular file operations work, e.g. resize.
+ resizeFileOnMicrodroid(authfsPath, 15000);
+ assertEquals(getFileSizeInBytesOnMicrodroid(authfsPath), 15000);
+ expectBackingFileConsistency(
+ authfsPath,
+ expectedAndroidPath,
+ "567c89f62586e0d33369157afdfe99a2fa36cdffb01e91dcdc0b7355262d610d");
+ }
+
+ @Test
+ public void testOutputDirectory_MkdirAndWriteFile() throws Exception {
+ // Setup
+ String androidOutputDir = TEST_OUTPUT_DIR + "/dir";
+ String authfsOutputDir = MOUNT_DIR + "/3";
+ sAndroid.run("mkdir " + androidOutputDir);
+ runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
+ runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
+
+ // Action
+ // Can create nested directories and can create a file in one.
+ runOnMicrodroid("mkdir " + authfsOutputDir + "/new_dir");
+ runOnMicrodroid("mkdir -p " + authfsOutputDir + "/we/need/to/go/deeper");
+ createFileWithOnesOnMicrodroid(authfsOutputDir + "/new_dir/file1", 10000);
+ createFileWithOnesOnMicrodroid(authfsOutputDir + "/we/need/file2", 10000);
+
+ // Verify
+ // Directories show up in Android.
+ sAndroid.run("test -d " + androidOutputDir + "/new_dir");
+ sAndroid.run("test -d " + androidOutputDir + "/we/need/to/go/deeper");
+ // Files exist in Android. Hashes on Microdroid and Android are consistent.
+ assertEquals(getFileSizeInBytesOnMicrodroid(authfsOutputDir + "/new_dir/file1"), 10000);
+ expectBackingFileConsistency(
+ authfsOutputDir + "/new_dir/file1",
+ androidOutputDir + "/new_dir/file1",
+ "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
+ // Same to file in a nested directory.
+ assertEquals(getFileSizeInBytesOnMicrodroid(authfsOutputDir + "/we/need/file2"), 10000);
+ expectBackingFileConsistency(
+ authfsOutputDir + "/we/need/file2",
+ androidOutputDir + "/we/need/file2",
+ "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
+ }
+
+ @Test
+ public void testOutputDirectory_CreateAndTruncateExistingFile() throws Exception {
+ // Setup
+ String androidOutputDir = TEST_OUTPUT_DIR + "/dir";
+ String authfsOutputDir = MOUNT_DIR + "/3";
+ sAndroid.run("mkdir " + androidOutputDir);
+ runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
+ runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
+
+ // Action & Verify
+ runOnMicrodroid("echo -n foo > " + authfsOutputDir + "/file");
+ assertEquals(getFileSizeInBytesOnMicrodroid(authfsOutputDir + "/file"), 3);
+ // Can override a file and write normally.
+ createFileWithOnesOnMicrodroid(authfsOutputDir + "/file", 10000);
+ assertEquals(getFileSizeInBytesOnMicrodroid(authfsOutputDir + "/file"), 10000);
+ expectBackingFileConsistency(
+ authfsOutputDir + "/file",
+ androidOutputDir + "/file",
+ "684ad25fdc2bbb80cbc910dd1bde6d5499ccf860ca6ee44704b77ec445271353");
+ }
+
+ @Test
+ public void testOutputDirectory_CannotRecreateDirectoryIfNameExists() throws Exception {
+ // Setup
+ String androidOutputDir = TEST_OUTPUT_DIR + "/dir";
+ String authfsOutputDir = MOUNT_DIR + "/3";
+ sAndroid.run("mkdir " + androidOutputDir);
+ runFdServerOnAndroid("--open-dir 3:" + androidOutputDir, "--rw-dirs 3");
+ runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST);
+
+ runOnMicrodroid("touch " + authfsOutputDir + "/some_file");
+ runOnMicrodroid("mkdir " + authfsOutputDir + "/some_dir");
+ runOnMicrodroid("touch " + authfsOutputDir + "/some_dir/file");
+ runOnMicrodroid("mkdir " + authfsOutputDir + "/some_dir/dir");
+
+ // Action & Verify
+ // Cannot create directory if an entry with the same name already exists.
+ assertFailedOnMicrodroid("mkdir " + authfsOutputDir + "/some_file");
+ assertFailedOnMicrodroid("mkdir " + authfsOutputDir + "/some_dir");
+ assertFailedOnMicrodroid("mkdir " + authfsOutputDir + "/some_dir/file");
+ assertFailedOnMicrodroid("mkdir " + authfsOutputDir + "/some_dir/dir");
+ }
+
private void expectBackingFileConsistency(
String authFsPath, String backendPath, String expectedHash)
throws DeviceNotAvailableException {
@@ -350,6 +456,24 @@
return Long.parseLong(runOnMicrodroid("stat -c '%s' " + path));
}
+ private void createFileWithOnesOnMicrodroid(String filePath, long numberOfOnes) {
+ runOnMicrodroid(
+ "yes $'\\x01' | tr -d '\\n' | dd bs=1 count=" + numberOfOnes + " of=" + filePath);
+ }
+
+ private boolean writeZerosAtFileOffsetOnMicrodroid(
+ String filePath, long offset, long numberOfZeros, boolean writeThrough) {
+ String cmd = "dd if=/dev/zero of=" + filePath + " bs=1 count=" + numberOfZeros;
+ if (offset > 0) {
+ cmd += " skip=" + offset;
+ }
+ if (writeThrough) {
+ cmd += " direct";
+ }
+ CommandResult result = runOnMicrodroidForResult(cmd);
+ return result.getStatus() == CommandStatus.SUCCESS;
+ }
+
private void runAuthFsOnMicrodroid(String flags) {
String cmd = AUTHFS_BIN + " " + MOUNT_DIR + " " + flags;
diff --git a/authfs/tests/open_then_run.rs b/authfs/tests/open_then_run.rs
index 3e6ae71..fca8953 100644
--- a/authfs/tests/open_then_run.rs
+++ b/authfs/tests/open_then_run.rs
@@ -122,8 +122,13 @@
// them for the provided program, and are not supposed to do anything else.
OpenOptions::new()
.custom_flags(libc::O_PATH | libc::O_DIRECTORY)
+ // The custom flags above is not taken into consideration by the unix implementation of
+ // OpenOptions for flag validation. So even though the man page of open(2) says that
+ // most flags include access mode are ignored, we still need to set a "valid" mode to
+ // make the library happy. The value does not appear to matter elsewhere in the library.
+ .read(true)
.open(path)
- .with_context(|| format!("Open {} directory", path))
+ .with_context(|| format!("Open {} directory as path", path))
})?;
let cmdline_args: Vec<_> = matches.values_of("args").unwrap().map(|s| s.to_string()).collect();
diff --git a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
index b3a76ce..8d9a7e3 100644
--- a/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
+++ b/tests/hostside/helper/java/android/virt/test/VirtualizationTestCaseBase.java
@@ -152,6 +152,12 @@
.runTimedCmd(timeout, "adb", "-s", MICRODROID_SERIAL, "shell", join(cmd));
}
+ // Asserts the command will fail on Microdroid.
+ public static void assertFailedOnMicrodroid(String... cmd) {
+ CommandResult result = runOnMicrodroidForResult(cmd);
+ assertThat(result.getStatus(), is(CommandStatus.FAILED));
+ }
+
private static String join(String... strs) {
return String.join(" ", Arrays.asList(strs));
}