diff --git a/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl b/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl index bf4ac61c..64828fb2 100644 --- a/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl +++ b/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl @@ -79,4 +79,23 @@ interface IVirtFdService { * @return file FD that represents the new created directory. */ int createDirectoryInDirectory(int dirFd, String basename); + + /** Filesystem stats that AuthFS is interested in.*/ + parcelable FsStat { + /** Block size of the filesystem */ + long blockSize; + /** Fragment size of the filesystem */ + long fragmentSize; + /** Number of blocks in the filesystem */ + long blockNumbers; + /** Number of free blocks */ + long blockAvailable; + /** Number of free inodes */ + long inodesAvailable; + /** Maximum filename length */ + long maxFilename; + } + + /** Returns relevant filesystem stats. */ + FsStat statfs(); } diff --git a/authfs/fd_server/src/aidl.rs b/authfs/fd_server/src/aidl.rs index fa1914a1..92be504e 100644 --- a/authfs/fd_server/src/aidl.rs +++ b/authfs/fd_server/src/aidl.rs @@ -18,6 +18,7 @@ use anyhow::Result; use log::error; use nix::{ dir::Dir, errno::Errno, fcntl::openat, fcntl::OFlag, sys::stat::mkdirat, sys::stat::Mode, + sys::statvfs::statvfs, sys::statvfs::Statvfs, }; use std::cmp::min; use std::collections::{btree_map, BTreeMap}; @@ -31,7 +32,7 @@ use std::sync::{Arc, Mutex}; use crate::fsverity; use authfs_aidl_interface::aidl::com::android::virt::fs::IVirtFdService::{ - BnVirtFdService, IVirtFdService, MAX_REQUESTING_DATA, + BnVirtFdService, FsStat::FsStat, IVirtFdService, MAX_REQUESTING_DATA, }; use authfs_aidl_interface::binder::{ BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, StatusCode, Strong, @@ -331,6 +332,22 @@ impl IVirtFdService for FdService { _ => Err(new_errno_error(Errno::ENOTDIR)), }) } + + fn statfs(&self) -> BinderResult { + let st = statvfs("/data").map_err(new_errno_error)?; + try_into_fs_stat(st).map_err(|_e| new_errno_error(Errno::EINVAL)) + } +} + +fn try_into_fs_stat(st: Statvfs) -> Result { + Ok(FsStat { + blockSize: st.block_size().try_into()?, + fragmentSize: st.fragment_size().try_into()?, + blockNumbers: st.blocks().try_into()?, + blockAvailable: st.blocks_available().try_into()?, + inodesAvailable: st.files_available().try_into()?, + maxFilename: st.name_max().try_into()?, + }) } fn read_into_buf(file: &File, max_size: usize, offset: u64) -> io::Result> { diff --git a/authfs/src/fsstat.rs b/authfs/src/fsstat.rs new file mode 100644 index 00000000..81eaca16 --- /dev/null +++ b/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 { + 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 { + 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/authfs/src/fusefs.rs b/authfs/src/fusefs.rs index b456f33d..89bac45c 100644 --- a/authfs/src/fusefs.rs +++ b/authfs/src/fusefs.rs @@ -21,7 +21,7 @@ use std::convert::TryFrom; use std::ffi::{CStr, OsStr}; use std::fs::OpenOptions; use std::io; -use std::mem::MaybeUninit; +use std::mem::{zeroed, MaybeUninit}; use std::option::Option; use std::os::unix::{ffi::OsStrExt, io::AsRawFd}; use std::path::{Component, Path, PathBuf}; @@ -40,6 +40,7 @@ use crate::file::{ validate_basename, InMemoryDir, RandomWrite, ReadByChunk, RemoteDirEditor, RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader, }; +use crate::fsstat::RemoteFsStatsReader; use crate::fsverity::{VerifiedFileEditor, VerifiedFileReader}; pub type Inode = u64; @@ -66,7 +67,7 @@ pub enum AuthFsEntry { reader: VerifiedFileReader, file_size: u64, }, - /// A file type that is a read-only passthrough from a file on a remote serrver. + /// 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. @@ -84,17 +85,25 @@ pub struct AuthFs { /// The next available inode number. next_inode: 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() -> AuthFs { + pub fn new(remote_fs_stats_reader: RemoteFsStatsReader) -> AuthFs { let mut inode_table = BTreeMap::new(); inode_table.insert(ROOT_INODE, AuthFsEntry::ReadonlyDirectory { dir: InMemoryDir::new() }); - AuthFs { inode_table: Mutex::new(inode_table), next_inode: AtomicU64::new(ROOT_INODE + 1) } + AuthFs { + inode_table: Mutex::new(inode_table), + next_inode: AtomicU64::new(ROOT_INODE + 1), + remote_fs_stats_reader, + } } /// Add an `AuthFsEntry` as `basename` to the filesystem root. @@ -672,6 +681,32 @@ impl FileSystem for AuthFs { attr_timeout: DEFAULT_METADATA_TIMEOUT, }) } + + fn statfs(&self, _ctx: Context, _inode: Self::Inode) -> io::Result { + let remote_stat = self.remote_fs_stats_reader.statfs()?; + + // Safe because 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.lock().unwrap().len() as u64; + + Ok(st) + } } /// Mount and start the FUSE instance. This requires CAP_SYS_ADMIN. diff --git a/authfs/src/main.rs b/authfs/src/main.rs index 0bc71cef..00a46141 100644 --- a/authfs/src/main.rs +++ b/authfs/src/main.rs @@ -37,6 +37,7 @@ mod auth; mod common; mod crypto; mod file; +mod fsstat; mod fsverity; mod fusefs; @@ -44,6 +45,7 @@ use auth::FakeAuthenticator; use file::{ InMemoryDir, RemoteDirEditor, RemoteFileEditor, RemoteFileReader, RemoteMerkleTreeReader, }; +use fsstat::RemoteFsStatsReader; use fsverity::{VerifiedFileEditor, VerifiedFileReader}; use fusefs::{AuthFs, AuthFsEntry}; @@ -204,9 +206,11 @@ fn new_remote_new_verified_dir_entry( Ok(AuthFsEntry::VerifiedNewDirectory { dir }) } -fn prepare_root_dir_entries(authfs: &mut AuthFs, args: &Args) -> Result<()> { - let service = file::get_rpc_binder_service(args.cid)?; - +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), @@ -303,8 +307,10 @@ fn try_main() -> Result<()> { android_logger::Config::default().with_tag("authfs").with_min_level(log_level), ); - let mut authfs = AuthFs::new(); - prepare_root_dir_entries(&mut authfs, &args)?; + 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::loop_forever(authfs, &args.mount_point, &args.extra_options)?; bail!("Unexpected exit after the handler loop") } diff --git a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java index c27c5cd0..2d7668af 100644 --- a/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java +++ b/authfs/tests/java/src/com/android/fs/AuthFsHostTest.java @@ -439,6 +439,18 @@ public final class AuthFsHostTest extends VirtualizationTestCaseBase { assertFailedOnMicrodroid("test -f " + authfsInputDir + "/system/bin/sh"); } + @Test + public void testStatfs() throws Exception { + // Setup + runFdServerOnAndroid("--open-dir 3:" + TEST_OUTPUT_DIR, "--rw-dirs 3"); + runAuthFsOnMicrodroid("--remote-new-rw-dir 3 --cid " + VMADDR_CID_HOST); + + // Verify + // Magic matches. Has only 2 inodes (root and "/3"). + assertEquals( + FUSE_SUPER_MAGIC_HEX + " 2", runOnMicrodroid("stat -f -c '%t %c' " + MOUNT_DIR)); + } + private void expectBackingFileConsistency( String authFsPath, String backendPath, String expectedHash) throws DeviceNotAvailableException {