Re-organize authfs directories
* authfs/fd_server -> android/fd_server
* authfs/service -> guest/authfs_service
* authfs -> guest/authfs
* authfs/aidl -> libs/authfs_aidl_interface
* authfs/tests -> tests/authfs
* authfs/testdata -> tests/authfs/testdata
Bug: 352458998
Test: pass TH
Change-Id: I5962d2fafc9f05b240068740ee1b6369406eb1f5
diff --git a/guest/authfs/Android.bp b/guest/authfs/Android.bp
new file mode 100644
index 0000000..b11da3d
--- /dev/null
+++ b/guest/authfs/Android.bp
@@ -0,0 +1,52 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+ name: "authfs_defaults",
+ crate_name: "authfs",
+ edition: "2021",
+ srcs: [":authfs_src"],
+ rustlibs: [
+ "authfs_aidl_interface-rust",
+ "libandroid_logger",
+ "libanyhow",
+ "libauthfs_fsverity_metadata",
+ "libbinder_rs",
+ "libcfg_if",
+ "libclap",
+ "libfsverity_digests_proto_rust",
+ "libfuse_rust",
+ "libhex",
+ "liblibc",
+ "liblog_rust",
+ "libnix",
+ "libopenssl",
+ "libprotobuf",
+ "librpcbinder_rs",
+ "libthiserror",
+ ],
+ prefer_rlib: true,
+ target: {
+ darwin: {
+ enabled: false,
+ },
+ },
+ defaults: [
+ "crosvm_defaults",
+ "avf_build_flags_rust",
+ ],
+}
+
+filegroup {
+ name: "authfs_src",
+ srcs: [
+ "src/main.rs",
+ ],
+}
+
+rust_binary {
+ name: "authfs",
+ defaults: ["authfs_defaults"],
+ apex_available: ["com.android.virt"],
+}
diff --git a/guest/authfs/TEST_MAPPING b/guest/authfs/TEST_MAPPING
new file mode 100644
index 0000000..62bc18f
--- /dev/null
+++ b/guest/authfs/TEST_MAPPING
@@ -0,0 +1,23 @@
+// When adding or removing tests here, don't forget to amend _all_modules list in
+// wireless/android/busytown/ath_config/configs/prod/avf/tests.gcl
+{
+ "avf-presubmit": [
+ {
+ "name": "authfs_device_test_src_lib"
+ },
+ {
+ "name": "fd_server.test"
+ },
+ {
+ "name": "open_then_run.test"
+ },
+ {
+ "name": "AuthFsHostTest"
+ }
+ ],
+ "avf-postsubmit": [
+ {
+ "name": "AuthFsBenchmarks"
+ }
+ ]
+}
diff --git a/guest/authfs/src/common.rs b/guest/authfs/src/common.rs
new file mode 100644
index 0000000..6556fde
--- /dev/null
+++ b/guest/authfs/src/common.rs
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+/// Common block and page size in Linux.
+pub const CHUNK_SIZE: u64 = 4096;
+
+pub fn divide_roundup(dividend: u64, divisor: u64) -> u64 {
+ (dividend + divisor - 1) / divisor
+}
+
+/// Given `offset` and `length`, generates (offset, size) tuples that together form the same length,
+/// and aligned to `alignment`.
+pub struct ChunkedSizeIter {
+ remaining: usize,
+ offset: u64,
+ alignment: usize,
+}
+
+impl ChunkedSizeIter {
+ pub fn new(remaining: usize, offset: u64, alignment: usize) -> Self {
+ ChunkedSizeIter { remaining, offset, alignment }
+ }
+}
+
+impl Iterator for ChunkedSizeIter {
+ type Item = (u64, usize);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.remaining == 0 {
+ return None;
+ }
+ let chunk_data_size = std::cmp::min(
+ self.remaining,
+ self.alignment - (self.offset % self.alignment as u64) as usize,
+ );
+ let retval = (self.offset, chunk_data_size);
+ self.offset += chunk_data_size as u64;
+ self.remaining = self.remaining.saturating_sub(chunk_data_size);
+ Some(retval)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn collect_chunk_read_iter(remaining: usize, offset: u64) -> Vec<(u64, usize)> {
+ ChunkedSizeIter::new(remaining, offset, 4096).collect::<Vec<_>>()
+ }
+
+ #[test]
+ fn test_chunk_read_iter() {
+ assert_eq!(collect_chunk_read_iter(4096, 0), [(0, 4096)]);
+ assert_eq!(collect_chunk_read_iter(8192, 0), [(0, 4096), (4096, 4096)]);
+ assert_eq!(collect_chunk_read_iter(8192, 4096), [(4096, 4096), (8192, 4096)]);
+
+ assert_eq!(
+ collect_chunk_read_iter(16384, 1),
+ [(1, 4095), (4096, 4096), (8192, 4096), (12288, 4096), (16384, 1)]
+ );
+
+ assert_eq!(collect_chunk_read_iter(0, 0), []);
+ assert_eq!(collect_chunk_read_iter(0, 100), []);
+ }
+}
diff --git a/guest/authfs/src/file.rs b/guest/authfs/src/file.rs
new file mode 100644
index 0000000..55c783b
--- /dev/null
+++ b/guest/authfs/src/file.rs
@@ -0,0 +1,114 @@
+mod attr;
+mod dir;
+mod remote_file;
+
+pub use attr::Attr;
+pub use dir::{InMemoryDir, RemoteDirEditor};
+pub use remote_file::{RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader};
+
+use crate::common::{divide_roundup, CHUNK_SIZE};
+use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::IVirtFdService;
+use binder::{Status, StatusCode, Strong};
+use rpcbinder::RpcSession;
+use std::convert::TryFrom;
+use std::io;
+use std::path::{Path, MAIN_SEPARATOR};
+
+pub type VirtFdService = Strong<dyn IVirtFdService>;
+pub type VirtFdServiceStatus = Status;
+
+pub type ChunkBuffer = [u8; CHUNK_SIZE as usize];
+
+pub const RPC_SERVICE_PORT: u32 = 3264;
+
+pub fn get_rpc_binder_service(cid: u32) -> io::Result<VirtFdService> {
+ RpcSession::new().setup_vsock_client(cid, RPC_SERVICE_PORT).map_err(|e| match e {
+ StatusCode::BAD_VALUE => {
+ io::Error::new(io::ErrorKind::InvalidInput, "Invalid raw AIBinder")
+ }
+ _ => io::Error::new(
+ io::ErrorKind::AddrNotAvailable,
+ format!("Cannot connect to RPC service: {}", e),
+ ),
+ })
+}
+
+/// A trait for reading data by chunks. Chunks can be read by specifying the chunk index. Only the
+/// last chunk may have incomplete chunk size.
+pub trait ReadByChunk {
+ /// Reads the `chunk_index`-th chunk to a `ChunkBuffer`. Returns the size read, which has to be
+ /// `CHUNK_SIZE` except for the last incomplete chunk. Reading beyond the file size (including
+ /// empty file) should return 0.
+ fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize>;
+}
+
+/// A trait to write a buffer to the destination at a given offset. The implementation does not
+/// necessarily own or maintain the destination state.
+///
+/// NB: The trait is required in a member of `fusefs::AuthFs`, which is required to be Sync and
+/// immutable (this the member).
+pub trait RandomWrite {
+ /// Writes `buf` to the destination at `offset`. Returns the written size, which may not be the
+ /// full buffer.
+ fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize>;
+
+ /// Writes the full `buf` to the destination at `offset`.
+ fn write_all_at(&self, buf: &[u8], offset: u64) -> io::Result<()> {
+ let mut input_offset = 0;
+ let mut output_offset = offset;
+ while input_offset < buf.len() {
+ let size = self.write_at(&buf[input_offset..], output_offset)?;
+ input_offset += size;
+ output_offset += size as u64;
+ }
+ Ok(())
+ }
+
+ /// Resizes the file to the new size.
+ fn resize(&self, size: u64) -> io::Result<()>;
+}
+
+/// Checks whether the path is a simple file name without any directory separator.
+pub fn validate_basename(path: &Path) -> io::Result<()> {
+ if matches!(path.to_str(), Some(path_str) if !path_str.contains(MAIN_SEPARATOR)) {
+ Ok(())
+ } else {
+ Err(io::Error::from_raw_os_error(libc::EINVAL))
+ }
+}
+
+pub struct EagerChunkReader {
+ buffer: Vec<u8>,
+}
+
+impl EagerChunkReader {
+ pub fn new<F: ReadByChunk>(chunked_file: F, file_size: u64) -> io::Result<EagerChunkReader> {
+ let last_index = divide_roundup(file_size, CHUNK_SIZE);
+ let file_size = usize::try_from(file_size).unwrap();
+ let mut buffer = Vec::with_capacity(file_size);
+ let mut chunk_buffer = [0; CHUNK_SIZE as usize];
+ for index in 0..last_index {
+ let size = chunked_file.read_chunk(index, &mut chunk_buffer)?;
+ buffer.extend_from_slice(&chunk_buffer[..size]);
+ }
+ if buffer.len() < file_size {
+ Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ format!("Insufficient data size ({} < {})", buffer.len(), file_size),
+ ))
+ } else {
+ Ok(EagerChunkReader { buffer })
+ }
+ }
+}
+
+impl ReadByChunk for EagerChunkReader {
+ fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+ if let Some(chunk) = &self.buffer.chunks(CHUNK_SIZE as usize).nth(chunk_index as usize) {
+ buf[..chunk.len()].copy_from_slice(chunk);
+ Ok(chunk.len())
+ } else {
+ Ok(0) // Read beyond EOF is normal
+ }
+ }
+}
diff --git a/guest/authfs/src/file/attr.rs b/guest/authfs/src/file/attr.rs
new file mode 100644
index 0000000..48084aa
--- /dev/null
+++ b/guest/authfs/src/file/attr.rs
@@ -0,0 +1,93 @@
+/*
+ * 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 log::error;
+use nix::sys::stat::{mode_t, Mode, SFlag};
+use std::io;
+
+use super::VirtFdService;
+
+/// Default/assumed mode of files not created by authfs.
+///
+/// For files that are given to authfs as FDs (i.e. not created through authfs), their mode is
+/// unknown (or untrusted) until it is ever set. The default mode is just to make it
+/// readable/writable to VFS. When the mode is set, the value on fd_server is supposed to become
+/// consistent.
+const DEFAULT_FILE_MODE: Mode =
+ Mode::from_bits_truncate(Mode::S_IRUSR.bits() | Mode::S_IWUSR.bits());
+
+/// Default/assumed mode of directories not created by authfs.
+///
+/// See above.
+const DEFAULT_DIR_MODE: Mode = Mode::S_IRWXU;
+
+/// `Attr` maintains the local truth for attributes (e.g. mode and type) while allowing setting the
+/// remote attribute for the file description.
+pub struct Attr {
+ service: VirtFdService,
+ mode: Mode,
+ remote_fd: i32,
+ is_dir: bool,
+}
+
+impl Attr {
+ pub fn new_file(service: VirtFdService, remote_fd: i32) -> Attr {
+ Attr { service, mode: DEFAULT_FILE_MODE, remote_fd, is_dir: false }
+ }
+
+ pub fn new_dir(service: VirtFdService, remote_fd: i32) -> Attr {
+ Attr { service, mode: DEFAULT_DIR_MODE, remote_fd, is_dir: true }
+ }
+
+ pub fn new_file_with_mode(service: VirtFdService, remote_fd: i32, mode: mode_t) -> Attr {
+ Attr { service, mode: Mode::from_bits_truncate(mode), remote_fd, is_dir: false }
+ }
+
+ pub fn new_dir_with_mode(service: VirtFdService, remote_fd: i32, mode: mode_t) -> Attr {
+ Attr { service, mode: Mode::from_bits_truncate(mode), remote_fd, is_dir: true }
+ }
+
+ pub fn mode(&self) -> u32 {
+ self.mode.bits()
+ }
+
+ /// Sets the file mode.
+ ///
+ /// In addition to the actual file mode, `encoded_mode` also contains information of the file
+ /// type.
+ pub fn set_mode(&mut self, encoded_mode: u32) -> io::Result<()> {
+ let new_sflag = SFlag::from_bits_truncate(encoded_mode);
+ let new_mode = Mode::from_bits_truncate(encoded_mode);
+
+ let type_flag = if self.is_dir { SFlag::S_IFDIR } else { SFlag::S_IFREG };
+ if !type_flag.contains(new_sflag) {
+ return Err(io::Error::from_raw_os_error(libc::EINVAL));
+ }
+
+ // Request for update only if changing.
+ if new_mode != self.mode {
+ self.service.chmod(self.remote_fd, new_mode.bits() as i32).map_err(|e| {
+ error!(
+ "Failed to chmod (fd: {}, mode: {:o}) on fd_server: {:?}",
+ self.remote_fd, new_mode, e
+ );
+ io::Error::from_raw_os_error(libc::EIO)
+ })?;
+ self.mode = new_mode;
+ }
+ Ok(())
+ }
+}
diff --git a/guest/authfs/src/file/dir.rs b/guest/authfs/src/file/dir.rs
new file mode 100644
index 0000000..5d2ec9f
--- /dev/null
+++ b/guest/authfs/src/file/dir.rs
@@ -0,0 +1,273 @@
+/*
+ * 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 log::warn;
+use nix::sys::stat::Mode;
+use std::collections::{hash_map, HashMap};
+use std::ffi::{CString, OsString};
+use std::io;
+use std::os::unix::ffi::OsStringExt;
+use std::path::{Path, PathBuf};
+
+use super::attr::Attr;
+use super::remote_file::RemoteFileEditor;
+use super::{validate_basename, VirtFdService, VirtFdServiceStatus};
+use crate::fsverity::VerifiedFileEditor;
+use crate::fusefs::{AuthFsDirEntry, Inode};
+
+const MAX_ENTRIES: u16 = 1000; // Arbitrary limit
+
+struct InodeInfo {
+ inode: Inode,
+
+ // This information is duplicated since it is also available in `AuthFs::inode_table` via the
+ // type system. But it makes it simple to deal with deletion, where otherwise we need to get a
+ // mutable parent directory in the table, and query the table for directory/file type checking
+ // at the same time.
+ is_dir: bool,
+}
+
+/// 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. The actual file/directory is stored in
+ /// the global pool in fusefs.
+ entries: HashMap<PathBuf, InodeInfo>,
+}
+
+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 named `basename` with corresponding `inode` at the current directory.
+ pub fn create_file(
+ &mut self,
+ basename: &Path,
+ inode: Inode,
+ mode: libc::mode_t,
+ ) -> io::Result<(VerifiedFileEditor<RemoteFileEditor>, Attr)> {
+ let mode = self.validate_arguments(basename, mode)?;
+ 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, mode as i32)
+ .map_err(into_io_error)?;
+
+ let new_remote_file =
+ VerifiedFileEditor::new(RemoteFileEditor::new(self.service.clone(), new_fd));
+ self.entries.insert(basename.to_path_buf(), InodeInfo { inode, is_dir: false });
+ let new_attr = Attr::new_file_with_mode(self.service.clone(), new_fd, mode);
+ Ok((new_remote_file, new_attr))
+ }
+
+ /// Creates a remote directory named `basename` with corresponding `inode` at the current
+ /// directory.
+ pub fn mkdir(
+ &mut self,
+ basename: &Path,
+ inode: Inode,
+ mode: libc::mode_t,
+ ) -> io::Result<(RemoteDirEditor, Attr)> {
+ let mode = self.validate_arguments(basename, mode)?;
+ 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, mode as i32)
+ .map_err(into_io_error)?;
+
+ let new_remote_dir = RemoteDirEditor::new(self.service.clone(), new_fd);
+ self.entries.insert(basename.to_path_buf(), InodeInfo { inode, is_dir: true });
+ let new_attr = Attr::new_dir_with_mode(self.service.clone(), new_fd, mode);
+ Ok((new_remote_dir, new_attr))
+ }
+
+ /// Deletes a file
+ pub fn delete_file(&mut self, basename: &Path) -> io::Result<Inode> {
+ let inode = self.force_delete_entry(basename, /* expect_dir */ false)?;
+
+ let basename_str =
+ basename.to_str().ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
+ if let Err(e) = self.service.deleteFile(self.remote_dir_fd, basename_str) {
+ // Ignore the error to honor the local state.
+ warn!("Deletion on the host is reportedly failed: {:?}", e);
+ }
+ Ok(inode)
+ }
+
+ /// Forces to delete a directory. The caller must only call if `basename` is a directory and
+ /// empty.
+ pub fn force_delete_directory(&mut self, basename: &Path) -> io::Result<Inode> {
+ let inode = self.force_delete_entry(basename, /* expect_dir */ true)?;
+
+ let basename_str =
+ basename.to_str().ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
+ if let Err(e) = self.service.deleteDirectory(self.remote_dir_fd, basename_str) {
+ // Ignore the error to honor the local state.
+ warn!("Deletion on the host is reportedly failed: {:?}", e);
+ }
+ Ok(inode)
+ }
+
+ /// Returns the inode number of a file or directory named `name` previously created through
+ /// `RemoteDirEditor`.
+ pub fn find_inode(&self, name: &Path) -> io::Result<Inode> {
+ self.entries
+ .get(name)
+ .map(|entry| entry.inode)
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))
+ }
+
+ /// Returns whether the directory has an entry of the given name.
+ pub fn has_entry(&self, name: &Path) -> bool {
+ self.entries.contains_key(name)
+ }
+
+ pub fn retrieve_entries(&self) -> io::Result<Vec<AuthFsDirEntry>> {
+ self.entries
+ .iter()
+ .map(|(name, InodeInfo { inode, is_dir })| {
+ Ok(AuthFsDirEntry { inode: *inode, name: path_to_cstring(name)?, is_dir: *is_dir })
+ })
+ .collect::<io::Result<Vec<_>>>()
+ }
+
+ fn force_delete_entry(&mut self, basename: &Path, expect_dir: bool) -> io::Result<Inode> {
+ // Kernel should only give us a basename.
+ debug_assert!(validate_basename(basename).is_ok());
+
+ if let Some(entry) = self.entries.get(basename) {
+ match (expect_dir, entry.is_dir) {
+ (true, false) => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+ (false, true) => Err(io::Error::from_raw_os_error(libc::EISDIR)),
+ _ => {
+ let inode = entry.inode;
+ let _ = self.entries.remove(basename);
+ Ok(inode)
+ }
+ }
+ } else {
+ Err(io::Error::from_raw_os_error(libc::ENOENT))
+ }
+ }
+
+ fn validate_arguments(&self, basename: &Path, mode: u32) -> io::Result<u32> {
+ // Kernel should only give us a basename.
+ debug_assert!(validate_basename(basename).is_ok());
+
+ if self.entries.contains_key(basename) {
+ return Err(io::Error::from_raw_os_error(libc::EEXIST));
+ }
+
+ if self.entries.len() >= MAX_ENTRIES.into() {
+ return Err(io::Error::from_raw_os_error(libc::EMLINK));
+ }
+
+ Ok(Mode::from_bits_truncate(mode).bits())
+ }
+}
+
+/// An in-memory directory representation of a directory structure.
+pub struct InMemoryDir(HashMap<PathBuf, InodeInfo>);
+
+impl InMemoryDir {
+ /// Creates an empty instance of `InMemoryDir`.
+ pub fn new() -> Self {
+ // Hash map is empty since "." and ".." are excluded in entries.
+ InMemoryDir(HashMap::new())
+ }
+
+ /// Returns the number of entries in the directory (not including "." and "..").
+ pub fn number_of_entries(&self) -> u16 {
+ self.0.len() as u16 // limited to MAX_ENTRIES
+ }
+
+ /// Adds a directory name and its inode number to the directory. Fails if already exists. The
+ /// caller is responsible for ensure the inode uniqueness.
+ pub fn add_dir(&mut self, basename: &Path, inode: Inode) -> io::Result<()> {
+ self.add_entry(basename, InodeInfo { inode, is_dir: true })
+ }
+
+ /// Adds a file name and its inode number to the directory. Fails if already exists. The
+ /// caller is responsible for ensure the inode uniqueness.
+ pub fn add_file(&mut self, basename: &Path, inode: Inode) -> io::Result<()> {
+ self.add_entry(basename, InodeInfo { inode, is_dir: false })
+ }
+
+ fn add_entry(&mut self, basename: &Path, dir_entry: InodeInfo) -> io::Result<()> {
+ validate_basename(basename)?;
+ if self.0.len() >= MAX_ENTRIES.into() {
+ return Err(io::Error::from_raw_os_error(libc::EMLINK));
+ }
+
+ if let hash_map::Entry::Vacant(entry) = self.0.entry(basename.to_path_buf()) {
+ entry.insert(dir_entry);
+ Ok(())
+ } else {
+ Err(io::Error::from_raw_os_error(libc::EEXIST))
+ }
+ }
+
+ /// Looks up an entry inode by name. `None` if not found.
+ pub fn lookup_inode(&self, basename: &Path) -> Option<Inode> {
+ self.0.get(basename).map(|entry| entry.inode)
+ }
+
+ pub fn retrieve_entries(&self) -> io::Result<Vec<AuthFsDirEntry>> {
+ self.0
+ .iter()
+ .map(|(name, InodeInfo { inode, is_dir })| {
+ Ok(AuthFsDirEntry { inode: *inode, name: path_to_cstring(name)?, is_dir: *is_dir })
+ })
+ .collect::<io::Result<Vec<_>>>()
+ }
+}
+
+fn path_to_cstring(path: &Path) -> io::Result<CString> {
+ let bytes = OsString::from(path).into_vec();
+ CString::new(bytes).map_err(|_| io::Error::from_raw_os_error(libc::EILSEQ))
+}
+
+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/guest/authfs/src/file/remote_file.rs b/guest/authfs/src/file/remote_file.rs
new file mode 100644
index 0000000..4c112bd
--- /dev/null
+++ b/guest/authfs/src/file/remote_file.rs
@@ -0,0 +1,144 @@
+/*
+ * 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::cmp::min;
+use std::convert::TryFrom;
+use std::io;
+use std::path::Path;
+
+use super::{ChunkBuffer, RandomWrite, ReadByChunk, VirtFdService};
+use crate::common::CHUNK_SIZE;
+
+fn remote_read_chunk(
+ service: &VirtFdService,
+ remote_fd: i32,
+ chunk_index: u64,
+ buf: &mut ChunkBuffer,
+) -> io::Result<usize> {
+ let offset = i64::try_from(chunk_index * CHUNK_SIZE)
+ .map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
+
+ let chunk = service
+ .readFile(remote_fd, offset, buf.len() as i32)
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e.get_description()))?;
+ let size = min(buf.len(), chunk.len());
+ buf[..size].copy_from_slice(&chunk[..size]);
+ Ok(size)
+}
+
+pub struct RemoteFileReader {
+ service: VirtFdService,
+ file_fd: i32,
+}
+
+impl RemoteFileReader {
+ pub fn new(service: VirtFdService, file_fd: i32) -> Self {
+ RemoteFileReader { service, file_fd }
+ }
+
+ pub fn new_by_path(
+ service: VirtFdService,
+ dir_fd: i32,
+ related_path: &Path,
+ ) -> io::Result<Self> {
+ let file_fd =
+ service.openFileInDirectory(dir_fd, related_path.to_str().unwrap()).map_err(|e| {
+ io::Error::new(
+ io::ErrorKind::Other,
+ format!(
+ "Failed to create a remote file reader by path {}: {}",
+ related_path.display(),
+ e.get_description()
+ ),
+ )
+ })?;
+ Ok(RemoteFileReader { service, file_fd })
+ }
+
+ pub fn get_remote_fd(&self) -> i32 {
+ self.file_fd
+ }
+}
+
+impl ReadByChunk for RemoteFileReader {
+ fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+ remote_read_chunk(&self.service, self.file_fd, chunk_index, buf)
+ }
+}
+
+pub struct RemoteMerkleTreeReader {
+ service: VirtFdService,
+ file_fd: i32,
+}
+
+impl RemoteMerkleTreeReader {
+ pub fn new(service: VirtFdService, file_fd: i32) -> Self {
+ RemoteMerkleTreeReader { service, file_fd }
+ }
+}
+
+impl ReadByChunk for RemoteMerkleTreeReader {
+ fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+ let offset = i64::try_from(chunk_index * CHUNK_SIZE)
+ .map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
+
+ let chunk = self
+ .service
+ .readFsverityMerkleTree(self.file_fd, offset, buf.len() as i32)
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e.get_description()))?;
+ let size = min(buf.len(), chunk.len());
+ buf[..size].copy_from_slice(&chunk[..size]);
+ Ok(size)
+ }
+}
+
+pub struct RemoteFileEditor {
+ service: VirtFdService,
+ file_fd: i32,
+}
+
+impl RemoteFileEditor {
+ pub fn new(service: VirtFdService, file_fd: i32) -> Self {
+ RemoteFileEditor { service, file_fd }
+ }
+}
+
+impl RandomWrite for RemoteFileEditor {
+ fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
+ let offset =
+ i64::try_from(offset).map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
+ let size = self
+ .service
+ .writeFile(self.file_fd, buf, offset)
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e.get_description()))?;
+ Ok(size as usize) // within range because size is supposed to <= buf.len(), which is a usize
+ }
+
+ fn resize(&self, size: u64) -> io::Result<()> {
+ let size =
+ i64::try_from(size).map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
+ self.service
+ .resize(self.file_fd, size)
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e.get_description()))?;
+ Ok(())
+ }
+}
+
+impl ReadByChunk for RemoteFileEditor {
+ fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+ remote_read_chunk(&self.service, self.file_fd, chunk_index, buf)
+ }
+}
diff --git a/guest/authfs/src/fsstat.rs b/guest/authfs/src/fsstat.rs
new file mode 100644
index 0000000..81eaca1
--- /dev/null
+++ b/guest/authfs/src/fsstat.rs
@@ -0,0 +1,70 @@
+/*
+ * 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 log::error;
+use std::convert::TryInto;
+use std::io;
+
+use crate::file::VirtFdService;
+use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::FsStat::FsStat;
+
+/// Relevant/interesting stats of a remote filesystem.
+pub struct RemoteFsStats {
+ /// Block size of the filesystem
+ pub block_size: u64,
+ /// Fragment size of the filesystem
+ pub fragment_size: u64,
+ /// Number of blocks in the filesystem
+ pub block_numbers: u64,
+ /// Number of free blocks
+ pub block_available: u64,
+ /// Number of free inodes
+ pub inodes_available: u64,
+ /// Maximum filename length
+ pub max_filename: u64,
+}
+
+pub struct RemoteFsStatsReader {
+ service: VirtFdService,
+}
+
+impl RemoteFsStatsReader {
+ pub fn new(service: VirtFdService) -> Self {
+ Self { service }
+ }
+
+ pub fn statfs(&self) -> io::Result<RemoteFsStats> {
+ let st = self.service.statfs().map_err(|e| {
+ error!("Failed to call statfs on fd_server: {:?}", e);
+ io::Error::from_raw_os_error(libc::EIO)
+ })?;
+ try_into_remote_fs_stats(st).map_err(|_| {
+ error!("Received invalid stats from fd_server");
+ io::Error::from_raw_os_error(libc::EIO)
+ })
+ }
+}
+
+fn try_into_remote_fs_stats(st: FsStat) -> Result<RemoteFsStats, std::num::TryFromIntError> {
+ Ok(RemoteFsStats {
+ block_size: st.blockSize.try_into()?,
+ fragment_size: st.fragmentSize.try_into()?,
+ block_numbers: st.blockNumbers.try_into()?,
+ block_available: st.blockAvailable.try_into()?,
+ inodes_available: st.inodesAvailable.try_into()?,
+ max_filename: st.maxFilename.try_into()?,
+ })
+}
diff --git a/guest/authfs/src/fsverity.rs b/guest/authfs/src/fsverity.rs
new file mode 100644
index 0000000..61ae928
--- /dev/null
+++ b/guest/authfs/src/fsverity.rs
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+mod builder;
+mod common;
+mod editor;
+mod sys;
+mod verifier;
+
+pub use common::merkle_tree_size;
+pub use editor::VerifiedFileEditor;
+pub use verifier::VerifiedFileReader;
diff --git a/guest/authfs/src/fsverity/builder.rs b/guest/authfs/src/fsverity/builder.rs
new file mode 100644
index 0000000..6d724ca
--- /dev/null
+++ b/guest/authfs/src/fsverity/builder.rs
@@ -0,0 +1,271 @@
+/*
+ * 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 super::common::{
+ build_fsverity_digest, merkle_tree_height, FsverityError, Sha256Hash, SHA256_HASH_SIZE,
+};
+use crate::common::{divide_roundup, CHUNK_SIZE};
+use openssl::sha::Sha256;
+
+const HASH_SIZE: usize = SHA256_HASH_SIZE;
+const HASH_PER_PAGE: usize = CHUNK_SIZE as usize / HASH_SIZE;
+
+const HASH_OF_4096_ZEROS: Sha256Hash = [
+ 0xad, 0x7f, 0xac, 0xb2, 0x58, 0x6f, 0xc6, 0xe9, 0x66, 0xc0, 0x04, 0xd7, 0xd1, 0xd1, 0x6b, 0x02,
+ 0x4f, 0x58, 0x05, 0xff, 0x7c, 0xb4, 0x7c, 0x7a, 0x85, 0xda, 0xbd, 0x8b, 0x48, 0x89, 0x2c, 0xa7,
+];
+
+/// MerkleLeaves can be used by the class' customer for bookkeeping integrity data for their bytes.
+/// It can also be used to generate the standard fs-verity digest for the source data.
+///
+/// It's in-memory because for the initial use cases, we don't need to read back an existing file,
+/// and only need to deal with new files. Also, considering that the output file won't be large at
+/// the moment, it is sufficient to simply keep the Merkle tree in memory in the trusted world. To
+/// further simplify the initial implementation, we only need to keep the leaf nodes in memory, and
+/// generate the tree / root hash when requested.
+pub struct MerkleLeaves {
+ leaves: Vec<Sha256Hash>,
+ file_size: u64,
+}
+
+fn hash_all_pages(source: &[Sha256Hash]) -> Vec<Sha256Hash> {
+ source
+ .chunks(HASH_PER_PAGE)
+ .map(|chunk| {
+ let padding_bytes = (HASH_PER_PAGE - chunk.len()) * HASH_SIZE;
+ let mut ctx = Sha256::new();
+ for data in chunk {
+ ctx.update(data.as_ref());
+ }
+ ctx.update(&vec![0u8; padding_bytes]);
+ ctx.finish()
+ })
+ .collect()
+}
+
+impl MerkleLeaves {
+ /// Creates a `MerkleLeaves` instance with empty data.
+ pub fn new() -> Self {
+ Self { leaves: Vec::new(), file_size: 0 }
+ }
+
+ /// Gets size of the file represented by `MerkleLeaves`.
+ pub fn file_size(&self) -> u64 {
+ self.file_size
+ }
+
+ /// Grows the `MerkleLeaves` to the new file size. To keep the consistency, if any new leaves
+ /// are added, the leaves/hashes are as if the extended content is all zero.
+ ///
+ /// However, when the change shrinks the leaf number, `MerkleLeaves` does not know if the hash
+ /// of the last chunk has changed, or what the new value should be. As the result, it is up to
+ /// the caller to fix the last leaf if needed.
+ pub fn resize(&mut self, new_file_size: usize) {
+ let new_file_size = new_file_size as u64;
+ let leaves_size = divide_roundup(new_file_size, CHUNK_SIZE);
+ self.leaves.resize(leaves_size as usize, HASH_OF_4096_ZEROS);
+ self.file_size = new_file_size;
+ }
+
+ /// Updates the hash of the `index`-th leaf, and increase the size to `size_at_least` if the
+ /// current size is smaller.
+ pub fn update_hash(&mut self, index: usize, hash: &Sha256Hash, size_at_least: u64) {
+ // +1 since index is zero-based.
+ if self.leaves.len() < index + 1 {
+ // When resizing, fill in hash of zeros by default. This makes it easy to handle holes
+ // in a file.
+ self.leaves.resize(index + 1, HASH_OF_4096_ZEROS);
+ }
+ self.leaves[index].clone_from_slice(hash);
+
+ if size_at_least > self.file_size {
+ self.file_size = size_at_least;
+ }
+ }
+
+ /// Returns whether `index` is within the bound of leaves.
+ pub fn is_index_valid(&self, index: usize) -> bool {
+ index < self.leaves.len()
+ }
+
+ /// Returns whether the `index`-th hash is consistent to `hash`.
+ pub fn is_consistent(&self, index: usize, hash: &Sha256Hash) -> bool {
+ if let Some(element) = self.leaves.get(index) {
+ element == hash
+ } else {
+ false
+ }
+ }
+
+ fn calculate_root_hash(&self) -> Result<Sha256Hash, FsverityError> {
+ match self.leaves.len() {
+ // Special cases per fs-verity digest definition.
+ 0 => {
+ debug_assert_eq!(self.file_size, 0);
+ Ok([0u8; HASH_SIZE])
+ }
+ 1 => {
+ debug_assert!(self.file_size <= CHUNK_SIZE && self.file_size > 0);
+ Ok(self.leaves[0])
+ }
+ n => {
+ debug_assert_eq!((self.file_size - 1) / CHUNK_SIZE, n as u64);
+ let size_for_equivalent = n as u64 * CHUNK_SIZE;
+ let level = merkle_tree_height(size_for_equivalent).unwrap(); // safe since n > 0
+
+ // `leaves` is owned and can't be the initial state below. Here we manually hash it
+ // first to avoid a copy and to get the type right.
+ let second_level = hash_all_pages(&self.leaves);
+ let hashes = (1..=level).fold(second_level, |source, _| hash_all_pages(&source));
+ if hashes.len() != 1 {
+ Err(FsverityError::InvalidState)
+ } else {
+ Ok(hashes.into_iter().next().unwrap())
+ }
+ }
+ }
+ }
+
+ /// Returns the fs-verity digest based on the current tree and file size.
+ pub fn calculate_fsverity_digest(&self) -> Result<Sha256Hash, FsverityError> {
+ let root_hash = self.calculate_root_hash()?;
+ Ok(build_fsverity_digest(&root_hash, self.file_size))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ // Test data below can be generated by:
+ // $ perl -e 'print "\x{00}" x 6000' > foo
+ // $ perl -e 'print "\x{01}" x 5000' >> foo
+ // $ fsverity digest foo
+ use super::*;
+ use anyhow::Result;
+ use openssl::sha::sha256;
+
+ #[test]
+ fn merkle_tree_empty_file() -> Result<()> {
+ assert_eq!(
+ hex::decode("3d248ca542a24fc62d1c43b916eae5016878e2533c88238480b26128a1f1af95")?,
+ generate_fsverity_digest_sequentially(&Vec::new())?
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn merkle_tree_file_size_less_than_or_equal_to_4k() -> Result<()> {
+ // Test a file that contains 4096 '\01's.
+ assert_eq!(
+ hex::decode("cd0875ca59c7d37e962c5e8f5acd3770750ac80225e2df652ce5672fd34500af")?,
+ generate_fsverity_digest_sequentially(&vec![1; 4096])?
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn merkle_tree_more_sizes() -> Result<()> {
+ // Test files that contains >4096 '\01's.
+
+ assert_eq!(
+ hex::decode("2901b849fda2d91e3929524561c4a47e77bb64734319759507b2029f18b9cc52")?,
+ generate_fsverity_digest_sequentially(&vec![1; 4097])?
+ );
+
+ assert_eq!(
+ hex::decode("2a476d58eb80394052a3a783111e1458ac3ecf68a7878183fed86ca0ff47ec0d")?,
+ generate_fsverity_digest_sequentially(&vec![1; 8192])?
+ );
+
+ // Test with max size that still fits in 2 levels.
+ assert_eq!(
+ hex::decode("26b7c190a34e19f420808ee7ec233b09fa6c34543b5a9d2950530114c205d14f")?,
+ generate_fsverity_digest_sequentially(&vec![1; 524288])?
+ );
+
+ // Test with data that requires 3 levels.
+ assert_eq!(
+ hex::decode("316835d9be1c95b5cd55d07ae7965d651689efad186e26cbf680e40b683a3262")?,
+ generate_fsverity_digest_sequentially(&vec![1; 524289])?
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn merkle_tree_non_sequential() -> Result<()> {
+ let mut tree = MerkleLeaves::new();
+ let hash = sha256(&vec![1u8; CHUNK_SIZE as usize]);
+
+ // Update hashes of 4 1-blocks.
+ tree.update_hash(1, &hash, CHUNK_SIZE * 2);
+ tree.update_hash(3, &hash, CHUNK_SIZE * 4);
+ tree.update_hash(0, &hash, CHUNK_SIZE);
+ tree.update_hash(2, &hash, CHUNK_SIZE * 3);
+
+ assert_eq!(
+ hex::decode("7d3c0d2e1dc54230b20ed875f5f3a4bd3f9873df601936b3ca8127d4db3548f3")?,
+ tree.calculate_fsverity_digest()?
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn merkle_tree_grow_leaves() -> Result<()> {
+ let mut tree = MerkleLeaves::new();
+ tree.update_hash(0, &[42; HASH_SIZE], CHUNK_SIZE); // fake hash of a CHUNK_SIZE block
+
+ // Grows the leaves
+ tree.resize(4096 * 3 - 100);
+
+ assert!(tree.is_index_valid(0));
+ assert!(tree.is_index_valid(1));
+ assert!(tree.is_index_valid(2));
+ assert!(!tree.is_index_valid(3));
+ assert!(tree.is_consistent(1, &HASH_OF_4096_ZEROS));
+ assert!(tree.is_consistent(2, &HASH_OF_4096_ZEROS));
+ Ok(())
+ }
+
+ #[test]
+ fn merkle_tree_shrink_leaves() -> Result<()> {
+ let mut tree = MerkleLeaves::new();
+ tree.update_hash(0, &[42; HASH_SIZE], CHUNK_SIZE);
+ tree.update_hash(0, &[42; HASH_SIZE], CHUNK_SIZE * 3);
+
+ // Shrink the leaves
+ tree.resize(CHUNK_SIZE as usize * 2 - 100);
+
+ assert!(tree.is_index_valid(0));
+ assert!(tree.is_index_valid(1));
+ assert!(!tree.is_index_valid(2));
+ // The second chunk is a hole and full of zero. When shrunk, with zero padding, the hash
+ // happens to be consistent to a full-zero chunk.
+ assert!(tree.is_consistent(1, &HASH_OF_4096_ZEROS));
+ Ok(())
+ }
+
+ fn generate_fsverity_digest_sequentially(test_data: &[u8]) -> Result<Sha256Hash> {
+ let mut tree = MerkleLeaves::new();
+ for (index, chunk) in test_data.chunks(CHUNK_SIZE as usize).enumerate() {
+ let mut ctx = Sha256::new();
+ ctx.update(chunk);
+ ctx.update(&vec![0u8; CHUNK_SIZE as usize - chunk.len()]);
+ let hash = ctx.finish();
+
+ tree.update_hash(index, &hash, CHUNK_SIZE * index as u64 + chunk.len() as u64);
+ }
+ Ok(tree.calculate_fsverity_digest()?)
+ }
+}
diff --git a/guest/authfs/src/fsverity/common.rs b/guest/authfs/src/fsverity/common.rs
new file mode 100644
index 0000000..cb268ef
--- /dev/null
+++ b/guest/authfs/src/fsverity/common.rs
@@ -0,0 +1,109 @@
+/*
+ * 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::io;
+
+use thiserror::Error;
+
+use super::sys::{FS_VERITY_HASH_ALG_SHA256, FS_VERITY_LOG_BLOCKSIZE, FS_VERITY_VERSION};
+use crate::common::{divide_roundup, CHUNK_SIZE};
+use openssl::sha::Sha256;
+
+/// Output size of SHA-256 in bytes.
+pub const SHA256_HASH_SIZE: usize = 32;
+
+/// A SHA-256 hash.
+pub type Sha256Hash = [u8; SHA256_HASH_SIZE];
+
+#[derive(Error, Debug)]
+pub enum FsverityError {
+ #[error("Invalid digest")]
+ InvalidDigest,
+ #[error("Insufficient data, only got {0}")]
+ InsufficientData(usize),
+ #[error("Cannot verify a block")]
+ CannotVerify,
+ #[error("I/O error")]
+ Io(#[from] io::Error),
+ #[error("Invalid state")]
+ InvalidState,
+}
+
+fn log128_ceil(num: u64) -> Option<u64> {
+ match num {
+ 0 => None,
+ n => Some(divide_roundup(64 - (n - 1).leading_zeros() as u64, 7)),
+ }
+}
+
+/// Return the Merkle tree height for our tree configuration, or None if the size is 0.
+pub fn merkle_tree_height(data_size: u64) -> Option<u64> {
+ let hashes_per_node = CHUNK_SIZE / SHA256_HASH_SIZE as u64;
+ let hash_pages = divide_roundup(data_size, hashes_per_node * CHUNK_SIZE);
+ log128_ceil(hash_pages)
+}
+
+/// Returns the size of Merkle tree for `data_size` bytes amount of data.
+pub fn merkle_tree_size(mut data_size: u64) -> u64 {
+ let mut total = 0;
+ while data_size > CHUNK_SIZE {
+ let hash_size = divide_roundup(data_size, CHUNK_SIZE) * SHA256_HASH_SIZE as u64;
+ let hash_storage_size = divide_roundup(hash_size, CHUNK_SIZE) * CHUNK_SIZE;
+ total += hash_storage_size;
+ data_size = hash_storage_size;
+ }
+ total
+}
+
+pub fn build_fsverity_digest(root_hash: &Sha256Hash, file_size: u64) -> Sha256Hash {
+ // Little-endian byte representation of fsverity_descriptor from linux/fsverity.h
+ // Not FFI-ed as it seems easier to deal with the raw bytes manually.
+ let mut hash = Sha256::new();
+ hash.update(&FS_VERITY_VERSION.to_le_bytes()); // version
+ hash.update(&FS_VERITY_HASH_ALG_SHA256.to_le_bytes()); // hash_algorithm
+ hash.update(&FS_VERITY_LOG_BLOCKSIZE.to_le_bytes()); // log_blocksize
+ hash.update(&0u8.to_le_bytes()); // salt_size
+ hash.update(&0u32.to_le_bytes()); // sig_size
+ hash.update(&file_size.to_le_bytes()); // data_size
+ hash.update(root_hash); // root_hash, first 32 bytes
+ hash.update(&[0u8; 32]); // root_hash, last 32 bytes, always 0 because we are using sha256.
+ hash.update(&[0u8; 32]); // salt
+ hash.update(&[0u8; 32]); // reserved
+ hash.update(&[0u8; 32]); // reserved
+ hash.update(&[0u8; 32]); // reserved
+ hash.update(&[0u8; 32]); // reserved
+ hash.update(&[0u8; 16]); // reserved
+ hash.finish()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_merkle_tree_size() {
+ // To produce groundtruth:
+ // dd if=/dev/zero of=zeros bs=1 count=524289 && \
+ // fsverity digest --out-merkle-tree=tree zeros && \
+ // du -b tree
+ assert_eq!(merkle_tree_size(0), 0);
+ assert_eq!(merkle_tree_size(1), 0);
+ assert_eq!(merkle_tree_size(4096), 0);
+ assert_eq!(merkle_tree_size(4097), 4096);
+ assert_eq!(merkle_tree_size(524288), 4096);
+ assert_eq!(merkle_tree_size(524289), 12288);
+ }
+}
diff --git a/guest/authfs/src/fsverity/editor.rs b/guest/authfs/src/fsverity/editor.rs
new file mode 100644
index 0000000..c84500b
--- /dev/null
+++ b/guest/authfs/src/fsverity/editor.rs
@@ -0,0 +1,629 @@
+/*
+ * 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.
+ */
+
+//! A module for writing to a file from a trusted world to an untrusted storage.
+//!
+//! Architectural Model:
+//! * Trusted world: the writer, a signing secret, has some memory, but NO persistent storage.
+//! * Untrusted world: persistent storage, assuming untrusted.
+//! * IPC mechanism between trusted and untrusted world
+//!
+//! Use cases:
+//! * In the trusted world, we want to generate a large file, sign it, and share the signature for
+//! a third party to verify the file.
+//! * In the trusted world, we want to read a previously signed file back with signature check
+//! without having to touch the whole file.
+//!
+//! Requirements:
+//! * Communication between trusted and untrusted world is not cheap, and files can be large.
+//! * A file write pattern may not be sequential, neither does read.
+//!
+//! Considering the above, a technique similar to fs-verity is used. fs-verity uses an alternative
+//! hash function, a Merkle tree, to calculate the hash of file content. A file update at any
+//! location will propagate the hash update from the leaf to the root node. Unlike fs-verity, which
+//! assumes static files, to support write operation, we need to allow the file (thus tree) to
+//! update.
+//!
+//! For the trusted world to generate a large file with random write and hash it, the writer needs
+//! to hold some private information and update the Merkle tree during a file write (or even when
+//! the Merkle tree needs to be stashed to the untrusted storage).
+//!
+//! A write to a file must update the root hash. In order for the root hash to update, a tree
+//! walk to update from the write location to the root node is necessary. Importantly, in case when
+//! (part of) the Merkle tree needs to be read from the untrusted storage (e.g. not yet verified in
+//! cache), the original path must be verified by the trusted signature before the update to happen.
+//!
+//! Denial-of-service is a known weakness if the untrusted storage decides to simply remove the
+//! file. But there is nothing we can do in this architecture.
+//!
+//! Rollback attack is another possible attack, but can be addressed with a rollback counter when
+//! possible.
+
+use std::io;
+use std::sync::{Arc, RwLock};
+
+use super::builder::MerkleLeaves;
+use super::common::{Sha256Hash, SHA256_HASH_SIZE};
+use crate::common::{ChunkedSizeIter, CHUNK_SIZE};
+use crate::file::{ChunkBuffer, RandomWrite, ReadByChunk};
+use openssl::sha::{sha256, Sha256};
+
+fn debug_assert_usize_is_u64() {
+ // Since we don't need to support 32-bit CPU, make an assert to make conversion between
+ // u64 and usize easy below. Otherwise, we need to check `divide_roundup(offset + buf.len()
+ // <= usize::MAX` or handle `TryInto` errors.
+ debug_assert!(usize::MAX as u64 == u64::MAX, "Only 64-bit arch is supported");
+}
+
+/// VerifiedFileEditor provides an integrity layer to an underlying read-writable file, which may
+/// not be stored in a trusted environment. Only new, empty files are currently supported.
+pub struct VerifiedFileEditor<F: ReadByChunk + RandomWrite> {
+ file: F,
+ merkle_tree: Arc<RwLock<MerkleLeaves>>,
+}
+
+impl<F: ReadByChunk + RandomWrite> VerifiedFileEditor<F> {
+ /// Wraps a supposedly new file for integrity protection.
+ pub fn new(file: F) -> Self {
+ Self { file, merkle_tree: Arc::new(RwLock::new(MerkleLeaves::new())) }
+ }
+
+ /// Returns the fs-verity digest size in bytes.
+ pub fn get_fsverity_digest_size(&self) -> usize {
+ SHA256_HASH_SIZE
+ }
+
+ /// Calculates the fs-verity digest of the current file.
+ pub fn calculate_fsverity_digest(&self) -> io::Result<Sha256Hash> {
+ let merkle_tree = self.merkle_tree.read().unwrap();
+ merkle_tree.calculate_fsverity_digest().map_err(|e| io::Error::new(io::ErrorKind::Other, e))
+ }
+
+ fn read_backing_chunk_unverified(
+ &self,
+ chunk_index: u64,
+ buf: &mut ChunkBuffer,
+ ) -> io::Result<usize> {
+ self.file.read_chunk(chunk_index, buf)
+ }
+
+ fn read_backing_chunk_verified(
+ &self,
+ chunk_index: u64,
+ buf: &mut ChunkBuffer,
+ merkle_tree_locked: &MerkleLeaves,
+ ) -> io::Result<usize> {
+ debug_assert_usize_is_u64();
+
+ if merkle_tree_locked.is_index_valid(chunk_index as usize) {
+ let size = self.read_backing_chunk_unverified(chunk_index, buf)?;
+
+ // Ensure the returned buffer matches the known hash.
+ let hash = sha256(buf);
+ if !merkle_tree_locked.is_consistent(chunk_index as usize, &hash) {
+ return Err(io::Error::new(io::ErrorKind::InvalidData, "Inconsistent hash"));
+ }
+ Ok(size)
+ } else {
+ Ok(0)
+ }
+ }
+
+ fn new_hash_for_incomplete_write(
+ &self,
+ source: &[u8],
+ offset_from_alignment: usize,
+ output_chunk_index: usize,
+ merkle_tree: &mut MerkleLeaves,
+ ) -> io::Result<Sha256Hash> {
+ // The buffer is initialized to 0 purposely. To calculate the block hash, the data is
+ // 0-padded to the block size. When a chunk read is less than a chunk, the initial value
+ // conveniently serves the padding purpose.
+ let mut orig_data = [0u8; CHUNK_SIZE as usize];
+
+ // If previous data exists, read back and verify against the known hash (since the
+ // storage / remote server is not trusted).
+ if merkle_tree.is_index_valid(output_chunk_index) {
+ self.read_backing_chunk_unverified(output_chunk_index as u64, &mut orig_data)?;
+
+ // Verify original content
+ let hash = sha256(&orig_data);
+ if !merkle_tree.is_consistent(output_chunk_index, &hash) {
+ return Err(io::Error::new(io::ErrorKind::InvalidData, "Inconsistent hash"));
+ }
+ }
+
+ let mut ctx = Sha256::new();
+ ctx.update(&orig_data[..offset_from_alignment]);
+ ctx.update(source);
+ ctx.update(&orig_data[offset_from_alignment + source.len()..]);
+ Ok(ctx.finish())
+ }
+
+ fn new_chunk_hash(
+ &self,
+ source: &[u8],
+ offset_from_alignment: usize,
+ current_size: usize,
+ output_chunk_index: usize,
+ merkle_tree: &mut MerkleLeaves,
+ ) -> io::Result<Sha256Hash> {
+ if current_size as u64 == CHUNK_SIZE {
+ // Case 1: If the chunk is a complete one, just calculate the hash, regardless of
+ // write location.
+ Ok(sha256(source))
+ } else {
+ // Case 2: For an incomplete write, calculate the hash based on previous data (if
+ // any).
+ self.new_hash_for_incomplete_write(
+ source,
+ offset_from_alignment,
+ output_chunk_index,
+ merkle_tree,
+ )
+ }
+ }
+
+ pub fn size(&self) -> u64 {
+ self.merkle_tree.read().unwrap().file_size()
+ }
+}
+
+impl<F: ReadByChunk + RandomWrite> RandomWrite for VerifiedFileEditor<F> {
+ fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
+ debug_assert_usize_is_u64();
+
+ // The write range may not be well-aligned with the chunk boundary. There are various cases
+ // to deal with:
+ // 1. A write of a full 4K chunk.
+ // 2. A write of an incomplete chunk, possibly beyond the original EOF.
+ //
+ // Note that a write beyond EOF can create a hole. But we don't need to handle it here
+ // because holes are zeros, and leaves in MerkleLeaves are hashes of 4096-zeros by
+ // default.
+
+ // Now iterate on the input data, considering the alignment at the destination.
+ for (output_offset, current_size) in
+ ChunkedSizeIter::new(buf.len(), offset, CHUNK_SIZE as usize)
+ {
+ // Lock the tree for the whole write for now. There may be room to improve to increase
+ // throughput.
+ let mut merkle_tree = self.merkle_tree.write().unwrap();
+
+ let offset_in_buf = (output_offset - offset) as usize;
+ let source = &buf[offset_in_buf..offset_in_buf + current_size];
+ let output_chunk_index = (output_offset / CHUNK_SIZE) as usize;
+ let offset_from_alignment = (output_offset % CHUNK_SIZE) as usize;
+
+ let new_hash = match self.new_chunk_hash(
+ source,
+ offset_from_alignment,
+ current_size,
+ output_chunk_index,
+ &mut merkle_tree,
+ ) {
+ Ok(hash) => hash,
+ Err(e) => {
+ // Return early when any error happens before the right. Even if the hash is not
+ // consistent for the current chunk, we can still consider the earlier writes
+ // successful. Note that nothing persistent has been done in this iteration.
+ let written = output_offset - offset;
+ if written > 0 {
+ return Ok(written as usize);
+ }
+ return Err(e);
+ }
+ };
+
+ // A failed, partial write here will make the backing file inconsistent to the (old)
+ // hash. Nothing can be done within this writer, but at least it still maintains the
+ // (original) integrity for the file. To matches what write(2) describes for an error
+ // case (though it's about direct I/O), "Partial data may be written ... should be
+ // considered inconsistent", an error below is propagated.
+ self.file.write_all_at(source, output_offset)?;
+
+ // Update the hash only after the write succeeds. Note that this only attempts to keep
+ // the tree consistent to what has been written regardless the actual state beyond the
+ // writer.
+ let size_at_least = offset.saturating_add(buf.len() as u64);
+ merkle_tree.update_hash(output_chunk_index, &new_hash, size_at_least);
+ }
+ Ok(buf.len())
+ }
+
+ fn resize(&self, size: u64) -> io::Result<()> {
+ debug_assert_usize_is_u64();
+
+ let mut merkle_tree = self.merkle_tree.write().unwrap();
+ // In case when we are truncating the file, we may need to recalculate the hash of the (new)
+ // last chunk. Since the content is provided by the untrusted backend, we need to read the
+ // data back first, verify it, then override the truncated portion with 0-padding for
+ // hashing. As an optimization, we only need to read the data back if the new size isn't a
+ // multiple of CHUNK_SIZE (since the hash is already correct).
+ //
+ // The same thing does not need to happen when the size is growing. Since the new extended
+ // data is always 0, we can just resize the `MerkleLeaves`, where a new hash is always
+ // calculated from 4096 zeros.
+ if size < merkle_tree.file_size() && size % CHUNK_SIZE > 0 {
+ let new_tail_size = (size % CHUNK_SIZE) as usize;
+ let chunk_index = size / CHUNK_SIZE;
+ if new_tail_size > 0 {
+ let mut buf: ChunkBuffer = [0; CHUNK_SIZE as usize];
+ let s = self.read_backing_chunk_verified(chunk_index, &mut buf, &merkle_tree)?;
+ debug_assert!(new_tail_size <= s);
+
+ let zeros = vec![0; CHUNK_SIZE as usize - new_tail_size];
+ let mut ctx = Sha256::new();
+ ctx.update(&buf[..new_tail_size]);
+ ctx.update(&zeros);
+ let new_hash = ctx.finish();
+ merkle_tree.update_hash(chunk_index as usize, &new_hash, size);
+ }
+ }
+
+ self.file.resize(size)?;
+ merkle_tree.resize(size as usize);
+
+ Ok(())
+ }
+}
+
+impl<F: ReadByChunk + RandomWrite> ReadByChunk for VerifiedFileEditor<F> {
+ fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+ let merkle_tree = self.merkle_tree.read().unwrap();
+ self.read_backing_chunk_verified(chunk_index, buf, &merkle_tree)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ // Test data below can be generated by:
+ // $ perl -e 'print "\x{00}" x 6000' > foo
+ // $ perl -e 'print "\x{01}" x 5000' >> foo
+ // $ fsverity digest foo
+ use super::*;
+ use anyhow::Result;
+ use std::cell::RefCell;
+ use std::convert::TryInto;
+
+ struct InMemoryEditor {
+ data: RefCell<Vec<u8>>,
+ fail_read: bool,
+ }
+
+ impl InMemoryEditor {
+ pub fn new() -> InMemoryEditor {
+ InMemoryEditor { data: RefCell::new(Vec::new()), fail_read: false }
+ }
+ }
+
+ impl RandomWrite for InMemoryEditor {
+ fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
+ let begin: usize =
+ offset.try_into().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
+ let end = begin + buf.len();
+ if end > self.data.borrow().len() {
+ self.data.borrow_mut().resize(end, 0);
+ }
+ self.data.borrow_mut().as_mut_slice()[begin..end].copy_from_slice(buf);
+ Ok(buf.len())
+ }
+
+ fn resize(&self, size: u64) -> io::Result<()> {
+ let size: usize =
+ size.try_into().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
+ self.data.borrow_mut().resize(size, 0);
+ Ok(())
+ }
+ }
+
+ impl ReadByChunk for InMemoryEditor {
+ fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+ if self.fail_read {
+ return Err(io::Error::new(io::ErrorKind::Other, "test!"));
+ }
+
+ let borrowed = self.data.borrow();
+ let chunk = &borrowed
+ .chunks(CHUNK_SIZE as usize)
+ .nth(chunk_index as usize)
+ .ok_or_else(|| {
+ io::Error::new(
+ io::ErrorKind::InvalidInput,
+ format!("read_chunk out of bound: index {}", chunk_index),
+ )
+ })?;
+ buf[..chunk.len()].copy_from_slice(chunk);
+ Ok(chunk.len())
+ }
+ }
+
+ #[test]
+ fn test_writer() -> Result<()> {
+ let writer = InMemoryEditor::new();
+ let buf = [1; 4096];
+ assert_eq!(writer.data.borrow().len(), 0);
+
+ assert_eq!(writer.write_at(&buf, 16384)?, 4096);
+ assert_eq!(writer.data.borrow()[16384..16384 + 4096], buf);
+
+ assert_eq!(writer.write_at(&buf, 2048)?, 4096);
+ assert_eq!(writer.data.borrow()[2048..2048 + 4096], buf);
+
+ assert_eq!(writer.data.borrow().len(), 16384 + 4096);
+ Ok(())
+ }
+
+ #[test]
+ fn test_verified_writer_no_write() -> Result<()> {
+ // Verify fs-verity hash without any write.
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("3d248ca542a24fc62d1c43b916eae5016878e2533c88238480b26128a1f1af95")?
+ .as_slice()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_verified_writer_from_zero() -> Result<()> {
+ // Verify a write of a full chunk.
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 4096], 0)?, 4096);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("cd0875ca59c7d37e962c5e8f5acd3770750ac80225e2df652ce5672fd34500af")?
+ .as_slice()
+ );
+
+ // Verify a write of across multiple chunks.
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 4097], 0)?, 4097);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("2901b849fda2d91e3929524561c4a47e77bb64734319759507b2029f18b9cc52")?
+ .as_slice()
+ );
+
+ // Verify another write of across multiple chunks.
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 10000], 0)?, 10000);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("7545409b556071554d18973a29b96409588c7cda4edd00d5586b27a11e1a523b")?
+ .as_slice()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_verified_writer_unaligned() -> Result<()> {
+ // Verify small, unaligned write beyond EOF.
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 5], 3)?, 5);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("a23fc5130d3d7b3323fc4b4a5e79d5d3e9ddf3a3f5872639e867713512c6702f")?
+ .as_slice()
+ );
+
+ // Verify bigger, unaligned write beyond EOF.
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 6000], 4000)?, 6000);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("d16d4c1c186d757e646f76208b21254f50d7f07ea07b1505ff48b2a6f603f989")?
+ .as_slice()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_verified_writer_with_hole() -> Result<()> {
+ // Verify an aligned write beyond EOF with holes.
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 4096], 4096)?, 4096);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("4df2aefd8c2a9101d1d8770dca3ede418232eabce766bb8e020395eae2e97103")?
+ .as_slice()
+ );
+
+ // Verify an unaligned write beyond EOF with holes.
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 5000], 6000)?, 5000);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("47d5da26f6934484e260630a69eb2eebb21b48f69bc8fbf8486d1694b7dba94f")?
+ .as_slice()
+ );
+
+ // Just another example with a small write.
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 5], 16381)?, 5);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("8bd118821fb4aff26bb4b51d485cc481a093c68131b7f4f112e9546198449752")?
+ .as_slice()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_verified_writer_various_writes() -> Result<()> {
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 2048], 0)?, 2048);
+ assert_eq!(file.write_at(&[1; 2048], 4096 + 2048)?, 2048);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("4c433d8640c888b629dc673d318cbb8d93b1eebcc784d9353e07f09f0dcfe707")?
+ .as_slice()
+ );
+ assert_eq!(file.write_at(&[1; 2048], 2048)?, 2048);
+ assert_eq!(file.write_at(&[1; 2048], 4096)?, 2048);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("2a476d58eb80394052a3a783111e1458ac3ecf68a7878183fed86ca0ff47ec0d")?
+ .as_slice()
+ );
+ assert_eq!(file.write_at(&[0; 2048], 2048)?, 2048);
+ assert_eq!(file.write_at(&[0; 2048], 4096)?, 2048);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("4c433d8640c888b629dc673d318cbb8d93b1eebcc784d9353e07f09f0dcfe707")?
+ .as_slice()
+ );
+ assert_eq!(file.write_at(&[1; 4096], 2048)?, 4096);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("2a476d58eb80394052a3a783111e1458ac3ecf68a7878183fed86ca0ff47ec0d")?
+ .as_slice()
+ );
+ assert_eq!(file.write_at(&[1; 2048], 8192)?, 2048);
+ assert_eq!(file.write_at(&[1; 2048], 8192 + 2048)?, 2048);
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("23cbac08371e6ee838ebcc7ae6512b939d2226e802337be7b383c3e046047d24")?
+ .as_slice()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_verified_writer_inconsistent_read() -> Result<()> {
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 8192], 0)?, 8192);
+
+ // Replace the expected hash of the first/0-th chunk. An incomplete write will fail when it
+ // detects the inconsistent read.
+ {
+ let mut merkle_tree = file.merkle_tree.write().unwrap();
+ let overriding_hash = [42; SHA256_HASH_SIZE];
+ merkle_tree.update_hash(0, &overriding_hash, 8192);
+ }
+ assert!(file.write_at(&[1; 1], 2048).is_err());
+
+ // A write of full chunk can still succeed. Also fixed the inconsistency.
+ assert_eq!(file.write_at(&[1; 4096], 4096)?, 4096);
+
+ // Replace the expected hash of the second/1-th chunk. A write range from previous chunk can
+ // still succeed, but returns early due to an inconsistent read but still successfully. A
+ // resumed write will fail since no bytes can be written due to the same inconsistency.
+ {
+ let mut merkle_tree = file.merkle_tree.write().unwrap();
+ let overriding_hash = [42; SHA256_HASH_SIZE];
+ merkle_tree.update_hash(1, &overriding_hash, 8192);
+ }
+ assert_eq!(file.write_at(&[10; 8000], 0)?, 4096);
+ assert!(file.write_at(&[10; 8000 - 4096], 4096).is_err());
+ Ok(())
+ }
+
+ #[test]
+ fn test_verified_writer_failed_read_back() -> Result<()> {
+ let mut writer = InMemoryEditor::new();
+ writer.fail_read = true;
+ let file = VerifiedFileEditor::new(writer);
+ assert_eq!(file.write_at(&[1; 8192], 0)?, 8192);
+
+ // When a read back is needed, a read failure will fail to write.
+ assert!(file.write_at(&[1; 1], 2048).is_err());
+ Ok(())
+ }
+
+ #[test]
+ fn test_resize_to_same_size() -> Result<()> {
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 2048], 0)?, 2048);
+
+ assert!(file.resize(2048).is_ok());
+ assert_eq!(file.size(), 2048);
+
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("fef1b4f19bb7a2cd944d7cdee44d1accb12726389ca5b0f61ac0f548ae40876f")?
+ .as_slice()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_resize_to_grow() -> Result<()> {
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 2048], 0)?, 2048);
+
+ // Resize should grow with 0s.
+ assert!(file.resize(4096).is_ok());
+ assert_eq!(file.size(), 4096);
+
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("9e0e2745c21e4e74065240936d2047340d96a466680c3c9d177b82433e7a0bb1")?
+ .as_slice()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_resize_to_shrink() -> Result<()> {
+ let file = VerifiedFileEditor::new(InMemoryEditor::new());
+ assert_eq!(file.write_at(&[1; 4096], 0)?, 4096);
+
+ // Truncate.
+ file.resize(2048)?;
+ assert_eq!(file.size(), 2048);
+
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("fef1b4f19bb7a2cd944d7cdee44d1accb12726389ca5b0f61ac0f548ae40876f")?
+ .as_slice()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_resize_to_shrink_with_read_failure() -> Result<()> {
+ let mut writer = InMemoryEditor::new();
+ writer.fail_read = true;
+ let file = VerifiedFileEditor::new(writer);
+ assert_eq!(file.write_at(&[1; 4096], 0)?, 4096);
+
+ // A truncate needs a read back. If the read fail, the resize should fail.
+ assert!(file.resize(2048).is_err());
+ Ok(())
+ }
+
+ #[test]
+ fn test_resize_to_shirink_to_chunk_boundary() -> Result<()> {
+ let mut writer = InMemoryEditor::new();
+ writer.fail_read = true;
+ let file = VerifiedFileEditor::new(writer);
+ assert_eq!(file.write_at(&[1; 8192], 0)?, 8192);
+
+ // Truncate to a chunk boundary. A read error doesn't matter since we won't need to
+ // recalcuate the leaf hash.
+ file.resize(4096)?;
+ assert_eq!(file.size(), 4096);
+
+ assert_eq!(
+ file.calculate_fsverity_digest()?,
+ hex::decode("cd0875ca59c7d37e962c5e8f5acd3770750ac80225e2df652ce5672fd34500af")?
+ .as_slice()
+ );
+ Ok(())
+ }
+}
diff --git a/guest/authfs/src/fsverity/metadata/Android.bp b/guest/authfs/src/fsverity/metadata/Android.bp
new file mode 100644
index 0000000..c874c2b
--- /dev/null
+++ b/guest/authfs/src/fsverity/metadata/Android.bp
@@ -0,0 +1,27 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_bindgen {
+ name: "libauthfs_fsverity_metadata_bindgen",
+ wrapper_src: "metadata.hpp",
+ defaults: ["avf_build_flags_rust"],
+ crate_name: "authfs_fsverity_metadata_bindgen",
+ source_stem: "metadata_bindings",
+ apex_available: ["com.android.virt"],
+}
+
+rust_library {
+ name: "libauthfs_fsverity_metadata",
+ crate_name: "authfs_fsverity_metadata",
+ defaults: ["avf_build_flags_rust"],
+ srcs: [
+ "metadata.rs",
+ ],
+ rustlibs: [
+ "libauthfs_fsverity_metadata_bindgen",
+ "libopenssl",
+ ],
+ edition: "2021",
+ apex_available: ["com.android.virt"],
+}
diff --git a/guest/authfs/src/fsverity/metadata/metadata.hpp b/guest/authfs/src/fsverity/metadata/metadata.hpp
new file mode 100644
index 0000000..f05740e
--- /dev/null
+++ b/guest/authfs/src/fsverity/metadata/metadata.hpp
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+#ifndef AUTHFS_FSVERITY_METADATA_H
+#define AUTHFS_FSVERITY_METADATA_H
+
+// This file contains the format of fs-verity metadata (.fsv_meta).
+//
+// The header format of .fsv_meta is:
+//
+// +-----------+---------------------------------------------+------------+
+// | Address | Description | Size |
+// +-----------+---------------------------------------------+------------+
+// | 0x0000 | 32-bit LE, version of the format | 4 |
+// | | | |
+// | 0x0004 | fsverity_descriptor (see linux/fsverity.h) | 256 |
+// | | | |
+// | 0x0104 | 32-bit LE, type of signature | 4 |
+// | | (0: NONE, 1: PKCS7, 2: RAW) | |
+// | | | |
+// | 0x0108 | 32-bit LE, size of signature | 4 |
+// | | | |
+// | 0x010C | signature | See 0x0108 |
+// +-----------+---------------------------------------------+------------+
+//
+// After the header, merkle tree dump exists at the first 4K boundary. Usually it's 0x1000, but it
+// could be, for example, 0x2000 or 0x3000, depending on the size of header.
+//
+// TODO(b/193113326): sync with build/make/tools/releasetools/fsverity_metadata_generator.py
+
+#include <stddef.h>
+#include <stdint.h>
+#include <linux/fsverity.h>
+
+const uint64_t CHUNK_SIZE = 4096;
+
+// Give the macro value a name to export.
+const uint8_t FSVERITY_HASH_ALG_SHA256 = FS_VERITY_HASH_ALG_SHA256;
+
+enum class FSVERITY_SIGNATURE_TYPE : __le32 {
+ NONE = 0,
+ PKCS7 = 1,
+ RAW = 2,
+};
+
+struct fsverity_metadata_header {
+ __le32 version;
+ fsverity_descriptor descriptor;
+ FSVERITY_SIGNATURE_TYPE signature_type;
+ __le32 signature_size;
+} __attribute__((packed));
+
+#endif // AUTHFS_FSVERITY_METADATA_H
diff --git a/guest/authfs/src/fsverity/metadata/metadata.rs b/guest/authfs/src/fsverity/metadata/metadata.rs
new file mode 100644
index 0000000..54d0145
--- /dev/null
+++ b/guest/authfs/src/fsverity/metadata/metadata.rs
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+
+//! Rust bindgen interface for FSVerity Metadata file (.fsv_meta)
+use authfs_fsverity_metadata_bindgen::{
+ fsverity_descriptor, fsverity_metadata_header, FSVERITY_HASH_ALG_SHA256,
+ FSVERITY_SIGNATURE_TYPE_NONE, FSVERITY_SIGNATURE_TYPE_PKCS7, FSVERITY_SIGNATURE_TYPE_RAW,
+};
+
+use openssl::sha::sha256;
+use std::cmp::min;
+use std::ffi::OsString;
+use std::fs::File;
+use std::io::{self, Read, Seek};
+use std::mem::{size_of, zeroed};
+use std::os::unix::fs::{FileExt, MetadataExt};
+use std::path::{Path, PathBuf};
+use std::slice::from_raw_parts_mut;
+
+/// Offset of `descriptor` in `struct fsverity_metadatata_header`.
+const DESCRIPTOR_OFFSET: usize = 4;
+
+/// Structure for parsed metadata.
+pub struct FSVerityMetadata {
+ /// Header for the metadata.
+ pub header: fsverity_metadata_header,
+
+ /// fs-verity digest of the file, with hash algorithm defined in the fs-verity descriptor.
+ pub digest: Vec<u8>,
+
+ /// Optional signature for the metadata.
+ pub signature: Option<Vec<u8>>,
+
+ metadata_file: File,
+
+ merkle_tree_offset: u64,
+}
+
+impl FSVerityMetadata {
+ /// Read the raw Merkle tree from the metadata, if it exists. The API semantics is similar to a
+ /// regular pread(2), and may not return full requested buffer.
+ pub fn read_merkle_tree(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
+ let file_size = self.metadata_file.metadata()?.size();
+ let start = self.merkle_tree_offset + offset;
+ let end = min(file_size, start + buf.len() as u64);
+ let read_size = (end - start) as usize;
+ debug_assert!(read_size <= buf.len());
+ if read_size == 0 {
+ Ok(0)
+ } else {
+ self.metadata_file.read_exact_at(&mut buf[..read_size], start)?;
+ Ok(read_size)
+ }
+ }
+}
+
+/// Common block and page size in Linux.
+pub const CHUNK_SIZE: u64 = authfs_fsverity_metadata_bindgen::CHUNK_SIZE;
+
+/// Derive a path of metadata for a given path.
+/// e.g. "system/framework/foo.jar" -> "system/framework/foo.jar.fsv_meta"
+pub fn get_fsverity_metadata_path(path: &Path) -> PathBuf {
+ let mut os_string: OsString = path.into();
+ os_string.push(".fsv_meta");
+ os_string.into()
+}
+
+/// Parse metadata from given file, and returns a structure for the metadata.
+pub fn parse_fsverity_metadata(mut metadata_file: File) -> io::Result<Box<FSVerityMetadata>> {
+ let (header, digest) = {
+ // SAFETY: The header doesn't include any pointers.
+ let mut header: fsverity_metadata_header = unsafe { zeroed() };
+
+ // SAFETY: fsverity_metadata_header is packed, so reading/write from/to the back_buffer
+ // won't overflow.
+ let back_buffer = unsafe {
+ from_raw_parts_mut(
+ &mut header as *mut fsverity_metadata_header as *mut u8,
+ size_of::<fsverity_metadata_header>(),
+ )
+ };
+ metadata_file.read_exact(back_buffer)?;
+
+ // Digest needs to be calculated with the raw value (without changing the endianness).
+ let digest = match header.descriptor.hash_algorithm {
+ FSVERITY_HASH_ALG_SHA256 => Ok(sha256(
+ &back_buffer
+ [DESCRIPTOR_OFFSET..DESCRIPTOR_OFFSET + size_of::<fsverity_descriptor>()],
+ )
+ .to_vec()),
+ alg => Err(io::Error::new(
+ io::ErrorKind::Other,
+ format!("Unsupported hash algorithm {}, continue (likely failing soon)", alg),
+ )),
+ }?;
+
+ // TODO(inseob): This doesn't seem ideal. Maybe we can consider nom?
+ header.version = u32::from_le(header.version);
+ header.descriptor.data_size = u64::from_le(header.descriptor.data_size);
+ header.signature_type = u32::from_le(header.signature_type);
+ header.signature_size = u32::from_le(header.signature_size);
+ (header, digest)
+ };
+
+ if header.version != 1 {
+ return Err(io::Error::new(io::ErrorKind::Other, "unsupported metadata version"));
+ }
+
+ let signature = match header.signature_type {
+ FSVERITY_SIGNATURE_TYPE_NONE => None,
+ FSVERITY_SIGNATURE_TYPE_PKCS7 | FSVERITY_SIGNATURE_TYPE_RAW => {
+ // TODO: unpad pkcs7?
+ let mut buf = vec![0u8; header.signature_size as usize];
+ metadata_file.read_exact(&mut buf)?;
+ Some(buf)
+ }
+ _ => return Err(io::Error::new(io::ErrorKind::Other, "unknown signature type")),
+ };
+
+ // merkle tree is at the next 4K boundary
+ let merkle_tree_offset =
+ (metadata_file.stream_position()? + CHUNK_SIZE - 1) / CHUNK_SIZE * CHUNK_SIZE;
+
+ Ok(Box::new(FSVerityMetadata { header, digest, signature, metadata_file, merkle_tree_offset }))
+}
diff --git a/guest/authfs/src/fsverity/sys.rs b/guest/authfs/src/fsverity/sys.rs
new file mode 100644
index 0000000..51e10a5
--- /dev/null
+++ b/guest/authfs/src/fsverity/sys.rs
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/// fs-verity version that we are using
+pub const FS_VERITY_VERSION: u8 = 1;
+
+/// Hash algorithm to use from linux/fsverity.h
+pub const FS_VERITY_HASH_ALG_SHA256: u8 = 1;
+
+/// Log 2 of the block size (only 4096 is supported now)
+pub const FS_VERITY_LOG_BLOCKSIZE: u8 = 12;
diff --git a/guest/authfs/src/fsverity/verifier.rs b/guest/authfs/src/fsverity/verifier.rs
new file mode 100644
index 0000000..1434b7e
--- /dev/null
+++ b/guest/authfs/src/fsverity/verifier.rs
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2020 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 libc::EIO;
+use std::io;
+
+use super::common::{build_fsverity_digest, merkle_tree_height, FsverityError, SHA256_HASH_SIZE};
+use crate::common::{divide_roundup, CHUNK_SIZE};
+use crate::file::{ChunkBuffer, ReadByChunk};
+use openssl::sha::{sha256, Sha256};
+
+const ZEROS: [u8; CHUNK_SIZE as usize] = [0u8; CHUNK_SIZE as usize];
+
+type HashBuffer = [u8; SHA256_HASH_SIZE];
+
+fn hash_with_padding(chunk: &[u8], pad_to: usize) -> HashBuffer {
+ let padding_size = pad_to - chunk.len();
+ let mut ctx = Sha256::new();
+ ctx.update(chunk);
+ ctx.update(&ZEROS[..padding_size]);
+ ctx.finish()
+}
+
+fn verity_check<T: ReadByChunk>(
+ chunk: &[u8],
+ chunk_index: u64,
+ file_size: u64,
+ merkle_tree: &T,
+) -> Result<HashBuffer, FsverityError> {
+ // The caller should not be able to produce a chunk at the first place if `file_size` is 0. The
+ // current implementation expects to crash when a `ReadByChunk` implementation reads
+ // beyond the file size, including empty file.
+ assert_ne!(file_size, 0);
+
+ let chunk_hash = hash_with_padding(chunk, CHUNK_SIZE as usize);
+
+ // When the file is smaller or equal to CHUNK_SIZE, the root of Merkle tree is defined as the
+ // hash of the file content, plus padding.
+ if file_size <= CHUNK_SIZE {
+ return Ok(chunk_hash);
+ }
+
+ fsverity_walk(chunk_index, file_size, merkle_tree)?.try_fold(
+ chunk_hash,
+ |actual_hash, result| {
+ let (merkle_chunk, hash_offset_in_chunk) = result?;
+ let expected_hash =
+ &merkle_chunk[hash_offset_in_chunk..hash_offset_in_chunk + SHA256_HASH_SIZE];
+ if actual_hash != expected_hash {
+ return Err(FsverityError::CannotVerify);
+ }
+ Ok(hash_with_padding(&merkle_chunk, CHUNK_SIZE as usize))
+ },
+ )
+}
+
+/// Given a chunk index and the size of the file, returns an iterator that walks the Merkle tree
+/// from the leaf to the root. The iterator carries the slice of the chunk/node as well as the
+/// offset of the child node's hash. It is up to the iterator user to use the node and hash,
+/// e.g. for the actual verification.
+#[allow(clippy::needless_collect)]
+fn fsverity_walk<T: ReadByChunk>(
+ chunk_index: u64,
+ file_size: u64,
+ merkle_tree: &T,
+) -> Result<impl Iterator<Item = Result<([u8; 4096], usize), FsverityError>> + '_, FsverityError> {
+ let hashes_per_node = CHUNK_SIZE / SHA256_HASH_SIZE as u64;
+ debug_assert_eq!(hashes_per_node, 128u64);
+ let max_level = merkle_tree_height(file_size).expect("file should not be empty") as u32;
+ let root_to_leaf_steps = (0..=max_level)
+ .rev()
+ .map(|x| {
+ let leaves_per_hash = hashes_per_node.pow(x);
+ let leaves_size_per_hash = CHUNK_SIZE * leaves_per_hash;
+ let leaves_size_per_node = leaves_size_per_hash * hashes_per_node;
+ let nodes_at_level = divide_roundup(file_size, leaves_size_per_node);
+ let level_size = nodes_at_level * CHUNK_SIZE;
+ let offset_in_level = (chunk_index / leaves_per_hash) * SHA256_HASH_SIZE as u64;
+ (level_size, offset_in_level)
+ })
+ .scan(0, |level_offset, (level_size, offset_in_level)| {
+ let this_level_offset = *level_offset;
+ *level_offset += level_size;
+ let global_hash_offset = this_level_offset + offset_in_level;
+ Some(global_hash_offset)
+ })
+ .map(|global_hash_offset| {
+ let chunk_index = global_hash_offset / CHUNK_SIZE;
+ let hash_offset_in_chunk = (global_hash_offset % CHUNK_SIZE) as usize;
+ (chunk_index, hash_offset_in_chunk)
+ })
+ .collect::<Vec<_>>(); // Needs to collect first to be able to reverse below.
+
+ Ok(root_to_leaf_steps.into_iter().rev().map(move |(chunk_index, hash_offset_in_chunk)| {
+ let mut merkle_chunk = [0u8; 4096];
+ // read_chunk is supposed to return a full chunk, or an incomplete one at the end of the
+ // file. In the incomplete case, the hash is calculated with 0-padding to the chunk size.
+ // Therefore, we don't need to check the returned size here.
+ let _ = merkle_tree.read_chunk(chunk_index, &mut merkle_chunk)?;
+ Ok((merkle_chunk, hash_offset_in_chunk))
+ }))
+}
+
+pub struct VerifiedFileReader<F: ReadByChunk, M: ReadByChunk> {
+ pub file_size: u64,
+ chunked_file: F,
+ merkle_tree: M,
+ root_hash: HashBuffer,
+}
+
+impl<F: ReadByChunk, M: ReadByChunk> VerifiedFileReader<F, M> {
+ pub fn new(
+ chunked_file: F,
+ file_size: u64,
+ expected_digest: &[u8],
+ merkle_tree: M,
+ ) -> Result<VerifiedFileReader<F, M>, FsverityError> {
+ let mut buf = [0u8; CHUNK_SIZE as usize];
+ if file_size <= CHUNK_SIZE {
+ let _size = chunked_file.read_chunk(0, &mut buf)?;
+ // The rest of buffer is 0-padded.
+ } else {
+ let size = merkle_tree.read_chunk(0, &mut buf)?;
+ if buf.len() != size {
+ return Err(FsverityError::InsufficientData(size));
+ }
+ }
+ let root_hash = sha256(&buf[..]);
+ if expected_digest == build_fsverity_digest(&root_hash, file_size) {
+ // Once verified, use the root_hash for verification going forward.
+ Ok(VerifiedFileReader { chunked_file, file_size, merkle_tree, root_hash })
+ } else {
+ Err(FsverityError::InvalidDigest)
+ }
+ }
+}
+
+impl<F: ReadByChunk, M: ReadByChunk> ReadByChunk for VerifiedFileReader<F, M> {
+ fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+ let size = self.chunked_file.read_chunk(chunk_index, buf)?;
+ let root_hash = verity_check(&buf[..size], chunk_index, self.file_size, &self.merkle_tree)
+ .map_err(|_| io::Error::from_raw_os_error(EIO))?;
+ if root_hash != self.root_hash {
+ Err(io::Error::from_raw_os_error(EIO))
+ } else {
+ Ok(size)
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::file::ReadByChunk;
+ use anyhow::Result;
+ use authfs_fsverity_metadata::{parse_fsverity_metadata, FSVerityMetadata};
+ use std::cmp::min;
+ use std::fs::File;
+ use std::os::unix::fs::FileExt;
+
+ struct LocalFileReader {
+ file: File,
+ size: u64,
+ }
+
+ impl LocalFileReader {
+ fn new(file: File) -> io::Result<LocalFileReader> {
+ let size = file.metadata()?.len();
+ Ok(LocalFileReader { file, size })
+ }
+
+ fn len(&self) -> u64 {
+ self.size
+ }
+ }
+
+ impl ReadByChunk for LocalFileReader {
+ fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+ let start = chunk_index * CHUNK_SIZE;
+ if start >= self.size {
+ return Ok(0);
+ }
+ let end = min(self.size, start + CHUNK_SIZE);
+ let read_size = (end - start) as usize;
+ debug_assert!(read_size <= buf.len());
+ self.file.read_exact_at(&mut buf[..read_size], start)?;
+ Ok(read_size)
+ }
+ }
+
+ type LocalVerifiedFileReader = VerifiedFileReader<LocalFileReader, MerkleTreeReader>;
+
+ pub struct MerkleTreeReader {
+ metadata: Box<FSVerityMetadata>,
+ }
+
+ impl ReadByChunk for MerkleTreeReader {
+ fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+ self.metadata.read_merkle_tree(chunk_index * CHUNK_SIZE, buf)
+ }
+ }
+
+ fn total_chunk_number(file_size: u64) -> u64 {
+ (file_size + 4095) / 4096
+ }
+
+ // Returns a reader with fs-verity verification and the file size.
+ fn new_reader_with_fsverity(
+ content_path: &str,
+ metadata_path: &str,
+ ) -> Result<(LocalVerifiedFileReader, u64)> {
+ let file_reader = LocalFileReader::new(File::open(content_path)?)?;
+ let file_size = file_reader.len();
+ let metadata = parse_fsverity_metadata(File::open(metadata_path)?)?;
+ Ok((
+ VerifiedFileReader::new(
+ file_reader,
+ file_size,
+ &metadata.digest.clone(),
+ MerkleTreeReader { metadata },
+ )?,
+ file_size,
+ ))
+ }
+
+ #[test]
+ fn fsverity_verify_full_read_4k() -> Result<()> {
+ let (file_reader, file_size) =
+ new_reader_with_fsverity("testdata/input.4k", "testdata/input.4k.fsv_meta")?;
+
+ for i in 0..total_chunk_number(file_size) {
+ let mut buf = [0u8; 4096];
+ assert!(file_reader.read_chunk(i, &mut buf).is_ok());
+ }
+ Ok(())
+ }
+
+ #[test]
+ fn fsverity_verify_full_read_4k1() -> Result<()> {
+ let (file_reader, file_size) =
+ new_reader_with_fsverity("testdata/input.4k1", "testdata/input.4k1.fsv_meta")?;
+
+ for i in 0..total_chunk_number(file_size) {
+ let mut buf = [0u8; 4096];
+ assert!(file_reader.read_chunk(i, &mut buf).is_ok());
+ }
+ Ok(())
+ }
+
+ #[test]
+ fn fsverity_verify_full_read_4m() -> Result<()> {
+ let (file_reader, file_size) =
+ new_reader_with_fsverity("testdata/input.4m", "testdata/input.4m.fsv_meta")?;
+
+ for i in 0..total_chunk_number(file_size) {
+ let mut buf = [0u8; 4096];
+ assert!(file_reader.read_chunk(i, &mut buf).is_ok());
+ }
+ Ok(())
+ }
+
+ #[test]
+ fn fsverity_verify_bad_merkle_tree() -> Result<()> {
+ let (file_reader, _) = new_reader_with_fsverity(
+ "testdata/input.4m",
+ "testdata/input.4m.fsv_meta.bad_merkle", // First leaf node is corrupted.
+ )?;
+
+ // A lowest broken node (a 4K chunk that contains 128 sha256 hashes) will fail the read
+ // failure of the underlying chunks, but not before or after.
+ let mut buf = [0u8; 4096];
+ let num_hashes = 4096 / 32;
+ let last_index = num_hashes;
+ for i in 0..last_index {
+ assert!(file_reader.read_chunk(i, &mut buf).is_err());
+ }
+ assert!(file_reader.read_chunk(last_index, &mut buf).is_ok());
+ Ok(())
+ }
+}
diff --git a/guest/authfs/src/fusefs.rs b/guest/authfs/src/fusefs.rs
new file mode 100644
index 0000000..618b8ac
--- /dev/null
+++ b/guest/authfs/src/fusefs.rs
@@ -0,0 +1,1075 @@
+/*
+ * 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.
+ */
+
+mod file;
+mod mount;
+
+use anyhow::{anyhow, bail, Result};
+use fuse::filesystem::{
+ Context, DirEntry, DirectoryIterator, Entry, FileSystem, FsOptions, GetxattrReply,
+ SetattrValid, ZeroCopyReader, ZeroCopyWriter,
+};
+use fuse::sys::OpenOptions as FuseOpenOptions;
+use log::{error, trace, warn};
+use std::collections::{btree_map, BTreeMap};
+use std::convert::{TryFrom, TryInto};
+use std::ffi::{CStr, CString, OsStr};
+use std::io;
+use std::mem::{zeroed, MaybeUninit};
+use std::option::Option;
+use std::os::unix::ffi::OsStrExt;
+use std::path::{Component, Path, PathBuf};
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, RwLock};
+use std::time::Duration;
+
+use crate::common::{divide_roundup, ChunkedSizeIter, CHUNK_SIZE};
+use crate::file::{
+ validate_basename, Attr, InMemoryDir, RandomWrite, ReadByChunk, RemoteDirEditor,
+ RemoteFileEditor, RemoteFileReader,
+};
+use crate::fsstat::RemoteFsStatsReader;
+use crate::fsverity::VerifiedFileEditor;
+
+pub use self::file::LazyVerifiedReadonlyFile;
+pub use self::mount::mount_and_enter_message_loop;
+use self::mount::MAX_WRITE_BYTES;
+
+pub type Inode = u64;
+type Handle = u64;
+
+/// Maximum time for a file's metadata to be cached by the kernel. Since any file and directory
+/// changes (if not read-only) has to go through AuthFS to be trusted, the timeout can be maximum.
+const DEFAULT_METADATA_TIMEOUT: Duration = Duration::MAX;
+
+const ROOT_INODE: Inode = 1;
+
+/// `AuthFsEntry` defines the filesystem entry type supported by AuthFS.
+pub enum AuthFsEntry {
+ /// A read-only directory (writable during initialization). Root directory is an example.
+ ReadonlyDirectory { dir: InMemoryDir },
+ /// A file type that is verified against fs-verity signature (thus read-only). The file is
+ /// served from a remote server.
+ VerifiedReadonly { reader: LazyVerifiedReadonlyFile },
+ /// A file type that is a read-only passthrough from a file on a remote server.
+ UnverifiedReadonly { reader: RemoteFileReader, file_size: u64 },
+ /// 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>, attr: Attr },
+ /// 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, attr: Attr },
+}
+
+impl AuthFsEntry {
+ fn expect_empty_deletable_directory(&self) -> io::Result<()> {
+ match self {
+ AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+ if dir.number_of_entries() == 0 {
+ Ok(())
+ } else {
+ Err(io::Error::from_raw_os_error(libc::ENOTEMPTY))
+ }
+ }
+ AuthFsEntry::ReadonlyDirectory { .. } => {
+ Err(io::Error::from_raw_os_error(libc::EACCES))
+ }
+ _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+ }
+ }
+}
+
+struct InodeState {
+ /// Actual inode entry.
+ entry: AuthFsEntry,
+
+ /// Number of `Handle`s (i.e. file descriptors) that are currently referring to the this inode.
+ ///
+ /// Technically, this does not matter to readonly entries, since they live forever. The
+ /// reference count is only needed for manageing lifetime of writable entries like
+ /// `VerifiedNew` and `VerifiedNewDirectory`. That is, when an entry is deleted, the actual
+ /// entry needs to stay alive until the reference count reaches zero.
+ ///
+ /// Note: This is not to be confused with hardlinks, which AuthFS doesn't currently implement.
+ handle_ref_count: AtomicU64,
+
+ /// Whether the inode is already unlinked, i.e. should be removed, once `handle_ref_count` is
+ /// down to zero.
+ unlinked: bool,
+}
+
+impl InodeState {
+ fn new(entry: AuthFsEntry) -> Self {
+ InodeState { entry, handle_ref_count: AtomicU64::new(0), unlinked: false }
+ }
+
+ fn new_with_ref_count(entry: AuthFsEntry, handle_ref_count: u64) -> Self {
+ InodeState { entry, handle_ref_count: AtomicU64::new(handle_ref_count), unlinked: false }
+ }
+}
+
+/// Data type that a directory implementation should be able to present its entry to `AuthFs`.
+#[derive(Clone)]
+pub struct AuthFsDirEntry {
+ pub inode: Inode,
+ pub name: CString,
+ pub is_dir: bool,
+}
+
+/// A snapshot of a directory entries for supporting `readdir` operation.
+///
+/// The `readdir` implementation is required by FUSE to not return any entries that have been
+/// returned previously (while it's fine to not return new entries). Snapshot is the easiest way to
+/// be compliant. See `fuse::filesystem::readdir` for more details.
+///
+/// A `DirEntriesSnapshot` is created on `opendir`, and is associated with the returned
+/// `Handle`/FD. The snapshot is deleted when the handle is released in `releasedir`.
+type DirEntriesSnapshot = Vec<AuthFsDirEntry>;
+
+/// An iterator for reading from `DirEntriesSnapshot`.
+pub struct DirEntriesSnapshotIterator {
+ /// A reference to the `DirEntriesSnapshot` in `AuthFs`.
+ snapshot: Arc<DirEntriesSnapshot>,
+
+ /// A value determined by `Self` to identify the last entry. 0 is a reserved value by FUSE to
+ /// mean reading from the beginning.
+ prev_offset: usize,
+}
+
+impl DirectoryIterator for DirEntriesSnapshotIterator {
+ fn next(&mut self) -> Option<DirEntry> {
+ // This iterator should not be the only reference to the snapshot. The snapshot should
+ // still be hold in `dir_handle_table`, i.e. when the FD is not yet closed.
+ //
+ // This code is unreachable when `readdir` is called with a closed FD. Only when the FD is
+ // not yet closed, `DirEntriesSnapshotIterator` can be created (but still short-lived
+ // during `readdir`).
+ debug_assert!(Arc::strong_count(&self.snapshot) >= 2);
+
+ // Since 0 is reserved, let's use 1-based index for the offset. This allows us to
+ // resume from the previous read in the snapshot easily.
+ let current_offset = if self.prev_offset == 0 {
+ 1 // first element in the vector
+ } else {
+ self.prev_offset + 1 // next element in the vector
+ };
+ if current_offset > self.snapshot.len() {
+ None
+ } else {
+ let AuthFsDirEntry { inode, name, is_dir } = &self.snapshot[current_offset - 1];
+ let entry = DirEntry {
+ offset: current_offset as u64,
+ ino: *inode,
+ name,
+ type_: if *is_dir { libc::DT_DIR.into() } else { libc::DT_REG.into() },
+ };
+ self.prev_offset = current_offset;
+ Some(entry)
+ }
+ }
+}
+
+type DirHandleTable = BTreeMap<Handle, Arc<DirEntriesSnapshot>>;
+
+// AuthFS needs to be `Sync` to be used with the `fuse` crate.
+pub struct AuthFs {
+ /// Table for `Inode` to `InodeState` lookup.
+ inode_table: RwLock<BTreeMap<Inode, InodeState>>,
+
+ /// The next available inode number.
+ next_inode: AtomicU64,
+
+ /// Table for `Handle` to `Arc<DirEntriesSnapshot>` lookup. On `opendir`, a new directory
+ /// handle is created and the snapshot of the current directory is created. This is not
+ /// super efficient, but is the simplest way to be compliant to the FUSE contract (see
+ /// `fuse::filesystem::readdir`).
+ ///
+ /// Currently, no code locks `dir_handle_table` and `inode_table` at the same time to avoid
+ /// deadlock.
+ dir_handle_table: RwLock<DirHandleTable>,
+
+ /// The next available handle number.
+ next_handle: AtomicU64,
+
+ /// A reader to access the remote filesystem stats, which is supposed to be of "the" output
+ /// directory. We assume all output are stored in the same partition.
+ remote_fs_stats_reader: RemoteFsStatsReader,
+}
+
+// Implementation for preparing an `AuthFs` instance, before starting to serve.
+// TODO(victorhsieh): Consider implement a builder to separate the mutable initialization from the
+// immutable / interiorly mutable serving phase.
+impl AuthFs {
+ pub fn new(remote_fs_stats_reader: RemoteFsStatsReader) -> AuthFs {
+ let mut inode_table = BTreeMap::new();
+ inode_table.insert(
+ ROOT_INODE,
+ InodeState::new(AuthFsEntry::ReadonlyDirectory { dir: InMemoryDir::new() }),
+ );
+
+ AuthFs {
+ inode_table: RwLock::new(inode_table),
+ next_inode: AtomicU64::new(ROOT_INODE + 1),
+ dir_handle_table: RwLock::new(BTreeMap::new()),
+ next_handle: AtomicU64::new(1),
+ remote_fs_stats_reader,
+ }
+ }
+
+ /// Add an `AuthFsEntry` as `basename` to the filesystem root.
+ pub fn add_entry_at_root_dir(
+ &mut self,
+ basename: PathBuf,
+ entry: AuthFsEntry,
+ ) -> Result<Inode> {
+ validate_basename(&basename)?;
+ self.add_entry_at_ro_dir_by_path(ROOT_INODE, &basename, entry)
+ }
+
+ /// Add an `AuthFsEntry` by path from the `ReadonlyDirectory` represented by `dir_inode`. The
+ /// path must be a related path. If some ancestor directories do not exist, they will be
+ /// created (also as `ReadonlyDirectory`) automatically.
+ pub fn add_entry_at_ro_dir_by_path(
+ &mut self,
+ dir_inode: Inode,
+ path: &Path,
+ entry: AuthFsEntry,
+ ) -> Result<Inode> {
+ // 1. Make sure the parent directories all exist. Derive the entry's parent inode.
+ let parent_path =
+ path.parent().ok_or_else(|| anyhow!("No parent directory: {:?}", path))?;
+ let parent_inode =
+ parent_path.components().try_fold(dir_inode, |current_dir_inode, path_component| {
+ match path_component {
+ Component::RootDir => bail!("Absolute path is not supported"),
+ Component::Normal(name) => {
+ let inode_table = self.inode_table.get_mut().unwrap();
+ // Locate the internal directory structure.
+ let current_dir_entry = &mut inode_table
+ .get_mut(¤t_dir_inode)
+ .ok_or_else(|| {
+ anyhow!("Unknown directory inode {}", current_dir_inode)
+ })?
+ .entry;
+ let dir = match current_dir_entry {
+ AuthFsEntry::ReadonlyDirectory { dir } => dir,
+ _ => unreachable!("Not a ReadonlyDirectory"),
+ };
+ // Return directory inode. Create first if not exists.
+ if let Some(existing_inode) = dir.lookup_inode(name.as_ref()) {
+ Ok(existing_inode)
+ } else {
+ let new_inode = self.next_inode.fetch_add(1, Ordering::Relaxed);
+ let new_dir_entry =
+ AuthFsEntry::ReadonlyDirectory { dir: InMemoryDir::new() };
+
+ // Actually update the tables.
+ dir.add_dir(name.as_ref(), new_inode)?;
+ if inode_table
+ .insert(new_inode, InodeState::new(new_dir_entry))
+ .is_some()
+ {
+ bail!("Unexpected to find a duplicated inode");
+ }
+ Ok(new_inode)
+ }
+ }
+ _ => Err(anyhow!("Path is not canonical: {:?}", path)),
+ }
+ })?;
+
+ // 2. Insert the entry to the parent directory, as well as the inode table.
+ let inode_table = self.inode_table.get_mut().unwrap();
+ let inode_state = inode_table.get_mut(&parent_inode).expect("previously returned inode");
+ match &mut inode_state.entry {
+ AuthFsEntry::ReadonlyDirectory { dir } => {
+ let basename =
+ path.file_name().ok_or_else(|| anyhow!("Bad file name: {:?}", path))?;
+ let new_inode = self.next_inode.fetch_add(1, Ordering::Relaxed);
+
+ // Actually update the tables.
+ dir.add_file(basename.as_ref(), new_inode)?;
+ if inode_table.insert(new_inode, InodeState::new(entry)).is_some() {
+ bail!("Unexpected to find a duplicated inode");
+ }
+ Ok(new_inode)
+ }
+ _ => unreachable!("Not a ReadonlyDirectory"),
+ }
+ }
+}
+
+// Implementation for serving requests.
+impl AuthFs {
+ /// 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(&AuthFsEntry) -> io::Result<R>,
+ {
+ let inode_table = self.inode_table.read().unwrap();
+ handle_inode_locked(&inode_table, inode, |inode_state| handle_fn(&inode_state.entry))
+ }
+
+ /// Adds a new entry `name` created by `create_fn` at `parent_inode`, with an initial ref count
+ /// of one.
+ ///
+ /// The operation involves two updates: adding the name with a new allocated inode to the
+ /// parent directory, and insert the new inode and the actual `AuthFsEntry` to the global inode
+ /// table.
+ ///
+ /// `create_fn` receives the parent directory, through which it can create the new entry at and
+ /// register the new inode to. Its returned entry is then added to the inode table.
+ fn create_new_entry_with_ref_count<F>(
+ &self,
+ parent_inode: Inode,
+ name: &CStr,
+ create_fn: F,
+ ) -> io::Result<Inode>
+ where
+ F: FnOnce(&mut AuthFsEntry, &Path, Inode) -> io::Result<AuthFsEntry>,
+ {
+ let mut inode_table = self.inode_table.write().unwrap();
+ let (new_inode, new_file_entry) = handle_inode_mut_locked(
+ &mut inode_table,
+ &parent_inode,
+ |InodeState { entry, .. }| {
+ let new_inode = self.next_inode.fetch_add(1, Ordering::Relaxed);
+ let basename: &Path = cstr_to_path(name);
+ let new_file_entry = create_fn(entry, basename, new_inode)?;
+ Ok((new_inode, new_file_entry))
+ },
+ )?;
+
+ if let btree_map::Entry::Vacant(entry) = inode_table.entry(new_inode) {
+ entry.insert(InodeState::new_with_ref_count(new_file_entry, 1));
+ Ok(new_inode)
+ } else {
+ unreachable!("Unexpected duplication of inode {}", new_inode);
+ }
+ }
+
+ fn open_dir_store_snapshot(
+ &self,
+ dir_entries: Vec<AuthFsDirEntry>,
+ ) -> io::Result<(Option<Handle>, FuseOpenOptions)> {
+ let handle = self.next_handle.fetch_add(1, Ordering::Relaxed);
+ let mut dir_handle_table = self.dir_handle_table.write().unwrap();
+ if let btree_map::Entry::Vacant(value) = dir_handle_table.entry(handle) {
+ value.insert(Arc::new(dir_entries));
+ Ok((Some(handle), FuseOpenOptions::empty()))
+ } else {
+ unreachable!("Unexpected to see new handle {} to existing in the table", handle);
+ }
+ }
+}
+
+fn check_access_mode(flags: u32, mode: libc::c_int) -> io::Result<()> {
+ if (flags & libc::O_ACCMODE as u32) == mode as u32 {
+ Ok(())
+ } else {
+ Err(io::Error::from_raw_os_error(libc::EACCES))
+ }
+}
+
+cfg_if::cfg_if! {
+ if #[cfg(all(any(target_arch = "aarch64", target_arch = "riscv64"),
+ target_pointer_width = "64"))] {
+ fn blk_size() -> libc::c_int { CHUNK_SIZE as libc::c_int }
+ } else {
+ fn blk_size() -> libc::c_long { CHUNK_SIZE as libc::c_long }
+ }
+}
+
+#[allow(clippy::enum_variant_names)]
+enum AccessMode {
+ ReadOnly,
+ Variable(u32),
+}
+
+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 access_mode {
+ AccessMode::ReadOnly => {
+ // Until needed, let's just grant the owner access.
+ libc::S_IFREG | libc::S_IRUSR
+ }
+ AccessMode::Variable(mode) => libc::S_IFREG | mode,
+ };
+ st.st_nlink = 1;
+ st.st_uid = 0;
+ st.st_gid = 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();
+ // Per man stat(2), st_blocks is "Number of 512B blocks allocated".
+ st.st_blocks = libc::c_longlong::try_from(divide_roundup(file_size, 512))
+ .map_err(|_| io::Error::from_raw_os_error(libc::EFBIG))?;
+ Ok(st)
+}
+
+fn create_dir_stat(
+ ino: libc::ino_t,
+ file_number: u16,
+ 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 access_mode {
+ AccessMode::ReadOnly => {
+ // Until needed, let's just grant the owner access and search to group and others.
+ libc::S_IFDIR | libc::S_IXUSR | libc::S_IRUSR | libc::S_IXGRP | libc::S_IXOTH
+ }
+ AccessMode::Variable(mode) => libc::S_IFDIR | mode,
+ };
+
+ // 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
+}
+
+fn read_chunks<W: io::Write, T: ReadByChunk>(
+ mut w: W,
+ file: &T,
+ file_size: u64,
+ offset: u64,
+ size: u32,
+) -> io::Result<usize> {
+ let remaining = file_size.saturating_sub(offset);
+ let size_to_read = std::cmp::min(size as usize, remaining as usize);
+ let total = ChunkedSizeIter::new(size_to_read, offset, CHUNK_SIZE as usize).try_fold(
+ 0,
+ |total, (current_offset, planned_data_size)| {
+ // TODO(victorhsieh): There might be a non-trivial way to avoid this copy. For example,
+ // instead of accepting a buffer, the writer could expose the final destination buffer
+ // for the reader to write to. It might not be generally applicable though, e.g. with
+ // virtio transport, the buffer may not be continuous.
+ let mut buf = [0u8; CHUNK_SIZE as usize];
+ let read_size = file.read_chunk(offset_to_chunk_index(current_offset), &mut buf)?;
+ if read_size < planned_data_size {
+ return Err(io::Error::from_raw_os_error(libc::ENODATA));
+ }
+
+ let begin = (current_offset % CHUNK_SIZE) as usize;
+ let end = begin + planned_data_size;
+ let s = w.write(&buf[begin..end])?;
+ if s != planned_data_size {
+ return Err(io::Error::from_raw_os_error(libc::EIO));
+ }
+ Ok(total + s)
+ },
+ )?;
+
+ Ok(total)
+}
+
+impl FileSystem for AuthFs {
+ type Inode = Inode;
+ type Handle = Handle;
+ type DirIter = DirEntriesSnapshotIterator;
+
+ fn max_buffer_size(&self) -> u32 {
+ MAX_WRITE_BYTES
+ }
+
+ fn init(&self, _capable: FsOptions) -> io::Result<FsOptions> {
+ // Enable writeback cache for better performance especially since our bandwidth to the
+ // backend service is limited.
+ Ok(FsOptions::WRITEBACK_CACHE)
+ }
+
+ fn lookup(&self, _ctx: Context, parent: Inode, name: &CStr) -> io::Result<Entry> {
+ let inode_table = self.inode_table.read().unwrap();
+
+ // Look up the entry's inode number in parent directory.
+ let inode =
+ handle_inode_locked(&inode_table, &parent, |inode_state| match &inode_state.entry {
+ AuthFsEntry::ReadonlyDirectory { dir } => {
+ let path = cstr_to_path(name);
+ dir.lookup_inode(path).ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))
+ }
+ AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+ let path = cstr_to_path(name);
+ dir.find_inode(path)
+ }
+ _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+ })?;
+
+ // Create the entry's stat if found.
+ let st = handle_inode_locked(
+ &inode_table,
+ &inode,
+ |InodeState { entry, handle_ref_count, .. }| {
+ let st = match entry {
+ AuthFsEntry::ReadonlyDirectory { dir } => {
+ create_dir_stat(inode, dir.number_of_entries(), AccessMode::ReadOnly)
+ }
+ AuthFsEntry::UnverifiedReadonly { file_size, .. } => {
+ create_stat(inode, *file_size, AccessMode::ReadOnly)
+ }
+ AuthFsEntry::VerifiedReadonly { reader } => {
+ create_stat(inode, reader.file_size()?, AccessMode::ReadOnly)
+ }
+ AuthFsEntry::VerifiedNew { editor, attr, .. } => {
+ create_stat(inode, editor.size(), AccessMode::Variable(attr.mode()))
+ }
+ AuthFsEntry::VerifiedNewDirectory { dir, attr } => create_dir_stat(
+ inode,
+ dir.number_of_entries(),
+ AccessMode::Variable(attr.mode()),
+ ),
+ }?;
+ if handle_ref_count.fetch_add(1, Ordering::Relaxed) == u64::MAX {
+ panic!("Handle reference count overflow");
+ }
+ Ok(st)
+ },
+ )?;
+
+ Ok(Entry {
+ inode,
+ generation: 0,
+ attr: st,
+ entry_timeout: DEFAULT_METADATA_TIMEOUT,
+ attr_timeout: DEFAULT_METADATA_TIMEOUT,
+ })
+ }
+
+ fn forget(&self, _ctx: Context, inode: Self::Inode, count: u64) {
+ let mut inode_table = self.inode_table.write().unwrap();
+ let delete_now = handle_inode_mut_locked(
+ &mut inode_table,
+ &inode,
+ |InodeState { handle_ref_count, unlinked, .. }| {
+ let current = handle_ref_count.get_mut();
+ if count > *current {
+ error!(
+ "Trying to decrease refcount of inode {} by {} (> current {})",
+ inode, count, *current
+ );
+ panic!(); // log to logcat with error!
+ }
+ *current -= count;
+ Ok(*unlinked && *current == 0)
+ },
+ );
+
+ match delete_now {
+ Ok(true) => {
+ let _ignored = inode_table.remove(&inode).expect("Removed an existing entry");
+ }
+ Ok(false) => { /* Let the inode stay */ }
+ Err(e) => {
+ warn!(
+ "Unexpected failure when tries to forget an inode {} by refcount {}: {:?}",
+ inode, count, e
+ );
+ }
+ }
+ }
+
+ fn getattr(
+ &self,
+ _ctx: Context,
+ inode: Inode,
+ _handle: Option<Handle>,
+ ) -> io::Result<(libc::stat64, Duration)> {
+ self.handle_inode(&inode, |config| {
+ Ok((
+ match config {
+ AuthFsEntry::ReadonlyDirectory { dir } => {
+ create_dir_stat(inode, dir.number_of_entries(), AccessMode::ReadOnly)
+ }
+ AuthFsEntry::UnverifiedReadonly { file_size, .. } => {
+ create_stat(inode, *file_size, AccessMode::ReadOnly)
+ }
+ AuthFsEntry::VerifiedReadonly { reader } => {
+ create_stat(inode, reader.file_size()?, AccessMode::ReadOnly)
+ }
+ AuthFsEntry::VerifiedNew { editor, attr, .. } => {
+ create_stat(inode, editor.size(), AccessMode::Variable(attr.mode()))
+ }
+ AuthFsEntry::VerifiedNewDirectory { dir, attr } => create_dir_stat(
+ inode,
+ dir.number_of_entries(),
+ AccessMode::Variable(attr.mode()),
+ ),
+ }?,
+ DEFAULT_METADATA_TIMEOUT,
+ ))
+ })
+ }
+
+ fn open(
+ &self,
+ _ctx: Context,
+ inode: Self::Inode,
+ flags: u32,
+ ) -> io::Result<(Option<Self::Handle>, FuseOpenOptions)> {
+ // Since file handle is not really used in later operations (which use Inode directly),
+ // return None as the handle.
+ self.handle_inode(&inode, |config| {
+ match config {
+ AuthFsEntry::VerifiedReadonly { .. } | AuthFsEntry::UnverifiedReadonly { .. } => {
+ check_access_mode(flags, libc::O_RDONLY)?;
+ }
+ AuthFsEntry::VerifiedNew { .. } => {
+ // TODO(victorhsieh): Imeplement ACL check using the attr and ctx. Always allow
+ // for now.
+ }
+ AuthFsEntry::ReadonlyDirectory { .. }
+ | 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.
+ Ok((None, FuseOpenOptions::KEEP_CACHE))
+ })
+ }
+
+ fn create(
+ &self,
+ _ctx: Context,
+ parent: Self::Inode,
+ name: &CStr,
+ mode: u32,
+ _flags: u32,
+ umask: u32,
+ _security_ctx: Option<&CStr>,
+ ) -> io::Result<(Entry, Option<Self::Handle>, FuseOpenOptions)> {
+ let new_inode = self.create_new_entry_with_ref_count(
+ parent,
+ name,
+ |parent_entry, basename, new_inode| match parent_entry {
+ AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+ if dir.has_entry(basename) {
+ return Err(io::Error::from_raw_os_error(libc::EEXIST));
+ }
+ let mode = mode & !umask;
+ let (new_file, new_attr) = dir.create_file(basename, new_inode, mode)?;
+ Ok(AuthFsEntry::VerifiedNew { editor: new_file, attr: new_attr })
+ }
+ _ => 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::Variable(mode))?,
+ entry_timeout: DEFAULT_METADATA_TIMEOUT,
+ attr_timeout: DEFAULT_METADATA_TIMEOUT,
+ },
+ // See also `open`.
+ /* handle */ None,
+ FuseOpenOptions::KEEP_CACHE,
+ ))
+ }
+
+ fn read<W: io::Write + ZeroCopyWriter>(
+ &self,
+ _ctx: Context,
+ inode: Inode,
+ _handle: Handle,
+ w: W,
+ size: u32,
+ offset: u64,
+ _lock_owner: Option<u64>,
+ _flags: u32,
+ ) -> io::Result<usize> {
+ self.handle_inode(&inode, |config| {
+ match config {
+ AuthFsEntry::VerifiedReadonly { reader } => {
+ read_chunks(w, reader, reader.file_size()?, offset, size)
+ }
+ AuthFsEntry::UnverifiedReadonly { reader, file_size } => {
+ read_chunks(w, reader, *file_size, offset, size)
+ }
+ 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)
+ }
+ AuthFsEntry::ReadonlyDirectory { .. }
+ | AuthFsEntry::VerifiedNewDirectory { .. } => {
+ Err(io::Error::from_raw_os_error(libc::EISDIR))
+ }
+ }
+ })
+ }
+
+ fn write<R: io::Read + ZeroCopyReader>(
+ &self,
+ _ctx: Context,
+ inode: Self::Inode,
+ _handle: Self::Handle,
+ mut r: R,
+ size: u32,
+ offset: u64,
+ _lock_owner: Option<u64>,
+ _delayed_write: bool,
+ _flags: u32,
+ ) -> io::Result<usize> {
+ 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)
+ }
+ AuthFsEntry::VerifiedReadonly { .. } | AuthFsEntry::UnverifiedReadonly { .. } => {
+ Err(io::Error::from_raw_os_error(libc::EPERM))
+ }
+ AuthFsEntry::ReadonlyDirectory { .. } | AuthFsEntry::VerifiedNewDirectory { .. } => {
+ Err(io::Error::from_raw_os_error(libc::EISDIR))
+ }
+ })
+ }
+
+ fn setattr(
+ &self,
+ _ctx: Context,
+ inode: Inode,
+ in_attr: libc::stat64,
+ _handle: Option<Handle>,
+ valid: SetattrValid,
+ ) -> io::Result<(libc::stat64, Duration)> {
+ let mut inode_table = self.inode_table.write().unwrap();
+ handle_inode_mut_locked(&mut inode_table, &inode, |InodeState { entry, .. }| match entry {
+ AuthFsEntry::VerifiedNew { editor, attr } => {
+ check_unsupported_setattr_request(valid)?;
+
+ // Initialize the default stat.
+ let mut new_attr =
+ create_stat(inode, editor.size(), AccessMode::Variable(attr.mode()))?;
+ // `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
+ // negative size.
+ debug_assert!(in_attr.st_size >= 0);
+ new_attr.st_size = in_attr.st_size;
+ editor.resize(in_attr.st_size as u64)?;
+ }
+ if valid.contains(SetattrValid::MODE) {
+ attr.set_mode(in_attr.st_mode)?;
+ new_attr.st_mode = in_attr.st_mode;
+ }
+ Ok((new_attr, DEFAULT_METADATA_TIMEOUT))
+ }
+ AuthFsEntry::VerifiedNewDirectory { dir, attr } => {
+ check_unsupported_setattr_request(valid)?;
+ if valid.contains(SetattrValid::SIZE) {
+ return Err(io::Error::from_raw_os_error(libc::EISDIR));
+ }
+
+ // Initialize the default stat.
+ let mut new_attr = create_dir_stat(
+ inode,
+ dir.number_of_entries(),
+ AccessMode::Variable(attr.mode()),
+ )?;
+ if valid.contains(SetattrValid::MODE) {
+ attr.set_mode(in_attr.st_mode)?;
+ new_attr.st_mode = in_attr.st_mode;
+ }
+ Ok((new_attr, DEFAULT_METADATA_TIMEOUT))
+ }
+ _ => Err(io::Error::from_raw_os_error(libc::EPERM)),
+ })
+ }
+
+ fn getxattr(
+ &self,
+ _ctx: Context,
+ inode: Self::Inode,
+ name: &CStr,
+ size: u32,
+ ) -> io::Result<GetxattrReply> {
+ self.handle_inode(&inode, |config| {
+ match config {
+ 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.
+ if name != CStr::from_bytes_with_nul(b"authfs.fsverity.digest\0").unwrap() {
+ return Err(io::Error::from_raw_os_error(libc::ENODATA));
+ }
+
+ if size == 0 {
+ // Per protocol, when size is 0, return the value size.
+ Ok(GetxattrReply::Count(editor.get_fsverity_digest_size() as u32))
+ } else {
+ let digest = editor.calculate_fsverity_digest()?;
+ if digest.len() > size as usize {
+ Err(io::Error::from_raw_os_error(libc::ERANGE))
+ } else {
+ Ok(GetxattrReply::Value(digest.to_vec()))
+ }
+ }
+ }
+ _ => Err(io::Error::from_raw_os_error(libc::ENODATA)),
+ }
+ })
+ }
+
+ fn mkdir(
+ &self,
+ _ctx: Context,
+ parent: Self::Inode,
+ name: &CStr,
+ mode: u32,
+ umask: u32,
+ _security_ctx: Option<&CStr>,
+ ) -> io::Result<Entry> {
+ let new_inode = self.create_new_entry_with_ref_count(
+ parent,
+ name,
+ |parent_entry, basename, new_inode| match parent_entry {
+ AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+ if dir.has_entry(basename) {
+ return Err(io::Error::from_raw_os_error(libc::EEXIST));
+ }
+ let mode = mode & !umask;
+ let (new_dir, new_attr) = dir.mkdir(basename, new_inode, mode)?;
+ Ok(AuthFsEntry::VerifiedNewDirectory { dir: new_dir, attr: new_attr })
+ }
+ AuthFsEntry::ReadonlyDirectory { .. } => {
+ Err(io::Error::from_raw_os_error(libc::EACCES))
+ }
+ _ => 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, AccessMode::Variable(mode))?,
+ entry_timeout: DEFAULT_METADATA_TIMEOUT,
+ attr_timeout: DEFAULT_METADATA_TIMEOUT,
+ })
+ }
+
+ fn unlink(&self, _ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<()> {
+ let mut inode_table = self.inode_table.write().unwrap();
+ handle_inode_mut_locked(
+ &mut inode_table,
+ &parent,
+ |InodeState { entry, unlinked, .. }| match entry {
+ AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+ let basename: &Path = cstr_to_path(name);
+ // Delete the file from in both the local and remote directories.
+ let _inode = dir.delete_file(basename)?;
+ *unlinked = true;
+ Ok(())
+ }
+ AuthFsEntry::ReadonlyDirectory { .. } => {
+ Err(io::Error::from_raw_os_error(libc::EACCES))
+ }
+ AuthFsEntry::VerifiedNew { .. } => {
+ // Deleting a entry in filesystem root is not currently supported.
+ Err(io::Error::from_raw_os_error(libc::ENOSYS))
+ }
+ AuthFsEntry::UnverifiedReadonly { .. } | AuthFsEntry::VerifiedReadonly { .. } => {
+ Err(io::Error::from_raw_os_error(libc::ENOTDIR))
+ }
+ },
+ )
+ }
+
+ fn rmdir(&self, _ctx: Context, parent: Self::Inode, name: &CStr) -> io::Result<()> {
+ let mut inode_table = self.inode_table.write().unwrap();
+
+ // Check before actual removal, with readonly borrow.
+ handle_inode_locked(&inode_table, &parent, |inode_state| match &inode_state.entry {
+ AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+ let basename: &Path = cstr_to_path(name);
+ let existing_inode = dir.find_inode(basename)?;
+ handle_inode_locked(&inode_table, &existing_inode, |inode_state| {
+ inode_state.entry.expect_empty_deletable_directory()
+ })
+ }
+ AuthFsEntry::ReadonlyDirectory { .. } => {
+ Err(io::Error::from_raw_os_error(libc::EACCES))
+ }
+ _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+ })?;
+
+ // Look up again, this time with mutable borrow. This needs to be done separately because
+ // the previous lookup needs to borrow multiple entry references in the table.
+ handle_inode_mut_locked(
+ &mut inode_table,
+ &parent,
+ |InodeState { entry, unlinked, .. }| match entry {
+ AuthFsEntry::VerifiedNewDirectory { dir, .. } => {
+ let basename: &Path = cstr_to_path(name);
+ let _inode = dir.force_delete_directory(basename)?;
+ *unlinked = true;
+ Ok(())
+ }
+ _ => unreachable!("Mismatched entry type that is just checked"),
+ },
+ )
+ }
+
+ fn opendir(
+ &self,
+ _ctx: Context,
+ inode: Self::Inode,
+ _flags: u32,
+ ) -> io::Result<(Option<Self::Handle>, FuseOpenOptions)> {
+ let entries = self.handle_inode(&inode, |config| match config {
+ AuthFsEntry::VerifiedNewDirectory { dir, .. } => dir.retrieve_entries(),
+ AuthFsEntry::ReadonlyDirectory { dir } => dir.retrieve_entries(),
+ _ => Err(io::Error::from_raw_os_error(libc::ENOTDIR)),
+ })?;
+ self.open_dir_store_snapshot(entries)
+ }
+
+ fn readdir(
+ &self,
+ _ctx: Context,
+ _inode: Self::Inode,
+ handle: Self::Handle,
+ _size: u32,
+ offset: u64,
+ ) -> io::Result<Self::DirIter> {
+ let dir_handle_table = self.dir_handle_table.read().unwrap();
+ if let Some(entry) = dir_handle_table.get(&handle) {
+ Ok(DirEntriesSnapshotIterator {
+ snapshot: entry.clone(),
+ prev_offset: offset.try_into().unwrap(),
+ })
+ } else {
+ Err(io::Error::from_raw_os_error(libc::EBADF))
+ }
+ }
+
+ fn releasedir(
+ &self,
+ _ctx: Context,
+ inode: Self::Inode,
+ _flags: u32,
+ handle: Self::Handle,
+ ) -> io::Result<()> {
+ let mut dir_handle_table = self.dir_handle_table.write().unwrap();
+ if dir_handle_table.remove(&handle).is_none() {
+ unreachable!("Unknown directory handle {}, inode {}", handle, inode);
+ }
+ Ok(())
+ }
+
+ fn statfs(&self, _ctx: Context, _inode: Self::Inode) -> io::Result<libc::statvfs64> {
+ let remote_stat = self.remote_fs_stats_reader.statfs()?;
+
+ // SAFETY: We are zero-initializing a struct with only POD fields. Not all fields matter to
+ // FUSE. See also:
+ // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/fuse/inode.c?h=v5.15#n460
+ let mut st: libc::statvfs64 = unsafe { zeroed() };
+
+ // Use the remote stat as a template, since it'd matter the most to consider the writable
+ // files/directories that are written to the remote.
+ st.f_bsize = remote_stat.block_size;
+ st.f_frsize = remote_stat.fragment_size;
+ st.f_blocks = remote_stat.block_numbers;
+ st.f_bavail = remote_stat.block_available;
+ st.f_favail = remote_stat.inodes_available;
+ st.f_namemax = remote_stat.max_filename;
+ // Assuming we are not privileged to use all free spaces on the remote server, set the free
+ // blocks/fragment to the same available amount.
+ st.f_bfree = st.f_bavail;
+ st.f_ffree = st.f_favail;
+ // Number of inodes on the filesystem
+ st.f_files = self.inode_table.read().unwrap().len() as u64;
+
+ Ok(st)
+ }
+}
+
+fn handle_inode_locked<F, R>(
+ inode_table: &BTreeMap<Inode, InodeState>,
+ inode: &Inode,
+ handle_fn: F,
+) -> io::Result<R>
+where
+ F: FnOnce(&InodeState) -> io::Result<R>,
+{
+ if let Some(inode_state) = inode_table.get(inode) {
+ handle_fn(inode_state)
+ } else {
+ Err(io::Error::from_raw_os_error(libc::ENOENT))
+ }
+}
+
+fn handle_inode_mut_locked<F, R>(
+ inode_table: &mut BTreeMap<Inode, InodeState>,
+ inode: &Inode,
+ handle_fn: F,
+) -> io::Result<R>
+where
+ F: FnOnce(&mut InodeState) -> io::Result<R>,
+{
+ if let Some(inode_state) = inode_table.get_mut(inode) {
+ handle_fn(inode_state)
+ } else {
+ Err(io::Error::from_raw_os_error(libc::ENOENT))
+ }
+}
+
+fn check_unsupported_setattr_request(valid: SetattrValid) -> io::Result<()> {
+ if valid.contains(SetattrValid::UID) {
+ warn!("Changing st_uid is not currently supported");
+ return Err(io::Error::from_raw_os_error(libc::ENOSYS));
+ }
+ if valid.contains(SetattrValid::GID) {
+ warn!("Changing st_gid is not currently supported");
+ return Err(io::Error::from_raw_os_error(libc::ENOSYS));
+ }
+ if valid.intersects(
+ SetattrValid::CTIME
+ | SetattrValid::ATIME
+ | SetattrValid::ATIME_NOW
+ | SetattrValid::MTIME
+ | SetattrValid::MTIME_NOW,
+ ) {
+ trace!("Ignoring ctime/atime/mtime change as authfs does not maintain timestamp currently");
+ }
+ Ok(())
+}
+
+fn cstr_to_path(cstr: &CStr) -> &Path {
+ OsStr::from_bytes(cstr.to_bytes()).as_ref()
+}
diff --git a/guest/authfs/src/fusefs/file.rs b/guest/authfs/src/fusefs/file.rs
new file mode 100644
index 0000000..8c02281
--- /dev/null
+++ b/guest/authfs/src/fusefs/file.rs
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 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 log::error;
+use std::convert::TryInto;
+use std::io;
+use std::path::PathBuf;
+use std::sync::Mutex;
+
+use crate::file::{
+ ChunkBuffer, EagerChunkReader, ReadByChunk, RemoteFileReader, RemoteMerkleTreeReader,
+ VirtFdService,
+};
+use crate::fsverity::{merkle_tree_size, VerifiedFileReader};
+
+enum FileInfo {
+ ByPathUnderDirFd(i32, PathBuf),
+ ByFd(i32),
+}
+
+type Reader = VerifiedFileReader<RemoteFileReader, EagerChunkReader>;
+
+/// A lazily created read-only file that is verified against the given fs-verity digest.
+///
+/// The main purpose of this struct is to wrap and construct `VerifiedFileReader` lazily.
+pub struct LazyVerifiedReadonlyFile {
+ expected_digest: Vec<u8>,
+
+ service: VirtFdService,
+ file_info: FileInfo,
+
+ /// A lazily instantiated reader.
+ reader: Mutex<Option<Reader>>,
+}
+
+impl LazyVerifiedReadonlyFile {
+ /// Prepare the file by a remote path, related to a remote directory FD.
+ pub fn prepare_by_path(
+ service: VirtFdService,
+ remote_dir_fd: i32,
+ remote_path: PathBuf,
+ expected_digest: Vec<u8>,
+ ) -> Self {
+ LazyVerifiedReadonlyFile {
+ service,
+ file_info: FileInfo::ByPathUnderDirFd(remote_dir_fd, remote_path),
+ expected_digest,
+ reader: Mutex::new(None),
+ }
+ }
+
+ /// Prepare the file by a remote file FD.
+ pub fn prepare_by_fd(service: VirtFdService, remote_fd: i32, expected_digest: Vec<u8>) -> Self {
+ LazyVerifiedReadonlyFile {
+ service,
+ file_info: FileInfo::ByFd(remote_fd),
+ expected_digest,
+ reader: Mutex::new(None),
+ }
+ }
+
+ fn ensure_init_then<F, T>(&self, callback: F) -> io::Result<T>
+ where
+ F: FnOnce(&Reader) -> io::Result<T>,
+ {
+ let mut reader = self.reader.lock().unwrap();
+ if reader.is_none() {
+ let remote_file = match &self.file_info {
+ FileInfo::ByPathUnderDirFd(dir_fd, related_path) => {
+ RemoteFileReader::new_by_path(self.service.clone(), *dir_fd, related_path)?
+ }
+ FileInfo::ByFd(file_fd) => RemoteFileReader::new(self.service.clone(), *file_fd),
+ };
+ let remote_fd = remote_file.get_remote_fd();
+ let file_size = self
+ .service
+ .getFileSize(remote_fd)
+ .map_err(|e| {
+ error!("Failed to get file size of remote fd {}: {}", remote_fd, e);
+ io::Error::from_raw_os_error(libc::EIO)
+ })?
+ .try_into()
+ .map_err(|e| {
+ error!("Failed convert file size: {}", e);
+ io::Error::from_raw_os_error(libc::EIO)
+ })?;
+ let instance = VerifiedFileReader::new(
+ remote_file,
+ file_size,
+ &self.expected_digest,
+ EagerChunkReader::new(
+ RemoteMerkleTreeReader::new(self.service.clone(), remote_fd),
+ merkle_tree_size(file_size),
+ )?,
+ )
+ .map_err(|e| {
+ error!("Failed instantiate a verified file reader: {}", e);
+ io::Error::from_raw_os_error(libc::EIO)
+ })?;
+ *reader = Some(instance);
+ }
+ callback(reader.as_ref().unwrap())
+ }
+
+ pub fn file_size(&self) -> io::Result<u64> {
+ self.ensure_init_then(|reader| Ok(reader.file_size))
+ }
+}
+
+impl ReadByChunk for LazyVerifiedReadonlyFile {
+ fn read_chunk(&self, chunk_index: u64, buf: &mut ChunkBuffer) -> io::Result<usize> {
+ self.ensure_init_then(|reader| reader.read_chunk(chunk_index, buf))
+ }
+}
diff --git a/guest/authfs/src/fusefs/mount.rs b/guest/authfs/src/fusefs/mount.rs
new file mode 100644
index 0000000..7f8bac1
--- /dev/null
+++ b/guest/authfs/src/fusefs/mount.rs
@@ -0,0 +1,73 @@
+/*
+ * 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 fuse::mount::MountOption;
+use std::fs::OpenOptions;
+use std::num::NonZeroU8;
+use std::os::unix::io::AsRawFd;
+use std::path::Path;
+
+use super::AuthFs;
+
+/// Maximum bytes (excluding the FUSE header) `AuthFs` will receive from the kernel for write
+/// operations by another process.
+pub const MAX_WRITE_BYTES: u32 = 65536;
+
+/// Maximum bytes (excluding the FUSE header) `AuthFs` will receive from the kernel for read
+/// operations by another process.
+/// TODO(victorhsieh): This option is deprecated by FUSE. Figure out if we can remove this.
+const MAX_READ_BYTES: u32 = 65536;
+
+/// Mount and start the FUSE instance to handle messages. This requires CAP_SYS_ADMIN.
+pub fn mount_and_enter_message_loop(
+ authfs: AuthFs,
+ mountpoint: &Path,
+ extra_options: &Option<String>,
+ threads: Option<NonZeroU8>,
+) -> Result<(), fuse::Error> {
+ let dev_fuse = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open("/dev/fuse")
+ .expect("Failed to open /dev/fuse");
+
+ let mut mount_options = vec![
+ MountOption::FD(dev_fuse.as_raw_fd()),
+ MountOption::RootMode(libc::S_IFDIR | libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH),
+ MountOption::AllowOther,
+ MountOption::UserId(0),
+ MountOption::GroupId(0),
+ MountOption::MaxRead(MAX_READ_BYTES),
+ ];
+ if let Some(value) = extra_options {
+ mount_options.push(MountOption::Extra(value));
+ }
+
+ fuse::mount(
+ mountpoint,
+ "authfs",
+ libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC,
+ &mount_options,
+ )
+ .expect("Failed to mount fuse");
+
+ let mut config = fuse::FuseConfig::new();
+ config.dev_fuse(dev_fuse).max_write(MAX_WRITE_BYTES).max_read(MAX_READ_BYTES);
+ if let Some(num) = threads {
+ config.num_threads(u8::from(num).into());
+ }
+ config.enter_message_loop(authfs)
+}
diff --git a/guest/authfs/src/main.rs b/guest/authfs/src/main.rs
new file mode 100644
index 0000000..e46b197
--- /dev/null
+++ b/guest/authfs/src/main.rs
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+//! This crate implements AuthFS, a FUSE-based, non-generic filesystem where file access is
+//! authenticated. This filesystem assumes the underlying layer is not trusted, e.g. file may be
+//! provided by an untrusted host/VM, so that the content can't be simply trusted. However, with a
+//! known file hash from trusted party, this filesystem can still verify a (read-only) file even if
+//! the host/VM as the blob provider is malicious. With the Merkle tree, each read of file block can
+//! be verified individually only when needed.
+//!
+//! AuthFS only serve files that are specifically configured. Each remote file can be configured to
+//! appear as a local file at the mount point. A file configuration may include its remote file
+//! identifier and its verification method (e.g. by known digest).
+//!
+//! AuthFS also support remote directories. A remote directory may be defined by a manifest file,
+//! which contains file paths and their corresponding digests.
+//!
+//! AuthFS can also be configured for write, in which case the remote file server is treated as a
+//! (untrusted) storage. The file/directory integrity is maintained in memory in the VM. Currently,
+//! the state is not persistent, thus only new file/directory are supported.
+
+use anyhow::{anyhow, bail, Result};
+use clap::Parser;
+use log::error;
+use protobuf::Message;
+use std::convert::TryInto;
+use std::fs::File;
+use std::num::NonZeroU8;
+use std::path::{Path, PathBuf};
+
+mod common;
+mod file;
+mod fsstat;
+mod fsverity;
+mod fusefs;
+
+use file::{Attr, InMemoryDir, RemoteDirEditor, RemoteFileEditor, RemoteFileReader};
+use fsstat::RemoteFsStatsReader;
+use fsverity::VerifiedFileEditor;
+use fsverity_digests_proto::fsverity_digests::FSVerityDigests;
+use fusefs::{AuthFs, AuthFsEntry, LazyVerifiedReadonlyFile};
+
+#[derive(Parser)]
+struct Args {
+ /// Mount point of AuthFS.
+ mount_point: PathBuf,
+
+ /// CID of the VM where the service runs.
+ #[clap(long)]
+ cid: u32,
+
+ /// Extra options to FUSE
+ #[clap(short = 'o')]
+ extra_options: Option<String>,
+
+ /// Number of threads to serve FUSE requests.
+ #[clap(short = 'j')]
+ thread_number: Option<NonZeroU8>,
+
+ /// A read-only remote file with integrity check. Can be multiple.
+ ///
+ /// For example, `--remote-ro-file 5:sha256-1234abcd` tells the filesystem to associate the
+ /// file $MOUNTPOINT/5 with a remote FD 5, and has a fs-verity digest with sha256 of the hex
+ /// value 1234abcd.
+ #[clap(long, value_parser = parse_remote_ro_file_option)]
+ remote_ro_file: Vec<OptionRemoteRoFile>,
+
+ /// A read-only remote file without integrity check. Can be multiple.
+ ///
+ /// For example, `--remote-ro-file-unverified 5` tells the filesystem to associate the file
+ /// $MOUNTPOINT/5 with a remote FD 5.
+ #[clap(long)]
+ remote_ro_file_unverified: Vec<i32>,
+
+ /// A new read-writable remote file with integrity check. Can be multiple.
+ ///
+ /// For example, `--remote-new-rw-file 5` tells the filesystem to associate the file
+ /// $MOUNTPOINT/5 with a remote FD 5.
+ #[clap(long)]
+ remote_new_rw_file: Vec<i32>,
+
+ /// A read-only directory that represents a remote directory. The directory view is constructed
+ /// and finalized during the filesystem initialization based on the provided mapping file
+ /// (which is a serialized protobuf of android.security.fsverity.FSVerityDigests, which
+ /// essentially provides <file path, fs-verity digest> mappings of exported files). The mapping
+ /// file is supposed to come from a trusted location in order to provide a trusted view as well
+ /// as verified access of included files with their fs-verity digest. Not all files on the
+ /// remote host may be included in the mapping file, so the directory view may be partial. The
+ /// directory structure won't change throughout the filesystem lifetime.
+ ///
+ /// For example, `--remote-ro-dir 5:/path/to/mapping:prefix/` tells the filesystem to
+ /// construct a directory structure defined in the mapping file at $MOUNTPOINT/5, which may
+ /// include a file like /5/system/framework/framework.jar. "prefix/" tells the filesystem to
+ /// strip the path (e.g. "system/") from the mount point to match the expected location of the
+ /// remote FD (e.g. a directory FD of "/system" in the remote).
+ #[clap(long, value_parser = parse_remote_new_ro_dir_option)]
+ remote_ro_dir: Vec<OptionRemoteRoDir>,
+
+ /// 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.
+ #[clap(long)]
+ remote_new_rw_dir: Vec<i32>,
+
+ /// Enable debugging features.
+ #[clap(long)]
+ debug: bool,
+}
+
+#[derive(Clone)]
+struct OptionRemoteRoFile {
+ /// ID to refer to the remote file.
+ remote_fd: i32,
+
+ /// Expected fs-verity digest (with sha256) for the remote file.
+ digest: String,
+}
+
+#[derive(Clone)]
+struct OptionRemoteRoDir {
+ /// ID to refer to the remote dir.
+ remote_dir_fd: i32,
+
+ /// A mapping file that describes the expecting file/directory structure and integrity metadata
+ /// in the remote directory. The file contains serialized protobuf of
+ /// android.security.fsverity.FSVerityDigests.
+ mapping_file_path: PathBuf,
+
+ prefix: String,
+}
+
+fn parse_remote_ro_file_option(option: &str) -> Result<OptionRemoteRoFile> {
+ let strs: Vec<&str> = option.split(':').collect();
+ if strs.len() != 2 {
+ bail!("Invalid option: {}", option);
+ }
+ if let Some(digest) = strs[1].strip_prefix("sha256-") {
+ Ok(OptionRemoteRoFile { remote_fd: strs[0].parse::<i32>()?, digest: String::from(digest) })
+ } else {
+ bail!("Unsupported hash algorithm or invalid format: {}", strs[1]);
+ }
+}
+
+fn parse_remote_new_ro_dir_option(option: &str) -> Result<OptionRemoteRoDir> {
+ let strs: Vec<&str> = option.split(':').collect();
+ if strs.len() != 3 {
+ bail!("Invalid option: {}", option);
+ }
+ Ok(OptionRemoteRoDir {
+ remote_dir_fd: strs[0].parse::<i32>().unwrap(),
+ mapping_file_path: PathBuf::from(strs[1]),
+ prefix: String::from(strs[2]),
+ })
+}
+
+fn new_remote_verified_file_entry(
+ service: file::VirtFdService,
+ remote_fd: i32,
+ expected_digest: &str,
+) -> Result<AuthFsEntry> {
+ Ok(AuthFsEntry::VerifiedReadonly {
+ reader: LazyVerifiedReadonlyFile::prepare_by_fd(
+ service,
+ remote_fd,
+ hex::decode(expected_digest)?,
+ ),
+ })
+}
+
+fn new_remote_unverified_file_entry(
+ service: file::VirtFdService,
+ remote_fd: i32,
+ file_size: u64,
+) -> Result<AuthFsEntry> {
+ let reader = RemoteFileReader::new(service, remote_fd);
+ Ok(AuthFsEntry::UnverifiedReadonly { reader, file_size })
+}
+
+fn new_remote_new_verified_file_entry(
+ service: file::VirtFdService,
+ remote_fd: i32,
+) -> Result<AuthFsEntry> {
+ let remote_file = RemoteFileEditor::new(service.clone(), remote_fd);
+ Ok(AuthFsEntry::VerifiedNew {
+ editor: VerifiedFileEditor::new(remote_file),
+ attr: Attr::new_file(service, remote_fd),
+ })
+}
+
+fn new_remote_new_verified_dir_entry(
+ service: file::VirtFdService,
+ remote_fd: i32,
+) -> Result<AuthFsEntry> {
+ let dir = RemoteDirEditor::new(service.clone(), remote_fd);
+ let attr = Attr::new_dir(service, remote_fd);
+ Ok(AuthFsEntry::VerifiedNewDirectory { dir, attr })
+}
+
+fn prepare_root_dir_entries(
+ service: file::VirtFdService,
+ authfs: &mut AuthFs,
+ args: &Args,
+) -> Result<()> {
+ for config in &args.remote_ro_file {
+ authfs.add_entry_at_root_dir(
+ remote_fd_to_path_buf(config.remote_fd),
+ new_remote_verified_file_entry(service.clone(), config.remote_fd, &config.digest)?,
+ )?;
+ }
+
+ for remote_fd in &args.remote_ro_file_unverified {
+ let remote_fd = *remote_fd;
+ authfs.add_entry_at_root_dir(
+ remote_fd_to_path_buf(remote_fd),
+ new_remote_unverified_file_entry(
+ service.clone(),
+ remote_fd,
+ service.getFileSize(remote_fd)?.try_into()?,
+ )?,
+ )?;
+ }
+
+ for remote_fd in &args.remote_new_rw_file {
+ let remote_fd = *remote_fd;
+ authfs.add_entry_at_root_dir(
+ remote_fd_to_path_buf(remote_fd),
+ new_remote_new_verified_file_entry(service.clone(), remote_fd)?,
+ )?;
+ }
+
+ for remote_fd in &args.remote_new_rw_dir {
+ let remote_fd = *remote_fd;
+ authfs.add_entry_at_root_dir(
+ remote_fd_to_path_buf(remote_fd),
+ new_remote_new_verified_dir_entry(service.clone(), remote_fd)?,
+ )?;
+ }
+
+ for config in &args.remote_ro_dir {
+ let dir_root_inode = authfs.add_entry_at_root_dir(
+ remote_fd_to_path_buf(config.remote_dir_fd),
+ AuthFsEntry::ReadonlyDirectory { dir: InMemoryDir::new() },
+ )?;
+
+ // Build the directory tree based on the mapping file.
+ let mut reader = File::open(&config.mapping_file_path)?;
+ let proto = FSVerityDigests::parse_from_reader(&mut reader)?;
+ for (path_str, digest) in &proto.digests {
+ if digest.hash_alg != "sha256" {
+ bail!("Unsupported hash algorithm: {}", digest.hash_alg);
+ }
+
+ let file_entry = {
+ let remote_path_str = path_str.strip_prefix(&config.prefix).ok_or_else(|| {
+ anyhow!("Expect path {} to match prefix {}", path_str, config.prefix)
+ })?;
+ AuthFsEntry::VerifiedReadonly {
+ reader: LazyVerifiedReadonlyFile::prepare_by_path(
+ service.clone(),
+ config.remote_dir_fd,
+ PathBuf::from(remote_path_str),
+ digest.digest.clone(),
+ ),
+ }
+ };
+ authfs.add_entry_at_ro_dir_by_path(dir_root_inode, Path::new(path_str), file_entry)?;
+ }
+ }
+
+ Ok(())
+}
+
+fn remote_fd_to_path_buf(fd: i32) -> PathBuf {
+ PathBuf::from(fd.to_string())
+}
+
+fn try_main() -> Result<()> {
+ let args = Args::parse();
+
+ let log_level = if args.debug { log::LevelFilter::Debug } else { log::LevelFilter::Info };
+ android_logger::init_once(
+ android_logger::Config::default().with_tag("authfs").with_max_level(log_level),
+ );
+
+ let service = file::get_rpc_binder_service(args.cid)?;
+ let mut authfs = AuthFs::new(RemoteFsStatsReader::new(service.clone()));
+ prepare_root_dir_entries(service, &mut authfs, &args)?;
+
+ fusefs::mount_and_enter_message_loop(
+ authfs,
+ &args.mount_point,
+ &args.extra_options,
+ args.thread_number,
+ )?;
+ bail!("Unexpected exit after the handler loop")
+}
+
+fn main() {
+ if let Err(e) = try_main() {
+ error!("failed with {:?}", e);
+ std::process::exit(1);
+ }
+}
diff --git a/guest/authfs_service/Android.bp b/guest/authfs_service/Android.bp
new file mode 100644
index 0000000..2101a36
--- /dev/null
+++ b/guest/authfs_service/Android.bp
@@ -0,0 +1,25 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_binary {
+ name: "authfs_service",
+ srcs: [
+ "src/main.rs",
+ ],
+ edition: "2021",
+ rustlibs: [
+ "authfs_aidl_interface-rust",
+ "libandroid_logger",
+ "libanyhow",
+ "libbinder_rs",
+ "liblibc",
+ "liblog_rust",
+ "libnix",
+ "librpcbinder_rs",
+ "librustutils",
+ "libshared_child",
+ ],
+ prefer_rlib: true,
+ init_rc: ["authfs_service.rc"],
+}
diff --git a/guest/authfs_service/TEST_MAPPING b/guest/authfs_service/TEST_MAPPING
new file mode 100644
index 0000000..62bc18f
--- /dev/null
+++ b/guest/authfs_service/TEST_MAPPING
@@ -0,0 +1,23 @@
+// When adding or removing tests here, don't forget to amend _all_modules list in
+// wireless/android/busytown/ath_config/configs/prod/avf/tests.gcl
+{
+ "avf-presubmit": [
+ {
+ "name": "authfs_device_test_src_lib"
+ },
+ {
+ "name": "fd_server.test"
+ },
+ {
+ "name": "open_then_run.test"
+ },
+ {
+ "name": "AuthFsHostTest"
+ }
+ ],
+ "avf-postsubmit": [
+ {
+ "name": "AuthFsBenchmarks"
+ }
+ ]
+}
diff --git a/guest/authfs_service/authfs_service.rc b/guest/authfs_service/authfs_service.rc
new file mode 100644
index 0000000..bc67c83
--- /dev/null
+++ b/guest/authfs_service/authfs_service.rc
@@ -0,0 +1,6 @@
+service authfs_service /system/bin/authfs_service
+ disabled
+ socket authfs_service stream 0666 root system
+ # SYS_ADMIN capability allows to mount FUSE filesystem
+ capabilities SYS_ADMIN
+ user root
diff --git a/guest/authfs_service/src/authfs.rs b/guest/authfs_service/src/authfs.rs
new file mode 100644
index 0000000..cfd5766
--- /dev/null
+++ b/guest/authfs_service/src/authfs.rs
@@ -0,0 +1,189 @@
+/*
+ * 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 anyhow::{bail, Context, Result};
+use log::{debug, error, warn};
+use nix::mount::{umount2, MntFlags};
+use nix::sys::statfs::{statfs, FsType};
+use shared_child::SharedChild;
+use std::ffi::{OsStr, OsString};
+use std::fs::{remove_dir, OpenOptions};
+use std::path::PathBuf;
+use std::process::Command;
+use std::thread::sleep;
+use std::time::{Duration, Instant};
+
+use authfs_aidl_interface::aidl::com::android::virt::fs::AuthFsConfig::{
+ AuthFsConfig, InputDirFdAnnotation::InputDirFdAnnotation, InputFdAnnotation::InputFdAnnotation,
+ OutputDirFdAnnotation::OutputDirFdAnnotation, OutputFdAnnotation::OutputFdAnnotation,
+};
+use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFs::{BnAuthFs, IAuthFs};
+use binder::{self, BinderFeatures, Interface, ParcelFileDescriptor, Status, Strong};
+
+const AUTHFS_BIN: &str = "/system/bin/authfs";
+const AUTHFS_SETUP_POLL_INTERVAL_MS: Duration = Duration::from_millis(50);
+const AUTHFS_SETUP_TIMEOUT_SEC: Duration = Duration::from_secs(10);
+const FUSE_SUPER_MAGIC: FsType = FsType(0x65735546);
+
+/// An `AuthFs` instance is supposed to be backed by an `authfs` process. When the lifetime of the
+/// instance is over, it should leave no trace on the system: the process should be terminated, the
+/// FUSE should be unmounted, and the mount directory should be deleted.
+pub struct AuthFs {
+ mountpoint: OsString,
+ process: SharedChild,
+}
+
+impl Interface for AuthFs {}
+
+impl IAuthFs for AuthFs {
+ fn openFile(
+ &self,
+ remote_fd_name: i32,
+ writable: bool,
+ ) -> binder::Result<ParcelFileDescriptor> {
+ let mut path = PathBuf::from(&self.mountpoint);
+ path.push(remote_fd_name.to_string());
+ let file = OpenOptions::new().read(true).write(writable).open(&path).map_err(|e| {
+ Status::new_service_specific_error_str(
+ -1,
+ Some(format!("failed to open {:?} on authfs: {}", &path, e)),
+ )
+ })?;
+ Ok(ParcelFileDescriptor::new(file))
+ }
+
+ fn getMountPoint(&self) -> binder::Result<String> {
+ if let Some(s) = self.mountpoint.to_str() {
+ Ok(s.to_string())
+ } else {
+ Err(Status::new_service_specific_error_str(-1, Some("Bad string encoding")))
+ }
+ }
+}
+
+impl AuthFs {
+ /// Mount an authfs at `mountpoint` with specified FD annotations.
+ pub fn mount_and_wait(
+ mountpoint: OsString,
+ config: &AuthFsConfig,
+ debuggable: bool,
+ ) -> Result<Strong<dyn IAuthFs>> {
+ let child = run_authfs(
+ &mountpoint,
+ &config.inputFdAnnotations,
+ &config.outputFdAnnotations,
+ &config.inputDirFdAnnotations,
+ &config.outputDirFdAnnotations,
+ debuggable,
+ )?;
+ wait_until_authfs_ready(&child, &mountpoint).map_err(|e| {
+ match child.wait() {
+ Ok(status) => debug!("Wait for authfs: {}", status),
+ Err(e) => warn!("Failed to wait for child: {}", e),
+ }
+ e
+ })?;
+
+ let authfs = AuthFs { mountpoint, process: child };
+ Ok(BnAuthFs::new_binder(authfs, BinderFeatures::default()))
+ }
+}
+
+impl Drop for AuthFs {
+ /// On drop, try to erase all the traces for this authfs mount.
+ fn drop(&mut self) {
+ debug!("Dropping AuthFs instance at mountpoint {:?}", &self.mountpoint);
+ if let Err(e) = self.process.kill() {
+ error!("Failed to kill authfs: {}", e);
+ }
+ match self.process.wait() {
+ Ok(status) => debug!("authfs exit code: {}", status),
+ Err(e) => warn!("Failed to wait for authfs: {}", e),
+ }
+ // The client may still hold the file descriptors that refer to this filesystem. Use
+ // MNT_DETACH to detach the mountpoint, and automatically unmount when there is no more
+ // reference.
+ if let Err(e) = umount2(self.mountpoint.as_os_str(), MntFlags::MNT_DETACH) {
+ error!("Failed to umount authfs at {:?}: {}", &self.mountpoint, e)
+ }
+
+ if let Err(e) = remove_dir(&self.mountpoint) {
+ error!("Failed to clean up mount directory {:?}: {}", &self.mountpoint, e)
+ }
+ }
+}
+
+fn run_authfs(
+ mountpoint: &OsStr,
+ in_file_fds: &[InputFdAnnotation],
+ out_file_fds: &[OutputFdAnnotation],
+ in_dir_fds: &[InputDirFdAnnotation],
+ out_dir_fds: &[OutputDirFdAnnotation],
+ debuggable: bool,
+) -> Result<SharedChild> {
+ let mut args = vec![mountpoint.to_owned(), OsString::from("--cid=2")];
+ args.push(OsString::from("-o"));
+ args.push(OsString::from("fscontext=u:object_r:authfs_fuse:s0"));
+ for conf in in_file_fds {
+ // 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(conf.fd.to_string()));
+ }
+ for conf in out_file_fds {
+ args.push(OsString::from("--remote-new-rw-file"));
+ args.push(OsString::from(conf.fd.to_string()));
+ }
+ for conf in in_dir_fds {
+ args.push(OsString::from("--remote-ro-dir"));
+ args.push(OsString::from(format!("{}:{}:{}", conf.fd, conf.manifestPath, conf.prefix)));
+ }
+ for conf in out_dir_fds {
+ args.push(OsString::from("--remote-new-rw-dir"));
+ args.push(OsString::from(conf.fd.to_string()));
+ }
+ if debuggable {
+ args.push(OsString::from("--debug"));
+ }
+
+ let mut command = Command::new(AUTHFS_BIN);
+ command.args(&args);
+ debug!("Spawn authfs: {:?}", command);
+ SharedChild::spawn(&mut command).context("Spawn authfs")
+}
+
+fn wait_until_authfs_ready(child: &SharedChild, mountpoint: &OsStr) -> Result<()> {
+ let start_time = Instant::now();
+ loop {
+ if is_fuse(mountpoint)? {
+ break;
+ }
+ if let Some(exit_status) = child.try_wait()? {
+ // If the child has exited, we will never become ready.
+ bail!("Child has exited: {}", exit_status);
+ }
+ if start_time.elapsed() > AUTHFS_SETUP_TIMEOUT_SEC {
+ let _ignored = child.kill();
+ bail!("Time out mounting authfs");
+ }
+ sleep(AUTHFS_SETUP_POLL_INTERVAL_MS);
+ }
+ Ok(())
+}
+
+fn is_fuse(path: &OsStr) -> Result<bool> {
+ Ok(statfs(path)?.filesystem_type() == FUSE_SUPER_MAGIC)
+}
diff --git a/guest/authfs_service/src/main.rs b/guest/authfs_service/src/main.rs
new file mode 100644
index 0000000..97e684d
--- /dev/null
+++ b/guest/authfs_service/src/main.rs
@@ -0,0 +1,156 @@
+/*
+ * 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.
+ */
+
+//! AuthFsService facilitates authfs mounting (which is a privileged operation) for the client. The
+//! client will provide an `AuthFsConfig` which includes the backend address (only port for now) and
+//! the filesystem configuration. It is up to the client to ensure the backend server is running. On
+//! a successful mount, the client receives an `IAuthFs`, and through the binder object, the client
+//! is able to retrieve "remote file descriptors".
+
+mod authfs;
+
+use anyhow::{bail, Result};
+use log::*;
+use rpcbinder::RpcServer;
+use rustutils::sockets::android_get_control_socket;
+use std::ffi::OsString;
+use std::fs::{create_dir, read_dir, remove_dir_all, remove_file};
+use std::os::unix::io::{FromRawFd, OwnedFd};
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use authfs_aidl_interface::aidl::com::android::virt::fs::AuthFsConfig::AuthFsConfig;
+use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFs::IAuthFs;
+use authfs_aidl_interface::aidl::com::android::virt::fs::IAuthFsService::{
+ BnAuthFsService, IAuthFsService, AUTHFS_SERVICE_SOCKET_NAME,
+};
+use binder::{self, BinderFeatures, ExceptionCode, Interface, Status, Strong};
+
+const SERVICE_ROOT: &str = "/data/misc/authfs";
+
+/// Implementation of `IAuthFsService`.
+pub struct AuthFsService {
+ serial_number: AtomicUsize,
+ debuggable: bool,
+}
+
+impl Interface for AuthFsService {}
+
+impl IAuthFsService for AuthFsService {
+ fn mount(&self, config: &AuthFsConfig) -> binder::Result<Strong<dyn IAuthFs>> {
+ self.validate(config)?;
+
+ let mountpoint = self.get_next_mount_point();
+
+ // The directory is supposed to be deleted when `AuthFs` is dropped.
+ create_dir(&mountpoint).map_err(|e| {
+ Status::new_service_specific_error_str(
+ -1,
+ Some(format!("Cannot create mount directory {:?}: {:?}", &mountpoint, e)),
+ )
+ })?;
+
+ authfs::AuthFs::mount_and_wait(mountpoint, config, self.debuggable).map_err(|e| {
+ Status::new_service_specific_error_str(
+ -1,
+ Some(format!("mount_and_wait failed: {:?}", e)),
+ )
+ })
+ }
+}
+
+impl AuthFsService {
+ fn new_binder(debuggable: bool) -> Strong<dyn IAuthFsService> {
+ let service = AuthFsService { serial_number: AtomicUsize::new(1), debuggable };
+ BnAuthFsService::new_binder(service, BinderFeatures::default())
+ }
+
+ fn validate(&self, config: &AuthFsConfig) -> binder::Result<()> {
+ if config.port < 0 {
+ return Err(Status::new_exception_str(
+ ExceptionCode::ILLEGAL_ARGUMENT,
+ Some(format!("Invalid port: {}", config.port)),
+ ));
+ }
+ Ok(())
+ }
+
+ fn get_next_mount_point(&self) -> OsString {
+ let previous = self.serial_number.fetch_add(1, Ordering::Relaxed);
+ OsString::from(format!("{}/{}", SERVICE_ROOT, previous))
+ }
+}
+
+fn clean_up_working_directory() -> Result<()> {
+ for entry in read_dir(SERVICE_ROOT)? {
+ let entry = entry?;
+ let path = entry.path();
+ if path.is_dir() {
+ remove_dir_all(path)?;
+ } else if path.is_file() {
+ remove_file(path)?;
+ } else {
+ bail!("Unrecognized path type: {:?}", path);
+ }
+ }
+ Ok(())
+}
+
+/// Prepares a socket file descriptor for the authfs service.
+///
+/// # Safety requirement
+///
+/// The caller must ensure that this function is the only place that claims ownership
+/// of the file descriptor and it is called only once.
+unsafe fn prepare_authfs_service_socket() -> Result<OwnedFd> {
+ let raw_fd = android_get_control_socket(AUTHFS_SERVICE_SOCKET_NAME)?;
+
+ // Creating OwnedFd for stdio FDs is not safe.
+ if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
+ bail!("File descriptor {raw_fd} is standard I/O descriptor");
+ }
+ // SAFETY: Initializing OwnedFd for a RawFd created by the init.
+ // We checked that the integer value corresponds to a valid FD and that the caller
+ // ensures that this is the only place to claim its ownership.
+ Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
+}
+
+#[allow(clippy::eq_op)]
+fn try_main() -> Result<()> {
+ let debuggable = env!("TARGET_BUILD_VARIANT") != "user";
+ let log_level = if debuggable { log::LevelFilter::Trace } else { log::LevelFilter::Info };
+ android_logger::init_once(
+ android_logger::Config::default().with_tag("authfs_service").with_max_level(log_level),
+ );
+
+ clean_up_working_directory()?;
+
+ // SAFETY: This is the only place we take the ownership of the fd of the authfs service.
+ let socket_fd = unsafe { prepare_authfs_service_socket()? };
+ let service = AuthFsService::new_binder(debuggable).as_binder();
+ debug!("{} is starting as a rpc service.", AUTHFS_SERVICE_SOCKET_NAME);
+ let server = RpcServer::new_bound_socket(service, socket_fd)?;
+ info!("The RPC server '{}' is running.", AUTHFS_SERVICE_SOCKET_NAME);
+ server.join();
+ info!("The RPC server at '{}' has shut down gracefully.", AUTHFS_SERVICE_SOCKET_NAME);
+ Ok(())
+}
+
+fn main() {
+ if let Err(e) = try_main() {
+ error!("failed with {:?}", e);
+ std::process::exit(1);
+ }
+}