blob: a2e9bb40e7cf0537e98c6880538838e09c7763a9 [file] [log] [blame]
// Copyright 2021 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.
use crate::mm::PAGE_SIZE;
use crate::security;
use crate::task::{CurrentTask, Kernel};
use crate::vfs::memory_directory::MemoryDirectoryFile;
use crate::vfs::{
CacheMode, DirEntry, DirEntryHandle, FileOps, FileSystem, FileSystemHandle, FileSystemOps,
FileSystemOptions, FsNode, FsNodeHandle, FsNodeInfo, FsNodeOps, FsStr, FsString,
MemoryRegularNode, MemoryXattrStorage, SymlinkNode, XattrStorage as _, fs_args,
fs_node_impl_not_dir, fs_node_impl_xattr_delegate,
};
use starnix_logging::{log_warn, track_stub};
use starnix_sync::{FileOpsCore, LockEqualOrBefore, Locked, Unlocked};
use starnix_types::vfs::default_statfs;
use starnix_uapi::auth::FsCred;
use starnix_uapi::device_type::DeviceType;
use starnix_uapi::errors::Errno;
use starnix_uapi::file_mode::{FileMode, mode};
use starnix_uapi::open_flags::OpenFlags;
use starnix_uapi::seal_flags::SealFlags;
use starnix_uapi::{TMPFS_MAGIC, error, gid_t, statfs, uid_t};
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
pub struct TmpFs(&'static FsStr);
impl FileSystemOps for Arc<TmpFs> {
fn statfs(
&self,
_locked: &mut Locked<FileOpsCore>,
_fs: &FileSystem,
_current_task: &CurrentTask,
) -> Result<statfs, Errno> {
Ok(statfs {
// Pretend we have a ton of free space.
f_blocks: 0x100000000,
f_bavail: 0x100000000,
f_bfree: 0x100000000,
..default_statfs(TMPFS_MAGIC)
})
}
fn name(&self) -> &'static FsStr {
self.0
}
fn rename(
&self,
_locked: &mut Locked<FileOpsCore>,
_fs: &FileSystem,
_current_task: &CurrentTask,
old_parent: &FsNodeHandle,
_old_name: &FsStr,
new_parent: &FsNodeHandle,
_new_name: &FsStr,
renamed: &FsNodeHandle,
replaced: Option<&FsNodeHandle>,
) -> Result<(), Errno> {
fn child_count(node: &FsNodeHandle) -> &AtomicU32 {
// The following cast are safe, unless something is seriously wrong:
// - The filesystem should not be asked to rename node that it doesn't handle.
// - Parents in a rename operation need to be directories.
// - TmpFsDirectory is the ops for directories in this filesystem.
&node.downcast_ops::<TmpFsDirectory>().unwrap().child_count
}
if let Some(replaced) = replaced {
if replaced.is_dir() {
// Ensures that replaces is empty.
if child_count(replaced).load(Ordering::Acquire) != 0 {
return error!(ENOTEMPTY);
}
}
}
child_count(old_parent).fetch_sub(1, Ordering::Release);
child_count(new_parent).fetch_add(1, Ordering::Release);
if renamed.is_dir() {
old_parent.update_info(|info| {
info.link_count -= 1;
});
new_parent.update_info(|info| {
info.link_count += 1;
});
}
// Fix the wrong changes to new_parent due to the fact that the target element has
// been replaced instead of added.
if let Some(replaced) = replaced {
if replaced.is_dir() {
new_parent.update_info(|info| {
info.link_count -= 1;
});
}
child_count(new_parent).fetch_sub(1, Ordering::Release);
}
Ok(())
}
fn exchange(
&self,
_fs: &FileSystem,
_current_task: &CurrentTask,
node1: &FsNodeHandle,
parent1: &FsNodeHandle,
_name1: &FsStr,
node2: &FsNodeHandle,
parent2: &FsNodeHandle,
_name2: &FsStr,
) -> Result<(), Errno> {
if node1.is_dir() {
parent1.update_info(|info| {
info.link_count -= 1;
});
parent2.update_info(|info| {
info.link_count += 1;
});
}
if node2.is_dir() {
parent1.update_info(|info| {
info.link_count += 1;
});
parent2.update_info(|info| {
info.link_count -= 1;
});
}
Ok(())
}
}
pub fn tmp_fs(
locked: &mut Locked<Unlocked>,
current_task: &CurrentTask,
options: FileSystemOptions,
) -> Result<FileSystemHandle, Errno> {
TmpFs::new_fs_with_options(locked, &current_task.kernel(), options)
}
impl TmpFs {
pub fn new_fs<L>(locked: &mut Locked<L>, kernel: &Kernel) -> FileSystemHandle
where
L: LockEqualOrBefore<FileOpsCore>,
{
Self::new_fs_with_options(locked, kernel, Default::default())
.expect("empty options cannot fail")
}
pub fn new_fs_with_name<L>(
locked: &mut Locked<L>,
kernel: &Kernel,
name: &'static FsStr,
) -> FileSystemHandle
where
L: LockEqualOrBefore<FileOpsCore>,
{
Self::new_fs_with_options_and_name(locked, kernel, Default::default(), name)
.expect("empty options cannot fail")
}
pub fn new_fs_with_options<L>(
locked: &mut Locked<L>,
kernel: &Kernel,
options: FileSystemOptions,
) -> Result<FileSystemHandle, Errno>
where
L: LockEqualOrBefore<FileOpsCore>,
{
Self::new_fs_with_options_and_name(locked, kernel, options, "tmpfs".into())
}
fn new_fs_with_options_and_name<L>(
locked: &mut Locked<L>,
kernel: &Kernel,
options: FileSystemOptions,
name: &'static FsStr,
) -> Result<FileSystemHandle, Errno>
where
L: LockEqualOrBefore<FileOpsCore>,
{
let fs =
FileSystem::new(locked, kernel, CacheMode::Permanent, Arc::new(TmpFs(name)), options)?;
let mut mount_options = fs.options.params.clone();
let mode = if let Some(mode) = mount_options.remove(b"mode") {
FileMode::from_string(mode.as_ref())?
} else {
mode!(IFDIR, 0o777)
};
let uid = if let Some(uid) = mount_options.remove(b"uid") {
fs_args::parse::<uid_t>(uid.as_ref())?
} else {
0
};
let gid = if let Some(gid) = mount_options.remove(b"gid") {
fs_args::parse::<gid_t>(gid.as_ref())?
} else {
0
};
let root_ino = fs.allocate_ino();
let mut info = FsNodeInfo::new(mode!(IFDIR, 0o777), FsCred { uid, gid });
info.chmod(mode);
fs.create_root_with_info(root_ino, TmpFsDirectory::new(), info);
if !mount_options.is_empty() {
track_stub!(
TODO("https://fxbug.dev/322873419"),
"unknown tmpfs options, see logs for strings"
);
log_warn!("Unknown tmpfs options: {}", mount_options);
}
Ok(fs)
}
pub fn set_initial_content(kernel: &Kernel, fs: &FileSystemHandle, data: TmpFsData) {
fn create_dir_entry_from_data(
kernel: &Kernel,
fs: &FileSystemHandle,
data: TmpFsData,
this: Option<DirEntryHandle>,
name: FsString,
) -> DirEntryHandle {
// TODO: https://fxbug.dev/455771186 - Revise FsNode initialization to better ensure
// that all the things are appropriately labeled.
let new_direntry = |node, parent, name| {
let dir_entry = DirEntry::new(node, parent, name);
security::fs_node_init_with_dentry_deferred(kernel, &dir_entry);
dir_entry
};
match data.node_type {
TmpFsNodeType::Link(target) => {
assert!(this.is_none());
let node = TmpFsDirectory::new_symlink(fs, target.as_ref(), data.owner);
new_direntry(node, None, name)
}
TmpFsNodeType::Directory(children) => {
let this = this.unwrap_or_else(|| {
let info = FsNodeInfo::new(mode!(IFDIR, data.perm), data.owner);
let node = fs.create_node_and_allocate_node_id(TmpFsDirectory::new(), info);
new_direntry(node, None, name.clone())
});
this.node
.downcast_ops::<TmpFsDirectory>()
.expect("directory must be from tmpfs")
.child_count
.fetch_add(children.len() as u32, Ordering::Release);
let children = children
.into_iter()
.map(|(name, data)| {
let child =
create_dir_entry_from_data(kernel, fs, data, None, name.clone());
(name, child)
})
.collect::<BTreeMap<_, _>>();
this.set_children(children);
this
}
}
}
create_dir_entry_from_data(kernel, fs, data, Some(Arc::clone(fs.root())), "".into());
}
}
pub enum TmpFsNodeType {
Link(FsString),
Directory(BTreeMap<FsString, TmpFsData>),
}
pub struct TmpFsData {
pub owner: FsCred,
pub perm: u32,
pub node_type: TmpFsNodeType,
}
pub struct TmpFsDirectory {
xattrs: MemoryXattrStorage,
child_count: AtomicU32,
}
impl TmpFsDirectory {
pub fn new() -> Self {
Self { xattrs: MemoryXattrStorage::default(), child_count: AtomicU32::new(0) }
}
fn new_symlink(fs: &Arc<FileSystem>, target: &FsStr, owner: FsCred) -> FsNodeHandle {
let (link, info) = SymlinkNode::new(target, owner);
fs.create_node_and_allocate_node_id(link, info)
}
}
fn create_child_node(
parent: &FsNode,
mode: FileMode,
dev: DeviceType,
owner: FsCred,
) -> Result<FsNodeHandle, Errno> {
let ops: Box<dyn FsNodeOps> = match mode.fmt() {
FileMode::IFREG => Box::new(MemoryRegularNode::new()?),
FileMode::IFIFO | FileMode::IFBLK | FileMode::IFCHR | FileMode::IFSOCK => {
Box::new(TmpFsSpecialNode::new())
}
_ => return error!(EACCES),
};
let mut info = FsNodeInfo::new(mode, owner);
info.rdev = dev;
// blksize is PAGE_SIZE for in memory node.
info.blksize = *PAGE_SIZE as usize;
let child = parent.fs().create_node_and_allocate_node_id(ops, info);
if mode.fmt() == FileMode::IFREG {
// For files created in tmpfs, forbid sealing, by sealing the seal operation.
child.write_guard_state.lock().enable_sealing(SealFlags::SEAL);
}
Ok(child)
}
impl FsNodeOps for TmpFsDirectory {
fs_node_impl_xattr_delegate!(self, self.xattrs);
fn create_file_ops(
&self,
_locked: &mut Locked<FileOpsCore>,
_node: &FsNode,
_current_task: &CurrentTask,
_flags: OpenFlags,
) -> Result<Box<dyn FileOps>, Errno> {
Ok(Box::new(MemoryDirectoryFile::new()))
}
fn mkdir(
&self,
_locked: &mut Locked<FileOpsCore>,
node: &FsNode,
_current_task: &CurrentTask,
_name: &FsStr,
mode: FileMode,
owner: FsCred,
) -> Result<FsNodeHandle, Errno> {
node.update_info(|info| {
info.link_count += 1;
});
self.child_count.fetch_add(1, Ordering::Release);
Ok(node
.fs()
.create_node_and_allocate_node_id(TmpFsDirectory::new(), FsNodeInfo::new(mode, owner)))
}
fn mknod(
&self,
_locked: &mut Locked<FileOpsCore>,
node: &FsNode,
_current_task: &CurrentTask,
_name: &FsStr,
mode: FileMode,
dev: DeviceType,
owner: FsCred,
) -> Result<FsNodeHandle, Errno> {
let child = create_child_node(node, mode, dev, owner)?;
self.child_count.fetch_add(1, Ordering::Release);
Ok(child)
}
fn create_symlink(
&self,
_locked: &mut Locked<FileOpsCore>,
node: &FsNode,
_current_task: &CurrentTask,
_name: &FsStr,
target: &FsStr,
owner: FsCred,
) -> Result<FsNodeHandle, Errno> {
self.child_count.fetch_add(1, Ordering::Release);
Ok(Self::new_symlink(&node.fs(), target, owner))
}
fn create_tmpfile(
&self,
node: &FsNode,
_current_task: &CurrentTask,
mode: FileMode,
owner: FsCred,
) -> Result<FsNodeHandle, Errno> {
assert!(mode.is_reg());
create_child_node(node, mode, DeviceType::NONE, owner)
}
fn link(
&self,
_locked: &mut Locked<FileOpsCore>,
_node: &FsNode,
_current_task: &CurrentTask,
_name: &FsStr,
child: &FsNodeHandle,
) -> Result<(), Errno> {
child.update_info(|info| {
info.link_count += 1;
});
self.child_count.fetch_add(1, Ordering::Release);
Ok(())
}
fn unlink(
&self,
_locked: &mut Locked<FileOpsCore>,
node: &FsNode,
_current_task: &CurrentTask,
_name: &FsStr,
child_to_unlink: &FsNodeHandle,
) -> Result<(), Errno> {
if child_to_unlink.is_dir() {
// The following cast is safe, unless something is seriously wrong:
// - The filesystem should not be asked to unlink a node that it doesn't handle.
// - The child has already been determined to be a directory.
// - TmpFsDirectory is the ops for directories in this filesystem.
let child_count =
&child_to_unlink.downcast_ops::<TmpFsDirectory>().unwrap().child_count;
if child_count.load(Ordering::Relaxed) != 0 {
return error!(ENOTEMPTY);
}
node.update_info(|info| {
info.link_count -= 1;
});
}
child_to_unlink.update_info(|info| {
info.link_count -= 1;
});
self.child_count.fetch_sub(1, Ordering::Release);
Ok(())
}
}
struct TmpFsSpecialNode {
xattrs: MemoryXattrStorage,
}
impl TmpFsSpecialNode {
pub fn new() -> Self {
Self { xattrs: MemoryXattrStorage::default() }
}
}
impl FsNodeOps for TmpFsSpecialNode {
fs_node_impl_not_dir!();
fs_node_impl_xattr_delegate!(self, self.xattrs);
fn create_file_ops(
&self,
_locked: &mut Locked<FileOpsCore>,
_node: &FsNode,
_current_task: &CurrentTask,
_flags: OpenFlags,
) -> Result<Box<dyn FileOps>, Errno> {
unreachable!("Special nodes cannot be opened.");
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::testing::spawn_kernel_and_run;
use crate::vfs::buffers::{VecInputBuffer, VecOutputBuffer};
use crate::vfs::fs_args::MountParams;
use crate::vfs::{FdNumber, UnlinkKind};
use starnix_uapi::errno;
use starnix_uapi::file_mode::AccessCheck;
use starnix_uapi::mount_flags::MountFlags;
use starnix_uapi::vfs::ResolveFlags;
use zerocopy::IntoBytes;
#[::fuchsia::test]
async fn test_tmpfs() {
spawn_kernel_and_run(async |locked, current_task| {
let kernel = current_task.kernel();
let fs = TmpFs::new_fs(locked, &kernel);
let root = fs.root();
let usr = root.create_dir(locked, &current_task, "usr".into()).unwrap();
let _etc = root.create_dir(locked, &current_task, "etc".into()).unwrap();
let _usr_bin = usr.create_dir(locked, &current_task, "bin".into()).unwrap();
let mut names = root.copy_child_names();
names.sort();
assert!(names.iter().eq(["etc", "usr"].iter()));
})
.await;
}
#[::fuchsia::test]
async fn test_write_read() {
spawn_kernel_and_run(async |locked, current_task| {
let path = "test.bin";
let _file = current_task
.fs()
.root()
.create_node(
locked,
&current_task,
path.into(),
mode!(IFREG, 0o777),
DeviceType::NONE,
)
.unwrap();
let wr_file = current_task.open_file(locked, path.into(), OpenFlags::RDWR).unwrap();
let test_seq = 0..10000u16;
let test_vec = test_seq.collect::<Vec<_>>();
let test_bytes = test_vec.as_slice().as_bytes();
let written =
wr_file.write(locked, &current_task, &mut VecInputBuffer::new(test_bytes)).unwrap();
assert_eq!(written, test_bytes.len());
let mut read_buffer = VecOutputBuffer::new(test_bytes.len() + 1);
let read = wr_file.read_at(locked, &current_task, 0, &mut read_buffer).unwrap();
assert_eq!(read, test_bytes.len());
assert_eq!(test_bytes, read_buffer.data());
})
.await;
}
#[::fuchsia::test]
async fn test_read_past_eof() {
spawn_kernel_and_run(async |locked, current_task| {
// Open an empty file
let path = "test.bin";
let _file = current_task
.fs()
.root()
.create_node(
locked,
&current_task,
path.into(),
mode!(IFREG, 0o777),
DeviceType::NONE,
)
.unwrap();
let rd_file = current_task.open_file(locked, path.into(), OpenFlags::RDONLY).unwrap();
// Verify that attempting to read past the EOF (i.e. at a non-zero offset) returns 0
let buffer_size = 0x10000;
let mut output_buffer = VecOutputBuffer::new(buffer_size);
let test_offset = 100;
let result =
rd_file.read_at(locked, &current_task, test_offset, &mut output_buffer).unwrap();
assert_eq!(result, 0);
})
.await;
}
#[::fuchsia::test]
async fn test_permissions() {
spawn_kernel_and_run(async |locked, current_task| {
let path = "test.bin";
let file = current_task
.open_file_at(
locked,
FdNumber::AT_FDCWD,
path.into(),
OpenFlags::CREAT | OpenFlags::RDONLY,
FileMode::from_bits(0o777),
ResolveFlags::empty(),
AccessCheck::default(),
)
.expect("failed to create file");
assert_eq!(
0,
file.read(locked, &current_task, &mut VecOutputBuffer::new(0))
.expect("failed to read")
);
assert!(file.write(locked, &current_task, &mut VecInputBuffer::new(&[])).is_err());
let file = current_task
.open_file_at(
locked,
FdNumber::AT_FDCWD,
path.into(),
OpenFlags::WRONLY,
FileMode::EMPTY,
ResolveFlags::empty(),
AccessCheck::default(),
)
.expect("failed to open file WRONLY");
assert!(file.read(locked, &current_task, &mut VecOutputBuffer::new(0)).is_err());
assert_eq!(
0,
file.write(locked, &current_task, &mut VecInputBuffer::new(&[]))
.expect("failed to write")
);
let file = current_task
.open_file_at(
locked,
FdNumber::AT_FDCWD,
path.into(),
OpenFlags::RDWR,
FileMode::EMPTY,
ResolveFlags::empty(),
AccessCheck::default(),
)
.expect("failed to open file RDWR");
assert_eq!(
0,
file.read(locked, &current_task, &mut VecOutputBuffer::new(0))
.expect("failed to read")
);
assert_eq!(
0,
file.write(locked, &current_task, &mut VecInputBuffer::new(&[]))
.expect("failed to write")
);
})
.await;
}
#[::fuchsia::test]
async fn test_persistence() {
spawn_kernel_and_run(async |locked, current_task| {
{
let root = &current_task.fs().root().entry;
let usr = root
.create_dir(locked, &current_task, "usr".into())
.expect("failed to create usr");
root.create_dir(locked, &current_task, "etc".into())
.expect("failed to create usr/etc");
usr.create_dir(locked, &current_task, "bin".into())
.expect("failed to create usr/bin");
}
// At this point, all the nodes are dropped.
current_task
.open_file(locked, "/usr/bin".into(), OpenFlags::RDONLY | OpenFlags::DIRECTORY)
.expect("failed to open /usr/bin");
assert_eq!(
errno!(ENOENT),
current_task
.open_file(locked, "/usr/bin/test.txt".into(), OpenFlags::RDWR)
.unwrap_err()
);
current_task
.open_file_at(
locked,
FdNumber::AT_FDCWD,
"/usr/bin/test.txt".into(),
OpenFlags::RDWR | OpenFlags::CREAT,
FileMode::from_bits(0o777),
ResolveFlags::empty(),
AccessCheck::default(),
)
.expect("failed to create test.txt");
let txt = current_task
.open_file(locked, "/usr/bin/test.txt".into(), OpenFlags::RDWR)
.expect("failed to open test.txt");
let usr_bin = current_task
.open_file(locked, "/usr/bin".into(), OpenFlags::RDONLY)
.expect("failed to open /usr/bin");
usr_bin
.name
.unlink(locked, &current_task, "test.txt".into(), UnlinkKind::NonDirectory, false)
.expect("failed to unlink test.text");
assert_eq!(
errno!(ENOENT),
current_task
.open_file(locked, "/usr/bin/test.txt".into(), OpenFlags::RDWR)
.unwrap_err()
);
assert_eq!(
errno!(ENOENT),
usr_bin
.name
.unlink(
locked,
&current_task,
"test.txt".into(),
UnlinkKind::NonDirectory,
false
)
.unwrap_err()
);
assert_eq!(
0,
txt.read(locked, &current_task, &mut VecOutputBuffer::new(0))
.expect("failed to read")
);
std::mem::drop(txt);
assert_eq!(
errno!(ENOENT),
current_task
.open_file(locked, "/usr/bin/test.txt".into(), OpenFlags::RDWR)
.unwrap_err()
);
std::mem::drop(usr_bin);
let usr = current_task
.open_file(locked, "/usr".into(), OpenFlags::RDONLY)
.expect("failed to open /usr");
assert_eq!(
errno!(ENOENT),
current_task.open_file(locked, "/usr/foo".into(), OpenFlags::RDONLY).unwrap_err()
);
usr.name
.unlink(locked, &current_task, "bin".into(), UnlinkKind::Directory, false)
.expect("failed to unlink /usr/bin");
})
.await;
}
#[::fuchsia::test]
async fn test_data() {
spawn_kernel_and_run(async |locked, current_task| {
let kernel = current_task.kernel();
let fs = TmpFs::new_fs_with_options(
locked,
&kernel,
FileSystemOptions {
source: Default::default(),
flags: MountFlags::empty(),
params: MountParams::parse(b"mode=0123,uid=42,gid=84".into())
.expect("parsed correctly"),
},
)
.expect("new_fs");
let info = fs.root().node.info();
assert_eq!(info.mode, mode!(IFDIR, 0o123));
assert_eq!(info.uid, 42);
assert_eq!(info.gid, 84);
})
.await;
}
}