blob: 788ea6941a3ffa17852ee648593ddea420377cf9 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Library for filesystem management in rust.
//!
//! This library is analogous to the fs-management library in zircon. It provides support for
//! formatting, mounting, unmounting, and fsck-ing. It is implemented in a similar way to the C++
//! version - it uses the blobfs command line tool present in the base image. In order to use this
//! library inside of a sandbox, the following must be added to the relevant component manifest
//! file -
//!
//! ```
//! "sandbox": {
//! "services": [
//! "fuchsia.process.Launcher",
//! "fuchsia.tracing.provider.Registry"
//! ]
//! }
//! ```
//!
//! and the projects BUILD.gn file must contain
//!
//! ```
//! package("foo") {
//! deps = [
//! "//src/storage/bin/blobfs",
//! "//src/storage/bin/minfs",
//! ...
//! ]
//! binaries = [
//! { name = "blobfs" },
//! { name = "minfs" },
//! ...
//! ]
//! ...
//! }
//! ```
//!
//! for components v1. For components v2, add `/svc/fuchsia.process.Launcher` to `use` and add the
//! binaries as dependencies to your component.
//!
//! This library currently doesn't work outside of a component (the filesystem utility binary paths
//! are hard-coded strings).
#![deny(missing_docs)]
use {
anyhow::{format_err, Context as _, Error},
cstr::cstr,
fdio::{spawn_etc, Namespace, SpawnAction, SpawnOptions},
fidl_fuchsia_io::{
DirectoryAdminSynchronousProxy, NodeSynchronousProxy, CLONE_FLAG_SAME_RIGHTS,
OPEN_RIGHT_ADMIN,
},
fuchsia_runtime::{HandleInfo, HandleType},
fuchsia_zircon::{self as zx, AsHandleRef},
std::{ffi::CStr, marker::PhantomData},
};
/// Options to pass to the underlying filesystem process. They are passed as argument flags, and are
/// always present on the call even if they don't apply to the operation.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct FSOptions {
/// Filesystem will be mounted read-only. Applies to mounting.
pub readonly: bool,
/// Increase logging output from the filesystem. Applies to mounting, fsck, and format.
pub verbose: bool,
/// Configure metric collection by the filesystem. Applies to mounting.
pub metrics: bool,
}
/// Describes the information for working with a particular native filesystem.
pub trait Layout {
/// Path to the filesystem utility binary.
fn path() -> &'static CStr;
/// A human readable name for the filesystem.
fn name() -> &'static str;
/// Default options for the binary for this filesystem layout.
fn options() -> FSOptions;
}
/// Filesystem represents a managed filesystem partition with a particular layout. It is constructed
/// with functions associated with the [`Layout`] types. Right now, those include [`Blobfs`] and
/// [`Minfs`].
pub struct Filesystem<FSType>
where
FSType: Layout,
{
namespace: Namespace,
device: NodeSynchronousProxy,
mount_point: Option<String>,
launcher: FSLauncher<FSType, ProcLauncher>,
}
impl<FSType> Drop for Filesystem<FSType>
where
FSType: Layout,
{
fn drop(&mut self) {
if let Some(_) = self.mount_point {
let _result = self.unmount();
}
}
}
/// The blobfs layout type.
pub struct Blobfs;
impl Blobfs {
/// Manage a blobfs partition on the provided device. The device is not formatted, mounted, or
/// modified at this point.
pub fn new(device_path: &str) -> Result<Filesystem<Self>, Error> {
let (block_device, server_chan) = zx::Channel::create()?;
fdio::service_connect(device_path, server_chan).context("connecting to block device")?;
Filesystem::new(block_device)
}
/// Manage a blobfs partition on the provided device. The device is not formatted, mounted, or
/// modified at this point.
pub fn from_channel(device_channel: zx::Channel) -> Result<Filesystem<Self>, Error> {
Filesystem::new(device_channel)
}
}
impl Layout for Blobfs {
fn path() -> &'static CStr {
cstr!("/pkg/bin/blobfs")
}
fn name() -> &'static str {
"blobfs"
}
fn options() -> FSOptions {
FSOptions { readonly: false, verbose: false, metrics: false }
}
}
/// The minfs layout type.
pub struct Minfs;
impl Minfs {
/// Manage a minfs partition on the provided device. The device is not formatted, mounted, or
/// modified at this point.
pub fn new(device_path: &str) -> Result<Filesystem<Self>, Error> {
let (block_device, server_chan) = zx::Channel::create()?;
fdio::service_connect(device_path, server_chan).context("connecting to block device")?;
Filesystem::new(block_device)
}
/// Manage a minfs partition on the provided device. The device is not formatted, mounted, or
/// modified at this point.
pub fn from_channel(device_channel: zx::Channel) -> Result<Filesystem<Self>, Error> {
Filesystem::new(device_channel)
}
}
impl Layout for Minfs {
fn path() -> &'static CStr {
cstr!("/pkg/bin/minfs")
}
fn name() -> &'static str {
"minfs"
}
fn options() -> FSOptions {
FSOptions { readonly: false, verbose: false, metrics: false }
}
}
impl Filesystem<Minfs> {
/// Increase logging output from the filesystem process. Only the minfs binary has this option.
pub fn set_verbose(&mut self, enable: bool) {
self.launcher.options.verbose = enable;
}
}
/// The factoryfs layout type.
pub struct Factoryfs;
impl Factoryfs {
/// Manage a factoryfs partition on the provided device. The device is not formatted, mounted,
/// or modified at this point.
pub fn new(device_path: &str) -> Result<Filesystem<Self>, Error> {
let (block_device, server_chan) = zx::Channel::create()?;
fdio::service_connect(device_path, server_chan).context("connecting to block device")?;
Filesystem::new(block_device)
}
/// Manage a factory partition on the provided device. The device is not formatted, mounted, or
/// modified at this point.
pub fn from_channel(device_channel: zx::Channel) -> Result<Filesystem<Self>, Error> {
Filesystem::new(device_channel)
}
}
impl Layout for Factoryfs {
fn path() -> &'static CStr {
cstr!("/pkg/bin/factoryfs")
}
fn name() -> &'static str {
"factoryfs"
}
// factoryfs doesn't actually support any options, so even though it is "read only" we don't
// use the readonly flag.
fn options() -> FSOptions {
FSOptions { readonly: false, verbose: false, metrics: false }
}
}
impl<FSType> Filesystem<FSType>
where
FSType: Layout,
{
/// Manage a filesystem partition on the provided device. The device is not formatted, mounted,
/// or modified at this point.
///
/// This function is not public. The only way to construct a new filesystem type is through one
/// of the structs that implements Layout.
fn new(device: zx::Channel) -> Result<Filesystem<FSType>, Error> {
let namespace = Namespace::installed().context("failed to get installed namespace")?;
let device = NodeSynchronousProxy::new(device);
Ok(Filesystem {
namespace,
device,
mount_point: None,
launcher: FSLauncher::new(FSType::options()),
})
}
/// Mount the filesystem as read only.
pub fn set_readonly(&mut self, enable: bool) {
self.launcher.options.readonly = enable;
}
/// Configure the collection of metrics.
pub fn set_metrics(&mut self, enable: bool) {
self.launcher.options.metrics = enable;
}
/// Initialize the filesystem partition that exists on the provided block device, allowing it to
/// receive requests on the root channel. In order to be mounted in the traditional sense, the
/// client side of the provided root channel needs to be bound to a path in a namespace
/// somewhere.
fn initialize(&self, block_device: zx::Channel) -> Result<zx::Channel, Error> {
let (client_chan, server_chan) = zx::Channel::create()?;
let actions = vec![
// root handle is passed in as a PA_USER0 handle at argument 0
SpawnAction::add_handle(HandleInfo::new(HandleType::User0, 0), server_chan.into()),
// device handle is passed in as a PA_USER0 handle at argument 1
SpawnAction::add_handle(HandleInfo::new(HandleType::User0, 1), block_device.into()),
];
let _process =
self.launcher.run_command(cstr!("mount"), actions).context("failed to mount")?;
let signals = client_chan
.wait_handle(zx::Signals::USER_0 | zx::Signals::CHANNEL_PEER_CLOSED, zx::Time::INFINITE)
.context("failed to wait on root handle when mounting")?;
if signals.contains(zx::Signals::CHANNEL_PEER_CLOSED) {
return Err(format_err!("failed to mount: CHANNEL_PEER_CLOSED"));
}
Ok(client_chan)
}
/// Returns a channel to the block device.
fn get_channel(&mut self) -> Result<zx::Channel, Error> {
let (channel, server) = zx::Channel::create()?;
let () =
self.device.clone(CLONE_FLAG_SAME_RIGHTS, fidl::endpoints::ServerEnd::new(server))?;
Ok(channel)
}
/// Format the associated device with a fresh filesystem. It must not be mounted.
pub fn format(&mut self) -> Result<(), Error> {
if let Some(mount_point) = &self.mount_point {
// shouldn't be mounted if we are going to format it
return Err(format_err!(
"failed to format {}: mounted at {}",
FSType::name(),
mount_point
));
}
let device = self.get_channel()?;
self.launcher
.run_command_with_device(cstr!("mkfs"), device)
.context("failed to format device")?;
Ok(())
}
/// Mount the provided block device and bind it to the provided mount_point in the default
/// namespace. The filesystem can't already be mounted, and the mount will fail if the provided
/// mount path doesn't already exist. The path is relative to the root of the default namespace,
/// and can't contain any '.' or '..' entries.
pub fn mount(&mut self, mount_point: &str) -> Result<(), Error> {
if let Some(mount_point) = &self.mount_point {
// already mounted?
return Err(format_err!(
"failed to mount {}: already mounted at {}",
FSType::name(),
mount_point
));
}
let channel = self.get_channel()?;
let client_chan = self.initialize(channel)?;
self.namespace
.bind(mount_point, client_chan)
.context("failed to bind client channel into default namespace")?;
self.mount_point = Some(String::from(mount_point));
Ok(())
}
/// Unmount the filesystem partition. The partition must already be mounted.
pub fn unmount(&mut self) -> Result<(), Error> {
let (client_chan, server_chan) = zx::Channel::create()?;
let mount_point =
self.mount_point.take().ok_or_else(|| format_err!("failed to unmount: not mounted"))?;
self.namespace
.connect(&mount_point, OPEN_RIGHT_ADMIN, server_chan)
.context("failed to connect to filesystem")?;
let mut proxy = DirectoryAdminSynchronousProxy::new(client_chan);
proxy.unmount(zx::Time::INFINITE).context("failed to unmount")?;
self.namespace
.unbind(&mount_point)
.context("failed to unbind filesystem from default namespace")?;
Ok(())
}
/// Run fsck on the filesystem partition. Returns Ok(()) if fsck succeeds, or the associated
/// error if it doesn't. Will fail if run on a mounted partition.
pub fn fsck(&mut self) -> Result<(), Error> {
if let Some(mount_point) = &self.mount_point {
return Err(format_err!("failed to fsck: mounted at {}", mount_point));
}
let device = self.get_channel()?;
self.launcher.run_command_with_device(cstr!("fsck"), device).context("fsck failed")?;
Ok(())
}
}
trait Launcher {
fn launch_process(args: &[&CStr], actions: Vec<SpawnAction<'_>>) -> Result<zx::Process, Error>;
}
struct ProcLauncher;
impl Launcher for ProcLauncher {
fn launch_process(
args: &[&CStr],
mut actions: Vec<SpawnAction<'_>>,
) -> Result<zx::Process, Error> {
let options = SpawnOptions::CLONE_ALL;
let process = match spawn_etc(
&zx::Handle::invalid().into(),
options,
args[0],
args,
None,
&mut actions,
) {
Ok(process) => process,
Err((status, message)) => {
return Err(format_err!(
"failed to spawn process. launched with: {:?}, status: {}, message: {}",
args,
status,
message
));
}
};
Ok(process)
}
}
struct FSLauncher<FSType, L>
where
FSType: Layout,
L: Launcher,
{
pub options: FSOptions,
_type_marker: PhantomData<FSType>,
_launcher_marker: PhantomData<L>,
}
impl<FSType, L> FSLauncher<FSType, L>
where
FSType: Layout,
L: Launcher,
{
pub fn new(options: FSOptions) -> Self {
FSLauncher { options, _type_marker: PhantomData, _launcher_marker: PhantomData }
}
pub fn run_command_with_device(
&self,
command: &'static CStr,
block_device: zx::Channel,
) -> Result<(), Error> {
let actions = vec![
// device handle is passed in as a PA_USER0 handle at argument 1
SpawnAction::add_handle(HandleInfo::new(HandleType::User0, 1), block_device.into()),
];
let process = self.run_command(command, actions)?;
let _signals = process
.wait_handle(zx::Signals::PROCESS_TERMINATED, zx::Time::INFINITE)
.context(format!("failed to wait for process to complete"))?;
let info = process.info().context("failed to get process info")?;
if !info.exited || info.return_code != 0 {
return Err(format_err!("process returned non-zero exit code ({})", info.return_code));
}
Ok(())
}
pub fn run_command(
&self,
command: &'static CStr,
actions: Vec<SpawnAction<'_>>,
) -> Result<zx::Process, Error> {
let mut args = vec![FSType::path()];
if self.options.metrics {
args.push(cstr!("--metrics"));
}
if self.options.readonly {
args.push(cstr!("--readonly"));
}
if self.options.verbose {
args.push(cstr!("--verbose"));
}
args.push(command);
L::launch_process(&args, actions)
}
}
#[cfg(test)]
mod tests {
use {
super::{Blobfs, FSLauncher, FSOptions, Factoryfs, Filesystem, Launcher, Minfs},
anyhow::Error,
cstr::cstr,
fdio::SpawnAction,
fuchsia_zircon::{self as zx, HandleBased},
ramdevice_client::RamdiskClient,
std::ffi::CStr,
std::io::{Read, Seek, Write},
thiserror::Error,
};
/// the only way to really move info out of the launch_process function is through the return
/// value. we want to confirm that the correct one was called, so we return something only a
/// test impl can return - something defined in the test mod - through the error type.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
struct ExpectedError;
impl std::fmt::Display for ExpectedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "test passed")
}
}
#[test]
fn fs_launcher_no_args() {
struct TestLauncherNoArgs;
impl Launcher for TestLauncherNoArgs {
fn launch_process(
args: &[&CStr],
_actions: Vec<SpawnAction<'_>>,
) -> Result<zx::Process, Error> {
assert_eq!(args, &[cstr!("/pkg/bin/blobfs"), cstr!("mount")]);
Err(ExpectedError.into())
}
}
let launcher: FSLauncher<Blobfs, TestLauncherNoArgs> =
FSLauncher::new(FSOptions { readonly: false, verbose: false, metrics: false });
let res = launcher.run_command(cstr!("mount"), vec![]);
assert_eq!(res.unwrap_err().downcast_ref::<ExpectedError>().unwrap(), &ExpectedError);
}
#[test]
fn fs_launcher_all_args() {
struct TestLauncherAllArgs;
impl Launcher for TestLauncherAllArgs {
fn launch_process(
args: &[&CStr],
_actions: Vec<SpawnAction<'_>>,
) -> Result<zx::Process, Error> {
assert_eq!(
args,
&[
cstr!("/pkg/bin/blobfs"),
cstr!("--metrics"),
cstr!("--readonly"),
cstr!("--verbose"),
cstr!("mount")
]
);
Err(ExpectedError.into())
}
}
let launcher: FSLauncher<Blobfs, TestLauncherAllArgs> =
FSLauncher::new(FSOptions { readonly: true, verbose: true, metrics: true });
let res = launcher.run_command(cstr!("mount"), vec![]);
assert_eq!(res.unwrap_err().downcast_ref::<ExpectedError>().unwrap(), &ExpectedError);
}
fn ramdisk_blobfs(block_size: u64) -> (RamdiskClient, Filesystem<Blobfs>) {
isolated_driver_manager::launch_isolated_driver_manager().unwrap();
ramdevice_client::wait_for_device("/dev/misc/ramctl", std::time::Duration::from_secs(10))
.unwrap();
let ramdisk = RamdiskClient::create(block_size, 1 << 16).unwrap();
let device = ramdisk.open().unwrap();
let blobfs = Blobfs::from_channel(device).unwrap();
(ramdisk, blobfs)
}
#[test]
fn blobfs_format_fsck_success() {
let block_size = 512;
let (ramdisk, mut blobfs) = ramdisk_blobfs(block_size);
blobfs.format().expect("failed to format blobfs");
blobfs.fsck().expect("failed to fsck blobfs");
ramdisk.destroy().expect("failed to destroy ramdisk");
}
#[test]
fn blobfs_format_fsck_error() {
let block_size = 512;
let (ramdisk, mut blobfs) = ramdisk_blobfs(block_size);
blobfs.format().expect("failed to format blobfs");
// force fsck to fail by stomping all over one of blobfs's metadata blocks after formatting
// TODO(fxbug.dev/35860): corrupt something other than the superblock
let device_channel = ramdisk.open().expect("failed to get channel to device");
let mut file = fdio::create_fd::<std::fs::File>(device_channel.into_handle())
.expect("failed to convert to file descriptor");
let mut bytes: Vec<u8> = std::iter::repeat(0xff).take(block_size as usize).collect();
file.write(&mut bytes).expect("failed to write to device");
blobfs.fsck().expect_err("fsck succeeded when it shouldn't have");
ramdisk.destroy().expect("failed to destroy ramdisk");
}
#[test]
fn blobfs_format_mount_write_remount_read_unmount() {
let block_size = 512;
let mount_point = "/test-fs-root";
let (ramdisk, mut blobfs) = ramdisk_blobfs(block_size);
blobfs.format().expect("failed to format blobfs");
blobfs.mount(mount_point).expect("failed to mount blobfs the first time");
// pre-generated merkle test fixture data
let merkle = "be901a14ec42ee0a8ee220eb119294cdd40d26d573139ee3d51e4430e7d08c28";
let content = String::from("test content").into_bytes();
let path = format!("{}/{}", mount_point, merkle);
{
let mut test_file = std::fs::File::create(&path).expect("failed to create test file");
test_file.set_len(content.len() as u64).expect("failed to truncate file");
test_file.write(&content).expect("failed to write to test file");
}
blobfs.unmount().expect("failed to unmount blobfs the first time");
blobfs.mount(mount_point).expect("failed to mount blobfs the second time");
{
let mut test_file = std::fs::File::open(&path).expect("failed to open test file");
let mut read_content = Vec::new();
test_file.read_to_end(&mut read_content).expect("failed to read from test file");
assert_eq!(content, read_content);
}
blobfs.unmount().expect("failed to unmount blobfs the second time");
ramdisk.destroy().expect("failed to destroy ramdisk");
}
fn ramdisk_minfs(block_size: u64) -> (RamdiskClient, Filesystem<Minfs>) {
isolated_driver_manager::launch_isolated_driver_manager().unwrap();
ramdevice_client::wait_for_device("/dev/misc/ramctl", std::time::Duration::from_secs(10))
.unwrap();
let ramdisk = RamdiskClient::create(block_size, 1 << 16).unwrap();
let device = ramdisk.open().unwrap();
let minfs = Minfs::from_channel(device).unwrap();
(ramdisk, minfs)
}
#[test]
fn minfs_format_fsck_success() {
let block_size = 8192;
let (ramdisk, mut minfs) = ramdisk_minfs(block_size);
minfs.format().expect("failed to format minfs");
minfs.fsck().expect("failed to fsck minfs");
ramdisk.destroy().expect("failed to destroy ramdisk");
}
#[test]
fn minfs_format_fsck_error() {
let block_size = 8192;
let (ramdisk, mut minfs) = ramdisk_minfs(block_size);
minfs.format().expect("failed to format minfs");
// force fsck to fail by stomping all over one of minfs's metadata blocks after formatting
let device_channel = ramdisk.open().expect("failed to get channel to device");
let mut file = fdio::create_fd::<std::fs::File>(device_channel.into_handle())
.expect("failed to convert to file descriptor");
// when minfs isn't on an fvm, the location for it's bitmap offset is the 8th block.
// TODO(fxbug.dev/35861): parse the superblock for this offset and the block size.
let bitmap_block_offset = 8;
let bitmap_offset = block_size * bitmap_block_offset;
let mut stomping_bytes: Vec<u8> =
std::iter::repeat(0xff).take(block_size as usize).collect();
let actual_offset =
file.seek(std::io::SeekFrom::Start(bitmap_offset)).expect("failed to seek to bitmap");
assert_eq!(actual_offset, bitmap_offset);
file.write(&mut stomping_bytes).expect("failed to write to device");
minfs.fsck().expect_err("fsck succeeded when it shouldn't have");
ramdisk.destroy().expect("failed to destroy ramdisk");
}
#[test]
fn minfs_format_mount_write_remount_read_unmount() {
let block_size = 8192;
let mount_point = "/test-fs-root";
let (ramdisk, mut minfs) = ramdisk_minfs(block_size);
minfs.format().expect("failed to format minfs");
minfs.mount(mount_point).expect("failed to mount minfs the first time");
let filename = "test_file";
let content = String::from("test content").into_bytes();
let path = format!("{}/{}", mount_point, filename);
{
let mut test_file = std::fs::File::create(&path).expect("failed to create test file");
test_file.write(&content).expect("failed to write to test file");
}
minfs.unmount().expect("failed to unmount minfs the first time");
minfs.mount(mount_point).expect("failed to mount minfs the second time");
{
let mut test_file = std::fs::File::open(&path).expect("failed to open test file");
let mut read_content = Vec::new();
test_file.read_to_end(&mut read_content).expect("failed to read from test file");
assert_eq!(content, read_content);
}
minfs.unmount().expect("failed to unmount minfs the second time");
ramdisk.destroy().expect("failed to destroy ramdisk");
}
fn ramdisk_factoryfs(block_size: u64) -> (RamdiskClient, Filesystem<Factoryfs>) {
isolated_driver_manager::launch_isolated_driver_manager().unwrap();
ramdevice_client::wait_for_device("/dev/misc/ramctl", std::time::Duration::from_secs(10))
.unwrap();
let ramdisk = RamdiskClient::create(block_size, 1 << 16).unwrap();
let device = ramdisk.open().unwrap();
let factoryfs = Factoryfs::from_channel(device).unwrap();
(ramdisk, factoryfs)
}
#[test]
fn factoryfs_format_fsck_success() {
let block_size = 512;
let (ramdisk, mut factoryfs) = ramdisk_factoryfs(block_size);
factoryfs.format().expect("failed to format factoryfs");
factoryfs.fsck().expect("failed to fsck factoryfs");
ramdisk.destroy().expect("failed to destroy ramdisk");
}
#[test]
fn factoryfs_format_mount_unmount() {
let block_size = 512;
let mount_point = "/test-fs-root";
let (ramdisk, mut factoryfs) = ramdisk_factoryfs(block_size);
factoryfs.format().expect("failed to format factoryfs");
factoryfs.mount(mount_point).expect("failed to mount factoryfs");
factoryfs.unmount().expect("failed to unmount factoryfs");
ramdisk.destroy().expect("failed to destroy ramdisk");
}
}