diff --git a/Cargo.lock b/Cargo.lock index cf14da6..e9ea13a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -889,6 +889,7 @@ dependencies = [ "dashmap 5.0.0", "env_logger", "futures-util", + "libc", "log", "olivefs_common", "path-absolutize", diff --git a/olivefs/src/filesystem.rs b/olivefs/src/filesystem.rs index 17955dd..065de3c 100644 --- a/olivefs/src/filesystem.rs +++ b/olivefs/src/filesystem.rs @@ -47,6 +47,13 @@ fn test_bits(haystack: i32, needle: i32) -> bool { (haystack & needle) == needle } +fn absolutise_time_or_now(input: TimeOrNow) -> SystemTime { + match input { + TimeOrNow::SpecificTime(time) => time, + TimeOrNow::Now => SystemTime::now(), + } +} + impl Filesystem for OliveFilesystem { /// Initialize filesystem. /// Called before any other filesystem method. @@ -164,8 +171,8 @@ impl Filesystem for OliveFilesystem { uid: Option, gid: Option, size: Option, - _atime: Option, - _mtime: Option, + atime: Option, + mtime: Option, _ctime: Option, fh: Option, _crtime: Option, @@ -174,12 +181,44 @@ impl Filesystem for OliveFilesystem { flags: Option, reply: ReplyAttr, ) { - debug!( - "[Not Implemented] setattr(ino: {:#x?}, mode: {:?}, uid: {:?}, \ - gid: {:?}, size: {:?}, fh: {:?}, flags: {:?})", - ino, mode, uid, gid, size, fh, flags + let requester = self.requester.clone(); + + self.spawn_with_error_handler( + async move { + let vnode = VnodeId( + ino.try_into() + .context("Converting u64 inode to u32 VnodeId.")?, + ); + + match requester + .setattr( + vnode, + mode, + uid, + gid, + size, + atime.map(absolutise_time_or_now), + mtime.map(absolutise_time_or_now), + ) + .await? + { + DataResponse::Success(file_metadata) => { + reply.attr(&Duration::from_secs(5), &file_metadata.into()); + } + DataResponse::Error { code, message } => { + warn!( + "setattr(ino: {:#x?}, mode: {:?}, uid: {:?}, \ + gid: {:?}, size: {:?}, fh: {:?}, flags: {:?}): {:?}", + ino, mode, uid, gid, size, fh, flags, message + ); + reply.error(code as c_int); + } + } + + Ok(()) + }, + "setattr", ); - reply.error(ENOSYS); } /// Read symbolic link. diff --git a/olivefs/src/requester.rs b/olivefs/src/requester.rs index dfefbff..93711dd 100644 --- a/olivefs/src/requester.rs +++ b/olivefs/src/requester.rs @@ -12,7 +12,7 @@ use std::net::SocketAddr; use std::path::Path; use std::str::FromStr; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, SystemTime}; use crate::configuration::StreamingReaderConfig; use crate::requester::streaming_reader::StreamingReader; @@ -269,6 +269,29 @@ impl Requester { .await } + pub async fn setattr( + &self, + vnode: VnodeId, + mode: Option, + uid: Option, + gid: Option, + size: Option, + atime: Option, + mtime: Option, + ) -> anyhow::Result> { + self.internal + .command(&DataCommand::SetAttr { + vnode, + mode, + uid, + gid, + size, + atime, + mtime, + }) + .await + } + pub async fn start_streaming_reader( &self, file_handle: u32, diff --git a/olivefs_common/src/messages.rs b/olivefs_common/src/messages.rs index 963954b..0d9540f 100644 --- a/olivefs_common/src/messages.rs +++ b/olivefs_common/src/messages.rs @@ -79,6 +79,15 @@ pub enum DataCommand { FlushFile { file_handle: u32, }, + SetAttr { + vnode: VnodeId, + mode: Option, + uid: Option, + gid: Option, + size: Option, + atime: Option, + mtime: Option, + }, } pub trait DataResponseBase: Serialize + DeserializeOwned + Debug + Clone + 'static {} diff --git a/olivefsd/Cargo.toml b/olivefsd/Cargo.toml index c38306b..668877e 100644 --- a/olivefsd/Cargo.toml +++ b/olivefsd/Cargo.toml @@ -39,5 +39,8 @@ x509-parser = "0.12.0" # consider also: sharded-slab (concurrent access), thunderdome (generational indices) slab = "0.4.5" +## System +libc = "0.2.112" + ## Common olivefs_common = { path = "../olivefs_common" } diff --git a/olivefsd/src/server/connections.rs b/olivefsd/src/server/connections.rs index 51ab1ef..5597224 100644 --- a/olivefsd/src/server/connections.rs +++ b/olivefsd/src/server/connections.rs @@ -94,6 +94,24 @@ pub async fn handle_command_stream( ) .await?; } + DataCommand::SetAttr { + vnode, + mode, + uid, + gid, + size, + atime, + mtime, + } => { + send_bare_message( + &mut tx, + &file_access + .setattr(vnode, mode, uid, gid, size, atime, mtime) + .await + .unwrap_or_else(error_to_response), + ) + .await?; + } } } diff --git a/olivefsd/src/server/file_access.rs b/olivefsd/src/server/file_access.rs index b2e2915..8a3b986 100644 --- a/olivefsd/src/server/file_access.rs +++ b/olivefsd/src/server/file_access.rs @@ -1,5 +1,5 @@ use olivefs_common::error_codes; -use olivefs_common::error_codes::{EBADFD, EFAULT, ENOENT}; +use olivefs_common::error_codes::{EBADFD, EFAULT, ENOENT, ENOSYS}; use olivefs_common::messages::{ DataResponse, DirectoryEntry, FileKind, FileMetadata, OpenMode, VnodeId, }; @@ -7,18 +7,23 @@ use path_absolutize::Absolutize; use slab::Slab; use std::borrow::Borrow; use std::collections::HashMap; +use std::io; use std::io::SeekFrom; -use anyhow::bail; +use anyhow::{anyhow, bail}; +use libc::{ELOOP, O_NOFOLLOW}; +use log::{error, warn}; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::sync::Arc; -use std::time::{Duration, Instant}; -use log::{error, warn}; +use std::time::{Duration, Instant, SystemTime}; use tokio::fs::{OpenOptions, ReadDir}; use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; use tokio::sync::RwLock; +// 4 symlinks ought to be enough for anybody! +pub const MAX_SYMLINK_RESOLVE_TRIES: u32 = 4; + /// Server-wide state that all clients might need to mess with #[derive(Default)] pub struct ServerwideState { @@ -109,6 +114,7 @@ impl FileAccess { if let Some(inode_info) = inode_map.get(vnode.0 as usize) { Ok(inode_info.real_path.clone()) } else { + // TODO just make this an io::Error. Err(DataResponse::Error { code: error_codes::EFAULT, message: "No inode info".to_string(), @@ -116,6 +122,45 @@ impl FileAccess { } } + async fn resolve_vnode_including_follow_symlinks_safely_best_effort( + &self, + vnode: VnodeId, + ) -> anyhow::Result { + let real_path = self + .resolve_vnode::<()>(vnode) + .await + .map_err(|_| anyhow!("No inode info"))?; + Ok(self.safely_resolve_symlinks_best_effort(real_path).await?) + } + + /// Guarantees that the end file was not a symlink at the time of checking. + /// The caller must still prevent themselves from opening symlinks (but they may safely fail + /// on opening a symlink if it gets changed under their feet). + /// The symlinks will stop resolving if they ever point outside of the root directory, even + /// if they point to a symlink that points back into the restricted tree. + async fn safely_resolve_symlinks_best_effort( + &self, + path: PathBuf, + ) -> Result { + let mut path = path; + let root = self.client_info.root.clone(); + tokio::task::spawn_blocking(move || -> Result { + for _ in 0..MAX_SYMLINK_RESOLVE_TRIES { + let abs_path = &path.absolutize_virtually(&root)?; + let metadata = abs_path.symlink_metadata()?; + + if !metadata.file_type().is_symlink() { + return Ok(abs_path.to_path_buf()); + } + + path = abs_path.read_link()?; + } + Err(io::Error::from_raw_os_error(ELOOP)).into() + }) + .await + .map_err(|_| io::Error::from_raw_os_error(EFAULT))? + } + async fn allocate_vnode(&self, path: PathBuf) -> anyhow::Result { let paths_to_vnodes = self.client_state.existing_paths_to_inodes.read().await; if let Some(vi) = paths_to_vnodes.get(&path) { @@ -314,6 +359,9 @@ impl FileAccess { // We'll allocate the Vnode after we know the target doesn't already exist. Fill in 0 for now. let mut open_options = OpenOptions::new(); + // IMPORTANT: Don't follow symlinks + open_options.custom_flags(O_NOFOLLOW); + // Only create new files open_options.create_new(true); @@ -580,4 +628,37 @@ impl FileAccess { }), } } + + pub async fn setattr( + &self, + vnode: VnodeId, + mode: Option, + uid: Option, + gid: Option, + size: Option, + atime: Option, + mtime: Option, + ) -> anyhow::Result> { + if mode.is_some() || uid.is_some() || gid.is_some() || atime.is_some() || mtime.is_some() { + return Err(io::Error::from_raw_os_error(ENOSYS).into()); + } + + let path = self + .resolve_vnode_including_follow_symlinks_safely_best_effort(vnode) + .await?; + + if let Some(truncate_to) = size { + let mut open_options = OpenOptions::new(); + + // IMPORTANT: Don't follow symlinks + open_options.custom_flags(O_NOFOLLOW); + open_options.write(true); + let file = open_options.open(&path).await?; + file.set_len(truncate_to).await?; + } + + let metadata = self.read_metadata(&path, vnode).await?; + + Ok(DataResponse::Success(metadata)) + } }