Implement statfs for authfs

Fixes: 206465653
Test: atest AuthFsHostTest
Test: Manually run `stat -f /path/to/authfs`, result looks correct
Test: odrefresh finished without a hack to avoid calling statvfs
Change-Id: I4ddf9ab4f5d6dfd8369a045fcbff778be8a127da
This commit is contained in:
Victor Hsieh 2021-11-22 10:20:33 -08:00
parent 72c774c9bd
commit f7fc3d3665
6 changed files with 169 additions and 10 deletions

View File

@ -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();
}

View File

@ -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<FsStat> {
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<FsStat, std::num::TryFromIntError> {
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<Vec<u8>> {

70
authfs/src/fsstat.rs Normal file
View File

@ -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()?,
})
}

View File

@ -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<RemoteFileReader, RemoteMerkleTreeReader>,
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<libc::statvfs64> {
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.

View File

@ -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")
}

View File

@ -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 {