diff --git a/config/app-blocklist.toml b/config/app-blocklist.toml index 673c1446f..3e1eb3a31 100644 --- a/config/app-blocklist.toml +++ b/config/app-blocklist.toml @@ -66,4 +66,6 @@ blocked_apps = [ { name = "gvisor syscall tests", reason = "由于文件较大,因此屏蔽。如果要允许系统调用测试,则把这行取消注释即可" }, { name = "test_ebpf_new", reason = "2025.11.17,aya上游发版有问题,导致ci过不了,暂时禁用" }, { name = "test_ebpf_tp", reason = "2025.11.17,aya上游发版有问题,导致ci过不了,暂时禁用" }, + { name = "runcell", reason = "依赖项依赖github,可能网络问题导致编不过" }, + ] diff --git a/kernel/src/filesystem/devfs/mod.rs b/kernel/src/filesystem/devfs/mod.rs index 13ad41283..119d79345 100644 --- a/kernel/src/filesystem/devfs/mod.rs +++ b/kernel/src/filesystem/devfs/mod.rs @@ -148,6 +148,7 @@ impl DevFS { /// @brief 注册系统内部自带的设备 fn register_bultinin_device(&self) { + use crate::filesystem::fuse::dev::LockedFuseDevInode; use null_dev::LockedNullInode; use random_dev::LockedRandomInode; use zero_dev::LockedZeroInode; @@ -161,6 +162,9 @@ impl DevFS { dev_root .add_dev("random", LockedRandomInode::new()) .expect("DevFS: Failed to register /dev/random"); + dev_root + .add_dev("fuse", LockedFuseDevInode::new()) + .expect("DevFS: Failed to register /dev/fuse"); } /// @brief 在devfs内注册设备 diff --git a/kernel/src/filesystem/fuse/conn.rs b/kernel/src/filesystem/fuse/conn.rs new file mode 100644 index 000000000..ed7046c8d --- /dev/null +++ b/kernel/src/filesystem/fuse/conn.rs @@ -0,0 +1,858 @@ +use alloc::{collections::BTreeMap, collections::VecDeque, sync::Arc, vec::Vec}; +use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; + +use num_traits::FromPrimitive; +use system_error::SystemError; + +use crate::{ + arch::MMArch, + filesystem::epoll::{ + event_poll::EventPoll, event_poll::LockedEPItemLinkedList, EPollEventType, EPollItem, + }, + libs::{ + mutex::Mutex, + wait_queue::{WaitQueue, Waiter}, + }, + mm::MemoryManagementArch, + process::ProcessManager, +}; + +use crate::process::cred::CAPFlags; + +use super::protocol::{ + fuse_pack_struct, fuse_read_struct, FuseForgetIn, FuseInHeader, FuseInitIn, FuseInitOut, + FuseInterruptIn, FuseOutHeader, FuseWriteIn, FUSE_ABORT_ERROR, FUSE_ASYNC_DIO, FUSE_ASYNC_READ, + FUSE_ATOMIC_O_TRUNC, FUSE_AUTO_INVAL_DATA, FUSE_BIG_WRITES, FUSE_DESTROY, FUSE_DONT_MASK, + FUSE_DO_READDIRPLUS, FUSE_EXPLICIT_INVAL_DATA, FUSE_EXPORT_SUPPORT, FUSE_FLUSH, FUSE_FORGET, + FUSE_HANDLE_KILLPRIV, FUSE_INIT, FUSE_INIT_EXT, FUSE_INTERRUPT, FUSE_KERNEL_MINOR_VERSION, + FUSE_KERNEL_VERSION, FUSE_LOOKUP, FUSE_MAX_PAGES, FUSE_MIN_READ_BUFFER, FUSE_NOTIFY_DELETE, + FUSE_NOTIFY_INVAL_ENTRY, FUSE_NOTIFY_INVAL_INODE, FUSE_NOTIFY_POLL, FUSE_NOTIFY_RETRIEVE, + FUSE_NOTIFY_STORE, FUSE_NO_OPENDIR_SUPPORT, FUSE_NO_OPEN_SUPPORT, FUSE_PARALLEL_DIROPS, + FUSE_POSIX_ACL, FUSE_POSIX_LOCKS, FUSE_READDIRPLUS_AUTO, FUSE_WRITEBACK_CACHE, +}; + +fn wait_with_recheck(waitq: &WaitQueue, mut check: F) -> Result +where + F: FnMut() -> Result, SystemError>, +{ + if let Some(v) = check()? { + return Ok(v); + } + + loop { + let (waiter, waker) = Waiter::new_pair(); + waitq.register_waker(waker.clone())?; + + if let Some(v) = check()? { + waitq.remove_waker(&waker); + return Ok(v); + } + + if let Err(e) = waiter.wait(true) { + waitq.remove_waker(&waker); + return Err(e); + } + } +} + +#[derive(Debug)] +pub struct FuseRequest { + pub bytes: Vec, +} + +#[derive(Debug, Clone, Copy)] +struct FuseRequestCred { + uid: u32, + gid: u32, + pid: u32, +} + +#[derive(Debug)] +pub struct FusePendingState { + unique: u64, + opcode: u32, + response: Mutex, SystemError>>>, + wait: WaitQueue, +} + +impl FusePendingState { + pub fn new(unique: u64, opcode: u32) -> Self { + Self { + unique, + opcode, + response: Mutex::new(None), + wait: WaitQueue::default(), + } + } + + pub fn unique(&self) -> u64 { + self.unique + } + + pub fn complete(&self, v: Result, SystemError>) { + let mut guard = self.response.lock(); + if guard.is_some() { + // Duplicate replies are ignored (Linux does similarly). + return; + } + *guard = Some(v); + drop(guard); + self.wait.wakeup(None); + } + + pub fn wait_complete(&self) -> Result, SystemError> { + wait_with_recheck(&self.wait, || { + let mut guard = self.response.lock(); + if let Some(res) = guard.take() { + return Ok(Some(res)); + } + Ok(None) + })? + } +} + +#[derive(Debug, Clone, Copy)] +struct FuseInitNegotiated { + minor: u32, + max_readahead: u32, + max_write: u32, + time_gran: u32, + max_pages: u16, + flags: u64, +} + +impl Default for FuseInitNegotiated { + fn default() -> Self { + Self { + minor: 0, + max_readahead: 0, + // Linux guarantees max_write >= 4096 after init; before init keep sane default. + max_write: 4096, + time_gran: 0, + max_pages: 1, + flags: 0, + } + } +} + +#[derive(Debug)] +struct FuseConnInner { + connected: bool, + mounted: bool, + initialized: bool, + owner_uid: u32, + owner_gid: u32, + allow_other: bool, + init: FuseInitNegotiated, + no_open: bool, + no_opendir: bool, + no_readdirplus: bool, + pending: VecDeque>, + processing: BTreeMap>, +} + +/// FUSE connection object (roughly equivalent to Linux `struct fuse_conn`). +#[derive(Debug)] +pub struct FuseConn { + inner: Mutex, + next_unique: AtomicU64, + dev_count: AtomicUsize, + read_wait: WaitQueue, + init_wait: WaitQueue, + epitems: LockedEPItemLinkedList, +} + +impl FuseConn { + // Keep this in sync with `sys_read.rs` userspace chunking size. + const USER_READ_CHUNK: usize = 64 * 1024; + + pub fn new() -> Arc { + Arc::new(Self { + inner: Mutex::new(FuseConnInner { + connected: true, + mounted: false, + initialized: false, + owner_uid: 0, + owner_gid: 0, + allow_other: false, + init: FuseInitNegotiated::default(), + no_open: false, + no_opendir: false, + no_readdirplus: false, + pending: VecDeque::new(), + processing: BTreeMap::new(), + }), + // Use non-zero unique, keep even IDs for "ordinary" requests as Linux does. + next_unique: AtomicU64::new(2), + dev_count: AtomicUsize::new(1), + read_wait: WaitQueue::default(), + init_wait: WaitQueue::default(), + epitems: LockedEPItemLinkedList::default(), + }) + } + + #[allow(dead_code)] + pub fn is_mounted(&self) -> bool { + self.inner.lock().mounted + } + + pub fn mark_mounted(&self) -> Result<(), SystemError> { + let mut g = self.inner.lock(); + if !g.connected { + return Err(SystemError::ENOTCONN); + } + if g.mounted { + // Linux 6.6: mounting with an already-used /dev/fuse fd fails (-EINVAL). + return Err(SystemError::EINVAL); + } + g.mounted = true; + Ok(()) + } + + /// Roll back a mount reservation when mount setup fails before + /// the filesystem is actually attached to the mount tree. + pub fn rollback_mount_setup(&self) { + let mut g = self.inner.lock(); + g.mounted = false; + } + + pub fn is_initialized(&self) -> bool { + self.inner.lock().initialized + } + + pub fn configure_mount(&self, owner_uid: u32, owner_gid: u32, allow_other: bool) { + let mut g = self.inner.lock(); + g.owner_uid = owner_uid; + g.owner_gid = owner_gid; + g.allow_other = allow_other; + } + + fn has_init_flag(&self, flag: u64) -> bool { + let g = self.inner.lock(); + (g.init.flags & flag) != 0 + } + + pub fn should_skip_open(&self, opcode: u32) -> bool { + let g = self.inner.lock(); + match opcode { + super::protocol::FUSE_OPEN => g.no_open, + super::protocol::FUSE_OPENDIR => g.no_opendir, + _ => false, + } + } + + pub fn open_enosys_is_supported(&self, opcode: u32) -> bool { + match opcode { + super::protocol::FUSE_OPEN => self.has_init_flag(FUSE_NO_OPEN_SUPPORT), + super::protocol::FUSE_OPENDIR => self.has_init_flag(FUSE_NO_OPENDIR_SUPPORT), + _ => false, + } + } + + pub fn mark_no_open(&self, opcode: u32) { + let mut g = self.inner.lock(); + match opcode { + super::protocol::FUSE_OPEN => g.no_open = true, + super::protocol::FUSE_OPENDIR => g.no_opendir = true, + _ => {} + } + } + + pub fn use_readdirplus(&self) -> bool { + let g = self.inner.lock(); + !g.no_readdirplus && (g.init.flags & FUSE_DO_READDIRPLUS) != 0 + } + + pub fn disable_readdirplus(&self) { + let mut g = self.inner.lock(); + g.no_readdirplus = true; + } + + fn alloc_unique(&self) -> u64 { + self.next_unique.fetch_add(2, Ordering::Relaxed) + } + + fn allow_current_process(&self, cred: &crate::process::cred::Cred) -> bool { + let g = self.inner.lock(); + + if !g.mounted { + return true; + } + + if g.allow_other { + return true; + } + + // Linux: allow sysadmin to bypass allow_other restrictions (configurable). + if cred.has_capability(CAPFlags::CAP_SYS_ADMIN) { + return true; + } + + let owner_uid = g.owner_uid as usize; + let owner_gid = g.owner_gid as usize; + cred.uid.data() == owner_uid + && cred.euid.data() == owner_uid + && cred.suid.data() == owner_uid + && cred.gid.data() == owner_gid + && cred.egid.data() == owner_gid + && cred.sgid.data() == owner_gid + } + + pub fn check_allow_current_process(&self) -> Result<(), SystemError> { + let cred = ProcessManager::current_pcb().cred(); + if !self.allow_current_process(&cred) { + return Err(SystemError::EACCES); + } + Ok(()) + } + + fn wait_initialized(&self) -> Result<(), SystemError> { + wait_with_recheck(&self.init_wait, || { + let g = self.inner.lock(); + if !g.connected { + return Err(SystemError::ENOTCONN); + } + if g.initialized { + return Ok(Some(())); + } + Ok(None) + }) + } + + pub fn abort(&self) { + let processing: Vec> = { + let mut g = self.inner.lock(); + g.connected = false; + g.mounted = false; + g.pending.clear(); + let processing = g.processing.values().cloned().collect(); + g.processing.clear(); + processing + }; + for p in processing { + p.complete(Err(SystemError::ENOTCONN)); + } + self.read_wait.wakeup(None); + self.init_wait.wakeup(None); + let _ = EventPoll::wakeup_epoll( + &self.epitems, + EPollEventType::EPOLLERR | EPollEventType::EPOLLHUP, + ); + } + + /// Unmount path: fail in-flight requests and best-effort queue DESTROY. + /// + /// Keep the connection readable for daemon-side teardown; actual disconnect + /// happens when /dev/fuse is closed or explicit abort path is triggered. + pub fn on_umount(&self) { + let processing: Vec>; + let should_destroy: bool; + { + let mut g = self.inner.lock(); + should_destroy = g.connected && g.initialized; + g.mounted = false; + g.pending.clear(); + processing = g.processing.values().cloned().collect(); + g.processing.clear(); + } + + for p in processing { + p.complete(Err(SystemError::ENOTCONN)); + } + self.init_wait.wakeup(None); + + if !should_destroy { + self.abort(); + return; + } + + if self.enqueue_noreply(FUSE_DESTROY, 0, &[]).is_err() { + self.abort(); + return; + } + } + + /// Queue a FORGET message (no reply expected). + pub fn queue_forget(&self, nodeid: u64, nlookup: u64) -> Result<(), SystemError> { + if nodeid == 0 || nlookup == 0 { + return Ok(()); + } + let can_send = { + let g = self.inner.lock(); + g.connected && g.mounted && g.initialized + }; + if !can_send { + return Ok(()); + } + let inarg = FuseForgetIn { nlookup }; + self.enqueue_noreply(FUSE_FORGET, nodeid, fuse_pack_struct(&inarg)) + } + + fn queue_interrupt(&self, unique: u64) -> Result<(), SystemError> { + if unique == 0 { + return Ok(()); + } + let can_send = { + let g = self.inner.lock(); + g.connected && g.mounted && g.initialized + }; + if !can_send { + return Ok(()); + } + let inarg = FuseInterruptIn { unique }; + let _ = self.enqueue_request(FUSE_INTERRUPT, 0, fuse_pack_struct(&inarg))?; + Ok(()) + } + + /// Acquire a new `/dev/fuse` file handle reference to this connection. + pub fn dev_acquire(&self) { + self.dev_count.fetch_add(1, Ordering::Relaxed); + } + + /// Release a `/dev/fuse` file handle reference. When the last handle is closed, + /// abort the connection (Linux: daemon exits). + pub fn dev_release(&self) { + if self.dev_count.fetch_sub(1, Ordering::AcqRel) == 1 { + self.abort(); + } + } + + pub fn poll_mask(&self, have_pending: bool) -> EPollEventType { + let mut events = EPollEventType::EPOLLOUT | EPollEventType::EPOLLWRNORM; + let g = self.inner.lock(); + if !g.connected { + return EPollEventType::EPOLLERR; + } + if have_pending { + events |= EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM; + } + events + } + + pub fn poll(&self) -> EPollEventType { + let g = self.inner.lock(); + let have_pending = !g.pending.is_empty(); + drop(g); + self.poll_mask(have_pending) + } + + pub fn add_epitem(&self, epitem: Arc) -> Result<(), SystemError> { + self.epitems.lock().push_back(epitem); + Ok(()) + } + + pub fn remove_epitem(&self, epitem: &Arc) -> Result<(), SystemError> { + let mut guard = self.epitems.lock(); + let len = guard.len(); + guard.retain(|x| !Arc::ptr_eq(x, epitem)); + if len != guard.len() { + return Ok(()); + } + Err(SystemError::ENOENT) + } + + fn calc_min_read_buffer(max_write: usize) -> usize { + core::cmp::max( + FUSE_MIN_READ_BUFFER, + core::mem::size_of::() + core::mem::size_of::() + max_write, + ) + } + + fn max_write_cap_for_user_read_chunk() -> usize { + let overhead = core::mem::size_of::() + core::mem::size_of::(); + if Self::USER_READ_CHUNK <= overhead { + 4096 + } else { + Self::USER_READ_CHUNK - overhead + } + } + + fn min_read_buffer(&self) -> usize { + let g = self.inner.lock(); + Self::calc_min_read_buffer(g.init.max_write as usize) + } + + fn pop_pending_nonblock(&self) -> Result, SystemError> { + let mut g = self.inner.lock(); + if !g.connected { + return Err(SystemError::ENOTCONN); + } + g.pending + .pop_front() + .ok_or(SystemError::EAGAIN_OR_EWOULDBLOCK) + } + + fn pop_pending_blocking(&self) -> Result, SystemError> { + wait_with_recheck(&self.read_wait, || { + let mut g = self.inner.lock(); + if !g.connected { + return Err(SystemError::ENOTCONN); + } + if let Some(req) = g.pending.pop_front() { + return Ok(Some(req)); + } + Ok(None) + }) + } + + pub fn read_request(&self, nonblock: bool, out: &mut [u8]) -> Result { + // Linux: require a sane minimum read buffer for all reads. + let min_read = self.min_read_buffer(); + if out.len() < min_read { + log::warn!( + "fuse: read buffer too small: got={} min={} nonblock={}", + out.len(), + min_read, + nonblock + ); + return Err(SystemError::EINVAL); + } + + // Linux: if O_NONBLOCK and no pending request, return EAGAIN. + let req = if nonblock { + self.pop_pending_nonblock()? + } else { + self.pop_pending_blocking()? + }; + + if out.len() < req.bytes.len() { + // Put it back and report EINVAL: userspace must provide a sufficiently large buffer. + let req_len = req.bytes.len(); + let mut g = self.inner.lock(); + if g.connected { + g.pending.push_front(req); + } + log::warn!( + "fuse: read buffer smaller than queued request: got={} need={}", + out.len(), + req_len + ); + return Err(SystemError::EINVAL); + } + + out[..req.bytes.len()].copy_from_slice(&req.bytes); + Ok(req.bytes.len()) + } + + fn kernel_init_flags() -> u64 { + FUSE_ASYNC_READ + | FUSE_POSIX_LOCKS + | FUSE_ATOMIC_O_TRUNC + | FUSE_EXPORT_SUPPORT + | FUSE_BIG_WRITES + | FUSE_DONT_MASK + | FUSE_AUTO_INVAL_DATA + | FUSE_DO_READDIRPLUS + | FUSE_READDIRPLUS_AUTO + | FUSE_ASYNC_DIO + | FUSE_WRITEBACK_CACHE + | FUSE_NO_OPEN_SUPPORT + | FUSE_PARALLEL_DIROPS + | FUSE_HANDLE_KILLPRIV + | FUSE_POSIX_ACL + | FUSE_ABORT_ERROR + | FUSE_MAX_PAGES + | FUSE_NO_OPENDIR_SUPPORT + | FUSE_EXPLICIT_INVAL_DATA + | FUSE_INIT_EXT + } + + pub fn enqueue_init(&self) -> Result<(), SystemError> { + let flags = Self::kernel_init_flags(); + let init_in = FuseInitIn { + major: FUSE_KERNEL_VERSION, + minor: FUSE_KERNEL_MINOR_VERSION, + max_readahead: 0, + flags: flags as u32, + flags2: (flags >> 32) as u32, + unused: [0; 11], + }; + self.enqueue_request(FUSE_INIT, 0, fuse_pack_struct(&init_in)) + .map(|_| ()) + } + + pub fn request( + &self, + opcode: u32, + nodeid: u64, + payload: &[u8], + ) -> Result, SystemError> { + if opcode != FUSE_INIT { + let cred = ProcessManager::current_pcb().cred(); + if !self.allow_current_process(&cred) { + return Err(SystemError::EACCES); + } + self.wait_initialized()?; + } + let pending = self.enqueue_request(opcode, nodeid, payload)?; + match pending.wait_complete() { + Err(SystemError::EINTR) | Err(SystemError::ERESTARTSYS) => { + if opcode != FUSE_INTERRUPT { + let _ = self.queue_interrupt(pending.unique()); + } + Err(SystemError::EINTR) + } + x => x, + } + } + + fn enqueue_noreply(&self, opcode: u32, nodeid: u64, payload: &[u8]) -> Result<(), SystemError> { + let unique = self.alloc_unique(); + let req = self.build_request( + unique, + opcode, + nodeid, + payload, + FuseRequestCred { + uid: 0, + gid: 0, + pid: 0, + }, + ); + self.push_request(req, None, unique)?; + Ok(()) + } + + fn enqueue_request( + &self, + opcode: u32, + nodeid: u64, + payload: &[u8], + ) -> Result, SystemError> { + let unique = self.alloc_unique(); + + let pcb = ProcessManager::current_pcb(); + let cred = pcb.cred(); + if !self.allow_current_process(&cred) { + return Err(SystemError::EACCES); + } + let pid = pcb.task_tgid_vnr().map(|p| p.data() as u32).unwrap_or(0); + let pending_state = Arc::new(FusePendingState::new(unique, opcode)); + let req = self.build_request( + unique, + opcode, + nodeid, + payload, + FuseRequestCred { + uid: cred.fsuid.data() as u32, + gid: cred.fsgid.data() as u32, + pid, + }, + ); + self.push_request(req, Some(pending_state.clone()), unique)?; + Ok(pending_state) + } + + fn build_request( + &self, + unique: u64, + opcode: u32, + nodeid: u64, + payload: &[u8], + req_cred: FuseRequestCred, + ) -> Arc { + let hdr = FuseInHeader { + len: (core::mem::size_of::() + payload.len()) as u32, + opcode, + unique, + nodeid, + uid: req_cred.uid, + gid: req_cred.gid, + pid: req_cred.pid, + total_extlen: 0, + padding: 0, + }; + + let mut bytes = Vec::with_capacity(hdr.len as usize); + bytes.extend_from_slice(fuse_pack_struct(&hdr)); + bytes.extend_from_slice(payload); + Arc::new(FuseRequest { bytes }) + } + + fn push_request( + &self, + req: Arc, + pending_state: Option>, + unique: u64, + ) -> Result<(), SystemError> { + { + let mut g = self.inner.lock(); + if !g.connected { + return Err(SystemError::ENOTCONN); + } + g.pending.push_back(req); + if let Some(pending) = pending_state { + g.processing.insert(unique, pending); + } + } + + self.read_wait.wakeup(None); + let _ = EventPoll::wakeup_epoll( + &self.epitems, + EPollEventType::EPOLLIN | EPollEventType::EPOLLRDNORM, + ); + Ok(()) + } + + pub fn write_reply(&self, data: &[u8]) -> Result { + if data.len() < core::mem::size_of::() { + return Err(SystemError::EINVAL); + } + + let out_hdr: FuseOutHeader = fuse_read_struct(data)?; + if out_hdr.len as usize != data.len() { + return Err(SystemError::EINVAL); + } + + if out_hdr.unique == 0 { + let payload = &data[core::mem::size_of::()..]; + self.handle_notify(out_hdr.error, payload)?; + return Ok(data.len()); + } + + let pending = { + let mut g = self.inner.lock(); + if !g.connected { + return Err(SystemError::ENOENT); + } + g.processing + .remove(&out_hdr.unique) + .ok_or(SystemError::ENOENT)? + }; + + let payload = &data[core::mem::size_of::()..]; + let error = out_hdr.error; + + if error != 0 { + // Negative errno from userspace. + let errno = -error; + let e = SystemError::from_i32(errno).unwrap_or(SystemError::EIO); + if Self::is_expected_reply_error(pending.opcode, errno) { + log::trace!( + "fuse: reply error opcode={} unique={} errno={}", + pending.opcode, + out_hdr.unique, + errno + ); + } else { + log::warn!( + "fuse: reply error opcode={} unique={} errno={}", + pending.opcode, + out_hdr.unique, + errno + ); + } + pending.complete(Err(e)); + if pending.opcode == FUSE_INIT { + self.abort(); + } + return Ok(data.len()); + } + + if pending.opcode == FUSE_INIT { + let init_out: FuseInitOut = match fuse_read_struct(payload) { + Ok(v) => v, + Err(e) => { + pending.complete(Err(e)); + self.abort(); + return Ok(data.len()); + } + }; + + if init_out.major != FUSE_KERNEL_VERSION { + pending.complete(Err(SystemError::EINVAL)); + self.abort(); + return Ok(data.len()); + } + + let mut negotiated_flags = init_out.flags as u64; + if (negotiated_flags & FUSE_INIT_EXT) != 0 { + negotiated_flags |= (init_out.flags2 as u64) << 32; + } + let negotiated_minor = core::cmp::min(init_out.minor, FUSE_KERNEL_MINOR_VERSION); + let negotiated_max_pages_raw = + if (negotiated_flags & FUSE_MAX_PAGES) != 0 && init_out.max_pages != 0 { + init_out.max_pages + } else { + 1 + }; + let negotiated_max_write = core::cmp::max(4096usize, init_out.max_write as usize); + let max_write_cap = Self::max_write_cap_for_user_read_chunk(); + let capped_max_write = core::cmp::min(negotiated_max_write, max_write_cap); + if capped_max_write < negotiated_max_write { + log::trace!( + "fuse: cap negotiated max_write from {} to {} due user read chunk limit", + negotiated_max_write, + capped_max_write + ); + } + let pages_from_write = + core::cmp::max(1usize, capped_max_write.div_ceil(MMArch::PAGE_SIZE)) as u16; + let negotiated_max_pages = core::cmp::min(negotiated_max_pages_raw, pages_from_write); + + { + let mut g = self.inner.lock(); + if g.connected { + g.initialized = true; + g.init = FuseInitNegotiated { + minor: negotiated_minor, + max_readahead: init_out.max_readahead, + max_write: capped_max_write as u32, + time_gran: init_out.time_gran, + max_pages: negotiated_max_pages, + flags: negotiated_flags, + }; + } + } + self.init_wait.wakeup(None); + } + + pending.complete(Ok(payload.to_vec())); + Ok(data.len()) + } + + fn handle_notify(&self, code: i32, payload: &[u8]) -> Result<(), SystemError> { + if code <= 0 { + return Err(SystemError::EINVAL); + } + match code { + FUSE_NOTIFY_POLL + | FUSE_NOTIFY_INVAL_INODE + | FUSE_NOTIFY_INVAL_ENTRY + | FUSE_NOTIFY_STORE + | FUSE_NOTIFY_RETRIEVE + | FUSE_NOTIFY_DELETE => { + log::debug!("fuse: notify code={} len={}", code, payload.len()); + Ok(()) + } + _ => Err(SystemError::EINVAL), + } + } + + fn is_expected_reply_error(opcode: u32, errno: i32) -> bool { + matches!( + (opcode, SystemError::from_i32(errno)), + (FUSE_LOOKUP, Some(SystemError::ENOENT)) + | (FUSE_FLUSH, Some(SystemError::ENOSYS)) + | (FUSE_INTERRUPT, Some(SystemError::EAGAIN_OR_EWOULDBLOCK)) + ) + } + + #[allow(dead_code)] + pub fn negotiated_state(&self) -> (u32, u32, u32, u32, u16, u64) { + let g = self.inner.lock(); + ( + g.init.minor, + g.init.max_readahead, + g.init.max_write, + g.init.time_gran, + g.init.max_pages, + g.init.flags, + ) + } + + pub fn max_write(&self) -> usize { + let g = self.inner.lock(); + core::cmp::max(4096usize, g.init.max_write as usize) + } +} diff --git a/kernel/src/filesystem/fuse/dev.rs b/kernel/src/filesystem/fuse/dev.rs new file mode 100644 index 000000000..db10aba78 --- /dev/null +++ b/kernel/src/filesystem/fuse/dev.rs @@ -0,0 +1,298 @@ +use alloc::{ + string::String, + sync::{Arc, Weak}, + vec::Vec, +}; + +use system_error::SystemError; + +use crate::{ + driver::base::device::device_number::DeviceNumber, + filesystem::{ + devfs::{DevFS, DeviceINode, LockedDevFSInode}, + epoll::EPollItem, + vfs::{ + file::FileFlags, vcore::generate_inode_id, FilePrivateData, FileSystem, FileType, + IndexNode, InodeFlags, InodeMode, Metadata, PollableInode, + }, + }, + libs::mutex::{Mutex, MutexGuard}, + process::ProcessManager, + syscall::user_access::UserBufferReader, + time::PosixTimeSpec, +}; + +use super::{ + conn::FuseConn, + private_data::{FuseDevPrivateData, FuseFilePrivateData}, +}; +const FUSE_DEV_IOC_CLONE: u32 = 0x8004_e500; // _IOR(229, 0, uint32_t) + +#[derive(Debug)] +pub struct FuseDevInode { + self_ref: Weak, + fs: Weak, + parent: Weak, + metadata: Metadata, +} + +#[derive(Debug)] +pub struct LockedFuseDevInode(Mutex); + +impl LockedFuseDevInode { + pub fn new() -> Arc { + let inode = FuseDevInode { + self_ref: Weak::default(), + fs: Weak::default(), + parent: Weak::default(), + metadata: Metadata { + dev_id: 1, + inode_id: generate_inode_id(), + size: 0, + blk_size: 0, + blocks: 0, + atime: PosixTimeSpec::default(), + mtime: PosixTimeSpec::default(), + ctime: PosixTimeSpec::default(), + btime: PosixTimeSpec::default(), + file_type: FileType::CharDevice, + mode: InodeMode::from_bits_truncate(0o666), + flags: InodeFlags::empty(), + nlinks: 1, + uid: 0, + gid: 0, + raw_dev: DeviceNumber::default(), + }, + }; + let result = Arc::new(LockedFuseDevInode(Mutex::new(inode))); + result.0.lock().self_ref = Arc::downgrade(&result); + result + } +} + +impl DeviceINode for LockedFuseDevInode { + fn set_fs(&self, fs: Weak) { + self.0.lock().fs = fs; + } + + fn set_parent(&self, parent: Weak) { + self.0.lock().parent = parent; + } +} + +impl PollableInode for LockedFuseDevInode { + fn poll(&self, private_data: &FilePrivateData) -> Result { + let FilePrivateData::Fuse(FuseFilePrivateData::Dev(p)) = private_data else { + return Err(SystemError::EINVAL); + }; + let conn = p.conn_ref()?; + Ok(conn.poll().bits() as usize) + } + + fn add_epitem( + &self, + epitem: Arc, + private_data: &FilePrivateData, + ) -> Result<(), SystemError> { + let FilePrivateData::Fuse(FuseFilePrivateData::Dev(p)) = private_data else { + return Err(SystemError::EINVAL); + }; + let conn = p.conn_ref()?; + conn.add_epitem(epitem) + } + + fn remove_epitem( + &self, + epitem: &Arc, + private_data: &FilePrivateData, + ) -> Result<(), SystemError> { + let FilePrivateData::Fuse(FuseFilePrivateData::Dev(p)) = private_data else { + return Err(SystemError::EINVAL); + }; + let conn = p.conn_ref()?; + conn.remove_epitem(epitem) + } +} + +impl IndexNode for LockedFuseDevInode { + fn is_stream(&self) -> bool { + true + } + + fn open( + &self, + mut data: MutexGuard, + flags: &FileFlags, + ) -> Result<(), SystemError> { + let nonblock = flags.contains(FileFlags::O_NONBLOCK); + let conn = FuseConn::new(); + let conn_any: Arc = conn; + *data = FilePrivateData::Fuse(FuseFilePrivateData::Dev(FuseDevPrivateData { + conn: conn_any, + nonblock, + })); + Ok(()) + } + + fn close(&self, data: MutexGuard) -> Result<(), SystemError> { + if let FilePrivateData::Fuse(FuseFilePrivateData::Dev(p)) = &*data { + if let Ok(conn) = p.conn_ref() { + conn.dev_release(); + } + } + Ok(()) + } + + fn read_at( + &self, + _offset: usize, + len: usize, + buf: &mut [u8], + data: MutexGuard, + ) -> Result { + if buf.len() < len { + return Err(SystemError::EINVAL); + } + let (conn_any, nonblock) = { + let FilePrivateData::Fuse(FuseFilePrivateData::Dev(p)) = &*data else { + return Err(SystemError::EINVAL); + }; + (p.conn.clone(), p.nonblock) + }; + // Drop private_data lock before potentially blocking in read_request(). + drop(data); + let conn = conn_any + .downcast::() + .map_err(|_| SystemError::EINVAL)?; + match conn.read_request(nonblock, &mut buf[..len]) { + Err(SystemError::ENOTCONN) => Err(SystemError::ENODEV), + x => x, + } + } + + fn write_at( + &self, + _offset: usize, + len: usize, + buf: &[u8], + data: MutexGuard, + ) -> Result { + if buf.len() < len { + return Err(SystemError::EINVAL); + } + let conn_any = { + let FilePrivateData::Fuse(FuseFilePrivateData::Dev(p)) = &*data else { + return Err(SystemError::EINVAL); + }; + p.conn.clone() + }; + // Drop private_data lock before potentially blocking in write_reply(). + drop(data); + let conn = conn_any + .downcast::() + .map_err(|_| SystemError::EINVAL)?; + match conn.write_reply(&buf[..len]) { + Err(SystemError::ENOTCONN) => Err(SystemError::ENOENT), + x => x, + } + } + + fn metadata(&self) -> Result { + Ok(self.0.lock().metadata.clone()) + } + + fn set_metadata(&self, metadata: &Metadata) -> Result<(), SystemError> { + let mut inode = self.0.lock(); + inode.metadata.atime = metadata.atime; + inode.metadata.mtime = metadata.mtime; + inode.metadata.ctime = metadata.ctime; + inode.metadata.btime = metadata.btime; + inode.metadata.mode = metadata.mode; + inode.metadata.uid = metadata.uid; + inode.metadata.gid = metadata.gid; + Ok(()) + } + + fn fs(&self) -> Arc { + self.0.lock().fs.upgrade().unwrap() + } + + fn as_any_ref(&self) -> &dyn core::any::Any { + self + } + + fn list(&self) -> Result, SystemError> { + Err(SystemError::EINVAL) + } + + fn parent(&self) -> Result, SystemError> { + let parent = self.0.lock().parent.upgrade().ok_or(SystemError::ENOENT)?; + Ok(parent) + } + + fn as_pollable_inode(&self) -> Result<&dyn PollableInode, SystemError> { + Ok(self) + } + + fn ioctl( + &self, + cmd: u32, + data: usize, + mut private_data: MutexGuard, + ) -> Result { + match cmd { + FUSE_DEV_IOC_CLONE => { + if data == 0 { + return Err(SystemError::EFAULT); + } + + let reader = + UserBufferReader::new(data as *const u32, core::mem::size_of::(), true)?; + let oldfd = reader.buffer_protected(0)?.read_one::(0)? as i32; + + let old_file = ProcessManager::current_pcb() + .fd_table() + .read() + .get_file_by_fd(oldfd) + .ok_or(SystemError::EINVAL)?; + + let old_conn = { + let guard = old_file.private_data.lock(); + let FilePrivateData::Fuse(fuse_data) = &*guard else { + return Err(SystemError::EINVAL); + }; + let FuseFilePrivateData::Dev(p) = fuse_data else { + return Err(SystemError::EINVAL); + }; + p.conn.clone() + }; + + let FilePrivateData::Fuse(FuseFilePrivateData::Dev(p)) = &mut *private_data else { + return Err(SystemError::EINVAL); + }; + let old_fc = old_conn + .clone() + .downcast::() + .map_err(|_| SystemError::EINVAL)?; + + // If this fd already points to the same connection, this is a no-op. + if let Ok(cur_fc) = p.conn.clone().downcast::() { + if Arc::ptr_eq(&cur_fc, &old_fc) { + return Ok(0); + } + cur_fc.dev_release(); + } + + old_fc.dev_acquire(); + p.conn = old_conn; + + Ok(0) + } + _ => Err(SystemError::ENOTTY), + } + } + + fn absolute_path(&self) -> Result { + Ok(String::from("/dev/fuse")) + } +} diff --git a/kernel/src/filesystem/fuse/fs.rs b/kernel/src/filesystem/fuse/fs.rs new file mode 100644 index 000000000..2cf7750b6 --- /dev/null +++ b/kernel/src/filesystem/fuse/fs.rs @@ -0,0 +1,339 @@ +use alloc::{ + collections::BTreeMap, + sync::{Arc, Weak}, + vec::Vec, +}; + +use system_error::SystemError; + +use crate::{ + filesystem::vfs::{ + FileSystem, FileSystemMakerData, FileType, FsInfo, IndexNode, InodeFlags, InodeId, + InodeMode, Magic, Metadata, MountableFileSystem, SuperBlock, FSMAKER, + }, + libs::mutex::Mutex, + process::ProcessManager, + register_mountable_fs, + time::PosixTimeSpec, +}; + +use linkme::distributed_slice; + +use super::{ + conn::FuseConn, + inode::FuseNode, + private_data::FuseFilePrivateData, + protocol::{fuse_read_struct, FuseStatfsOut, FUSE_ROOT_ID, FUSE_STATFS}, +}; + +#[derive(Debug)] +pub struct FuseMountData { + pub rootmode: u32, + pub user_id: u32, + pub group_id: u32, + pub allow_other: bool, + pub default_permissions: bool, + pub conn: Arc, +} + +impl FileSystemMakerData for FuseMountData { + fn as_any(&self) -> &dyn core::any::Any { + self + } +} + +#[derive(Debug)] +pub struct FuseFS { + root: Arc, + super_block: SuperBlock, + conn: Arc, + nodes: Mutex>>, + default_permissions: bool, +} + +impl FuseFS { + fn parse_opt_u32_decimal(v: &str) -> Result { + v.parse::().map_err(|_| SystemError::EINVAL) + } + + fn parse_opt_i32_decimal(v: &str) -> Result { + v.parse::().map_err(|_| SystemError::EINVAL) + } + + fn parse_opt_u32_octal(v: &str) -> Result { + u32::from_str_radix(v, 8).map_err(|_| SystemError::EINVAL) + } + + fn parse_opt_bool_switch(v: &str) -> bool { + v.is_empty() || v != "0" + } + + fn parse_mount_options( + raw: Option<&str>, + ) -> Result<(i32, u32, u32, u32, bool, bool), SystemError> { + let mut fd: Option = None; + let mut rootmode: Option = None; + let mut user_id: Option = None; + let mut group_id: Option = None; + let mut default_permissions = false; + let mut allow_other = false; + + let s = raw.unwrap_or(""); + for part in s.split(',') { + let part = part.trim(); + if part.is_empty() { + continue; + } + let (k, v) = match part.split_once('=') { + Some((k, v)) => (k.trim(), v.trim()), + None => (part, ""), + }; + match k { + "fd" => { + fd = Some(Self::parse_opt_i32_decimal(v)?); + } + "rootmode" => { + // Linux expects octal representation. + rootmode = Some(Self::parse_opt_u32_octal(v)?); + } + "user_id" => { + user_id = Some(Self::parse_opt_u32_decimal(v)?); + } + "group_id" => { + group_id = Some(Self::parse_opt_u32_decimal(v)?); + } + "default_permissions" => { + default_permissions = Self::parse_opt_bool_switch(v); + } + "allow_other" => { + allow_other = Self::parse_opt_bool_switch(v); + } + _ => {} + } + } + + let fd = fd.ok_or(SystemError::EINVAL)?; + let pcb = ProcessManager::current_pcb(); + let cred = pcb.cred(); + let user_id = user_id.unwrap_or(cred.fsuid.data() as u32); + let group_id = group_id.unwrap_or(cred.fsgid.data() as u32); + // Default root mode: directory 0755 (with type bit). + let rootmode = rootmode.unwrap_or(0o040755); + + Ok(( + fd, + rootmode, + user_id, + group_id, + default_permissions, + allow_other, + )) + } + + pub fn root_node(&self) -> Arc { + self.root.clone() + } + + pub fn get_or_create_node( + self: &Arc, + nodeid: u64, + parent_nodeid: u64, + cached: Option, + ) -> Arc { + if nodeid == FUSE_ROOT_ID { + return self.root.clone(); + } + + let mut nodes = self.nodes.lock(); + if let Some(w) = nodes.get(&nodeid) { + if let Some(n) = w.upgrade() { + n.set_parent_nodeid(parent_nodeid); + if let Some(md) = cached { + n.set_cached_metadata(md); + } + return n; + } + } + + let n = FuseNode::new( + Arc::downgrade(self), + self.conn.clone(), + nodeid, + parent_nodeid, + cached, + ); + nodes.insert(nodeid, Arc::downgrade(&n)); + n + } +} + +impl MountableFileSystem for FuseFS { + fn make_mount_data( + raw_data: Option<&str>, + _source: &str, + ) -> Result>, SystemError> { + let (fd, rootmode, user_id, group_id, default_permissions, allow_other) = + Self::parse_mount_options(raw_data)?; + + let file = ProcessManager::current_pcb() + .fd_table() + .read() + .get_file_by_fd(fd) + .ok_or(SystemError::EBADF)?; + + let conn = { + let pdata = file.private_data.lock(); + match &*pdata { + crate::filesystem::vfs::FilePrivateData::Fuse(FuseFilePrivateData::Dev(p)) => { + p.conn_ref()? + } + _ => return Err(SystemError::EINVAL), + } + }; + + Ok(Some(Arc::new(FuseMountData { + rootmode, + user_id, + group_id, + allow_other, + default_permissions, + conn, + }))) + } + + fn make_fs( + data: Option<&dyn FileSystemMakerData>, + ) -> Result, SystemError> { + let mount_data = data + .and_then(|d| d.as_any().downcast_ref::()) + .ok_or(SystemError::EINVAL)?; + + let super_block = SuperBlock::new(Magic::FUSE_MAGIC, 4096, 255); + + let root_md = Metadata { + dev_id: 0, + inode_id: InodeId::new(FUSE_ROOT_ID as usize), + size: 0, + blk_size: 0, + blocks: 0, + atime: PosixTimeSpec::default(), + mtime: PosixTimeSpec::default(), + ctime: PosixTimeSpec::default(), + btime: PosixTimeSpec::default(), + file_type: FileType::Dir, + mode: InodeMode::from_bits_truncate(mount_data.rootmode), + flags: InodeFlags::empty(), + nlinks: 2, + uid: mount_data.user_id as usize, + gid: mount_data.group_id as usize, + raw_dev: crate::driver::base::device::device_number::DeviceNumber::default(), + }; + + let conn = mount_data.conn.clone(); + conn.mark_mounted()?; + conn.configure_mount( + mount_data.user_id, + mount_data.group_id, + mount_data.allow_other, + ); + + let fs = Arc::new_cyclic(|weak_fs| FuseFS { + root: FuseNode::new( + weak_fs.clone(), + conn.clone(), + FUSE_ROOT_ID, + FUSE_ROOT_ID, + Some(root_md), + ), + super_block, + conn: conn.clone(), + nodes: Mutex::new(BTreeMap::new()), + default_permissions: mount_data.default_permissions, + }); + + if let Err(e) = conn.enqueue_init() { + conn.rollback_mount_setup(); + return Err(e); + } + Ok(fs) + } +} + +register_mountable_fs!(FuseFS, FUSEFSMAKER, "fuse"); + +impl FileSystem for FuseFS { + fn root_inode(&self) -> Arc { + self.root.clone() + } + + fn info(&self) -> FsInfo { + FsInfo { + blk_dev_id: 0, + max_name_len: 255, + } + } + + fn as_any_ref(&self) -> &dyn core::any::Any { + self + } + + fn name(&self) -> &str { + "fuse" + } + + fn super_block(&self) -> SuperBlock { + self.super_block.clone() + } + + fn statfs(&self, inode: &Arc) -> Result { + match self.conn.check_allow_current_process() { + Ok(()) => {} + Err(SystemError::EACCES) => { + let mut sb = self.super_block.clone(); + sb.magic = Magic::FUSE_MAGIC; + return Ok(sb); + } + Err(e) => return Err(e), + } + + let nodeid = inode + .as_any_ref() + .downcast_ref::() + .map(|n| n.nodeid()) + .unwrap_or(FUSE_ROOT_ID); + + let payload = self.conn.request(FUSE_STATFS, nodeid, &[])?; + let out: FuseStatfsOut = fuse_read_struct(&payload)?; + + let mut sb = self.super_block.clone(); + sb.magic = Magic::FUSE_MAGIC; + sb.blocks = out.st.blocks; + sb.bfree = out.st.bfree; + sb.bavail = out.st.bavail; + sb.files = out.st.files; + sb.ffree = out.st.ffree; + sb.bsize = out.st.bsize as u64; + sb.namelen = out.st.namelen as u64; + sb.frsize = out.st.frsize as u64; + Ok(sb) + } + + fn permission_policy(&self) -> crate::filesystem::vfs::FsPermissionPolicy { + if self.default_permissions { + crate::filesystem::vfs::FsPermissionPolicy::Dac + } else { + crate::filesystem::vfs::FsPermissionPolicy::Remote + } + } + + fn on_umount(&self) { + let live_nodes: Vec> = { + let nodes = self.nodes.lock(); + nodes.values().filter_map(|w| w.upgrade()).collect() + }; + for node in live_nodes { + node.flush_forget(); + } + self.conn.on_umount(); + } +} diff --git a/kernel/src/filesystem/fuse/inode.rs b/kernel/src/filesystem/fuse/inode.rs new file mode 100644 index 000000000..19b1513e1 --- /dev/null +++ b/kernel/src/filesystem/fuse/inode.rs @@ -0,0 +1,986 @@ +use alloc::{ + string::{String, ToString}, + sync::{Arc, Weak}, + vec::Vec, +}; +use core::mem::size_of; +use core::sync::atomic::{AtomicU64, Ordering}; + +use system_error::SystemError; + +use crate::time::timekeep::ktime_get_real_ns; +use crate::{ + driver::base::device::device_number::DeviceNumber, + filesystem::vfs::{ + file::FileFlags, permission::PermissionMask, syscall::RenameFlags, FilePrivateData, + FileSystem, FileType, IndexNode, InodeFlags, InodeId, InodeMode, Metadata, + }, + libs::mutex::{Mutex, MutexGuard}, + time::PosixTimeSpec, +}; + +use super::{ + conn::FuseConn, + fs::FuseFS, + private_data::{FuseFilePrivateData, FuseOpenPrivateData}, + protocol::{ + fuse_pack_struct, fuse_read_struct, FuseAccessIn, FuseAttr, FuseAttrOut, FuseCreateIn, + FuseDirent, FuseDirentPlus, FuseEntryOut, FuseFlushIn, FuseFsyncIn, FuseGetattrIn, + FuseLinkIn, FuseMkdirIn, FuseMknodIn, FuseOpenIn, FuseOpenOut, FuseReadIn, FuseReleaseIn, + FuseRename2In, FuseRenameIn, FuseSetattrIn, FuseWriteIn, FuseWriteOut, FATTR_ATIME, + FATTR_CTIME, FATTR_GID, FATTR_MODE, FATTR_MTIME, FATTR_SIZE, FATTR_UID, FUSE_ACCESS, + FUSE_CREATE, FUSE_FLUSH, FUSE_FSYNC, FUSE_FSYNCDIR, FUSE_FSYNC_FDATASYNC, FUSE_GETATTR, + FUSE_LINK, FUSE_LOOKUP, FUSE_MKDIR, FUSE_MKNOD, FUSE_OPEN, FUSE_OPENDIR, FUSE_READ, + FUSE_READDIR, FUSE_READDIRPLUS, FUSE_READLINK, FUSE_RELEASE, FUSE_RELEASEDIR, FUSE_RENAME, + FUSE_RENAME2, FUSE_RMDIR, FUSE_ROOT_ID, FUSE_SETATTR, FUSE_SYMLINK, FUSE_UNLINK, + FUSE_WRITE, + }, +}; + +#[derive(Debug)] +pub struct FuseNode { + fs: Weak, + conn: Arc, + nodeid: u64, + parent_nodeid: Mutex, + cached_metadata: Mutex>, + cached_metadata_deadline_ns: AtomicU64, + lookup_count: AtomicU64, +} + +impl FuseNode { + const FUSE_DIRENT_ALIGN: usize = 8; + + pub fn new( + fs: Weak, + conn: Arc, + nodeid: u64, + parent_nodeid: u64, + cached: Option, + ) -> Arc { + let has_cached = cached.is_some(); + Arc::new(Self { + fs, + conn, + nodeid, + parent_nodeid: Mutex::new(parent_nodeid), + cached_metadata: Mutex::new(cached), + cached_metadata_deadline_ns: AtomicU64::new(if has_cached { u64::MAX } else { 0 }), + lookup_count: AtomicU64::new(0), + }) + } + + pub fn nodeid(&self) -> u64 { + self.nodeid + } + + pub fn set_parent_nodeid(&self, parent: u64) { + *self.parent_nodeid.lock() = parent; + } + + pub fn set_cached_metadata(&self, md: Metadata) { + *self.cached_metadata.lock() = Some(md); + self.cached_metadata_deadline_ns + .store(u64::MAX, Ordering::Relaxed); + } + + pub fn set_cached_metadata_with_valid(&self, md: Metadata, valid: u64, valid_nsec: u32) { + *self.cached_metadata.lock() = Some(md); + self.cached_metadata_deadline_ns + .store(Self::cache_deadline(valid, valid_nsec), Ordering::Relaxed); + } + + pub fn inc_lookup(&self, count: u64) { + if self.nodeid == FUSE_ROOT_ID || count == 0 { + return; + } + self.lookup_count.fetch_add(count, Ordering::Relaxed); + } + + pub fn flush_forget(&self) { + if self.nodeid == FUSE_ROOT_ID { + return; + } + let nlookup = self.lookup_count.swap(0, Ordering::Relaxed); + if nlookup == 0 { + return; + } + let _ = self.conn.queue_forget(self.nodeid, nlookup); + } + + fn now_ns() -> u64 { + ktime_get_real_ns().max(0) as u64 + } + + fn cache_deadline(valid: u64, valid_nsec: u32) -> u64 { + if valid == 0 && valid_nsec == 0 { + return 0; + } + let delta_ns = valid + .saturating_mul(1_000_000_000) + .saturating_add(valid_nsec as u64); + Self::now_ns().saturating_add(delta_ns) + } + + fn conn(&self) -> &Arc { + &self.conn + } + + fn request_name(&self, opcode: u32, nodeid: u64, name: &str) -> Result, SystemError> { + let payload = Self::pack_name_payload(name); + self.conn().request(opcode, nodeid, &payload) + } + + fn pack_name_payload(name: &str) -> Vec { + let mut payload = Vec::with_capacity(name.len() + 1); + payload.extend_from_slice(name.as_bytes()); + payload.push(0); + payload + } + + fn pack_struct_and_name_payload(inarg: &T, name: &str) -> Vec { + let mut payload = Vec::with_capacity(size_of::() + name.len() + 1); + payload.extend_from_slice(fuse_pack_struct(inarg)); + payload.extend_from_slice(name.as_bytes()); + payload.push(0); + payload + } + + fn pack_two_names_payload(first: &str, second: &str) -> Vec { + let mut payload = Vec::with_capacity(first.len() + second.len() + 2); + payload.extend_from_slice(first.as_bytes()); + payload.push(0); + payload.extend_from_slice(second.as_bytes()); + payload.push(0); + payload + } + + fn set_open_private_data( + &self, + data: &mut FilePrivateData, + opcode: u32, + fh: u64, + open_flags: u32, + no_open: bool, + ) -> Result<(), SystemError> { + let conn_any: Arc = self.conn.clone(); + *data = match opcode { + FUSE_OPEN => FilePrivateData::Fuse(FuseFilePrivateData::File(FuseOpenPrivateData { + conn: conn_any, + fh, + open_flags, + no_open, + })), + FUSE_OPENDIR => FilePrivateData::Fuse(FuseFilePrivateData::Dir(FuseOpenPrivateData { + conn: conn_any, + fh, + open_flags, + no_open, + })), + _ => return Err(SystemError::EINVAL), + }; + Ok(()) + } + + fn align_dirent_record_len(base_len: usize) -> usize { + (base_len + Self::FUSE_DIRENT_ALIGN - 1) & !(Self::FUSE_DIRENT_ALIGN - 1) + } + + fn cache_child_from_entry(&self, entry: &FuseEntryOut) { + if entry.nodeid == 0 { + return; + } + if let Some(fs) = self.fs.upgrade() { + let md = Self::attr_to_metadata(&entry.attr); + let child = fs.get_or_create_node(entry.nodeid, self.nodeid, Some(md.clone())); + child.inc_lookup(1); + child.set_cached_metadata_with_valid(md, entry.attr_valid, entry.attr_valid_nsec); + } + } + + fn parse_readdirplus_payload( + &self, + payload: &[u8], + names: &mut Vec, + mut last_off: u64, + ) -> Result { + let mut pos: usize = 0; + while pos + size_of::() <= payload.len() { + let plus: FuseDirentPlus = fuse_read_struct(&payload[pos..])?; + let dirent = plus.dirent; + let name_start = pos + size_of::(); + let name_end = name_start + dirent.namelen as usize; + if name_end > payload.len() { + break; + } + + let name_bytes = &payload[name_start..name_end]; + if let Ok(name) = core::str::from_utf8(name_bytes) { + if !name.is_empty() && name != "." && name != ".." { + names.push(name.to_string()); + self.cache_child_from_entry(&plus.entry_out); + } + } + + last_off = dirent.off; + let rec_len = Self::align_dirent_record_len( + size_of::() + dirent.namelen as usize, + ); + if rec_len == 0 { + break; + } + pos = pos.saturating_add(rec_len); + } + Ok(last_off) + } + + fn parse_readdir_payload( + payload: &[u8], + names: &mut Vec, + mut last_off: u64, + ) -> Result { + let mut pos: usize = 0; + while pos + size_of::() <= payload.len() { + let dirent: FuseDirent = fuse_read_struct(&payload[pos..])?; + let name_start = pos + size_of::(); + let name_end = name_start + dirent.namelen as usize; + if name_end > payload.len() { + break; + } + + let name_bytes = &payload[name_start..name_end]; + if let Ok(name) = core::str::from_utf8(name_bytes) { + if !name.is_empty() && name != "." && name != ".." { + names.push(name.to_string()); + } + } + + last_off = dirent.off; + let rec_len = + Self::align_dirent_record_len(size_of::() + dirent.namelen as usize); + if rec_len == 0 { + break; + } + pos = pos.saturating_add(rec_len); + } + Ok(last_off) + } + + fn attr_to_metadata(attr: &FuseAttr) -> Metadata { + let mode = InodeMode::from_bits_truncate(attr.mode); + let ifmt = mode.bits() & InodeMode::S_IFMT.bits(); + let file_type = if ifmt == InodeMode::S_IFDIR.bits() { + FileType::Dir + } else if ifmt == InodeMode::S_IFREG.bits() { + FileType::File + } else if ifmt == InodeMode::S_IFLNK.bits() { + FileType::SymLink + } else if ifmt == InodeMode::S_IFCHR.bits() { + FileType::CharDevice + } else if ifmt == InodeMode::S_IFBLK.bits() { + FileType::BlockDevice + } else if ifmt == InodeMode::S_IFSOCK.bits() { + FileType::Socket + } else if ifmt == InodeMode::S_IFIFO.bits() { + FileType::Pipe + } else { + FileType::File + }; + + let inode_id = InodeId::new(attr.ino as usize); + + Metadata { + dev_id: 0, + inode_id, + size: attr.size as i64, + blk_size: attr.blksize as usize, + blocks: attr.blocks as usize, + atime: PosixTimeSpec::new(attr.atime as i64, attr.atimensec as i64), + mtime: PosixTimeSpec::new(attr.mtime as i64, attr.mtimensec as i64), + ctime: PosixTimeSpec::new(attr.ctime as i64, attr.ctimensec as i64), + btime: PosixTimeSpec::default(), + file_type, + mode, + flags: InodeFlags::empty(), + nlinks: attr.nlink as usize, + uid: attr.uid as usize, + gid: attr.gid as usize, + raw_dev: DeviceNumber::default(), + } + } + + fn fetch_attr(&self) -> Result { + let getattr_in = FuseGetattrIn { + getattr_flags: 0, + dummy: 0, + fh: 0, + }; + let payload = + self.conn() + .request(FUSE_GETATTR, self.nodeid, fuse_pack_struct(&getattr_in))?; + let out: FuseAttrOut = fuse_read_struct(&payload)?; + let md = Self::attr_to_metadata(&out.attr); + self.set_cached_metadata_with_valid(md.clone(), out.attr_valid, out.attr_valid_nsec); + Ok(md) + } + + fn cached_or_fetch_metadata(&self) -> Result { + self.conn.check_allow_current_process()?; + if let Some(m) = self.cached_metadata.lock().clone() { + let deadline = self.cached_metadata_deadline_ns.load(Ordering::Relaxed); + if deadline == u64::MAX || (deadline != 0 && Self::now_ns() < deadline) { + return Ok(m); + } + } + self.fetch_attr() + } + + fn open_common( + &self, + opcode: u32, + data: &mut FilePrivateData, + flags: &FileFlags, + ) -> Result<(), SystemError> { + if self.conn.should_skip_open(opcode) { + return self.set_open_private_data(data, opcode, 0, flags.bits(), true); + } + + let open_in = FuseOpenIn { + flags: flags.bits(), + open_flags: 0, + }; + let payload = match self + .conn() + .request(opcode, self.nodeid, fuse_pack_struct(&open_in)) + { + Ok(v) => v, + Err(SystemError::ENOSYS) if self.conn.open_enosys_is_supported(opcode) => { + self.conn.mark_no_open(opcode); + return self.set_open_private_data(data, opcode, 0, open_in.flags, true); + } + Err(e) => return Err(e), + }; + let out: FuseOpenOut = fuse_read_struct(&payload)?; + self.set_open_private_data(data, opcode, out.fh, open_in.flags, false) + } + + fn release_common(&self, opcode: u32, fh: u64, open_flags: u32) -> Result<(), SystemError> { + let inarg = FuseReleaseIn { + fh, + flags: open_flags, + release_flags: 0, + lock_owner: 0, + }; + let _ = self + .conn() + .request(opcode, self.nodeid, fuse_pack_struct(&inarg))?; + Ok(()) + } + + fn ensure_dir(&self) -> Result<(), SystemError> { + let md = self.cached_or_fetch_metadata()?; + if md.file_type != FileType::Dir { + return Err(SystemError::ENOTDIR); + } + Ok(()) + } + + fn ensure_regular(&self) -> Result<(), SystemError> { + let md = self.cached_or_fetch_metadata()?; + if md.file_type != FileType::File { + return Err(SystemError::EINVAL); + } + Ok(()) + } + + fn fsync_common(&self, datasync: bool) -> Result<(), SystemError> { + let md = self.cached_or_fetch_metadata()?; + let opcode = match md.file_type { + FileType::File => FUSE_FSYNC, + FileType::Dir => FUSE_FSYNCDIR, + _ => return Ok(()), + }; + let inarg = FuseFsyncIn { + fh: 0, + fsync_flags: if datasync { FUSE_FSYNC_FDATASYNC } else { 0 }, + padding: 0, + }; + let _ = self + .conn() + .request(opcode, self.nodeid, fuse_pack_struct(&inarg))?; + Ok(()) + } + + fn fsync_with_file_data( + &self, + datasync: bool, + data: &FilePrivateData, + ) -> Result<(), SystemError> { + let (opcode, fh, no_open) = match data { + FilePrivateData::Fuse(FuseFilePrivateData::File(p)) => (FUSE_FSYNC, p.fh, p.no_open), + FilePrivateData::Fuse(FuseFilePrivateData::Dir(p)) => (FUSE_FSYNCDIR, p.fh, p.no_open), + _ => return self.fsync_common(datasync), + }; + + // Linux 对 no_open/no_opendir 语义允许缺省 open,fh 不可靠,直接成功返回。 + if no_open { + return Ok(()); + } + + let inarg = FuseFsyncIn { + fh, + fsync_flags: if datasync { FUSE_FSYNC_FDATASYNC } else { 0 }, + padding: 0, + }; + let _ = self + .conn() + .request(opcode, self.nodeid, fuse_pack_struct(&inarg))?; + Ok(()) + } + + fn parse_create_reply(payload: &[u8]) -> Result<(FuseEntryOut, FuseOpenOut), SystemError> { + let entry_size = size_of::(); + let open_size = size_of::(); + if payload.len() < entry_size + open_size { + return Err(SystemError::EINVAL); + } + let entry: FuseEntryOut = fuse_read_struct(&payload[..entry_size])?; + let open_out: FuseOpenOut = fuse_read_struct(&payload[entry_size..entry_size + open_size])?; + Ok((entry, open_out)) + } + + fn create_node_from_entry( + &self, + entry: &FuseEntryOut, + ) -> Result, SystemError> { + let md = Self::attr_to_metadata(&entry.attr); + let fs = self.fs.upgrade().ok_or(SystemError::ENOENT)?; + let child = fs.get_or_create_node(entry.nodeid, self.nodeid, Some(md)); + child.inc_lookup(1); + child.set_cached_metadata_with_valid( + Self::attr_to_metadata(&entry.attr), + entry.attr_valid, + entry.attr_valid_nsec, + ); + Ok(child) + } +} + +impl IndexNode for FuseNode { + fn as_any_ref(&self) -> &dyn core::any::Any { + self + } + + fn open( + &self, + mut data: MutexGuard, + flags: &FileFlags, + ) -> Result<(), SystemError> { + let md = self.cached_or_fetch_metadata()?; + match md.file_type { + FileType::Dir => self.open_common(FUSE_OPENDIR, &mut data, flags), + FileType::File => self.open_common(FUSE_OPEN, &mut data, flags), + _ => Err(SystemError::EINVAL), + } + } + + fn close(&self, data: MutexGuard) -> Result<(), SystemError> { + match &*data { + FilePrivateData::Fuse(FuseFilePrivateData::File(p)) => { + if p.no_open { + return Ok(()); + } + let flush_in = FuseFlushIn { + fh: p.fh, + unused: 0, + padding: 0, + lock_owner: 0, + }; + let _ = self + .conn() + .request(FUSE_FLUSH, self.nodeid, fuse_pack_struct(&flush_in)); + self.release_common(FUSE_RELEASE, p.fh, p.open_flags) + } + FilePrivateData::Fuse(FuseFilePrivateData::Dir(p)) => { + if p.no_open { + Ok(()) + } else { + self.release_common(FUSE_RELEASEDIR, p.fh, p.open_flags) + } + } + _ => Ok(()), + } + } + + fn read_at( + &self, + offset: usize, + len: usize, + buf: &mut [u8], + data: MutexGuard, + ) -> Result { + if buf.len() < len { + return Err(SystemError::EINVAL); + } + let md = self.cached_or_fetch_metadata()?; + if md.file_type == FileType::SymLink { + if offset != 0 { + return Ok(0); + } + let payload = self.conn().request(FUSE_READLINK, self.nodeid, &[])?; + let n = core::cmp::min(payload.len(), len); + buf[..n].copy_from_slice(&payload[..n]); + return Ok(n); + } + self.ensure_regular()?; + let FilePrivateData::Fuse(FuseFilePrivateData::File(p)) = &*data else { + return Err(SystemError::EBADF); + }; + let read_in = FuseReadIn { + fh: p.fh, + offset: offset as u64, + size: len as u32, + read_flags: 0, + lock_owner: 0, + flags: 0, + padding: 0, + }; + let payload = self + .conn() + .request(FUSE_READ, self.nodeid, fuse_pack_struct(&read_in))?; + let n = core::cmp::min(payload.len(), len); + buf[..n].copy_from_slice(&payload[..n]); + Ok(n) + } + + fn write_at( + &self, + offset: usize, + len: usize, + buf: &[u8], + data: MutexGuard, + ) -> Result { + self.ensure_regular()?; + if buf.len() < len { + return Err(SystemError::EINVAL); + } + let FilePrivateData::Fuse(FuseFilePrivateData::File(p)) = &*data else { + return Err(SystemError::EBADF); + }; + let max_write = self.conn().max_write(); + let mut total_written = 0usize; + + while total_written < len { + let chunk = core::cmp::min(max_write, len - total_written); + let chunk_offset = offset + .checked_add(total_written) + .ok_or(SystemError::EOVERFLOW)?; + + let write_in = FuseWriteIn { + fh: p.fh, + offset: chunk_offset as u64, + size: chunk as u32, + write_flags: 0, + lock_owner: 0, + flags: 0, + padding: 0, + }; + let mut payload_in = Vec::with_capacity(size_of::() + chunk); + payload_in.extend_from_slice(fuse_pack_struct(&write_in)); + payload_in.extend_from_slice(&buf[total_written..total_written + chunk]); + let payload = self.conn().request(FUSE_WRITE, self.nodeid, &payload_in)?; + let out: FuseWriteOut = fuse_read_struct(&payload)?; + let wrote = core::cmp::min(out.size as usize, chunk); + total_written += wrote; + if wrote < chunk { + break; + } + } + + Ok(total_written) + } + + fn metadata(&self) -> Result { + self.cached_or_fetch_metadata() + } + + fn check_access(&self, mask: PermissionMask) -> Result<(), SystemError> { + let inarg = FuseAccessIn { + mask: mask.bits() & PermissionMask::MAY_RWX.bits(), + padding: 0, + }; + let _ = self + .conn() + .request(FUSE_ACCESS, self.nodeid, fuse_pack_struct(&inarg))?; + Ok(()) + } + + fn set_metadata(&self, metadata: &Metadata) -> Result<(), SystemError> { + let old = self.cached_or_fetch_metadata()?; + let mut valid = 0u32; + if metadata.mode != old.mode { + valid |= FATTR_MODE; + } + if metadata.uid != old.uid { + valid |= FATTR_UID; + } + if metadata.gid != old.gid { + valid |= FATTR_GID; + } + if metadata.size != old.size { + valid |= FATTR_SIZE; + } + if metadata.atime != old.atime { + valid |= FATTR_ATIME; + } + if metadata.mtime != old.mtime { + valid |= FATTR_MTIME; + } + if metadata.ctime != old.ctime { + valid |= FATTR_CTIME; + } + if valid == 0 { + return Ok(()); + } + + let inarg = FuseSetattrIn { + valid, + padding: 0, + fh: 0, + size: metadata.size as u64, + lock_owner: 0, + atime: metadata.atime.tv_sec as u64, + mtime: metadata.mtime.tv_sec as u64, + ctime: metadata.ctime.tv_sec as u64, + atimensec: metadata.atime.tv_nsec as u32, + mtimensec: metadata.mtime.tv_nsec as u32, + ctimensec: metadata.ctime.tv_nsec as u32, + mode: metadata.mode.bits(), + unused4: 0, + uid: metadata.uid as u32, + gid: metadata.gid as u32, + unused5: 0, + }; + let payload = self + .conn() + .request(FUSE_SETATTR, self.nodeid, fuse_pack_struct(&inarg))?; + let out: FuseAttrOut = fuse_read_struct(&payload)?; + let md = Self::attr_to_metadata(&out.attr); + self.set_cached_metadata_with_valid(md, out.attr_valid, out.attr_valid_nsec); + Ok(()) + } + + fn resize(&self, len: usize) -> Result<(), SystemError> { + let inarg = FuseSetattrIn { + valid: FATTR_SIZE, + padding: 0, + fh: 0, + size: len as u64, + lock_owner: 0, + atime: 0, + mtime: 0, + ctime: 0, + atimensec: 0, + mtimensec: 0, + ctimensec: 0, + mode: 0, + unused4: 0, + uid: 0, + gid: 0, + unused5: 0, + }; + let payload = self + .conn() + .request(FUSE_SETATTR, self.nodeid, fuse_pack_struct(&inarg))?; + let out: FuseAttrOut = fuse_read_struct(&payload)?; + let md = Self::attr_to_metadata(&out.attr); + self.set_cached_metadata_with_valid(md, out.attr_valid, out.attr_valid_nsec); + Ok(()) + } + + fn sync(&self) -> Result<(), SystemError> { + self.fsync_common(false) + } + + fn datasync(&self) -> Result<(), SystemError> { + self.fsync_common(true) + } + + fn sync_file( + &self, + datasync: bool, + data: MutexGuard, + ) -> Result<(), SystemError> { + self.fsync_with_file_data(datasync, &data) + } + + fn fs(&self) -> Arc { + self.fs.upgrade().unwrap() + } + + fn list(&self) -> Result, SystemError> { + self.ensure_dir()?; + + // OPENDIR + let mut pdata = FilePrivateData::Unused; + let flags = FileFlags::O_RDONLY; + self.open_common(FUSE_OPENDIR, &mut pdata, &flags)?; + let FilePrivateData::Fuse(FuseFilePrivateData::Dir(dir_p)) = &pdata else { + return Err(SystemError::EINVAL); + }; + let fh = dir_p.fh; + let open_flags = dir_p.open_flags; + let mut use_readdirplus = self.conn.use_readdirplus(); + + let mut names: Vec = Vec::new(); + let mut offset: u64 = 0; + + loop { + let read_in = FuseReadIn { + fh, + offset, + size: 64 * 1024, + read_flags: 0, + lock_owner: 0, + flags: 0, + padding: 0, + }; + let opcode = if use_readdirplus { + FUSE_READDIRPLUS + } else { + FUSE_READDIR + }; + let payload = match self + .conn() + .request(opcode, self.nodeid, fuse_pack_struct(&read_in)) + { + Ok(v) => v, + Err(SystemError::ENOSYS) if use_readdirplus => { + self.conn.disable_readdirplus(); + use_readdirplus = false; + continue; + } + Err(e) => return Err(e), + }; + if payload.is_empty() { + break; + } + + let mut last_off: u64 = offset; + if use_readdirplus { + last_off = self.parse_readdirplus_payload(&payload, &mut names, last_off)?; + } else { + last_off = Self::parse_readdir_payload(&payload, &mut names, last_off)?; + } + + if last_off == offset { + // Avoid infinite loop if userspace doesn't advance offsets. + break; + } + offset = last_off; + } + + // RELEASEDIR (best-effort) + if !dir_p.no_open { + let _ = self.release_common(FUSE_RELEASEDIR, fh, open_flags); + } + Ok(names) + } + + fn find(&self, name: &str) -> Result, SystemError> { + self.ensure_dir()?; + if name == "." { + let fs = self.fs.upgrade().ok_or(SystemError::ENOENT)?; + return Ok(fs.get_or_create_node(self.nodeid, *self.parent_nodeid.lock(), None)); + } + if name == ".." { + return self.parent(); + } + + let payload = self.request_name(FUSE_LOOKUP, self.nodeid, name)?; + let entry: FuseEntryOut = fuse_read_struct(&payload)?; + let md = Self::attr_to_metadata(&entry.attr); + + let fs = self.fs.upgrade().ok_or(SystemError::ENOENT)?; + let child = fs.get_or_create_node(entry.nodeid, self.nodeid, Some(md)); + child.inc_lookup(1); + child.set_cached_metadata_with_valid( + Self::attr_to_metadata(&entry.attr), + entry.attr_valid, + entry.attr_valid_nsec, + ); + Ok(child) + } + + fn parent(&self) -> Result, SystemError> { + let fs = self.fs.upgrade().ok_or(SystemError::ENOENT)?; + let parent_nodeid = *self.parent_nodeid.lock(); + if parent_nodeid == self.nodeid { + return Ok(fs.root_node()); + } + Ok(fs.get_or_create_node(parent_nodeid, parent_nodeid, None)) + } + + fn create( + &self, + name: &str, + file_type: FileType, + mode: InodeMode, + ) -> Result, SystemError> { + self.ensure_dir()?; + if file_type != FileType::File { + return self.create_with_data(name, file_type, mode, 0); + } + + let inarg = FuseCreateIn { + flags: FileFlags::O_RDONLY.bits(), + mode: (InodeMode::S_IFREG | mode).bits(), + umask: 0, + open_flags: 0, + }; + let payload_in = Self::pack_struct_and_name_payload(&inarg, name); + + let payload = match self.conn().request(FUSE_CREATE, self.nodeid, &payload_in) { + Ok(v) => v, + Err(SystemError::ENOSYS) => return self.create_with_data(name, file_type, mode, 0), + Err(e) => return Err(e), + }; + let (entry, _) = Self::parse_create_reply(&payload)?; + self.create_node_from_entry(&entry) + } + + fn create_with_data( + &self, + name: &str, + file_type: FileType, + mode: InodeMode, + _data: usize, + ) -> Result, SystemError> { + self.ensure_dir()?; + + match file_type { + FileType::Dir => { + let inarg = FuseMkdirIn { + mode: (InodeMode::S_IFDIR | mode).bits(), + umask: 0, + }; + let payload_in = Self::pack_struct_and_name_payload(&inarg, name); + let payload = self.conn().request(FUSE_MKDIR, self.nodeid, &payload_in)?; + let entry: FuseEntryOut = fuse_read_struct(&payload)?; + self.create_node_from_entry(&entry) + } + FileType::File => { + let inarg = FuseMknodIn { + mode: (InodeMode::S_IFREG | mode).bits(), + rdev: 0, + umask: 0, + padding: 0, + }; + let payload_in = Self::pack_struct_and_name_payload(&inarg, name); + let payload = self.conn().request(FUSE_MKNOD, self.nodeid, &payload_in)?; + let entry: FuseEntryOut = fuse_read_struct(&payload)?; + self.create_node_from_entry(&entry) + } + FileType::SymLink => { + let mut payload_in = Vec::with_capacity(name.len() + 2); + payload_in.push(0); + payload_in.extend_from_slice(name.as_bytes()); + payload_in.push(0); + let payload = self + .conn() + .request(FUSE_SYMLINK, self.nodeid, &payload_in)?; + let entry: FuseEntryOut = fuse_read_struct(&payload)?; + self.create_node_from_entry(&entry) + } + _ => Err(SystemError::ENOSYS), + } + } + + fn symlink(&self, name: &str, target: &str) -> Result, SystemError> { + self.ensure_dir()?; + let payload_in = Self::pack_two_names_payload(target, name); + let payload = self + .conn() + .request(FUSE_SYMLINK, self.nodeid, &payload_in)?; + let entry: FuseEntryOut = fuse_read_struct(&payload)?; + self.create_node_from_entry(&entry) + } + + fn link(&self, name: &str, other: &Arc) -> Result<(), SystemError> { + self.ensure_dir()?; + let target = other + .as_any_ref() + .downcast_ref::() + .ok_or(SystemError::EXDEV)?; + let inarg = FuseLinkIn { + oldnodeid: target.nodeid, + }; + let payload_in = Self::pack_struct_and_name_payload(&inarg, name); + let payload = self.conn().request(FUSE_LINK, self.nodeid, &payload_in)?; + let _entry: FuseEntryOut = fuse_read_struct(&payload)?; + Ok(()) + } + + fn unlink(&self, name: &str) -> Result<(), SystemError> { + self.ensure_dir()?; + let _ = self.request_name(FUSE_UNLINK, self.nodeid, name)?; + Ok(()) + } + + fn rmdir(&self, name: &str) -> Result<(), SystemError> { + self.ensure_dir()?; + let _ = self.request_name(FUSE_RMDIR, self.nodeid, name)?; + Ok(()) + } + + fn move_to( + &self, + old_name: &str, + target: &Arc, + new_name: &str, + flag: RenameFlags, + ) -> Result<(), SystemError> { + self.ensure_dir()?; + let target_any = target + .as_any_ref() + .downcast_ref::() + .ok_or(SystemError::EXDEV)?; + + let mut payload_in = Vec::new(); + let opcode = if flag.is_empty() { + let inarg = FuseRenameIn { + newdir: target_any.nodeid, + }; + payload_in.extend_from_slice(fuse_pack_struct(&inarg)); + FUSE_RENAME + } else { + let inarg = FuseRename2In { + newdir: target_any.nodeid, + flags: flag.bits(), + padding: 0, + }; + payload_in.extend_from_slice(fuse_pack_struct(&inarg)); + FUSE_RENAME2 + }; + payload_in.extend_from_slice(old_name.as_bytes()); + payload_in.push(0); + payload_in.extend_from_slice(new_name.as_bytes()); + payload_in.push(0); + let r = self.conn().request(opcode, self.nodeid, &payload_in); + if opcode == FUSE_RENAME2 && matches!(r, Err(SystemError::ENOSYS)) { + return Err(SystemError::EINVAL); + } + let _ = r?; + Ok(()) + } + + fn absolute_path(&self) -> Result { + Ok(format!("fuse:{}", self.nodeid)) + } +} + +impl Drop for FuseNode { + fn drop(&mut self) { + self.flush_forget(); + } +} diff --git a/kernel/src/filesystem/fuse/mod.rs b/kernel/src/filesystem/fuse/mod.rs new file mode 100644 index 000000000..26e7dc358 --- /dev/null +++ b/kernel/src/filesystem/fuse/mod.rs @@ -0,0 +1,6 @@ +pub mod conn; +pub mod dev; +pub mod fs; +pub mod inode; +pub mod private_data; +pub mod protocol; diff --git a/kernel/src/filesystem/fuse/private_data.rs b/kernel/src/filesystem/fuse/private_data.rs new file mode 100644 index 000000000..b43fd3001 --- /dev/null +++ b/kernel/src/filesystem/fuse/private_data.rs @@ -0,0 +1,40 @@ +use alloc::sync::Arc; +use core::any::Any; +use system_error::SystemError; + +use super::conn::FuseConn; + +#[derive(Debug, Clone)] +pub struct FuseDevPrivateData { + pub conn: Arc, + pub nonblock: bool, +} + +impl FuseDevPrivateData { + pub fn conn_ref(&self) -> Result, SystemError> { + downcast_conn(&self.conn) + } +} + +#[derive(Debug, Clone)] +pub struct FuseOpenPrivateData { + pub conn: Arc, + pub fh: u64, + pub open_flags: u32, + pub no_open: bool, +} + +#[derive(Debug, Clone)] +pub enum FuseFilePrivateData { + Dev(FuseDevPrivateData), + File(FuseOpenPrivateData), + Dir(FuseOpenPrivateData), +} + +#[inline] +fn downcast_conn(conn_any: &Arc) -> Result, SystemError> { + conn_any + .clone() + .downcast::() + .map_err(|_| SystemError::EINVAL) +} diff --git a/kernel/src/filesystem/fuse/protocol.rs b/kernel/src/filesystem/fuse/protocol.rs new file mode 100644 index 000000000..1f37d8fb0 --- /dev/null +++ b/kernel/src/filesystem/fuse/protocol.rs @@ -0,0 +1,401 @@ +//! FUSE protocol structures (Linux 6.6 uapi compatible subset). +//! +//! Reference: linux-6.6.21 `include/uapi/linux/fuse.h`. + +use core::mem::size_of; + +use system_error::SystemError; + +pub const FUSE_KERNEL_VERSION: u32 = 7; +pub const FUSE_KERNEL_MINOR_VERSION: u32 = 39; + +/// The read buffer is required to be at least 8k on Linux. +pub const FUSE_MIN_READ_BUFFER: usize = 8192; + +pub const FUSE_ROOT_ID: u64 = 1; + +// Opcodes (subset) +pub const FUSE_LOOKUP: u32 = 1; +pub const FUSE_FORGET: u32 = 2; // no reply +pub const FUSE_GETATTR: u32 = 3; +pub const FUSE_SETATTR: u32 = 4; +pub const FUSE_READLINK: u32 = 5; +pub const FUSE_SYMLINK: u32 = 6; +pub const FUSE_MKNOD: u32 = 8; +pub const FUSE_MKDIR: u32 = 9; +pub const FUSE_UNLINK: u32 = 10; +pub const FUSE_RMDIR: u32 = 11; +pub const FUSE_RENAME: u32 = 12; +pub const FUSE_LINK: u32 = 13; +pub const FUSE_OPEN: u32 = 14; +pub const FUSE_READ: u32 = 15; +pub const FUSE_WRITE: u32 = 16; +pub const FUSE_STATFS: u32 = 17; +pub const FUSE_RELEASE: u32 = 18; +pub const FUSE_FSYNC: u32 = 20; +pub const FUSE_FLUSH: u32 = 25; +pub const FUSE_INIT: u32 = 26; +pub const FUSE_OPENDIR: u32 = 27; +pub const FUSE_READDIR: u32 = 28; +pub const FUSE_RELEASEDIR: u32 = 29; +pub const FUSE_FSYNCDIR: u32 = 30; +pub const FUSE_ACCESS: u32 = 34; +pub const FUSE_CREATE: u32 = 35; +pub const FUSE_INTERRUPT: u32 = 36; +pub const FUSE_DESTROY: u32 = 38; // no reply +pub const FUSE_READDIRPLUS: u32 = 44; +pub const FUSE_RENAME2: u32 = 45; + +// INIT flags (subset) +pub const FUSE_ASYNC_READ: u64 = 1 << 0; +pub const FUSE_POSIX_LOCKS: u64 = 1 << 1; +pub const FUSE_ATOMIC_O_TRUNC: u64 = 1 << 3; +pub const FUSE_EXPORT_SUPPORT: u64 = 1 << 4; +pub const FUSE_BIG_WRITES: u64 = 1 << 5; +pub const FUSE_DONT_MASK: u64 = 1 << 6; +pub const FUSE_AUTO_INVAL_DATA: u64 = 1 << 12; +pub const FUSE_DO_READDIRPLUS: u64 = 1 << 13; +pub const FUSE_READDIRPLUS_AUTO: u64 = 1 << 14; +pub const FUSE_ASYNC_DIO: u64 = 1 << 15; +pub const FUSE_WRITEBACK_CACHE: u64 = 1 << 16; +pub const FUSE_NO_OPEN_SUPPORT: u64 = 1 << 17; +pub const FUSE_PARALLEL_DIROPS: u64 = 1 << 18; +pub const FUSE_HANDLE_KILLPRIV: u64 = 1 << 19; +pub const FUSE_POSIX_ACL: u64 = 1 << 20; +pub const FUSE_ABORT_ERROR: u64 = 1 << 21; +pub const FUSE_MAX_PAGES: u64 = 1 << 22; +pub const FUSE_NO_OPENDIR_SUPPORT: u64 = 1 << 24; +pub const FUSE_EXPLICIT_INVAL_DATA: u64 = 1 << 25; +pub const FUSE_INIT_EXT: u64 = 1 << 30; + +// getattr/setattr valid bits (subset) +pub const FATTR_MODE: u32 = 1 << 0; +pub const FATTR_UID: u32 = 1 << 1; +pub const FATTR_GID: u32 = 1 << 2; +pub const FATTR_SIZE: u32 = 1 << 3; +pub const FATTR_ATIME: u32 = 1 << 4; +pub const FATTR_MTIME: u32 = 1 << 5; +#[allow(dead_code)] +pub const FATTR_FH: u32 = 1 << 6; +#[allow(dead_code)] +pub const FATTR_ATIME_NOW: u32 = 1 << 7; +#[allow(dead_code)] +pub const FATTR_MTIME_NOW: u32 = 1 << 8; +pub const FATTR_CTIME: u32 = 1 << 10; + +pub const FUSE_FSYNC_FDATASYNC: u32 = 1 << 0; +pub const FUSE_NOTIFY_POLL: i32 = 1; +pub const FUSE_NOTIFY_INVAL_INODE: i32 = 2; +pub const FUSE_NOTIFY_INVAL_ENTRY: i32 = 3; +pub const FUSE_NOTIFY_STORE: i32 = 4; +pub const FUSE_NOTIFY_RETRIEVE: i32 = 5; +pub const FUSE_NOTIFY_DELETE: i32 = 6; + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseInHeader { + pub len: u32, + pub opcode: u32, + pub unique: u64, + pub nodeid: u64, + pub uid: u32, + pub gid: u32, + pub pid: u32, + pub total_extlen: u16, + pub padding: u16, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseOutHeader { + pub len: u32, + pub error: i32, + pub unique: u64, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseInitIn { + pub major: u32, + pub minor: u32, + pub max_readahead: u32, + pub flags: u32, + pub flags2: u32, + pub unused: [u32; 11], +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseInitOut { + pub major: u32, + pub minor: u32, + pub max_readahead: u32, + pub flags: u32, + pub max_background: u16, + pub congestion_threshold: u16, + pub max_write: u32, + pub time_gran: u32, + pub max_pages: u16, + pub map_alignment: u16, + pub flags2: u32, + pub unused: [u32; 7], +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseAttr { + pub ino: u64, + pub size: u64, + pub blocks: u64, + pub atime: u64, + pub mtime: u64, + pub ctime: u64, + pub atimensec: u32, + pub mtimensec: u32, + pub ctimensec: u32, + pub mode: u32, + pub nlink: u32, + pub uid: u32, + pub gid: u32, + pub rdev: u32, + pub blksize: u32, + pub flags: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseEntryOut { + pub nodeid: u64, + pub generation: u64, + pub entry_valid: u64, + pub attr_valid: u64, + pub entry_valid_nsec: u32, + pub attr_valid_nsec: u32, + pub attr: FuseAttr, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseForgetIn { + pub nlookup: u64, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseInterruptIn { + pub unique: u64, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseGetattrIn { + pub getattr_flags: u32, + pub dummy: u32, + pub fh: u64, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseAttrOut { + pub attr_valid: u64, + pub attr_valid_nsec: u32, + pub dummy: u32, + pub attr: FuseAttr, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseOpenIn { + pub flags: u32, + pub open_flags: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseOpenOut { + pub fh: u64, + pub open_flags: u32, + pub padding: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseReadIn { + pub fh: u64, + pub offset: u64, + pub size: u32, + pub read_flags: u32, + pub lock_owner: u64, + pub flags: u32, + pub padding: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseWriteIn { + pub fh: u64, + pub offset: u64, + pub size: u32, + pub write_flags: u32, + pub lock_owner: u64, + pub flags: u32, + pub padding: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseWriteOut { + pub size: u32, + pub padding: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseKstatfs { + pub blocks: u64, + pub bfree: u64, + pub bavail: u64, + pub files: u64, + pub ffree: u64, + pub bsize: u32, + pub namelen: u32, + pub frsize: u32, + pub padding: u32, + pub spare: [u32; 6], +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseStatfsOut { + pub st: FuseKstatfs, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseReleaseIn { + pub fh: u64, + pub flags: u32, + pub release_flags: u32, + pub lock_owner: u64, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseMknodIn { + pub mode: u32, + pub rdev: u32, + pub umask: u32, + pub padding: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseMkdirIn { + pub mode: u32, + pub umask: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseRenameIn { + pub newdir: u64, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseRename2In { + pub newdir: u64, + pub flags: u32, + pub padding: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseLinkIn { + pub oldnodeid: u64, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseSetattrIn { + pub valid: u32, + pub padding: u32, + pub fh: u64, + pub size: u64, + pub lock_owner: u64, + pub atime: u64, + pub mtime: u64, + pub ctime: u64, + pub atimensec: u32, + pub mtimensec: u32, + pub ctimensec: u32, + pub mode: u32, + pub unused4: u32, + pub uid: u32, + pub gid: u32, + pub unused5: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseDirent { + pub ino: u64, + pub off: u64, + pub namelen: u32, + pub typ: u32, + // name follows +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseDirentPlus { + pub entry_out: FuseEntryOut, + pub dirent: FuseDirent, + // name follows +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseCreateIn { + pub flags: u32, + pub mode: u32, + pub umask: u32, + pub open_flags: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseFlushIn { + pub fh: u64, + pub unused: u32, + pub padding: u32, + pub lock_owner: u64, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseFsyncIn { + pub fh: u64, + pub fsync_flags: u32, + pub padding: u32, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct FuseAccessIn { + pub mask: u32, + pub padding: u32, +} + +pub fn fuse_pack_struct(v: &T) -> &[u8] { + unsafe { core::slice::from_raw_parts((v as *const T).cast::(), size_of::()) } +} + +pub fn fuse_read_struct(buf: &[u8]) -> Result { + if buf.len() < size_of::() { + return Err(SystemError::EINVAL); + } + // FUSE messages are packed and 64-bit aligned, but userspace may still + // pass unaligned buffers; use unaligned reads for robustness. + Ok(unsafe { core::ptr::read_unaligned(buf.as_ptr().cast::()) }) +} diff --git a/kernel/src/filesystem/mod.rs b/kernel/src/filesystem/mod.rs index 8170cf3e1..6b4284669 100644 --- a/kernel/src/filesystem/mod.rs +++ b/kernel/src/filesystem/mod.rs @@ -5,6 +5,7 @@ pub mod eventfd; pub mod ext4; pub mod fat; pub mod fs; +pub mod fuse; pub mod kernfs; pub mod mbr; pub mod overlayfs; diff --git a/kernel/src/filesystem/vfs/file.rs b/kernel/src/filesystem/vfs/file.rs index 5616f3d11..b270bf6d6 100644 --- a/kernel/src/filesystem/vfs/file.rs +++ b/kernel/src/filesystem/vfs/file.rs @@ -21,6 +21,7 @@ use crate::{ }, filesystem::{ epoll::{event_poll::EPollPrivateData, EPollItem}, + fuse::private_data::FuseFilePrivateData, procfs::ProcfsFilePrivateData, vfs::FilldirContext, }, @@ -132,6 +133,8 @@ pub enum FilePrivateData { Namespace(NamespaceFilePrivateData), /// Socket file created by socket syscalls (not by VFS open(2)). SocketCreate, + /// FUSE file private data. + Fuse(FuseFilePrivateData), /// 不需要文件私有信息 Unused, } @@ -486,10 +489,10 @@ impl File { if need_data_sync || inode_sync { if need_metadata_sync || inode_sync { // O_SYNC 或 S_SYNC: 完整同步(数据 + 元数据) - self.inode.sync()?; + self.inode.sync_file(false, self.private_data.lock())?; } else { // O_DSYNC: 仅数据同步 - self.inode.datasync()?; + self.inode.sync_file(true, self.private_data.lock())?; } } Ok(()) diff --git a/kernel/src/filesystem/vfs/mod.rs b/kernel/src/filesystem/vfs/mod.rs index c68440e18..96cacbe7e 100644 --- a/kernel/src/filesystem/vfs/mod.rs +++ b/kernel/src/filesystem/vfs/mod.rs @@ -544,6 +544,15 @@ pub trait IndexNode: Any + Sync + Send + Debug + CastFromSync { return Err(SystemError::ENOSYS); } + /// @brief 在当前目录下创建符号链接(name -> target) + fn symlink(&self, name: &str, target: &str) -> Result, SystemError> { + let inode = self.create_with_data(name, FileType::SymLink, InodeMode::S_IRWXUGO, 0)?; + let bytes = target.as_bytes(); + let len = bytes.len(); + inode.write_at(0, len, bytes, Mutex::new(FilePrivateData::Unused).lock())?; + Ok(inode) + } + /// @brief 在当前目录下,创建一个名为Name的硬链接,指向另一个IndexNode /// /// @param name 硬链接的名称 @@ -592,6 +601,11 @@ pub trait IndexNode: Any + Sync + Send + Debug + CastFromSync { return Err(SystemError::ENOSYS); } + /// @brief 专用于 remote 权限模型下 access(2) 的检查 + fn check_access(&self, _mask: PermissionMask) -> Result<(), SystemError> { + Err(SystemError::ENOSYS) + } + /// @brief 寻找一个名为Name的inode /// /// @param name 要寻找的inode的名称 @@ -765,6 +779,21 @@ pub trait IndexNode: Any + Sync + Send + Debug + CastFromSync { self.datasync() } + /// @brief 基于打开文件上下文执行同步(可使用文件句柄等私有信息) + /// + /// 默认实现回退到 inode 级 `sync/datasync`。 + fn sync_file( + &self, + datasync: bool, + _data: MutexGuard, + ) -> Result<(), SystemError> { + if datasync { + self.datasync() + } else { + self.sync() + } + } + /// @brief 仅同步数据到磁盘(不包括元数据) /// /// O_DSYNC 语义:确保数据写入完成,但不保证元数据(如 mtime)更新 @@ -993,9 +1022,6 @@ impl dyn IndexNode { let process_root_inode = ProcessManager::current_pcb().fs_struct().root(); let trailing_slash = path.ends_with('/'); - // 获取当前进程的凭证(用于路径遍历的权限检查) - let cred = ProcessManager::current_pcb().cred(); - // 处理绝对路径 // result: 上一个被找到的inode // rest_path: 还没有查找的路径 @@ -1014,9 +1040,9 @@ impl dyn IndexNode { } // 检查当前目录的执行权限(搜索权限) - // 这确保了进程有权限遍历到此目录 + // 这确保了进程有权限遍历到此目录(对 Remote 权限模型的 FS,该检查会被绕过) let metadata = result.metadata()?; - cred.inode_permission(&metadata, PermissionMask::MAY_EXEC.bits())?; + permission::check_inode_permission(&result, &metadata, PermissionMask::MAY_EXEC)?; let name; // 寻找“/” @@ -1275,6 +1301,7 @@ bitflags! { const DEVFS_MAGIC = 0x1373; const FAT_MAGIC = 0xf2f52011; const EXT4_MAGIC = 0xef53; + const FUSE_MAGIC = 0x65735546; const TMPFS_MAGIC = 0x01021994; const KER_MAGIC = 0x3153464b; const PROC_MAGIC = 0x9fa0; @@ -1286,6 +1313,18 @@ bitflags! { } } +/// Filesystem-level permission checking policy used by VFS. +/// +/// - `Dac`: VFS performs Unix DAC permission checks (mode/uid/gid) locally. +/// - `Remote`: VFS bypasses local DAC checks and lets the filesystem/server decide. +/// For Linux FUSE remote model, execute permission is still checked locally for +/// regular files; see `vfs::permission::check_inode_permission()`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FsPermissionPolicy { + Dac, + Remote, +} + /// @brief 所有文件系统都应该实现的trait pub trait FileSystem: Any + Sync + Send + Debug { /// @brief 获取当前文件系统的root inode的指针 @@ -1310,6 +1349,24 @@ pub trait FileSystem: Any + Sync + Send + Debug { fn super_block(&self) -> SuperBlock; + /// @brief 获取文件系统统计信息(statfs) + /// + /// 默认实现直接返回 super_block。需要自定义 statfs 行为的文件系统可覆写此方法。 + fn statfs(&self, _inode: &Arc) -> Result { + Ok(self.super_block()) + } + + /// VFS permission checking policy for this filesystem instance. + /// + /// Default is `Dac` (local Unix DAC checks). + fn permission_policy(&self) -> FsPermissionPolicy { + FsPermissionPolicy::Dac + } + + /// Called after a filesystem is successfully unmounted. + /// Default is no-op. + fn on_umount(&self) {} + unsafe fn fault(&self, _pfm: &mut PageFaultMessage) -> VmFaultReason { VmFaultReason::VM_FAULT_SIGBUS } @@ -1495,7 +1552,13 @@ pub fn produce_fs( data: Option<&str>, source: &str, ) -> Result, SystemError> { - match FSMAKER.iter().find(|&m| m.name == filesystem) { + let canonical_filesystem = if filesystem.starts_with("fuse.") { + "fuse" + } else { + filesystem + }; + + match FSMAKER.iter().find(|&m| m.name == canonical_filesystem) { Some(maker) => { let mount_data = (maker.builder)(data, source)?; let mount_data_ref = mount_data.as_ref().map(|arc| arc.as_ref()); diff --git a/kernel/src/filesystem/vfs/mount.rs b/kernel/src/filesystem/vfs/mount.rs index 33ed0e44f..9945a7d9f 100644 --- a/kernel/src/filesystem/vfs/mount.rs +++ b/kernel/src/filesystem/vfs/mount.rs @@ -400,21 +400,18 @@ impl MountFS { /// # Errors /// 如果当前文件系统是根文件系统,那么将会返回`EINVAL` pub fn umount(&self) -> Result, SystemError> { - // Unregister from peer group before unmounting - let propagation = self.propagation(); - if propagation.is_shared() { - let group_id = propagation.peer_group_id(); - unregister_peer(group_id, &self.self_ref()); - } - - let r = self + let result = self .self_mountpoint() .ok_or(SystemError::EINVAL)? .do_umount(); - self.self_mountpoint.write().take(); + // Only clear mountpoint state and notify filesystem after successful detach. + if result.is_ok() { + self.self_mountpoint.write().take(); + self.inner_filesystem.on_umount(); + } - return r; + return result; } } @@ -536,21 +533,14 @@ impl MountFSInode { let mountpoint_id = self.inner_inode.metadata()?.inode_id; - // Get the child mount that will be unmounted + // Detach first. Follow-up bookkeeping (peer registry and propagation) + // must not run if detach itself failed. let child_mount = self .mount_fs .mountpoints .lock() - .get(&mountpoint_id) - .cloned(); - - if let Some(ref child) = child_mount { - // Unregister from peer group if shared - let child_prop = child.propagation(); - if child_prop.is_shared() { - unregister_peer(child_prop.peer_group_id(), child); - } - } + .remove(&mountpoint_id) + .ok_or(SystemError::ENOENT)?; // Propagate umount to peers and slaves of the parent mount let parent_prop = self.mount_fs.propagation(); @@ -560,13 +550,13 @@ impl MountFSInode { } } - // Remove the mount - return self - .mount_fs - .mountpoints - .lock() - .remove(&mountpoint_id) - .ok_or(SystemError::ENOENT); + // Remove detached mount from peer registry if needed. + let child_prop = child_mount.propagation(); + if child_prop.is_shared() { + unregister_peer(child_prop.peer_group_id(), &child_mount); + } + + return Ok(child_mount); } #[inline(never)] @@ -660,6 +650,14 @@ impl IndexNode for MountFSInode { return self.inner_inode.sync(); } + fn sync_file( + &self, + datasync: bool, + data: MutexGuard, + ) -> Result<(), SystemError> { + self.inner_inode.sync_file(datasync, data) + } + fn fadvise( &self, file: &Arc, @@ -799,6 +797,15 @@ impl IndexNode for MountFSInode { return self.inner_inode.link(name, &other_inner); } + fn symlink(&self, name: &str, target: &str) -> Result, SystemError> { + let inner_inode = self.inner_inode.symlink(name, target)?; + Ok(Arc::new_cyclic(|self_ref| MountFSInode { + inner_inode, + mount_fs: self.mount_fs.clone(), + self_ref: self_ref.clone(), + })) + } + /// @brief 在挂载文件系统中删除文件/文件夹 #[inline] fn unlink(&self, name: &str) -> Result<(), SystemError> { @@ -851,6 +858,13 @@ impl IndexNode for MountFSInode { .move_to(old_name, &target_inner, new_name, flags); } + fn check_access( + &self, + mask: crate::filesystem::vfs::permission::PermissionMask, + ) -> Result<(), SystemError> { + self.inner_inode.check_access(mask) + } + fn find(&self, name: &str) -> Result, SystemError> { match name { // 查找的是当前目录 @@ -1100,6 +1114,21 @@ impl FileSystem for MountFS { sb } + fn statfs(&self, inode: &Arc) -> Result { + let inner_inode = inode + .as_any_ref() + .downcast_ref::() + .map(|mnt| mnt.inner_inode.clone()) + .unwrap_or_else(|| inode.clone()); + let mut sb = self.inner_filesystem.statfs(&inner_inode)?; + sb.flags = self.mount_flags.bits() as u64; + Ok(sb) + } + + fn permission_policy(&self) -> crate::filesystem::vfs::FsPermissionPolicy { + self.inner_filesystem.permission_policy() + } + unsafe fn fault(&self, pfm: &mut PageFaultMessage) -> VmFaultReason { self.inner_filesystem.fault(pfm) } diff --git a/kernel/src/filesystem/vfs/open.rs b/kernel/src/filesystem/vfs/open.rs index b11be1afc..488e42891 100644 --- a/kernel/src/filesystem/vfs/open.rs +++ b/kernel/src/filesystem/vfs/open.rs @@ -7,8 +7,8 @@ use super::{ permission::PermissionMask, syscall::{OpenHow, OpenHowResolve}, utils::{rsplit_path, should_remove_sgid_on_chown, user_path_at}, - vcore::{check_parent_dir_permission, resolve_parent_inode}, - FileType, IndexNode, InodeMode, MAX_PATHLEN, VFS_MAX_FOLLOW_SYMLINK_TIMES, + vcore::{check_parent_dir_permission_inode, resolve_parent_inode}, + FileType, FsPermissionPolicy, IndexNode, InodeMode, MAX_PATHLEN, VFS_MAX_FOLLOW_SYMLINK_TIMES, }; use crate::{filesystem::vfs::syscall::UtimensFlags, process::cred::Kgid}; use crate::{ @@ -67,9 +67,34 @@ pub(super) fn do_faccessat( let (inode, path) = user_path_at(&ProcessManager::current_pcb(), dirfd, path)?; // 如果找不到文件,则返回错误码ENOENT - let _inode = inode.lookup_follow_symlink(path.as_str(), VFS_MAX_FOLLOW_SYMLINK_TIMES)?; + let inode = inode.lookup_follow_symlink(path.as_str(), VFS_MAX_FOLLOW_SYMLINK_TIMES)?; + if mode.bits() == 0 { + return Ok(0); + } + + let mut mask = PermissionMask::empty(); + if mode.contains(InodeMode::S_IROTH) { + mask |= PermissionMask::MAY_READ; + } + if mode.contains(InodeMode::S_IWOTH) { + mask |= PermissionMask::MAY_WRITE; + } + if mode.contains(InodeMode::S_IXOTH) { + mask |= PermissionMask::MAY_EXEC; + } + + let metadata = inode.metadata()?; + match inode.fs().permission_policy() { + FsPermissionPolicy::Dac => { + super::permission::check_inode_permission(&inode, &metadata, mask)?; + } + FsPermissionPolicy::Remote => match inode.check_access(mask) { + Ok(()) => {} + Err(SystemError::ENOSYS) => {} + Err(e) => return Err(e), + }, + } - // todo: 接着完善(可以借鉴linux 6.1.9的do_faccessat) return Ok(0); } @@ -265,7 +290,7 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result Result Result, SystemError> return Err(SystemError::EACCES); } - // 检查执行权限 - let cred = ProcessManager::current_pcb().cred(); - cred.inode_permission(&metadata, PermissionMask::MAY_EXEC.bits())?; - - // 同时需要读权限(用于读取ELF内容) - cred.inode_permission(&metadata, PermissionMask::MAY_READ.bits())?; + super::permission::check_inode_permission(&inode, &metadata, PermissionMask::MAY_EXEC)?; + super::permission::check_inode_permission(&inode, &metadata, PermissionMask::MAY_READ)?; // 创建File对象,使用O_RDONLY | O_CLOEXEC let file = File::new(inode, FileFlags::O_RDONLY | FileFlags::O_CLOEXEC)?; diff --git a/kernel/src/filesystem/vfs/permission.rs b/kernel/src/filesystem/vfs/permission.rs index d393f9ba1..5c13ca8ab 100644 --- a/kernel/src/filesystem/vfs/permission.rs +++ b/kernel/src/filesystem/vfs/permission.rs @@ -7,9 +7,13 @@ use super::Metadata; use crate::{ filesystem::vfs::{FileType, InodeMode}, process::cred::{CAPFlags, Cred}, + process::ProcessManager, }; +use alloc::sync::Arc; use system_error::SystemError; +use super::{FsPermissionPolicy, IndexNode}; + bitflags! { pub struct PermissionMask: u32 { /// 测试执行权限 @@ -31,6 +35,35 @@ bitflags! { } } +/// VFS permission check wrapper that respects per-filesystem policy. +/// +/// This is the single entry point that should be used by VFS/pathwalk/syscalls +/// when deciding whether to apply local Unix DAC checks. +/// +/// Linux FUSE remote permission model: +/// - Without `default_permissions`, the kernel bypasses most DAC checks and +/// lets the userspace daemon decide. +/// - Execute permission is still checked locally for regular files. +pub fn check_inode_permission( + inode: &Arc, + metadata: &Metadata, + mask: PermissionMask, +) -> Result<(), SystemError> { + let cred = ProcessManager::current_pcb().cred(); + match inode.fs().permission_policy() { + FsPermissionPolicy::Dac => cred.inode_permission(metadata, mask.bits()), + FsPermissionPolicy::Remote => { + if mask.contains(PermissionMask::MAY_EXEC) + && metadata.file_type == FileType::File + && (metadata.mode.bits() & InodeMode::S_IXUGO.bits()) == 0 + { + return Err(SystemError::EACCES); + } + Ok(()) + } + } +} + impl Cred { /// 检查具有给定凭证的进程是否有权限访问 inode。 /// diff --git a/kernel/src/filesystem/vfs/syscall/link_utils.rs b/kernel/src/filesystem/vfs/syscall/link_utils.rs index 613dad936..5fe257b00 100644 --- a/kernel/src/filesystem/vfs/syscall/link_utils.rs +++ b/kernel/src/filesystem/vfs/syscall/link_utils.rs @@ -1,3 +1,4 @@ +use crate::filesystem::vfs::permission::PermissionMask; use crate::filesystem::vfs::syscall::AtFlags; use crate::filesystem::vfs::utils::rsplit_path; use crate::filesystem::vfs::utils::user_path_at; @@ -8,9 +9,9 @@ use crate::filesystem::vfs::Metadata; use crate::filesystem::vfs::SystemError; use crate::filesystem::vfs::VFS_MAX_FOLLOW_SYMLINK_TIMES; use crate::process::cred::CAPFlags; -use crate::process::cred::Cred; use crate::process::ProcessManager; use alloc::sync::Arc; + /// **创建硬连接的系统调用** /// /// ## 参数 @@ -129,7 +130,7 @@ fn may_linkat(old_inode: &Arc) -> Result<(), SystemError> { } // 检查是否是"安全的"硬链接源 - if !safe_hardlink_source(&metadata, &cred)? { + if !safe_hardlink_source(old_inode, &metadata)? { return Err(SystemError::EPERM); } @@ -159,7 +160,10 @@ fn may_linkat(old_inode: &Arc) -> Result<(), SystemError> { /// - `Ok(true)`: 硬链接源安全,允许创建硬链接 /// - `Ok(false)`: 硬链接源不安全,不允许创建硬链接 /// - `Err(SystemError::EACCES)`: 权限检查失败 -fn safe_hardlink_source(metadata: &Metadata, cred: &Arc) -> Result { +fn safe_hardlink_source( + old_inode: &Arc, + metadata: &Metadata, +) -> Result { let mode = metadata.mode; let file_type = metadata.file_type; @@ -178,10 +182,14 @@ fn safe_hardlink_source(metadata: &Metadata, cred: &Arc) -> Result(0).unwrap(); - let file = File::new(inode, FileFlags::O_RDONLY)?; - - let len = file.read(buf_size, ubuf)?; + let len = inode.read_at( + 0, + buf_size, + ubuf, + Mutex::new(FilePrivateData::Unused).lock(), + )?; return Ok(len); } diff --git a/kernel/src/filesystem/vfs/syscall/rename_utils.rs b/kernel/src/filesystem/vfs/syscall/rename_utils.rs index 4d96a07b9..a073611d6 100644 --- a/kernel/src/filesystem/vfs/syscall/rename_utils.rs +++ b/kernel/src/filesystem/vfs/syscall/rename_utils.rs @@ -98,20 +98,18 @@ pub fn do_renameat2( // 非空目录覆盖应由具体文件系统在 move_to/rename 实现中返回 ENOTEMPTY。 // 权限检查:根据 Linux 语义,rename 需要对源父目录和目标父目录都拥有写+搜索权限 - let cred = pcb.cred(); - - // 检查源父目录的写+搜索权限(需要删除旧目录项) let old_parent_metadata = old_parent_inode.metadata()?; - cred.inode_permission( + crate::filesystem::vfs::permission::check_inode_permission( + &old_parent_inode, &old_parent_metadata, - (PermissionMask::MAY_WRITE | PermissionMask::MAY_EXEC).bits(), + PermissionMask::MAY_WRITE | PermissionMask::MAY_EXEC, )?; - // 检查目标父目录的写+搜索权限(需要创建新目录项) let new_parent_metadata = new_parent_inode.metadata()?; - cred.inode_permission( + crate::filesystem::vfs::permission::check_inode_permission( + &new_parent_inode, &new_parent_metadata, - (PermissionMask::MAY_WRITE | PermissionMask::MAY_EXEC).bits(), + PermissionMask::MAY_WRITE | PermissionMask::MAY_EXEC, )?; old_parent_inode.move_to(old_filename, &new_parent_inode, new_filename, flags)?; diff --git a/kernel/src/filesystem/vfs/syscall/symlink_utils.rs b/kernel/src/filesystem/vfs/syscall/symlink_utils.rs index 89e2b9da9..de12f5987 100644 --- a/kernel/src/filesystem/vfs/syscall/symlink_utils.rs +++ b/kernel/src/filesystem/vfs/syscall/symlink_utils.rs @@ -4,14 +4,11 @@ use crate::{ filesystem::vfs::{ fcntl::AtFlags, utils::{rsplit_path, user_path_at}, - FilePrivateData, FileType, NAME_MAX, VFS_MAX_FOLLOW_SYMLINK_TIMES, + FileType, NAME_MAX, VFS_MAX_FOLLOW_SYMLINK_TIMES, }, - libs::mutex::Mutex, process::ProcessManager, }; -use super::InodeMode; - pub fn do_symlinkat(from: &str, newdfd: Option, to: &str) -> Result { let newdfd = match newdfd { Some(fd) => fd, @@ -52,12 +49,7 @@ pub fn do_symlinkat(from: &str, newdfd: Option, to: &str) -> Result String::from("/"), - _ => proc.basic().cwd(), - }; - let mut cwd_vec: Vec<_> = cwd.split('/').filter(|&x| !x.is_empty()).collect(); - let path_split = path.split('/').filter(|&x| !x.is_empty()); - for seg in path_split { - if seg == ".." { - cwd_vec.pop(); - } else if seg == "." { - // 当前目录 - } else { - cwd_vec.push(seg); - } - } - for seg in cwd_vec { - new_path.push('/'); - new_path.push_str(seg); + crate::filesystem::vfs::permission::check_inode_permission( + &inode, + &metadata, + PermissionMask::MAY_EXEC | PermissionMask::MAY_CHDIR, + )?; + + // 维护一个“用户可见”的 cwd 字符串(用于 getcwd 的快速路径) + // 注意:路径解析不再依赖该字符串,避免 chroot 后出现语义偏差。 + let mut new_path = String::from(""); + let cwd = match path.as_bytes()[0] { + b'/' => String::from("/"), + _ => proc.basic().cwd(), + }; + let mut cwd_vec: Vec<_> = cwd.split('/').filter(|&x| !x.is_empty()).collect(); + let path_split = path.split('/').filter(|&x| !x.is_empty()); + for seg in path_split { + if seg == ".." { + cwd_vec.pop(); + } else if seg == "." { + // 当前目录 + } else { + cwd_vec.push(seg); } - if new_path.is_empty() { - new_path = String::from("/"); - } - - proc.basic_mut().set_cwd(new_path); - proc.fs_struct_mut().set_pwd(inode); - return Ok(0); - } else { - return Err(SystemError::ENOTDIR); } + for seg in cwd_vec { + new_path.push('/'); + new_path.push_str(seg); + } + if new_path.is_empty() { + new_path = String::from("/"); + } + + proc.basic_mut().set_cwd(new_path); + proc.fs_struct_mut().set_pwd(inode); + Ok(0) } /// Formats the syscall parameters for display/debug purposes diff --git a/kernel/src/filesystem/vfs/syscall/sys_chroot.rs b/kernel/src/filesystem/vfs/syscall/sys_chroot.rs index 283bc8c0d..13d094569 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_chroot.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_chroot.rs @@ -63,8 +63,11 @@ impl Syscall for SysChrootHandle { return Err(SystemError::ENOTDIR); } - // 目录搜索权限(execute) - cred.inode_permission(&meta, PermissionMask::MAY_EXEC.bits())?; + crate::filesystem::vfs::permission::check_inode_permission( + &target, + &meta, + PermissionMask::MAY_EXEC, + )?; // 更新进程 fs root;不改变 cwd(Linux 行为) pcb.fs_struct_mut().set_root(target); diff --git a/kernel/src/filesystem/vfs/syscall/sys_fchdir.rs b/kernel/src/filesystem/vfs/syscall/sys_fchdir.rs index ecfe462ad..c8e7fcad2 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_fchdir.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_fchdir.rs @@ -4,6 +4,7 @@ use system_error::SystemError; use crate::arch::interrupt::TrapFrame; use crate::arch::syscall::nr::SYS_FCHDIR; +use crate::filesystem::vfs::permission::PermissionMask; use crate::process::ProcessManager; use crate::syscall::table::FormattedSyscallParam; use crate::syscall::table::Syscall; @@ -41,8 +42,14 @@ impl Syscall for SysFchdirHandle { let inode = file.inode(); let metadata = inode.metadata()?; - let cred = pcb.cred(); - cred.check_chdir_permission(&metadata)?; + if metadata.file_type != crate::filesystem::vfs::FileType::Dir { + return Err(SystemError::ENOTDIR); + } + crate::filesystem::vfs::permission::check_inode_permission( + &inode, + &metadata, + PermissionMask::MAY_EXEC | PermissionMask::MAY_CHDIR, + )?; let path = inode.absolute_path()?; pcb.basic_mut().set_cwd(path); diff --git a/kernel/src/filesystem/vfs/syscall/sys_fstatfs.rs b/kernel/src/filesystem/vfs/syscall/sys_fstatfs.rs index 73f465d7a..ee85a03d6 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_fstatfs.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_fstatfs.rs @@ -25,7 +25,9 @@ impl Syscall for SysFstatfsHandle { .get_file_by_fd(fd) .ok_or(SystemError::EBADF)?; drop(fd_table_guard); - let statfs = PosixStatfs::from(file.inode().fs().super_block()); + let inode = file.inode(); + let sb = inode.fs().statfs(&inode)?; + let statfs = PosixStatfs::from(sb); writer.copy_one_to_user(&statfs, 0)?; return Ok(0); } diff --git a/kernel/src/filesystem/vfs/syscall/sys_fsync.rs b/kernel/src/filesystem/vfs/syscall/sys_fsync.rs index b0c7835ef..01a3f43ee 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_fsync.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_fsync.rs @@ -29,8 +29,7 @@ impl Syscall for SysFsyncHandle { .get_file_by_fd(fd) .ok_or(system_error::SystemError::EBADF)?; drop(fd_table_guard); - let inode = file.inode(); - inode.sync()?; + file.inode().sync_file(false, file.private_data.lock())?; Ok(0) } diff --git a/kernel/src/filesystem/vfs/syscall/sys_mount.rs b/kernel/src/filesystem/vfs/syscall/sys_mount.rs index abfda513b..d23021489 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_mount.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_mount.rs @@ -268,7 +268,7 @@ fn do_new_mount( let fs_type_str = filesystemtype.ok_or(SystemError::EINVAL)?; let source = source.ok_or(SystemError::EINVAL)?; let fs = produce_fs(&fs_type_str, data.as_deref(), &source).inspect_err(|e| { - log::error!("Failed to produce filesystem: {:?}", e); + log::warn!("Failed to produce filesystem: {:?}", e); })?; // 若目标是挂载点根,则尝试在其父目录挂载,避免 EBUSY 并与 Linux 叠加语义接近 diff --git a/kernel/src/filesystem/vfs/syscall/sys_statfs.rs b/kernel/src/filesystem/vfs/syscall/sys_statfs.rs index 7ce3e194a..28d80a1f1 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_statfs.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_statfs.rs @@ -33,7 +33,8 @@ impl Syscall for SysStatfsHandle { let pcb = ProcessManager::current_pcb(); let (inode_begin, remain_path) = user_path_at(&pcb, fd as i32, &path)?; let inode = inode_begin.lookup_follow_symlink(&remain_path, MAX_PATHLEN)?; - let statfs = PosixStatfs::from(inode.fs().super_block()); + let sb = inode.fs().statfs(&inode)?; + let statfs = PosixStatfs::from(sb); writer.copy_one_to_user(&statfs, 0)?; return Ok(0); } diff --git a/kernel/src/filesystem/vfs/syscall/sys_truncate.rs b/kernel/src/filesystem/vfs/syscall/sys_truncate.rs index 8b1ef5f4b..a205a6868 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_truncate.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_truncate.rs @@ -52,9 +52,11 @@ impl Syscall for SysTruncateHandle { .lookup_follow_symlink(remain_path.as_str(), VFS_MAX_FOLLOW_SYMLINK_TIMES)?; let md = target.metadata()?; - // DAC write permission check - let cred = ProcessManager::current_pcb().cred(); - cred.inode_permission(&md, PermissionMask::MAY_WRITE.bits())?; + crate::filesystem::vfs::permission::check_inode_permission( + &target, + &md, + PermissionMask::MAY_WRITE, + )?; // RLIMIT_FSIZE enforcement for regular files if md.file_type == FileType::File { diff --git a/kernel/src/filesystem/vfs/vcore.rs b/kernel/src/filesystem/vfs/vcore.rs index 08aac5840..d0376bad9 100644 --- a/kernel/src/filesystem/vfs/vcore.rs +++ b/kernel/src/filesystem/vfs/vcore.rs @@ -296,12 +296,15 @@ pub fn do_mkdir_at( return Err(SystemError::ENOTDIR); } let pcb = ProcessManager::current_pcb(); - let cred = pcb.cred(); // Linux 语义:目录执行权限控制能否遍历(查找子项) // 先检查执行权限,再检查文件是否存在,最后检查写权限 // 顺序很重要:无执行权限 → EACCES;有执行权限且已存在 → EEXIST;有执行权限无写权限 → EACCES - cred.inode_permission(&parent_md, PermissionMask::MAY_EXEC.bits())?; + crate::filesystem::vfs::permission::check_inode_permission( + ¤t_inode, + &parent_md, + PermissionMask::MAY_EXEC, + )?; // 已确认有执行权限,可以查找子项 if current_inode.find(name).is_ok() { @@ -309,7 +312,11 @@ pub fn do_mkdir_at( } // 创建目录还需要对父目录拥有写权限 - cred.inode_permission(&parent_md, PermissionMask::MAY_WRITE.bits())?; + crate::filesystem::vfs::permission::check_inode_permission( + ¤t_inode, + &parent_md, + PermissionMask::MAY_WRITE, + )?; let mut final_mode_bits = mode.bits() & InodeMode::S_IRWXUGO.bits(); if (parent_md.mode.bits() & InodeMode::S_ISGID.bits()) != 0 { @@ -345,15 +352,15 @@ pub(super) fn resolve_parent_inode( } } -/// 检查父目录权限(写+执行权限) -/// -/// Linux 语义:删除/创建文件/目录需要对父目录拥有 W+X(写+执行)权限 -/// 注意:权限检查必须在 find 之前进行,否则当文件不存在时会返回 ENOENT 而不是 EACCES -pub(super) fn check_parent_dir_permission(parent_md: &super::Metadata) -> Result<(), SystemError> { - let cred = ProcessManager::current_pcb().cred(); - cred.inode_permission( +/// 检查父目录权限(写+执行权限),并尊重文件系统权限策略(如 FUSE remote 模型)。 +pub(super) fn check_parent_dir_permission_inode( + parent_inode: &Arc, + parent_md: &super::Metadata, +) -> Result<(), SystemError> { + crate::filesystem::vfs::permission::check_inode_permission( + parent_inode, parent_md, - (PermissionMask::MAY_WRITE | PermissionMask::MAY_EXEC).bits(), + PermissionMask::MAY_WRITE | PermissionMask::MAY_EXEC, ) } @@ -386,7 +393,7 @@ pub fn do_remove_dir(dirfd: i32, path: &str) -> Result { // Linux 语义:删除目录需要对父目录拥有 W+X(写+搜索)权限 // 注意:权限检查必须在 find 之前进行,否则当目录不存在时会返回 ENOENT 而不是 EACCES - check_parent_dir_permission(&parent_md)?; + check_parent_dir_permission_inode(&parent_inode, &parent_md)?; // 在目标点为symlink时也返回ENOTDIR let target_inode = parent_inode.find(filename)?; @@ -424,7 +431,7 @@ pub fn do_unlink_at(dirfd: i32, path: &str) -> Result { // Linux 语义:删除文件需要对父目录拥有 W+X(写+搜索)权限 // 注意:权限检查必须在 find 之前进行,否则当文件不存在时会返回 ENOENT 而不是 EACCES - check_parent_dir_permission(&parent_md)?; + check_parent_dir_permission_inode(&parent_inode, &parent_md)?; // Linux 语义:unlink(2)/unlinkat(2) 删除目录项本身,不跟随最后一个符号链接。 // 我们已解析到父目录,因此这里必须用 find() 直接取目录项对应 inode, diff --git a/kernel/src/process/namespace/propagation.rs b/kernel/src/process/namespace/propagation.rs index b39c62e21..55c7413d6 100644 --- a/kernel/src/process/namespace/propagation.rs +++ b/kernel/src/process/namespace/propagation.rs @@ -920,20 +920,12 @@ pub fn propagate_umount( /// Umount at a specific peer mount. fn umount_at_peer(peer_mnt: &Arc, mountpoint_id: InodeId) -> Result<(), SystemError> { - let mountpoints = peer_mnt.mountpoints(); - if let Some(child_mount) = mountpoints.get(&mountpoint_id) { - let child = child_mount.clone(); - drop(mountpoints); - + if let Some(child) = peer_mnt.mountpoints().remove(&mountpoint_id) { // Unregister the child from its peer group if shared let child_prop = child.propagation(); if child_prop.is_shared() { unregister_peer(child_prop.peer_group_id(), &child); } - - // Remove from parent's mountpoints - peer_mnt.mountpoints().remove(&mountpoint_id); - // log::debug!("umount_at_peer: removed mount at {:?}", mountpoint_id); } Ok(()) diff --git a/user/apps/c_unitest/Makefile b/user/apps/c_unitest/Makefile index b78106934..909d97748 100644 --- a/user/apps/c_unitest/Makefile +++ b/user/apps/c_unitest/Makefile @@ -11,26 +11,19 @@ CFLAGS := -Wall -O2 -static -lpthread SRCS := $(wildcard *.c) BINS := $(SRCS:.c=) - - -$(C_TARGETS): %.o: %.c - $(CC) -c $< -o $@ - all: $(BINS) # @echo "src: $(SRCS)" @echo "bins: $(BINS)" -%: %.c - $(CC) $(CFLAGS) $< -o $@ - +# 依赖同目录下所有 .h,任意头文件变更都会触发重编 +%: %.c $(wildcard *.h) + $(CC) $(CFLAGS) $< -o $@ install: all @echo "Installing binaries to $(DADK_CURRENT_BUILD_DIR)/" mv $(BINS) $(DADK_CURRENT_BUILD_DIR)/ - clean: rm -f $(BINS) - .PHONY: all install clean diff --git a/user/apps/fuse3_demo/.gitignore b/user/apps/fuse3_demo/.gitignore new file mode 100644 index 000000000..59dad267e --- /dev/null +++ b/user/apps/fuse3_demo/.gitignore @@ -0,0 +1,3 @@ +.build/ +fuse3_demo +test_fuse3_demo diff --git a/user/apps/fuse3_demo/Makefile b/user/apps/fuse3_demo/Makefile new file mode 100644 index 000000000..1ee3c7dbb --- /dev/null +++ b/user/apps/fuse3_demo/Makefile @@ -0,0 +1,104 @@ +ARCH ?= x86_64 +ifeq ($(ARCH), x86_64) + CROSS_COMPILE = x86_64-linux-musl- +else ifeq ($(ARCH), riscv64) + CROSS_COMPILE = riscv64-linux-musl- +endif + +CC = $(CROSS_COMPILE)gcc +AR = $(CROSS_COMPILE)ar +RANLIB = $(CROSS_COMPILE)ranlib +STRIP = $(CROSS_COMPILE)strip + +PKG_CONFIG ?= pkg-config +MESON ?= meson + +LIBFUSE_VERSION ?= 3.18.1 +LIBFUSE_NAME := fuse-$(LIBFUSE_VERSION) +LIBFUSE_URL_PRIMARY ?= https://github.com/libfuse/libfuse/releases/download/fuse-$(LIBFUSE_VERSION)/$(LIBFUSE_NAME).tar.gz +LIBFUSE_URL_MIRROR ?= https://git.mirrors.dragonos.org.cn/DragonOS-Community/libfuse/archive/fuse-$(LIBFUSE_VERSION).tar.gz + +LIBFUSE_CACHE_DIR := $(CURDIR)/.cache +LIBFUSE_ARCHIVE ?= $(LIBFUSE_CACHE_DIR)/$(LIBFUSE_NAME).tar.gz +LIBFUSE_SRC_ROOT := $(CURDIR)/.build/libfuse-src +LIBFUSE_SRC_DIR := $(LIBFUSE_SRC_ROOT)/$(LIBFUSE_NAME) +LIBFUSE_BUILD_DIR := $(CURDIR)/.build/libfuse-build-$(ARCH) +LIBFUSE_PREFIX := $(CURDIR)/.build/libfuse-install-$(ARCH) +LIBFUSE_PC_DIR := $(LIBFUSE_PREFIX)/lib/pkgconfig +LIBFUSE_STAMP := $(LIBFUSE_PREFIX)/.built-$(LIBFUSE_VERSION) + +CFLAGS_COMMON := -Wall -Wextra -O2 -static -D_FILE_OFFSET_BITS=64 +LIBFUSE_MESON_CFLAGS ?= -static +LIBFUSE_MESON_LDFLAGS ?= -static +LIBFUSE_MESON_JOBS ?= 1 + +BINS := fuse3_demo test_fuse3_demo + +all: $(BINS) + @echo "bins: $(BINS)" + +$(LIBFUSE_ARCHIVE): + @mkdir -p $(LIBFUSE_CACHE_DIR) + @if [ -s "$@" ]; then \ + echo "[libfuse] use cached archive: $@"; \ + exit 0; \ + fi + @echo "[libfuse] downloading $(LIBFUSE_VERSION)" + @if command -v curl >/dev/null 2>&1; then \ + curl -fL --retry 3 --retry-delay 1 -o "$@.tmp" "$(LIBFUSE_URL_PRIMARY)" || \ + curl -fL --retry 3 --retry-delay 1 -o "$@.tmp" "$(LIBFUSE_URL_MIRROR)"; \ + elif command -v wget >/dev/null 2>&1; then \ + wget -O "$@.tmp" "$(LIBFUSE_URL_PRIMARY)" || \ + wget -O "$@.tmp" "$(LIBFUSE_URL_MIRROR)"; \ + else \ + echo "error: neither curl nor wget found"; \ + exit 1; \ + fi + @mv "$@.tmp" "$@" + +$(LIBFUSE_SRC_DIR): $(LIBFUSE_ARCHIVE) + @mkdir -p $(LIBFUSE_SRC_ROOT) + @rm -rf "$(LIBFUSE_SRC_DIR)" + @tar -xzf "$(LIBFUSE_ARCHIVE)" -C "$(LIBFUSE_SRC_ROOT)" + +$(LIBFUSE_STAMP): $(LIBFUSE_SRC_DIR) + @command -v "$(MESON)" >/dev/null 2>&1 || (echo "error: meson not found" && exit 1) + @command -v ninja >/dev/null 2>&1 || (echo "error: ninja not found" && exit 1) + @command -v "$(PKG_CONFIG)" >/dev/null 2>&1 || (echo "error: pkg-config not found" && exit 1) + @rm -rf "$(LIBFUSE_BUILD_DIR)" "$(LIBFUSE_PREFIX)" + @mkdir -p "$(LIBFUSE_BUILD_DIR)" "$(LIBFUSE_PREFIX)" + @CC="$(CC)" AR="$(AR)" RANLIB="$(RANLIB)" STRIP="$(STRIP)" \ + CFLAGS="$(LIBFUSE_MESON_CFLAGS)" LDFLAGS="$(LIBFUSE_MESON_LDFLAGS)" \ + $(MESON) setup "$(LIBFUSE_BUILD_DIR)" "$(LIBFUSE_SRC_DIR)" \ + --buildtype=release \ + --default-library=static \ + --prefix="$(LIBFUSE_PREFIX)" \ + --libdir=lib \ + -Dexamples=false \ + -Dutils=false \ + -Ddisable-mtab=true \ + -Duseroot=false + @env -u MAKEFLAGS $(MESON) compile -C "$(LIBFUSE_BUILD_DIR)" -j "$(LIBFUSE_MESON_JOBS)" + @env -u MAKEFLAGS $(MESON) install -C "$(LIBFUSE_BUILD_DIR)" + @touch "$@" + +fuse3_demo: fuse3_demo.c $(LIBFUSE_STAMP) + @FUSE_CFLAGS="$$(PKG_CONFIG_PATH="$(LIBFUSE_PC_DIR)" $(PKG_CONFIG) --cflags fuse3)"; \ + FUSE_LIBS="$$(PKG_CONFIG_PATH="$(LIBFUSE_PC_DIR)" $(PKG_CONFIG) --static --libs fuse3)"; \ + $(CC) $(CFLAGS_COMMON) $$FUSE_CFLAGS $< -o $@ $$FUSE_LIBS + +test_fuse3_demo: test_fuse3_demo.c + $(CC) $(CFLAGS_COMMON) $< -o $@ + +install: all + @echo "Installing binaries to $(DADK_CURRENT_BUILD_DIR)/" + mv $(BINS) $(DADK_CURRENT_BUILD_DIR)/ + +clean: + rm -f $(BINS) + rm -rf "$(LIBFUSE_BUILD_DIR)" "$(LIBFUSE_PREFIX)" + +distclean: clean + rm -rf "$(LIBFUSE_SRC_ROOT)" "$(LIBFUSE_CACHE_DIR)" + +.PHONY: all install clean distclean diff --git a/user/apps/fuse3_demo/README.md b/user/apps/fuse3_demo/README.md new file mode 100644 index 000000000..5317a866e --- /dev/null +++ b/user/apps/fuse3_demo/README.md @@ -0,0 +1,55 @@ +# fuse3_demo(基于 libfuse3 的 DragonOS FUSE 演示) + +`fuse3_demo` 是基于 `libfuse3` 的最小可运行 demo,用于推进 `TODO_FUSE_FULL_PLAN_CN.md` 的 P4(生态兼容)目标。 + +## 设计目标 + +- 自动下载 `libfuse3` 源码并本地构建静态库 +- `fuse3_demo` 二进制静态链接 `libfuse3` +- 提供一个可回归的集成测试 `test_fuse3_demo` + +## 构建 + +在项目根目录下: + +```bash +make -C user/apps/fuse3_demo -j8 +``` + +默认行为: + +1. 自动下载 `fuse-3.18.1.tar.gz` +2. 使用 meson/ninja 构建 `libfuse3.a` +3. 静态链接生成: + - `fuse3_demo` + - `test_fuse3_demo` + +可选变量: + +- `LIBFUSE_VERSION`:指定 libfuse 版本(默认 `3.18.1`) +- `LIBFUSE_URL_PRIMARY`:主下载地址 +- `LIBFUSE_URL_MIRROR`:镜像下载地址 +- `LIBFUSE_ARCHIVE`:指定本地 tarball(离线构建时可用) +- `LIBFUSE_MESON_JOBS`:libfuse 编译并行度(默认 `1`,避免 jobserver 兼容问题) + +## 运行 demo + +```bash +mkdir -p /tmp/fuse3_mnt +fuse3_demo /tmp/fuse3_mnt --single +``` + +默认会创建临时 backing 目录,并在挂载点导出 `hello.txt`。 + +## 运行测试 + +```bash +test_fuse3_demo +``` + +测试会: + +1. 启动 `fuse3_demo` +2. 校验 `hello.txt` 读取 +3. 校验创建/写入/重命名/删除文件 +4. 发送信号停止 daemon 并清理挂载点 diff --git a/user/apps/fuse3_demo/fuse3_demo.c b/user/apps/fuse3_demo/fuse3_demo.c new file mode 100644 index 000000000..5f5b61e50 --- /dev/null +++ b/user/apps/fuse3_demo/fuse3_demo.c @@ -0,0 +1,613 @@ +#define FUSE_USE_VERSION 31 +#define _GNU_SOURCE + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static char g_backing_dir[PATH_MAX]; +static int g_cleanup_backing = 0; +static int g_verbose_log = -1; + +static int demo_verbose_enabled(void) { + if (g_verbose_log < 0) { + const char *v = getenv("FUSE3_TEST_LOG"); + g_verbose_log = (v && v[0] && strcmp(v, "0") != 0) ? 1 : 0; + } + return g_verbose_log; +} + +static void demo_logf(const char *fmt, ...) { + if (!demo_verbose_enabled()) { + return; + } + va_list ap; + va_start(ap, fmt); + fprintf(stderr, "[fuse3-demo] "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + va_end(ap); +} + +static int demo_sanitize_open_flags(int flags, int for_create) { + int keep = O_ACCMODE | O_APPEND | O_NONBLOCK | O_DSYNC | O_DIRECT | O_LARGEFILE | + O_DIRECTORY | O_NOFOLLOW | O_NOATIME | O_CLOEXEC; +#ifdef O_PATH + keep |= O_PATH; +#endif +#ifdef O_SYNC + keep |= O_SYNC; +#endif +#ifdef O_TRUNC + keep |= O_TRUNC; +#endif + if (for_create) { + keep |= O_CREAT; +#ifdef O_EXCL + keep |= O_EXCL; +#endif + } + return flags & keep; +} + +static int demo_realpath(const char *path, char *buf, size_t buflen) { + if (!path || path[0] != '/') { + return -EINVAL; + } + int n = snprintf(buf, buflen, "%s%s", g_backing_dir, path); + if (n < 0 || (size_t)n >= buflen) { + return -ENAMETOOLONG; + } + return 0; +} + +static void remove_tree(const char *root) { + DIR *dir = opendir(root); + if (!dir) { + return; + } + + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { + continue; + } + char full[PATH_MAX]; + int n = snprintf(full, sizeof(full), "%s/%s", root, ent->d_name); + if (n < 0 || (size_t)n >= sizeof(full)) { + continue; + } + + struct stat st; + if (lstat(full, &st) != 0) { + continue; + } + + if (S_ISDIR(st.st_mode)) { + remove_tree(full); + rmdir(full); + } else { + unlink(full); + } + } + closedir(dir); +} + +static int prepare_backing_dir(const char *custom) { + const char *base = custom; + char tmp[] = "/tmp/fuse3_demo_backing_XXXXXX"; + + if (!base) { + base = mkdtemp(tmp); + if (!base) { + return -errno; + } + g_cleanup_backing = 1; + } else { + struct stat st; + if (stat(base, &st) != 0) { + if (mkdir(base, 0755) != 0) { + return -errno; + } + } else if (!S_ISDIR(st.st_mode)) { + return -ENOTDIR; + } + } + + if (strlen(base) >= sizeof(g_backing_dir)) { + return -ENAMETOOLONG; + } + strcpy(g_backing_dir, base); + + char hello_path[PATH_MAX]; + int err = demo_realpath("/hello.txt", hello_path, sizeof(hello_path)); + if (err) { + return err; + } + + int fd = open(hello_path, O_CREAT | O_WRONLY | O_TRUNC, 0644); + if (fd < 0) { + return -errno; + } + + const char *msg = "hello from libfuse3\n"; + ssize_t wn = write(fd, msg, strlen(msg)); + close(fd); + if (wn != (ssize_t)strlen(msg)) { + return -EIO; + } + return 0; +} + +static int demo_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi) { + (void)fi; + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + if (lstat(full, stbuf) != 0) { + return -errno; + } + return 0; +} + +static int demo_access(const char *path, int mask) { + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + if (access(full, mask) != 0) { + return -errno; + } + return 0; +} + +static int demo_readlink(const char *path, char *buf, size_t size) { + if (size == 0) { + return -EINVAL; + } + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + ssize_t len = readlink(full, buf, size - 1); + if (len < 0) { + return -errno; + } + buf[len] = '\0'; + return 0; +} + +static int demo_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, + struct fuse_file_info *fi, enum fuse_readdir_flags flags) { + (void)offset; + (void)fi; + (void)flags; + + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + + DIR *dir = opendir(full); + if (!dir) { + return -errno; + } + + struct dirent *ent; + while ((ent = readdir(dir)) != NULL) { + struct stat st; + memset(&st, 0, sizeof(st)); + st.st_ino = ent->d_ino; + st.st_mode = (mode_t)(ent->d_type << 12); + if (filler(buf, ent->d_name, &st, 0, 0) != 0) { + break; + } + } + closedir(dir); + return 0; +} + +static int demo_mknod(const char *path, mode_t mode, dev_t rdev) { + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + + int ret; + if (S_ISREG(mode)) { + ret = open(full, O_CREAT | O_EXCL | O_WRONLY, mode); + if (ret >= 0) { + close(ret); + ret = 0; + } + } else if (S_ISFIFO(mode)) { + ret = mkfifo(full, mode); + } else { + ret = mknod(full, mode, rdev); + } + if (ret != 0) { + return -errno; + } + return 0; +} + +static int demo_mkdir(const char *path, mode_t mode) { + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + if (mkdir(full, mode) != 0) { + return -errno; + } + return 0; +} + +static int demo_unlink(const char *path) { + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + if (unlink(full) != 0) { + return -errno; + } + return 0; +} + +static int demo_rmdir(const char *path) { + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + if (rmdir(full) != 0) { + return -errno; + } + return 0; +} + +static int demo_symlink(const char *from, const char *to) { + char full_to[PATH_MAX]; + int err = demo_realpath(to, full_to, sizeof(full_to)); + if (err) { + return err; + } + if (symlink(from, full_to) != 0) { + return -errno; + } + return 0; +} + +static int demo_rename(const char *from, const char *to, unsigned int flags) { + if (flags != 0) { + return -EINVAL; + } + + char full_from[PATH_MAX]; + char full_to[PATH_MAX]; + int err = demo_realpath(from, full_from, sizeof(full_from)); + if (err) { + return err; + } + err = demo_realpath(to, full_to, sizeof(full_to)); + if (err) { + return err; + } + + if (rename(full_from, full_to) != 0) { + return -errno; + } + return 0; +} + +static int demo_link(const char *from, const char *to) { + char full_from[PATH_MAX]; + char full_to[PATH_MAX]; + int err = demo_realpath(from, full_from, sizeof(full_from)); + if (err) { + return err; + } + err = demo_realpath(to, full_to, sizeof(full_to)); + if (err) { + return err; + } + if (link(full_from, full_to) != 0) { + return -errno; + } + return 0; +} + +static int demo_chmod(const char *path, mode_t mode, struct fuse_file_info *fi) { + (void)fi; + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + if (chmod(full, mode) != 0) { + return -errno; + } + return 0; +} + +static int demo_chown(const char *path, uid_t uid, gid_t gid, struct fuse_file_info *fi) { + (void)fi; + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + if (lchown(full, uid, gid) != 0) { + return -errno; + } + return 0; +} + +static int demo_truncate(const char *path, off_t size, struct fuse_file_info *fi) { + if (fi != NULL) { + if (ftruncate((int)fi->fh, size) != 0) { + demo_logf("truncate fh=%llu size=%lld errno=%d", (unsigned long long)fi->fh, + (long long)size, errno); + return -errno; + } + demo_logf("truncate fh=%llu size=%lld ok", (unsigned long long)fi->fh, (long long)size); + return 0; + } + + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + if (truncate(full, size) != 0) { + demo_logf("truncate path=%s size=%lld errno=%d", path, (long long)size, errno); + return -errno; + } + demo_logf("truncate path=%s size=%lld ok", path, (long long)size); + return 0; +} + +static int demo_open(const char *path, struct fuse_file_info *fi) { + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + int open_flags = demo_sanitize_open_flags(fi->flags, 0); + int fd = open(full, open_flags); + if (fd < 0) { + demo_logf("open path=%s flags=0x%x sanitized=0x%x errno=%d", path, fi->flags, open_flags, + errno); + return -errno; + } + demo_logf("open path=%s flags=0x%x sanitized=0x%x fd=%d", path, fi->flags, open_flags, fd); + fi->fh = (uint64_t)fd; + return 0; +} + +static int demo_create(const char *path, mode_t mode, struct fuse_file_info *fi) { + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + int create_flags = demo_sanitize_open_flags(fi->flags | O_CREAT, 1); + int fd = open(full, create_flags, mode); + if (fd < 0) { + demo_logf("create path=%s flags=0x%x sanitized=0x%x mode=0%o errno=%d", path, fi->flags, + create_flags, (unsigned)mode, errno); + return -errno; + } + demo_logf("create path=%s flags=0x%x sanitized=0x%x mode=0%o fd=%d", path, fi->flags, + create_flags, (unsigned)mode, fd); + fi->fh = (uint64_t)fd; + return 0; +} + +static int demo_read(const char *path, char *buf, size_t size, off_t offset, + struct fuse_file_info *fi) { + (void)path; + int fd = (int)fi->fh; + ssize_t n = pread(fd, buf, size, offset); + if (n < 0) { + return -errno; + } + return (int)n; +} + +static int demo_write(const char *path, const char *buf, size_t size, off_t offset, + struct fuse_file_info *fi) { + (void)path; + int fd = (int)fi->fh; + ssize_t n = pwrite(fd, buf, size, offset); + if (n < 0) { + return -errno; + } + return (int)n; +} + +static int demo_statfs(const char *path, struct statvfs *stbuf) { + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + if (statvfs(full, stbuf) != 0) { + return -errno; + } + return 0; +} + +static int demo_flush(const char *path, struct fuse_file_info *fi) { + (void)path; + if (fi == NULL) { + return 0; + } + + int dupfd = dup((int)fi->fh); + if (dupfd < 0) { + return -errno; + } + if (close(dupfd) != 0) { + return -errno; + } + return 0; +} + +static int demo_release(const char *path, struct fuse_file_info *fi) { + (void)path; + if (close((int)fi->fh) != 0) { + return -errno; + } + return 0; +} + +static int demo_fsync(const char *path, int isdatasync, struct fuse_file_info *fi) { + (void)path; + int ret = isdatasync ? fdatasync((int)fi->fh) : fsync((int)fi->fh); + if (ret != 0) { + return -errno; + } + return 0; +} + +static int demo_fsyncdir(const char *path, int isdatasync, struct fuse_file_info *fi) { + (void)path; + (void)isdatasync; + (void)fi; + return 0; +} + +static int demo_utimens(const char *path, const struct timespec tv[2], struct fuse_file_info *fi) { + (void)fi; + char full[PATH_MAX]; + int err = demo_realpath(path, full, sizeof(full)); + if (err) { + return err; + } + if (utimensat(AT_FDCWD, full, tv, AT_SYMLINK_NOFOLLOW) != 0) { + return -errno; + } + return 0; +} + +static void *demo_init(struct fuse_conn_info *conn, struct fuse_config *cfg) { + (void)cfg; + fprintf(stderr, "fuse3_demo: INIT proto=%u.%u capable=0x%llx want=0x%llx\n", conn->proto_major, + conn->proto_minor, (unsigned long long)conn->capable, + (unsigned long long)conn->want); + return NULL; +} + +static struct fuse_operations g_ops = { + .init = demo_init, + .getattr = demo_getattr, + .readlink = demo_readlink, + .mknod = demo_mknod, + .mkdir = demo_mkdir, + .unlink = demo_unlink, + .rmdir = demo_rmdir, + .symlink = demo_symlink, + .rename = demo_rename, + .link = demo_link, + .chmod = demo_chmod, + .chown = demo_chown, + .truncate = demo_truncate, + .open = demo_open, + .read = demo_read, + .write = demo_write, + .statfs = demo_statfs, + .flush = demo_flush, + .release = demo_release, + .fsync = demo_fsync, + .fsyncdir = demo_fsyncdir, + .readdir = demo_readdir, + .create = demo_create, + .utimens = demo_utimens, + .access = demo_access, +}; + +static void usage(const char *prog) { + fprintf(stderr, + "Usage: %s [--backing-dir DIR] [--single] [--debug] [libfuse opts...]\n", + prog); +} + +int main(int argc, char **argv) { + if (argc < 2) { + usage(argv[0]); + return 1; + } + + const char *mountpoint = argv[1]; + const char *backing_dir = NULL; + + char **fuse_argv = calloc((size_t)argc + 4, sizeof(char *)); + if (!fuse_argv) { + perror("calloc"); + return 1; + } + + int fuse_argc = 0; + fuse_argv[fuse_argc++] = argv[0]; + fuse_argv[fuse_argc++] = "-f"; + + for (int i = 2; i < argc; i++) { + if (strcmp(argv[i], "--backing-dir") == 0) { + if (i + 1 >= argc) { + usage(argv[0]); + free(fuse_argv); + return 1; + } + backing_dir = argv[++i]; + continue; + } + if (strcmp(argv[i], "--single") == 0) { + fuse_argv[fuse_argc++] = "-s"; + continue; + } + if (strcmp(argv[i], "--debug") == 0) { + fuse_argv[fuse_argc++] = "-d"; + continue; + } + fuse_argv[fuse_argc++] = argv[i]; + } + + fuse_argv[fuse_argc++] = (char *)mountpoint; + + int err = prepare_backing_dir(backing_dir); + if (err != 0) { + fprintf(stderr, "fuse3_demo: prepare backing dir failed: %s (%d)\n", strerror(-err), -err); + free(fuse_argv); + return 1; + } + + fprintf(stderr, "fuse3_demo: mount=%s backing=%s\n", mountpoint, g_backing_dir); + int ret = fuse_main(fuse_argc, fuse_argv, &g_ops, NULL); + + if (g_cleanup_backing) { + remove_tree(g_backing_dir); + rmdir(g_backing_dir); + } + free(fuse_argv); + return ret; +} diff --git a/user/apps/fuse3_demo/test_fuse3_demo.c b/user/apps/fuse3_demo/test_fuse3_demo.c new file mode 100644 index 000000000..fb6174c22 --- /dev/null +++ b/user/apps/fuse3_demo/test_fuse3_demo.c @@ -0,0 +1,214 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void log_fail(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vprintf(fmt, ap); + va_end(ap); +} + +static int read_all(const char *path, char *buf, size_t cap) { + int fd = open(path, O_RDONLY); + if (fd < 0) { + return -1; + } + ssize_t n = read(fd, buf, cap - 1); + int saved = errno; + close(fd); + if (n < 0) { + errno = saved; + return -1; + } + buf[n] = '\0'; + return (int)n; +} + +static int wait_hello_ready(const char *mountpoint, int timeout_ms) { + char path[256]; + snprintf(path, sizeof(path), "%s/hello.txt", mountpoint); + + int rounds = timeout_ms / 20; + if (rounds < 1) { + rounds = 1; + } + + for (int i = 0; i < rounds; i++) { + char buf[128]; + if (read_all(path, buf, sizeof(buf)) >= 0) { + if (strncmp(buf, "hello from libfuse3\n", 20) == 0) { + return 0; + } + } + usleep(20 * 1000); + } + errno = ETIMEDOUT; + return -1; +} + +static int stop_daemon(pid_t pid, int *status) { + for (int i = 0; i < 100; i++) { + pid_t w = waitpid(pid, status, WNOHANG); + if (w == pid) { + return 0; + } + usleep(20 * 1000); + } + + kill(pid, SIGINT); + for (int i = 0; i < 100; i++) { + pid_t w = waitpid(pid, status, WNOHANG); + if (w == pid) { + return 0; + } + usleep(20 * 1000); + } + + kill(pid, SIGTERM); + for (int i = 0; i < 100; i++) { + pid_t w = waitpid(pid, status, WNOHANG); + if (w == pid) { + return 0; + } + usleep(20 * 1000); + } + + kill(pid, SIGKILL); + return waitpid(pid, status, 0) == pid ? 0 : -1; +} + +int main(void) { + char mnt_template[] = "/tmp/test_fuse3_demo_XXXXXX"; + char *mountpoint = mkdtemp(mnt_template); + if (!mountpoint) { + log_fail("[FAIL] mkdtemp mountpoint: %s (errno=%d)\n", strerror(errno), errno); + return 1; + } + + const char *daemon_path = "/bin/fuse3_demo"; + if (access(daemon_path, X_OK) != 0) { + daemon_path = "./fuse3_demo"; + } + + pid_t pid = fork(); + if (pid < 0) { + log_fail("[FAIL] fork: %s (errno=%d)\n", strerror(errno), errno); + rmdir(mountpoint); + return 1; + } + + if (pid == 0) { + execl(daemon_path, daemon_path, mountpoint, "--single", NULL); + _exit(127); + } + + if (wait_hello_ready(mountpoint, 5000) != 0) { + log_fail("[FAIL] wait hello ready: %s (errno=%d)\n", strerror(errno), errno); + int st = 0; + stop_daemon(pid, &st); + umount(mountpoint); + rmdir(mountpoint); + return 1; + } + + char note[256]; + snprintf(note, sizeof(note), "%s/note.txt", mountpoint); + int fd = open(note, O_CREAT | O_RDWR | O_TRUNC, 0644); + if (fd < 0) { + log_fail("[FAIL] create note: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + + const char *content = "dragonos fuse3 test\n"; + ssize_t wn = write(fd, content, strlen(content)); + close(fd); + if (wn != (ssize_t)strlen(content)) { + log_fail("[FAIL] write note: wn=%zd errno=%d (%s)\n", wn, errno, strerror(errno)); + goto fail; + } + + char read_buf[256]; + if (read_all(note, read_buf, sizeof(read_buf)) < 0) { + log_fail("[FAIL] read note: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + if (strcmp(read_buf, content) != 0) { + log_fail("[FAIL] content mismatch: got='%s' expect='%s'\n", read_buf, content); + goto fail; + } + + char renamed[256]; + snprintf(renamed, sizeof(renamed), "%s/note2.txt", mountpoint); + if (rename(note, renamed) != 0) { + log_fail("[FAIL] rename note: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + + if (unlink(renamed) != 0) { + log_fail("[FAIL] unlink note2: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + + int dirfd = open(mountpoint, O_RDONLY | O_DIRECTORY); + if (dirfd < 0) { + log_fail("[FAIL] open mountpoint dir: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + if (fsync(dirfd) != 0) { + log_fail("[FAIL] fsyncdir mountpoint: %s (errno=%d)\n", strerror(errno), errno); + close(dirfd); + goto fail; + } + close(dirfd); + + if (umount(mountpoint) != 0) { + log_fail("[FAIL] umount(%s): %s (errno=%d)\n", mountpoint, strerror(errno), errno); + int status = 0; + stop_daemon(pid, &status); + rmdir(mountpoint); + return 1; + } + + { + int status = 0; + if (stop_daemon(pid, &status) != 0) { + log_fail("[FAIL] stop daemon failed\n"); + rmdir(mountpoint); + return 1; + } + if (!WIFEXITED(status)) { + log_fail("[FAIL] daemon not exited normally, status=%d\n", status); + rmdir(mountpoint); + return 1; + } + int code = WEXITSTATUS(status); + if (code != 0 && code != 8) { + log_fail("[FAIL] daemon exit code=%d (raw=%d)\n", code, status); + rmdir(mountpoint); + return 1; + } + } + + rmdir(mountpoint); + printf("[PASS] fuse3_demo\n"); + return 0; + +fail: + { + int status = 0; + stop_daemon(pid, &status); + } + umount(mountpoint); + rmdir(mountpoint); + return 1; +} diff --git a/user/apps/fuse_demo/Makefile b/user/apps/fuse_demo/Makefile new file mode 100644 index 000000000..cef89a59a --- /dev/null +++ b/user/apps/fuse_demo/Makefile @@ -0,0 +1,27 @@ +ARCH ?= x86_64 +ifeq ($(ARCH), x86_64) + CROSS_COMPILE=x86_64-linux-musl- +else ifeq ($(ARCH), riscv64) + CROSS_COMPILE=riscv64-linux-musl- +endif + +CC=$(CROSS_COMPILE)gcc +CFLAGS := -Wall -O2 -static -lpthread + +SRCS := $(wildcard *.c) +BINS := $(SRCS:.c=) + +all: $(BINS) + @echo "bins: $(BINS)" + +%: %.c $(wildcard *.h) + $(CC) $(CFLAGS) $< -o $@ + +install: all + @echo "Installing binaries to $(DADK_CURRENT_BUILD_DIR)/" + mv $(BINS) $(DADK_CURRENT_BUILD_DIR)/ + +clean: + rm -f $(BINS) + +.PHONY: all install clean diff --git a/user/apps/fuse_demo/README.md b/user/apps/fuse_demo/README.md new file mode 100644 index 000000000..c53d61a98 --- /dev/null +++ b/user/apps/fuse_demo/README.md @@ -0,0 +1,80 @@ +# fuse_demo(DragonOS 最小 FUSE daemon,无 libfuse) + +`fuse_demo` 是 DragonOS 内核 FUSE 功能的**用户态回归/演示程序**,直接读写 `/dev/fuse` 协议,不依赖 `libfuse`。 + +它提供一个极简的内存文件系统: + +- `/hello.txt`:内容为 `hello from fuse\n` + +## 用法 + +``` +fuse_demo [--rw] [--allow-other] [--default-permissions] [--threads N] +``` + +参数说明: + +- ``:挂载点目录(需已存在,或可被创建) +- `--rw`:启用写相关 opcode(create/write/truncate/rename/unlink/mkdir/rmdir) +- `--allow-other`:允许非挂载者/非同 uid 进程访问(对齐 Linux FUSE 的 `allow_other` 行为) +- `--default-permissions`:启用内核侧 DAC 权限检查(对齐 Linux FUSE 的 `default_permissions`) +- `--threads N`:启动 N 个 worker 线程(`N>=1`)。当 `N>1` 时会使用 `FUSE_DEV_IOC_CLONE` 复制连接到新的 `/dev/fuse` fd。 + +调试: + +- `FUSE_TEST_LOG=1`:输出更详细的 request/reply 日志到 stderr + +## 典型示例 + +### 只读演示(ls/stat/cat) + +``` +mkdir -p /mnt/fuse +FUSE_TEST_LOG=1 fuse_demo /mnt/fuse +``` + +另一个终端(或后台运行后): + +``` +ls -l /mnt/fuse +cat /mnt/fuse/hello.txt +``` + +停止: + +- 前台运行时按 `Ctrl-C`,程序会 best-effort `umount` 并退出 +- 若仍残留挂载:`umount /mnt/fuse` + +### 读写演示(创建/写入/重命名/删除) + +``` +mkdir -p /mnt/fuse +fuse_demo /mnt/fuse --rw + +echo hi > /mnt/fuse/new.txt +cat /mnt/fuse/new.txt +mv /mnt/fuse/new.txt /mnt/fuse/renamed.txt +rm /mnt/fuse/renamed.txt +``` + +### 多线程 / clone 演示 + +``` +mkdir -p /mnt/fuse +fuse_demo /mnt/fuse --threads 4 +``` + +如果内核尚未支持 `FUSE_DEV_IOC_CLONE`,`--threads > 1` 会在 clone 阶段失败并提前退出(或只跑单线程,取决于当时实现)。 + +## 测试入口迁移说明 + +FUSE 回归测试已统一迁移到 `dunitest` 的 `suites/fuse/` 下,按以下两个 gtest 二进制维护: + +- `fuse/fuse_core` +- `fuse/fuse_extended` + +## 权限语义备注(对应 Phase E) + +- 未指定 `--allow-other`:内核会限制“非挂载者允许的进程”调用到该 FUSE 挂载(更安全,类似 Linux 默认行为)。 +- 未指定 `--default-permissions`:内核会绕过大部分本地 DAC 权限检查(remote model),把权限决策交给用户态 daemon。 + - 本 demo daemon **不做权限拒绝**,因此 remote model 下通常会更“宽松”。 diff --git a/user/apps/fuse_demo/fuse_demo.c b/user/apps/fuse_demo/fuse_demo.c new file mode 100644 index 000000000..dee5a9b01 --- /dev/null +++ b/user/apps/fuse_demo/fuse_demo.c @@ -0,0 +1,191 @@ +/** + * @file fuse_demo.c + * @brief Minimal FUSE demo daemon for DragonOS (no libfuse). + * + * Usage: + * fuse_demo [--rw] [--allow-other] [--default-permissions] [--threads N] + * + * This demo serves a tiny in-memory filesystem: + * /hello.txt (contains "hello from fuse\n") + */ + +#include "fuse_test_simplefs.h" + +#include +#include + +#ifndef FUSE_DEV_IOC_CLONE +#define FUSE_DEV_IOC_CLONE 0x8004e500 /* _IOR(229, 0, uint32_t) */ +#endif + +static volatile int g_stop = 0; + +static void on_sigint(int signo) { + (void)signo; + g_stop = 1; +} + +static int parse_int(const char *s, int *out) { + char *end = NULL; + long v = strtol(s, &end, 10); + if (!s[0] || !end || *end != '\0') + return -1; + if (v < 0 || v > 1024) + return -1; + *out = (int)v; + return 0; +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, + "usage: %s [--rw] [--allow-other] [--default-permissions] [--threads N]\n", + argv[0]); + return 1; + } + + fprintf(stderr, "fuse_demo simplefs rev: %s\n", FUSE_SIMPLEFS_REV); + + const char *mp = argv[1]; + int enable_write_ops = 0; + int allow_other = 0; + int default_permissions = 0; + int threads = 1; + + for (int i = 2; i < argc; i++) { + if (strcmp(argv[i], "--rw") == 0) { + enable_write_ops = 1; + } else if (strcmp(argv[i], "--allow-other") == 0) { + allow_other = 1; + } else if (strcmp(argv[i], "--default-permissions") == 0) { + default_permissions = 1; + } else if (strcmp(argv[i], "--threads") == 0) { + if (i + 1 >= argc || parse_int(argv[i + 1], &threads) != 0 || threads < 1) { + fprintf(stderr, "invalid --threads\n"); + return 1; + } + i++; + } else { + fprintf(stderr, "unknown arg: %s\n", argv[i]); + return 1; + } + } + + if (ensure_dir(mp) != 0) { + perror("ensure_dir"); + return 1; + } + + signal(SIGINT, on_sigint); + signal(SIGTERM, on_sigint); + + int master_fd = open("/dev/fuse", O_RDWR); + if (master_fd < 0) { + perror("open(/dev/fuse)"); + return 1; + } + + volatile int init_done = 0; + volatile int stop = 0; + + struct fuse_daemon_args master_args; + memset(&master_args, 0, sizeof(master_args)); + master_args.fd = master_fd; + master_args.stop = &stop; + master_args.init_done = &init_done; + master_args.enable_write_ops = enable_write_ops; + master_args.stop_on_destroy = 1; + + pthread_t *ths = calloc((size_t)threads, sizeof(pthread_t)); + struct fuse_daemon_args *args = calloc((size_t)threads, sizeof(struct fuse_daemon_args)); + int *fds = calloc((size_t)threads, sizeof(int)); + if (!ths || !args || !fds) { + fprintf(stderr, "oom\n"); + close(master_fd); + return 1; + } + + fds[0] = master_fd; + args[0] = master_args; + if (pthread_create(&ths[0], NULL, fuse_daemon_thread, &args[0]) != 0) { + fprintf(stderr, "pthread_create(master) failed\n"); + close(master_fd); + return 1; + } + + char opts[512]; + snprintf(opts, sizeof(opts), "fd=%d,rootmode=040755,user_id=%u,group_id=%u%s%s", master_fd, + (unsigned)getuid(), (unsigned)getgid(), allow_other ? ",allow_other" : "", + default_permissions ? ",default_permissions" : ""); + + if (mount("none", mp, "fuse", 0, opts) != 0) { + perror("mount(fuse)"); + stop = 1; + close(master_fd); + pthread_join(ths[0], NULL); + return 1; + } + + for (int i = 0; i < 200; i++) { + if (init_done) + break; + usleep(10 * 1000); + } + if (!init_done) { + fprintf(stderr, "init handshake timeout\n"); + umount(mp); + stop = 1; + close(master_fd); + pthread_join(ths[0], NULL); + return 1; + } + + /* Optional extra threads via clone */ + for (int i = 1; i < threads; i++) { + int fd = open("/dev/fuse", O_RDWR); + if (fd < 0) { + perror("open(/dev/fuse) for clone"); + break; + } + uint32_t oldfd_u32 = (uint32_t)master_fd; + if (ioctl(fd, FUSE_DEV_IOC_CLONE, &oldfd_u32) != 0) { + perror("ioctl(FUSE_DEV_IOC_CLONE)"); + close(fd); + break; + } + fds[i] = fd; + memset(&args[i], 0, sizeof(args[i])); + args[i].fd = fd; + args[i].stop = &stop; + args[i].init_done = &init_done; + args[i].enable_write_ops = enable_write_ops; + args[i].stop_on_destroy = 1; + if (pthread_create(&ths[i], NULL, fuse_daemon_thread, &args[i]) != 0) { + perror("pthread_create(clone)"); + close(fd); + break; + } + } + + fprintf(stderr, "fuse_demo mounted at %s (threads=%d). Ctrl-C to stop.\n", mp, threads); + + while (!g_stop) { + sleep(1); + } + + /* Best-effort cleanup */ + umount(mp); + stop = 1; + for (int i = 0; i < threads; i++) { + if (fds[i] > 0) + close(fds[i]); + } + for (int i = 0; i < threads; i++) { + if (ths[i]) + pthread_join(ths[i], NULL); + } + free(fds); + free(args); + free(ths); + return 0; +} diff --git a/user/apps/fuse_demo/fuse_test_simplefs.h b/user/apps/fuse_demo/fuse_test_simplefs.h new file mode 100644 index 000000000..6c1636d2d --- /dev/null +++ b/user/apps/fuse_demo/fuse_test_simplefs.h @@ -0,0 +1,1413 @@ +/* + * Minimal FUSE userspace daemon for DragonOS kernel tests (no libfuse). + * + * This header provides a tiny in-memory filesystem and request handlers for + * a subset of FUSE opcodes used by Phase C/D tests. + */ + +#pragma once + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FUSE_TEST_LOG_PREFIX "[fuse-test] " +#define FUSE_SIMPLEFS_REV "statfs-v1" + +static inline int fuse_test_log_enabled(void) { + static int inited = 0; + static int enabled = 0; + if (!inited) { + const char *v = getenv("FUSE_TEST_LOG"); + enabled = (v && v[0] && strcmp(v, "0") != 0); + inited = 1; + } + return enabled; +} + +#define FUSE_TEST_LOG(fmt, ...) \ + do { \ + if (fuse_test_log_enabled()) { \ + fprintf(stderr, FUSE_TEST_LOG_PREFIX fmt "\n", ##__VA_ARGS__); \ + } \ + } while (0) + +#ifndef DT_DIR +#define DT_DIR 4 +#endif +#ifndef DT_REG +#define DT_REG 8 +#endif +#ifndef DT_LNK +#define DT_LNK 10 +#endif + +/* Keep test buffers off small thread stacks. */ +#define FUSE_TEST_BUF_SIZE (64 * 1024) + +/* Opcodes (subset) */ +#ifndef FUSE_LOOKUP +#define FUSE_LOOKUP 1 +#endif +#ifndef FUSE_FORGET +#define FUSE_FORGET 2 +#endif +#ifndef FUSE_GETATTR +#define FUSE_GETATTR 3 +#endif +#ifndef FUSE_SETATTR +#define FUSE_SETATTR 4 +#endif +#ifndef FUSE_READLINK +#define FUSE_READLINK 5 +#endif +#ifndef FUSE_SYMLINK +#define FUSE_SYMLINK 6 +#endif +#ifndef FUSE_MKNOD +#define FUSE_MKNOD 8 +#endif +#ifndef FUSE_MKDIR +#define FUSE_MKDIR 9 +#endif +#ifndef FUSE_UNLINK +#define FUSE_UNLINK 10 +#endif +#ifndef FUSE_RMDIR +#define FUSE_RMDIR 11 +#endif +#ifndef FUSE_RENAME +#define FUSE_RENAME 12 +#endif +#ifndef FUSE_LINK +#define FUSE_LINK 13 +#endif +#ifndef FUSE_OPEN +#define FUSE_OPEN 14 +#endif +#ifndef FUSE_READ +#define FUSE_READ 15 +#endif +#ifndef FUSE_WRITE +#define FUSE_WRITE 16 +#endif +#ifndef FUSE_STATFS +#define FUSE_STATFS 17 +#endif +#ifndef FUSE_RELEASE +#define FUSE_RELEASE 18 +#endif +#ifndef FUSE_FSYNC +#define FUSE_FSYNC 20 +#endif +#ifndef FUSE_FLUSH +#define FUSE_FLUSH 25 +#endif +#ifndef FUSE_INIT +#define FUSE_INIT 26 +#endif +#ifndef FUSE_OPENDIR +#define FUSE_OPENDIR 27 +#endif +#ifndef FUSE_READDIR +#define FUSE_READDIR 28 +#endif +#ifndef FUSE_RELEASEDIR +#define FUSE_RELEASEDIR 29 +#endif +#ifndef FUSE_FSYNCDIR +#define FUSE_FSYNCDIR 30 +#endif +#ifndef FUSE_ACCESS +#define FUSE_ACCESS 34 +#endif +#ifndef FUSE_CREATE +#define FUSE_CREATE 35 +#endif +#ifndef FUSE_INTERRUPT +#define FUSE_INTERRUPT 36 +#endif +#ifndef FUSE_DESTROY +#define FUSE_DESTROY 38 +#endif +#ifndef FUSE_READDIRPLUS +#define FUSE_READDIRPLUS 44 +#endif +#ifndef FUSE_RENAME2 +#define FUSE_RENAME2 45 +#endif + +#ifndef FUSE_MIN_READ_BUFFER +#define FUSE_MIN_READ_BUFFER 8192 +#endif + +/* INIT flags (subset) */ +#ifndef FUSE_INIT_EXT +#define FUSE_INIT_EXT (1u << 30) +#endif +#ifndef FUSE_MAX_PAGES +#define FUSE_MAX_PAGES (1u << 22) +#endif +#ifndef FUSE_DO_READDIRPLUS +#define FUSE_DO_READDIRPLUS (1u << 13) +#endif +#ifndef FUSE_READDIRPLUS_AUTO +#define FUSE_READDIRPLUS_AUTO (1u << 14) +#endif +#ifndef FUSE_NO_OPEN_SUPPORT +#define FUSE_NO_OPEN_SUPPORT (1u << 17) +#endif +#ifndef FUSE_NO_OPENDIR_SUPPORT +#define FUSE_NO_OPENDIR_SUPPORT (1u << 24) +#endif +#ifndef FUSE_FSYNC_FDATASYNC +#define FUSE_FSYNC_FDATASYNC (1u << 0) +#endif + +#ifndef FUSE_NOTIFY_INVAL_INODE +#define FUSE_NOTIFY_INVAL_INODE 2 +#endif + +#ifndef RENAME_NOREPLACE +#define RENAME_NOREPLACE (1u << 0) +#endif +#ifndef RENAME_EXCHANGE +#define RENAME_EXCHANGE (1u << 1) +#endif +#ifndef RENAME_WHITEOUT +#define RENAME_WHITEOUT (1u << 2) +#endif + +/* setattr valid bits (subset) */ +#ifndef FATTR_MODE +#define FATTR_MODE (1u << 0) +#endif +#ifndef FATTR_UID +#define FATTR_UID (1u << 1) +#endif +#ifndef FATTR_GID +#define FATTR_GID (1u << 2) +#endif +#ifndef FATTR_SIZE +#define FATTR_SIZE (1u << 3) +#endif + +struct fuse_in_header { + uint32_t len; + uint32_t opcode; + uint64_t unique; + uint64_t nodeid; + uint32_t uid; + uint32_t gid; + uint32_t pid; + uint16_t total_extlen; + uint16_t padding; +}; + +struct fuse_out_header { + uint32_t len; + int32_t error; /* -errno */ + uint64_t unique; +}; + +struct fuse_init_in { + uint32_t major; + uint32_t minor; + uint32_t max_readahead; + uint32_t flags; + uint32_t flags2; + uint32_t unused[11]; +}; + +struct fuse_init_out { + uint32_t major; + uint32_t minor; + uint32_t max_readahead; + uint32_t flags; + uint16_t max_background; + uint16_t congestion_threshold; + uint32_t max_write; + uint32_t time_gran; + uint16_t max_pages; + uint16_t map_alignment; + uint32_t flags2; + uint32_t unused[7]; +}; + +struct fuse_attr { + uint64_t ino; + uint64_t size; + uint64_t blocks; + uint64_t atime; + uint64_t mtime; + uint64_t ctime; + uint32_t atimensec; + uint32_t mtimensec; + uint32_t ctimensec; + uint32_t mode; + uint32_t nlink; + uint32_t uid; + uint32_t gid; + uint32_t rdev; + uint32_t blksize; + uint32_t flags; +}; + +struct fuse_entry_out { + uint64_t nodeid; + uint64_t generation; + uint64_t entry_valid; + uint64_t attr_valid; + uint32_t entry_valid_nsec; + uint32_t attr_valid_nsec; + struct fuse_attr attr; +}; + +struct fuse_forget_in { + uint64_t nlookup; +}; + +struct fuse_interrupt_in { + uint64_t unique; +}; + +struct fuse_getattr_in { + uint32_t getattr_flags; + uint32_t dummy; + uint64_t fh; +}; + +struct fuse_attr_out { + uint64_t attr_valid; + uint32_t attr_valid_nsec; + uint32_t dummy; + struct fuse_attr attr; +}; + +struct fuse_open_in { + uint32_t flags; + uint32_t open_flags; +}; + +struct fuse_create_in { + uint32_t flags; + uint32_t mode; + uint32_t umask; + uint32_t open_flags; +}; + +struct fuse_open_out { + uint64_t fh; + uint32_t open_flags; + uint32_t padding; +}; + +struct fuse_read_in { + uint64_t fh; + uint64_t offset; + uint32_t size; + uint32_t read_flags; + uint64_t lock_owner; + uint32_t flags; + uint32_t padding; +}; + +struct fuse_write_in { + uint64_t fh; + uint64_t offset; + uint32_t size; + uint32_t write_flags; + uint64_t lock_owner; + uint32_t flags; + uint32_t padding; +}; + +struct fuse_write_out { + uint32_t size; + uint32_t padding; +}; + +struct fuse_kstatfs { + uint64_t blocks; + uint64_t bfree; + uint64_t bavail; + uint64_t files; + uint64_t ffree; + uint32_t bsize; + uint32_t namelen; + uint32_t frsize; + uint32_t padding; + uint32_t spare[6]; +}; + +struct fuse_statfs_out { + struct fuse_kstatfs st; +}; + +struct fuse_release_in { + uint64_t fh; + uint32_t flags; + uint32_t release_flags; + uint64_t lock_owner; +}; + +struct fuse_flush_in { + uint64_t fh; + uint32_t unused; + uint32_t padding; + uint64_t lock_owner; +}; + +struct fuse_fsync_in { + uint64_t fh; + uint32_t fsync_flags; + uint32_t padding; +}; + +struct fuse_access_in { + uint32_t mask; + uint32_t padding; +}; + +struct fuse_mknod_in { + uint32_t mode; + uint32_t rdev; + uint32_t umask; + uint32_t padding; +}; + +struct fuse_mkdir_in { + uint32_t mode; + uint32_t umask; +}; + +struct fuse_rename_in { + uint64_t newdir; +}; + +struct fuse_rename2_in { + uint64_t newdir; + uint32_t flags; + uint32_t padding; +}; + +struct fuse_link_in { + uint64_t oldnodeid; +}; + +struct fuse_setattr_in { + uint32_t valid; + uint32_t padding; + uint64_t fh; + uint64_t size; + uint64_t lock_owner; + uint64_t atime; + uint64_t mtime; + uint64_t ctime; + uint32_t atimensec; + uint32_t mtimensec; + uint32_t ctimensec; + uint32_t mode; + uint32_t unused4; + uint32_t uid; + uint32_t gid; + uint32_t unused5; +}; + +struct fuse_dirent { + uint64_t ino; + uint64_t off; + uint32_t namelen; + uint32_t type; + /* char name[]; */ +}; + +struct fuse_direntplus { + struct fuse_entry_out entry_out; + struct fuse_dirent dirent; + /* char name[]; */ +}; + +struct fuse_notify_inval_inode_out { + uint64_t ino; + int64_t off; + int64_t len; +}; + +static inline size_t fuse_dirent_rec_len(size_t namelen) { + size_t unaligned = sizeof(struct fuse_dirent) + namelen; + return (unaligned + 8 - 1) & ~(size_t)(8 - 1); +} + +static inline size_t fuse_direntplus_rec_len(size_t namelen) { + size_t unaligned = sizeof(struct fuse_direntplus) + namelen; + return (unaligned + 8 - 1) & ~(size_t)(8 - 1); +} + +/* ===== in-memory FS ===== */ + +#define SIMPLEFS_MAX_NODES 64 +#define SIMPLEFS_NAME_MAX 64 +#define SIMPLEFS_DATA_MAX 8192 + +struct simplefs_node { + int used; + uint64_t nodeid; + uint64_t ino; + uint64_t parent; + int is_dir; + int is_symlink; + uint32_t mode; /* includes type bits */ + char name[SIMPLEFS_NAME_MAX]; + unsigned char data[SIMPLEFS_DATA_MAX]; + size_t size; +}; + +struct simplefs { + struct simplefs_node nodes[SIMPLEFS_MAX_NODES]; + uint64_t next_nodeid; + uint64_t next_ino; +}; + +static inline void simplefs_init(struct simplefs *fs) { + memset(fs, 0, sizeof(*fs)); + fs->next_nodeid = 2; + fs->next_ino = 2; + + /* root nodeid=1 */ + fs->nodes[0].used = 1; + fs->nodes[0].nodeid = 1; + fs->nodes[0].ino = 1; + fs->nodes[0].parent = 1; + fs->nodes[0].is_dir = 1; + fs->nodes[0].is_symlink = 0; + fs->nodes[0].mode = 0040755; + strcpy(fs->nodes[0].name, ""); + fs->nodes[0].size = 0; + + /* hello.txt under root */ + fs->nodes[1].used = 1; + fs->nodes[1].nodeid = 2; + fs->nodes[1].ino = 2; + fs->nodes[1].parent = 1; + fs->nodes[1].is_dir = 0; + fs->nodes[1].is_symlink = 0; + fs->nodes[1].mode = 0100644; + strcpy(fs->nodes[1].name, "hello.txt"); + const char *msg = "hello from fuse\n"; + fs->nodes[1].size = strlen(msg); + memcpy(fs->nodes[1].data, msg, fs->nodes[1].size); + + fs->next_nodeid = 3; + fs->next_ino = 3; +} + +static inline int simplefs_mode_is_dir(uint32_t mode) { + return (mode & 0170000u) == 0040000u; +} + +static inline int simplefs_mode_is_symlink(uint32_t mode) { + return (mode & 0170000u) == 0120000u; +} + +static inline struct simplefs_node *simplefs_find_node(struct simplefs *fs, uint64_t nodeid) { + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + if (fs->nodes[i].used && fs->nodes[i].nodeid == nodeid) { + return &fs->nodes[i]; + } + } + return NULL; +} + +static inline struct simplefs_node *simplefs_find_child(struct simplefs *fs, uint64_t parent, + const char *name) { + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + if (!fs->nodes[i].used) + continue; + if (fs->nodes[i].parent != parent) + continue; + if (strcmp(fs->nodes[i].name, name) == 0) + return &fs->nodes[i]; + } + return NULL; +} + +static inline int simplefs_has_children(struct simplefs *fs, uint64_t parent) { + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + if (!fs->nodes[i].used) + continue; + if (fs->nodes[i].parent == parent) + return 1; + } + return 0; +} + +static inline struct simplefs_node *simplefs_alloc(struct simplefs *fs) { + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + if (!fs->nodes[i].used) { + struct simplefs_node *n = &fs->nodes[i]; + memset(n, 0, sizeof(*n)); + n->used = 1; + n->nodeid = fs->next_nodeid++; + n->ino = fs->next_ino++; + return n; + } + } + return NULL; +} + +static inline void simplefs_fill_attr(const struct simplefs_node *n, struct fuse_attr *a) { + memset(a, 0, sizeof(*a)); + a->ino = n->ino; + a->size = n->size; + a->blocks = (n->size + 511) / 512; + a->mode = n->mode; + a->nlink = simplefs_mode_is_dir(n->mode) ? 2 : 1; + a->uid = getuid(); + a->gid = getgid(); + a->blksize = 4096; +} + +static inline int fuse_write_reply(int fd, uint64_t unique, int err_neg, const void *payload, + size_t payload_len) { + struct fuse_out_header out; + memset(&out, 0, sizeof(out)); + out.len = sizeof(out) + (uint32_t)payload_len; + out.error = err_neg; + out.unique = unique; + + size_t total = sizeof(out) + payload_len; + unsigned char *buf = malloc(total); + if (!buf) { + errno = ENOMEM; + return -1; + } + memcpy(buf, &out, sizeof(out)); + if (payload_len) { + memcpy(buf + sizeof(out), payload, payload_len); + } + ssize_t wn = write(fd, buf, total); + free(buf); + if (wn == (ssize_t)total) { + FUSE_TEST_LOG("reply unique=%llu err=%d len=%zu", + (unsigned long long)unique, (int)err_neg, total); + } + if (wn != (ssize_t)total) { + return -1; + } + return 0; +} + +struct fuse_daemon_args { + int fd; + volatile int *stop; + volatile int *init_done; + int enable_write_ops; + int exit_after_init; + int stop_on_destroy; + uint32_t root_mode_override; + uint32_t hello_mode_override; + volatile uint32_t *forget_count; + volatile uint64_t *forget_nlookup_sum; + volatile uint32_t *destroy_count; + volatile uint32_t *init_in_flags; + volatile uint32_t *init_in_flags2; + volatile uint32_t *init_in_max_readahead; + volatile uint32_t *access_count; + volatile uint32_t *flush_count; + volatile uint32_t *fsync_count; + volatile uint32_t *fsyncdir_count; + volatile uint32_t *create_count; + volatile uint32_t *rename2_count; + volatile uint32_t *open_count; + volatile uint32_t *opendir_count; + volatile uint32_t *release_count; + volatile uint32_t *releasedir_count; + volatile uint32_t *readdirplus_count; + volatile uint32_t *interrupt_count; + volatile uint64_t *blocked_read_unique; + volatile uint64_t *last_interrupt_target; + uint32_t access_deny_mask; + uint32_t init_out_flags_override; + int force_open_enosys; + int force_opendir_enosys; + int block_read_until_interrupt; + struct simplefs fs; +}; + +static inline int simplefs_node_is_dir(const struct simplefs_node *n) { + return n && (n->is_dir || simplefs_mode_is_dir(n->mode)); +} + +static inline int simplefs_node_is_symlink(const struct simplefs_node *n) { + return n && (n->is_symlink || simplefs_mode_is_symlink(n->mode)); +} + +static inline uint32_t simplefs_dirent_type(const struct simplefs_node *n) { + if (simplefs_node_is_dir(n)) { + return DT_DIR; + } + if (simplefs_node_is_symlink(n)) { + return DT_LNK; + } + return DT_REG; +} + +static inline int simplefs_fill_entry_reply(struct fuse_daemon_args *a, const struct fuse_in_header *h, + const struct simplefs_node *node) { + struct fuse_entry_out out; + memset(&out, 0, sizeof(out)); + out.nodeid = node->nodeid; + simplefs_fill_attr(node, &out.attr); + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); +} + +static inline int simplefs_parse_two_names(const unsigned char *payload, size_t payload_len, + size_t fixed_len, const char **oldname_out, + const char **newname_out) { + if (payload_len < fixed_len + 3) { + return -1; + } + const char *names = (const char *)(payload + fixed_len); + size_t names_len = payload_len - fixed_len; + const char *oldname = names; + size_t oldlen = strnlen(oldname, names_len); + if (oldlen == names_len) { + return -1; + } + const char *newname = names + oldlen + 1; + size_t remain = names_len - oldlen - 1; + if (remain == 0) { + return -1; + } + size_t newlen = strnlen(newname, remain); + if (newlen == remain) { + return -1; + } + *oldname_out = oldname; + *newname_out = newname; + return 0; +} + +static inline int simplefs_do_rename(struct fuse_daemon_args *a, const struct fuse_in_header *h, + uint64_t newdir, uint32_t flags, const char *oldname, + const char *newname) { + if ((flags & (RENAME_EXCHANGE | RENAME_WHITEOUT)) != 0) { + return fuse_write_reply(a->fd, h->unique, -EINVAL, NULL, 0); + } + struct simplefs_node *src = simplefs_find_child(&a->fs, h->nodeid, oldname); + if (!src) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + struct simplefs_node *dst_parent = simplefs_find_node(&a->fs, newdir); + if (!dst_parent || !simplefs_node_is_dir(dst_parent)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + struct simplefs_node *dst = simplefs_find_child(&a->fs, newdir, newname); + if (dst) { + if (flags & RENAME_NOREPLACE) { + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + src->parent = newdir; + strncpy(src->name, newname, sizeof(src->name) - 1); + src->name[sizeof(src->name) - 1] = '\0'; + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); +} + +static inline int fuse_handle_one(struct fuse_daemon_args *a, const unsigned char *req, size_t n) { + if (n < sizeof(struct fuse_in_header)) { + return -1; + } + const struct fuse_in_header *h = (const struct fuse_in_header *)req; + const unsigned char *payload = req + sizeof(*h); + size_t payload_len = n - sizeof(*h); + FUSE_TEST_LOG("handle opcode=%u unique=%llu nodeid=%llu len=%u payload=%zu", + h->opcode, (unsigned long long)h->unique, (unsigned long long)h->nodeid, + h->len, payload_len); + + switch (h->opcode) { + case FUSE_INIT: { + if (payload_len < sizeof(struct fuse_init_in)) { + return -1; + } + const struct fuse_init_in *in = (const struct fuse_init_in *)payload; + if (a->init_in_flags) + *a->init_in_flags = in->flags; + if (a->init_in_flags2) + *a->init_in_flags2 = in->flags2; + if (a->init_in_max_readahead) + *a->init_in_max_readahead = in->max_readahead; + + struct fuse_init_out out; + memset(&out, 0, sizeof(out)); + out.major = 7; + out.minor = 39; + uint32_t init_flags = a->init_out_flags_override; + if (init_flags == 0) { + init_flags = FUSE_INIT_EXT | FUSE_MAX_PAGES; + } + out.flags = init_flags; + out.flags2 = 0; + out.max_write = 4096; + out.max_pages = 32; + if (fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)) != 0) { + return -1; + } + *a->init_done = 1; + return 0; + } + case FUSE_FORGET: { + if (payload_len < sizeof(struct fuse_forget_in)) + return -1; + const struct fuse_forget_in *in = (const struct fuse_forget_in *)payload; + if (a->forget_count) + (*a->forget_count)++; + if (a->forget_nlookup_sum) + (*a->forget_nlookup_sum) += in->nlookup; + return 0; + } + case FUSE_LOOKUP: { + const char *name = (const char *)payload; + if (payload_len == 0 || name[payload_len - 1] != '\0') { + return -1; + } + struct simplefs_node *parent = simplefs_find_node(&a->fs, h->nodeid); + if (!parent || !simplefs_node_is_dir(parent)) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + struct simplefs_node *child = simplefs_find_child(&a->fs, h->nodeid, name); + if (!child) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + struct fuse_entry_out out; + memset(&out, 0, sizeof(out)); + out.nodeid = child->nodeid; + simplefs_fill_attr(child, &out.attr); + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_GETATTR: { + (void)payload; + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + struct fuse_attr_out out; + memset(&out, 0, sizeof(out)); + simplefs_fill_attr(node, &out.attr); + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_OPENDIR: + case FUSE_OPEN: { + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + if (h->opcode == FUSE_OPEN && a->open_count) { + (*a->open_count)++; + } + if (h->opcode == FUSE_OPENDIR && a->opendir_count) { + (*a->opendir_count)++; + } + if (h->opcode == FUSE_OPEN && a->force_open_enosys) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (h->opcode == FUSE_OPENDIR && a->force_opendir_enosys) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (h->opcode == FUSE_OPENDIR && !simplefs_node_is_dir(node)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + if (h->opcode == FUSE_OPEN && simplefs_node_is_dir(node)) { + return fuse_write_reply(a->fd, h->unique, -EISDIR, NULL, 0); + } + struct fuse_open_out out; + memset(&out, 0, sizeof(out)); + out.fh = node->nodeid; + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_READLINK: { + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + if (!simplefs_node_is_symlink(node)) { + return fuse_write_reply(a->fd, h->unique, -EINVAL, NULL, 0); + } + return fuse_write_reply(a->fd, h->unique, 0, node->data, node->size); + } + case FUSE_READ: { + if (payload_len < sizeof(struct fuse_read_in)) { + return -1; + } + const struct fuse_read_in *in = (const struct fuse_read_in *)payload; + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node || simplefs_node_is_dir(node) || simplefs_node_is_symlink(node)) { + return fuse_write_reply(a->fd, h->unique, -EINVAL, NULL, 0); + } + if (a->block_read_until_interrupt > 0) { + if (a->blocked_read_unique && *a->blocked_read_unique == 0) { + *a->blocked_read_unique = h->unique; + } + usleep((useconds_t)a->block_read_until_interrupt * 1000); + } + if (in->offset >= node->size) { + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + } + size_t remain = node->size - (size_t)in->offset; + size_t to_copy = in->size; + if (to_copy > remain) { + to_copy = remain; + } + return fuse_write_reply(a->fd, h->unique, 0, node->data + in->offset, to_copy); + } + case FUSE_READDIR: + case FUSE_READDIRPLUS: { + if (payload_len < sizeof(struct fuse_read_in)) { + return -1; + } + const struct fuse_read_in *in = (const struct fuse_read_in *)payload; + (void)in; + int is_plus = (h->opcode == FUSE_READDIRPLUS); + if (is_plus && a->readdirplus_count) { + (*a->readdirplus_count)++; + } + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node || !simplefs_node_is_dir(node)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + + /* offset is an entry index: 0=".", 1="..", then children */ + uint64_t idx = in->offset; + unsigned char *outbuf = malloc(FUSE_TEST_BUF_SIZE); + if (!outbuf) { + return fuse_write_reply(a->fd, h->unique, -ENOMEM, NULL, 0); + } + size_t outlen = 0; + + const char *fixed_names[2] = {".", ".."}; + for (; idx < 2; idx++) { + const char *nm = fixed_names[idx]; + size_t nmlen = strlen(nm); + size_t reclen = is_plus ? fuse_direntplus_rec_len(nmlen) : fuse_dirent_rec_len(nmlen); + if (outlen + reclen > FUSE_TEST_BUF_SIZE) + break; + if (is_plus) { + struct fuse_direntplus dp; + memset(&dp, 0, sizeof(dp)); + dp.entry_out.nodeid = 1; + simplefs_fill_attr(&a->fs.nodes[0], &dp.entry_out.attr); + dp.dirent.ino = 1; + dp.dirent.off = idx + 1; + dp.dirent.namelen = (uint32_t)nmlen; + dp.dirent.type = DT_DIR; + memcpy(outbuf + outlen, &dp, sizeof(dp)); + memcpy(outbuf + outlen + sizeof(dp), nm, nmlen); + } else { + struct fuse_dirent de; + memset(&de, 0, sizeof(de)); + de.ino = 1; + de.off = idx + 1; + de.namelen = (uint32_t)nmlen; + de.type = DT_DIR; + memcpy(outbuf + outlen, &de, sizeof(de)); + memcpy(outbuf + outlen + sizeof(de), nm, nmlen); + } + outlen += reclen; + } + + /* children in insertion order */ + uint64_t child_base = 2; + uint64_t cur = idx; + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + struct simplefs_node *c = &a->fs.nodes[i]; + if (!c->used || c->parent != h->nodeid) + continue; + if (cur < child_base) { + cur = child_base; + } + if (cur > child_base) { + /* skip until we reach this entry index */ + child_base++; + continue; + } + + size_t nmlen = strlen(c->name); + size_t reclen = is_plus ? fuse_direntplus_rec_len(nmlen) : fuse_dirent_rec_len(nmlen); + if (outlen + reclen > FUSE_TEST_BUF_SIZE) + break; + if (is_plus) { + struct fuse_direntplus dp; + memset(&dp, 0, sizeof(dp)); + dp.entry_out.nodeid = c->nodeid; + simplefs_fill_attr(c, &dp.entry_out.attr); + dp.dirent.ino = c->ino; + dp.dirent.off = child_base + 1; + dp.dirent.namelen = (uint32_t)nmlen; + dp.dirent.type = simplefs_dirent_type(c); + memcpy(outbuf + outlen, &dp, sizeof(dp)); + memcpy(outbuf + outlen + sizeof(dp), c->name, nmlen); + } else { + struct fuse_dirent de; + memset(&de, 0, sizeof(de)); + de.ino = c->ino; + de.off = child_base + 1; + de.namelen = (uint32_t)nmlen; + de.type = simplefs_dirent_type(c); + memcpy(outbuf + outlen, &de, sizeof(de)); + memcpy(outbuf + outlen + sizeof(de), c->name, nmlen); + } + outlen += reclen; + + child_base++; + cur++; + } + + int ret = fuse_write_reply(a->fd, h->unique, 0, outbuf, outlen); + free(outbuf); + return ret; + } + case FUSE_STATFS: { + struct fuse_statfs_out out; + memset(&out, 0, sizeof(out)); + + unsigned used = 0; + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + if (a->fs.nodes[i].used) { + used++; + } + } + + out.st.blocks = 1024; + out.st.bfree = 512; + out.st.bavail = 512; + out.st.files = SIMPLEFS_MAX_NODES; + out.st.ffree = (used > SIMPLEFS_MAX_NODES) ? 0 : (SIMPLEFS_MAX_NODES - used); + out.st.bsize = 4096; + out.st.frsize = 4096; + out.st.namelen = SIMPLEFS_NAME_MAX - 1; + FUSE_TEST_LOG("statfs reply ok blocks=%llu bfree=%llu bavail=%llu", + (unsigned long long)out.st.blocks, + (unsigned long long)out.st.bfree, + (unsigned long long)out.st.bavail); + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_RELEASE: + if (a->release_count) { + (*a->release_count)++; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + case FUSE_RELEASEDIR: + if (a->releasedir_count) { + (*a->releasedir_count)++; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + case FUSE_INTERRUPT: { + if (payload_len < sizeof(struct fuse_interrupt_in)) { + return -1; + } + const struct fuse_interrupt_in *in = (const struct fuse_interrupt_in *)payload; + if (a->interrupt_count) { + (*a->interrupt_count)++; + } + if (a->last_interrupt_target) { + *a->last_interrupt_target = in->unique; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + } + case FUSE_FLUSH: + if (a->flush_count) { + (*a->flush_count)++; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + case FUSE_FSYNC: + if (a->fsync_count) { + (*a->fsync_count)++; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + case FUSE_FSYNCDIR: + if (a->fsyncdir_count) { + (*a->fsyncdir_count)++; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + case FUSE_ACCESS: { + if (payload_len < sizeof(struct fuse_access_in)) { + return -1; + } + const struct fuse_access_in *in = (const struct fuse_access_in *)payload; + if (a->access_count) { + (*a->access_count)++; + } + if ((in->mask & a->access_deny_mask) != 0) { + return fuse_write_reply(a->fd, h->unique, -EACCES, NULL, 0); + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + } + case FUSE_DESTROY: + if (a->destroy_count) + (*a->destroy_count)++; + if (a->stop_on_destroy && a->stop) + *a->stop = 1; + return 0; + case FUSE_WRITE: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (payload_len < sizeof(struct fuse_write_in)) { + return -1; + } + const struct fuse_write_in *in = (const struct fuse_write_in *)payload; + const unsigned char *data = payload + sizeof(*in); + size_t data_len = payload_len - sizeof(*in); + if (data_len < in->size) { + return -1; + } + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node || simplefs_node_is_dir(node) || simplefs_node_is_symlink(node)) { + return fuse_write_reply(a->fd, h->unique, -EINVAL, NULL, 0); + } + if (in->offset >= SIMPLEFS_DATA_MAX) { + return fuse_write_reply(a->fd, h->unique, -EFBIG, NULL, 0); + } + size_t to_copy = in->size; + if (in->offset + to_copy > SIMPLEFS_DATA_MAX) { + to_copy = SIMPLEFS_DATA_MAX - (size_t)in->offset; + } + memcpy(node->data + in->offset, data, to_copy); + if (node->size < in->offset + to_copy) { + node->size = (size_t)in->offset + to_copy; + } + struct fuse_write_out out; + memset(&out, 0, sizeof(out)); + out.size = (uint32_t)to_copy; + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_CREATE: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (payload_len < sizeof(struct fuse_create_in) + 1) { + return -1; + } + const struct fuse_create_in *in = (const struct fuse_create_in *)payload; + const char *name = (const char *)(payload + sizeof(*in)); + if (name[payload_len - sizeof(*in) - 1] != '\0') { + return -1; + } + if (a->create_count) { + (*a->create_count)++; + } + struct simplefs_node *p = simplefs_find_node(&a->fs, h->nodeid); + if (!p || !simplefs_node_is_dir(p)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + if (simplefs_find_child(&a->fs, h->nodeid, name)) { + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + struct simplefs_node *nnode = simplefs_alloc(&a->fs); + if (!nnode) { + return fuse_write_reply(a->fd, h->unique, -ENOSPC, NULL, 0); + } + nnode->parent = h->nodeid; + nnode->is_dir = 0; + nnode->is_symlink = 0; + nnode->mode = in->mode; + strncpy(nnode->name, name, sizeof(nnode->name) - 1); + nnode->name[sizeof(nnode->name) - 1] = '\0'; + nnode->size = 0; + + struct { + struct fuse_entry_out entry; + struct fuse_open_out open_out; + } out; + memset(&out, 0, sizeof(out)); + out.entry.nodeid = nnode->nodeid; + simplefs_fill_attr(nnode, &out.entry.attr); + out.open_out.fh = nnode->nodeid; + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_SYMLINK: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + const char *target = (const char *)payload; + size_t target_len = strnlen(target, payload_len); + if (target_len == payload_len) { + return -1; + } + const char *name = target + target_len + 1; + size_t remain = payload_len - target_len - 1; + if (remain == 0) { + return -1; + } + size_t name_len = strnlen(name, remain); + if (name_len == remain) { + return -1; + } + struct simplefs_node *p = simplefs_find_node(&a->fs, h->nodeid); + if (!p || !simplefs_node_is_dir(p)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + if (simplefs_find_child(&a->fs, h->nodeid, name)) { + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + struct simplefs_node *nnode = simplefs_alloc(&a->fs); + if (!nnode) { + return fuse_write_reply(a->fd, h->unique, -ENOSPC, NULL, 0); + } + nnode->parent = h->nodeid; + nnode->is_dir = 0; + nnode->is_symlink = 1; + nnode->mode = 0120777; + strncpy(nnode->name, name, sizeof(nnode->name) - 1); + nnode->name[sizeof(nnode->name) - 1] = '\0'; + nnode->size = (target_len < SIMPLEFS_DATA_MAX) ? target_len : SIMPLEFS_DATA_MAX; + memcpy(nnode->data, target, nnode->size); + return simplefs_fill_entry_reply(a, h, nnode); + } + case FUSE_LINK: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (payload_len < sizeof(struct fuse_link_in) + 1) { + return -1; + } + const struct fuse_link_in *in = (const struct fuse_link_in *)payload; + const char *name = (const char *)(payload + sizeof(*in)); + if (name[payload_len - sizeof(*in) - 1] != '\0') { + return -1; + } + struct simplefs_node *src = simplefs_find_node(&a->fs, in->oldnodeid); + if (!src) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + if (simplefs_node_is_dir(src)) { + return fuse_write_reply(a->fd, h->unique, -EPERM, NULL, 0); + } + struct simplefs_node *dst_parent = simplefs_find_node(&a->fs, h->nodeid); + if (!dst_parent || !simplefs_node_is_dir(dst_parent)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + if (simplefs_find_child(&a->fs, h->nodeid, name)) { + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + struct simplefs_node *nnode = simplefs_alloc(&a->fs); + if (!nnode) { + return fuse_write_reply(a->fd, h->unique, -ENOSPC, NULL, 0); + } + nnode->parent = h->nodeid; + nnode->is_dir = 0; + nnode->is_symlink = src->is_symlink; + nnode->mode = src->mode; + strncpy(nnode->name, name, sizeof(nnode->name) - 1); + nnode->name[sizeof(nnode->name) - 1] = '\0'; + nnode->size = src->size; + if (nnode->size > SIMPLEFS_DATA_MAX) { + nnode->size = SIMPLEFS_DATA_MAX; + } + memcpy(nnode->data, src->data, nnode->size); + return simplefs_fill_entry_reply(a, h, nnode); + } + case FUSE_MKDIR: + case FUSE_MKNOD: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + const char *name = NULL; + size_t name_off = 0; + int is_dir = (h->opcode == FUSE_MKDIR); + uint32_t mode = 0; + if (is_dir) { + if (payload_len < sizeof(struct fuse_mkdir_in) + 1) + return -1; + const struct fuse_mkdir_in *in = (const struct fuse_mkdir_in *)payload; + mode = in->mode; + name_off = sizeof(*in); + } else { + if (payload_len < sizeof(struct fuse_mknod_in) + 1) + return -1; + const struct fuse_mknod_in *in = (const struct fuse_mknod_in *)payload; + mode = in->mode; + name_off = sizeof(*in); + } + name = (const char *)(payload + name_off); + if (name[payload_len - name_off - 1] != '\0') + return -1; + if (simplefs_find_child(&a->fs, h->nodeid, name)) { + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + struct simplefs_node *p = simplefs_find_node(&a->fs, h->nodeid); + if (!p || !simplefs_node_is_dir(p)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + struct simplefs_node *nnode = simplefs_alloc(&a->fs); + if (!nnode) { + return fuse_write_reply(a->fd, h->unique, -ENOSPC, NULL, 0); + } + nnode->parent = h->nodeid; + nnode->is_dir = is_dir; + nnode->is_symlink = 0; + nnode->mode = mode; + strncpy(nnode->name, name, sizeof(nnode->name) - 1); + nnode->name[sizeof(nnode->name) - 1] = '\0'; + nnode->size = 0; + + return simplefs_fill_entry_reply(a, h, nnode); + } + case FUSE_UNLINK: + case FUSE_RMDIR: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + const char *name = (const char *)payload; + if (payload_len == 0 || name[payload_len - 1] != '\0') { + return -1; + } + struct simplefs_node *child = simplefs_find_child(&a->fs, h->nodeid, name); + if (!child) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + if (h->opcode == FUSE_RMDIR) { + if (!simplefs_node_is_dir(child)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + if (simplefs_has_children(&a->fs, child->nodeid)) { + return fuse_write_reply(a->fd, h->unique, -ENOTEMPTY, NULL, 0); + } + } else { + if (simplefs_node_is_dir(child)) { + return fuse_write_reply(a->fd, h->unique, -EISDIR, NULL, 0); + } + } + child->used = 0; + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + } + case FUSE_RENAME: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + const struct fuse_rename_in *in = (const struct fuse_rename_in *)payload; + const char *oldname = NULL; + const char *newname = NULL; + if (simplefs_parse_two_names(payload, payload_len, sizeof(*in), &oldname, &newname) != 0) { + return -1; + } + return simplefs_do_rename(a, h, in->newdir, 0, oldname, newname); + } + case FUSE_RENAME2: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + const struct fuse_rename2_in *in = (const struct fuse_rename2_in *)payload; + const char *oldname = NULL; + const char *newname = NULL; + if (simplefs_parse_two_names(payload, payload_len, sizeof(*in), &oldname, &newname) != 0) { + return -1; + } + if (a->rename2_count) { + (*a->rename2_count)++; + } + return simplefs_do_rename(a, h, in->newdir, in->flags, oldname, newname); + } + case FUSE_SETATTR: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (payload_len < sizeof(struct fuse_setattr_in)) { + return -1; + } + const struct fuse_setattr_in *in = (const struct fuse_setattr_in *)payload; + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + if (simplefs_node_is_dir(node) || simplefs_node_is_symlink(node)) { + return fuse_write_reply(a->fd, h->unique, -EINVAL, NULL, 0); + } + if (in->valid & FATTR_SIZE) { + if (in->size > SIMPLEFS_DATA_MAX) { + return fuse_write_reply(a->fd, h->unique, -EFBIG, NULL, 0); + } + node->size = (size_t)in->size; + } + if (in->valid & FATTR_MODE) { + node->mode = in->mode; + } + struct fuse_attr_out out; + memset(&out, 0, sizeof(out)); + simplefs_fill_attr(node, &out.attr); + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + default: + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } +} + +static inline void *fuse_daemon_thread(void *arg) { + struct fuse_daemon_args *a = (struct fuse_daemon_args *)arg; + unsigned char *buf = malloc(FUSE_TEST_BUF_SIZE); + if (!buf) { + return NULL; + } + + simplefs_init(&a->fs); + if (a->root_mode_override) { + a->fs.nodes[0].mode = a->root_mode_override; + } + if (a->hello_mode_override) { + a->fs.nodes[1].mode = a->hello_mode_override; + } + + while (!*a->stop) { + FUSE_TEST_LOG("daemon read start"); + ssize_t n = read(a->fd, buf, FUSE_TEST_BUF_SIZE); + if (n < 0) { + FUSE_TEST_LOG("daemon read error n=%zd errno=%d", n, errno); + if (errno == EINTR) + continue; + if (errno == ENOTCONN) + break; + continue; + } + if (n == 0) { + FUSE_TEST_LOG("daemon read EOF"); + break; + } + FUSE_TEST_LOG("daemon read n=%zd", n); + struct fuse_in_header *h = (struct fuse_in_header *)buf; + if ((size_t)n != h->len) { + FUSE_TEST_LOG("daemon short read n=%zd hdr.len=%u", n, h->len); + continue; + } + (void)fuse_handle_one(a, buf, (size_t)n); + if (a->exit_after_init && a->init_done && *a->init_done) { + break; + } + } + free(buf); + return NULL; +} + +static inline int ensure_dir(const char *path) { + struct stat st; + if (stat(path, &st) == 0) { + if (S_ISDIR(st.st_mode)) + return 0; + errno = ENOTDIR; + return -1; + } + return mkdir(path, 0755); +} diff --git a/user/apps/tests/dunitest/Makefile b/user/apps/tests/dunitest/Makefile index 732e93088..a2d564b9e 100644 --- a/user/apps/tests/dunitest/Makefile +++ b/user/apps/tests/dunitest/Makefile @@ -20,7 +20,7 @@ RESULTS_DIR = results BIN_DIR = bin # 要编译的测试套件目录列表(suites/ 下的子目录名) -SUITES = demo normal +SUITES = demo normal fuse GTEST_ROOT = third_party/googletest GTEST_REPO = https://git.mirrors.dragonos.org.cn/DragonOS-Community/googletest diff --git a/user/apps/tests/dunitest/suites/fuse/fuse_core.cc b/user/apps/tests/dunitest/suites/fuse/fuse_core.cc new file mode 100644 index 000000000..250dd7605 --- /dev/null +++ b/user/apps/tests/dunitest/suites/fuse/fuse_core.cc @@ -0,0 +1,548 @@ +#include + +#include "fuse_gtest_common.h" + +static int core_test_nonblock_read_empty() { + int fd = open("/dev/fuse", O_RDWR | O_NONBLOCK); + if (fd < 0) { + printf("[FAIL] open(/dev/fuse): %s (errno=%d)\n", strerror(errno), errno); + return -1; + } + + unsigned char small[FUSE_MIN_READ_BUFFER / 2]; + ssize_t n = read(fd, small, sizeof(small)); + if (n != -1 || errno != EINVAL) { + printf("[FAIL] nonblock read with small buffer: n=%zd errno=%d (%s)\n", n, errno, + strerror(errno)); + close(fd); + return -1; + } + + unsigned char *big = (unsigned char *)malloc(FUSE_TEST_BUF_SIZE); + if (!big) { + printf("[FAIL] malloc big buffer failed\n"); + close(fd); + return -1; + } + memset(big, 0, FUSE_TEST_BUF_SIZE); + + n = read(fd, big, FUSE_TEST_BUF_SIZE); + if (n != -1 || (errno != EAGAIN && errno != EWOULDBLOCK)) { + printf("[FAIL] nonblock read empty: n=%zd errno=%d (%s)\n", n, errno, strerror(errno)); + free(big); + close(fd); + return -1; + } + + struct pollfd pfd; + pfd.fd = fd; + pfd.events = POLLIN; + int pr = poll(&pfd, 1, 100); + if (pr != 0) { + printf("[FAIL] poll empty expected timeout: pr=%d revents=%x errno=%d (%s)\n", pr, + pfd.revents, errno, strerror(errno)); + free(big); + close(fd); + return -1; + } + + free(big); + close(fd); + return 0; +} + +static int core_test_mount_init_single_use_fd() { + const char *mp = "/tmp/test_fuse_mp"; + const char *mp2 = "/tmp/test_fuse_mp2"; + + if (ensure_dir(mp) != 0) { + printf("[FAIL] ensure_dir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + return -1; + } + if (ensure_dir(mp2) != 0) { + printf("[FAIL] ensure_dir(%s): %s (errno=%d)\n", mp2, strerror(errno), errno); + rmdir(mp); + return -1; + } + + int fd = open("/dev/fuse", O_RDWR | O_NONBLOCK); + if (fd < 0) { + printf("[FAIL] open(/dev/fuse): %s (errno=%d)\n", strerror(errno), errno); + rmdir(mp); + rmdir(mp2); + return -1; + } + + char opts[256]; + snprintf(opts, sizeof(opts), "fd=%d,rootmode=040755,user_id=0,group_id=0", fd); + + if (mount("none", mp, "fuse", 0, opts) != 0) { + printf("[FAIL] mount(fuse): %s (errno=%d)\n", strerror(errno), errno); + close(fd); + rmdir(mp); + rmdir(mp2); + return -1; + } + + if (fuseg_do_init_handshake_basic(fd) != 0) { + printf("[FAIL] init handshake: %s (errno=%d)\n", strerror(errno), errno); + umount(mp); + close(fd); + rmdir(mp); + rmdir(mp2); + return -1; + } + + unsigned char *tmp = (unsigned char *)malloc(FUSE_TEST_BUF_SIZE); + if (!tmp) { + printf("[FAIL] malloc tmp buffer failed\n"); + umount(mp); + close(fd); + rmdir(mp); + rmdir(mp2); + return -1; + } + memset(tmp, 0, FUSE_TEST_BUF_SIZE); + + ssize_t rn = read(fd, tmp, FUSE_TEST_BUF_SIZE); + if (rn != -1 || (errno != EAGAIN && errno != EWOULDBLOCK)) { + printf("[FAIL] expected EAGAIN after init: rn=%zd errno=%d (%s)\n", rn, errno, + strerror(errno)); + free(tmp); + umount(mp); + close(fd); + rmdir(mp); + rmdir(mp2); + return -1; + } + free(tmp); + + if (mount("none", mp2, "fuse", 0, opts) == 0) { + printf("[FAIL] second mount with same fd unexpectedly succeeded\n"); + umount(mp); + umount(mp2); + close(fd); + rmdir(mp); + rmdir(mp2); + return -1; + } + if (errno != EINVAL) { + printf("[FAIL] second mount expected EINVAL got errno=%d (%s)\n", errno, strerror(errno)); + umount(mp); + close(fd); + rmdir(mp); + rmdir(mp2); + return -1; + } + + umount(mp); + rmdir(mp); + rmdir(mp2); + close(fd); + return 0; +} + +static int core_test_phase_c_read_path() { + const char *mp = "/tmp/test_fuse_c"; + if (ensure_dir(mp) != 0) { + printf("[FAIL] ensure_dir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + return -1; + } + + int fd = open("/dev/fuse", O_RDWR); + if (fd < 0) { + printf("[FAIL] open(/dev/fuse): %s (errno=%d)\n", strerror(errno), errno); + rmdir(mp); + return -1; + } + + volatile int stop = 0; + volatile int init_done = 0; + struct fuse_daemon_args args; + memset(&args, 0, sizeof(args)); + args.fd = fd; + args.stop = &stop; + args.init_done = &init_done; + args.enable_write_ops = 0; + + pthread_t th; + if (pthread_create(&th, NULL, fuse_daemon_thread, &args) != 0) { + printf("[FAIL] pthread_create\n"); + close(fd); + rmdir(mp); + return -1; + } + + char opts[256]; + snprintf(opts, sizeof(opts), "fd=%d,rootmode=040755,user_id=0,group_id=0", fd); + if (mount("none", mp, "fuse", 0, opts) != 0) { + printf("[FAIL] mount(fuse): %s (errno=%d)\n", strerror(errno), errno); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + for (int i = 0; i < 100; i++) { + if (init_done) + break; + usleep(10 * 1000); + } + if (!init_done) { + printf("[FAIL] init handshake timeout\n"); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + DIR *d = opendir(mp); + if (!d) { + printf("[FAIL] opendir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + int found = 0; + struct dirent *de; + while ((de = readdir(d)) != NULL) { + if (strcmp(de->d_name, "hello.txt") == 0) { + found = 1; + break; + } + } + closedir(d); + if (!found) { + printf("[FAIL] readdir: hello.txt not found\n"); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + char p[256]; + snprintf(p, sizeof(p), "%s/hello.txt", mp); + + struct stat st; + if (stat(p, &st) != 0) { + printf("[FAIL] stat(%s): %s (errno=%d)\n", p, strerror(errno), errno); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + if (!S_ISREG(st.st_mode)) { + printf("[FAIL] stat: expected regular file\n"); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + char buf[128]; + int n = fuseg_read_file(p, buf, sizeof(buf) - 1); + if (n < 0) { + printf("[FAIL] read(%s): %s (errno=%d)\n", p, strerror(errno), errno); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + buf[n] = '\0'; + if (strcmp(buf, "hello from fuse\n") != 0) { + printf("[FAIL] content mismatch: got='%s'\n", buf); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + umount(mp); + rmdir(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + return 0; +} + +static int core_test_phase_d_write_path() { + const char *mp = "/tmp/test_fuse_d"; + int f = -1; + int n = -1; + if (ensure_dir(mp) != 0) { + printf("[FAIL] ensure_dir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + return -1; + } + + int fd = open("/dev/fuse", O_RDWR); + if (fd < 0) { + printf("[FAIL] open(/dev/fuse): %s (errno=%d)\n", strerror(errno), errno); + rmdir(mp); + return -1; + } + + volatile int stop = 0; + volatile int init_done = 0; + struct fuse_daemon_args args; + memset(&args, 0, sizeof(args)); + args.fd = fd; + args.stop = &stop; + args.init_done = &init_done; + args.enable_write_ops = 1; + + pthread_t th; + if (pthread_create(&th, NULL, fuse_daemon_thread, &args) != 0) { + printf("[FAIL] pthread_create\n"); + close(fd); + rmdir(mp); + return -1; + } + + char opts[256]; + snprintf(opts, sizeof(opts), "fd=%d,rootmode=040755,user_id=0,group_id=0", fd); + if (mount("none", mp, "fuse", 0, opts) != 0) { + printf("[FAIL] mount(fuse): %s (errno=%d)\n", strerror(errno), errno); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + for (int i = 0; i < 100; i++) { + if (init_done) + break; + usleep(10 * 1000); + } + if (!init_done) { + printf("[FAIL] init handshake timeout\n"); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + char p1[256]; + snprintf(p1, sizeof(p1), "%s/new.txt", mp); + if (fuseg_write_file(p1, "abcdef") != 0) { + printf("[FAIL] write_all(%s): %s (errno=%d)\n", p1, strerror(errno), errno); + goto fail; + } + + f = open(p1, O_RDWR); + if (f < 0) { + printf("[FAIL] open for truncate: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + if (ftruncate(f, 3) != 0) { + printf("[FAIL] ftruncate: %s (errno=%d)\n", strerror(errno), errno); + close(f); + goto fail; + } + close(f); + + char buf[64]; + n = fuseg_read_file(p1, buf, sizeof(buf) - 1); + if (n < 0) { + printf("[FAIL] read_all after truncate: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + buf[n] = '\0'; + if (strcmp(buf, "abc") != 0) { + printf("[FAIL] truncate content mismatch got='%s'\n", buf); + goto fail; + } + + char p2[256]; + snprintf(p2, sizeof(p2), "%s/renamed.txt", mp); + if (rename(p1, p2) != 0) { + printf("[FAIL] rename: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + + if (unlink(p2) != 0) { + printf("[FAIL] unlink: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + + char d1[256]; + snprintf(d1, sizeof(d1), "%s/dir", mp); + if (mkdir(d1, 0755) != 0) { + printf("[FAIL] mkdir: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + if (rmdir(d1) != 0) { + printf("[FAIL] rmdir: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + + umount(mp); + rmdir(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + return 0; + +fail: + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; +} + +static int core_test_lifecycle_forget_destroy() { + const char *mp = "/tmp/test_fuse_p1_lifecycle"; + if (ensure_dir(mp) != 0) { + printf("[FAIL] ensure_dir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + return -1; + } + + int fd = open("/dev/fuse", O_RDWR); + if (fd < 0) { + printf("[FAIL] open(/dev/fuse): %s (errno=%d)\n", strerror(errno), errno); + rmdir(mp); + return -1; + } + + volatile int stop = 0; + volatile int init_done = 0; + volatile uint32_t forget_count = 0; + volatile uint64_t forget_nlookup_sum = 0; + volatile uint32_t destroy_count = 0; + + struct fuse_daemon_args args; + memset(&args, 0, sizeof(args)); + args.fd = fd; + args.stop = &stop; + args.init_done = &init_done; + args.enable_write_ops = 0; + args.stop_on_destroy = 1; + args.forget_count = &forget_count; + args.forget_nlookup_sum = &forget_nlookup_sum; + args.destroy_count = &destroy_count; + + pthread_t th; + if (pthread_create(&th, NULL, fuse_daemon_thread, &args) != 0) { + printf("[FAIL] pthread_create\n"); + close(fd); + rmdir(mp); + return -1; + } + + char opts[256]; + snprintf(opts, sizeof(opts), "fd=%d,rootmode=040755,user_id=0,group_id=0", fd); + if (mount("none", mp, "fuse", 0, opts) != 0) { + printf("[FAIL] mount(fuse): %s (errno=%d)\n", strerror(errno), errno); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + if (fuseg_wait_init(&init_done) != 0) { + printf("[FAIL] init handshake timeout\n"); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + char p[256]; + snprintf(p, sizeof(p), "%s/hello.txt", mp); + for (int i = 0; i < 8; i++) { + struct stat st; + if (stat(p, &st) != 0) { + printf("[FAIL] stat(%s): %s (errno=%d)\n", p, strerror(errno), errno); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + } + + usleep(100 * 1000); + + if (umount(mp) != 0) { + printf("[FAIL] umount(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + for (int i = 0; i < 100; i++) { + if (destroy_count > 0) + break; + usleep(10 * 1000); + } + + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + + if (forget_count == 0 || forget_nlookup_sum == 0) { + printf("[FAIL] expected FORGET requests, got count=%u nlookup_sum=%llu\n", forget_count, + (unsigned long long)forget_nlookup_sum); + return -1; + } + + if (destroy_count == 0) { + printf("[FAIL] expected DESTROY request on umount\n"); + return -1; + } + + return 0; +} + +TEST(FuseCore, DevNonblockReadEmpty) { + ASSERT_EQ(0, core_test_nonblock_read_empty()); +} + +TEST(FuseCore, MountInitAndSingleUseFd) { + ASSERT_EQ(0, core_test_mount_init_single_use_fd()); +} + +TEST(FuseCore, ReadPathLookupGetattrReaddirOpenRead) { + ASSERT_EQ(0, core_test_phase_c_read_path()); +} + +TEST(FuseCore, WritePathCreateTruncateRenameUnlinkMkdirRmdir) { + ASSERT_EQ(0, core_test_phase_d_write_path()); +} + +TEST(FuseCore, LifecycleForgetAndDestroy) { + ASSERT_EQ(0, core_test_lifecycle_forget_destroy()); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/user/apps/tests/dunitest/suites/fuse/fuse_extended.cc b/user/apps/tests/dunitest/suites/fuse/fuse_extended.cc new file mode 100644 index 000000000..1a899699c --- /dev/null +++ b/user/apps/tests/dunitest/suites/fuse/fuse_extended.cc @@ -0,0 +1,1015 @@ +#include + +#include +#include +#include +#include + +#include "fuse_gtest_common.h" + +#ifndef FUSE_DEV_IOC_CLONE +#define FUSE_DEV_IOC_CLONE 0x8004e500 +#endif + +static int ext_test_p2_ops() { + const char *mp = "/tmp/test_fuse_p2_ops"; + int f = -1; + int dfd = -1; + ssize_t tn = -1; + ssize_t rn = -1; + char hello[256]; + char created[256]; + char symlink_path[256]; + char target_buf[256]; + char hard_path[256]; + char rbuf[64]; + char dst_exist[256]; + char renamed[256]; + if (ensure_dir(mp) != 0) { + printf("[FAIL] ensure_dir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + return -1; + } + + int fd = open("/dev/fuse", O_RDWR); + if (fd < 0) { + printf("[FAIL] open(/dev/fuse): %s (errno=%d)\n", strerror(errno), errno); + rmdir(mp); + return -1; + } + + volatile int stop = 0; + volatile int init_done = 0; + volatile uint32_t access_count = 0; + volatile uint32_t flush_count = 0; + volatile uint32_t fsync_count = 0; + volatile uint32_t fsyncdir_count = 0; + volatile uint32_t create_count = 0; + volatile uint32_t rename2_count = 0; + + struct fuse_daemon_args args; + memset(&args, 0, sizeof(args)); + args.fd = fd; + args.stop = &stop; + args.init_done = &init_done; + args.enable_write_ops = 1; + args.stop_on_destroy = 1; + args.access_count = &access_count; + args.flush_count = &flush_count; + args.fsync_count = &fsync_count; + args.fsyncdir_count = &fsyncdir_count; + args.create_count = &create_count; + args.rename2_count = &rename2_count; + args.access_deny_mask = 2; + + pthread_t th; + if (pthread_create(&th, NULL, fuse_daemon_thread, &args) != 0) { + printf("[FAIL] pthread_create\n"); + close(fd); + rmdir(mp); + return -1; + } + + char opts[256]; + snprintf(opts, sizeof(opts), "fd=%d,rootmode=040755,user_id=0,group_id=0,allow_other", fd); + if (mount("none", mp, "fuse", 0, opts) != 0) { + printf("[FAIL] mount(fuse): %s (errno=%d)\n", strerror(errno), errno); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + if (fuseg_wait_init(&init_done) != 0) { + printf("[FAIL] init handshake timeout\n"); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + snprintf(hello, sizeof(hello), "%s/hello.txt", mp); + if (access(hello, R_OK) != 0) { + printf("[FAIL] access(R_OK): %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + if (access(hello, W_OK) == 0 || errno != EACCES) { + printf("[FAIL] access(W_OK) expected EACCES, errno=%d (%s)\n", errno, strerror(errno)); + goto fail; + } + + snprintf(created, sizeof(created), "%s/p2_create.txt", mp); + f = open(created, O_CREAT | O_RDWR, 0644); + if (f < 0) { + printf("[FAIL] open(O_CREAT): %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + if (fuseg_write_all_fd(f, "p2-data") != 0) { + printf("[FAIL] write created file: %s (errno=%d)\n", strerror(errno), errno); + close(f); + goto fail; + } + if (fsync(f) != 0) { + printf("[FAIL] fsync(file): %s (errno=%d)\n", strerror(errno), errno); + close(f); + goto fail; + } + close(f); + + snprintf(symlink_path, sizeof(symlink_path), "%s/p2_symlink.txt", mp); + if (symlink("p2_create.txt", symlink_path) != 0) { + printf("[FAIL] symlink: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + tn = readlink(symlink_path, target_buf, sizeof(target_buf) - 1); + if (tn <= 0) { + printf("[FAIL] readlink: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + target_buf[tn] = '\0'; + if (strcmp(target_buf, "p2_create.txt") != 0) { + printf("[FAIL] readlink target mismatch: got=%s\n", target_buf); + goto fail; + } + + snprintf(hard_path, sizeof(hard_path), "%s/p2_hard.txt", mp); + if (link(created, hard_path) != 0) { + printf("[FAIL] link: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + if (unlink(created) != 0) { + printf("[FAIL] unlink original: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + f = open(hard_path, O_RDONLY); + if (f < 0) { + printf("[FAIL] open hard link: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + rn = read(f, rbuf, sizeof(rbuf) - 1); + close(f); + if (rn <= 0) { + printf("[FAIL] read hard link: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + rbuf[rn] = '\0'; + if (strcmp(rbuf, "p2-data") != 0) { + printf("[FAIL] hard link content mismatch: got=%s\n", rbuf); + goto fail; + } + + snprintf(dst_exist, sizeof(dst_exist), "%s/p2_dst_exist.txt", mp); + f = open(dst_exist, O_CREAT | O_RDWR, 0644); + if (f < 0) { + printf("[FAIL] create dst_exist: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + close(f); + + if (syscall(SYS_renameat2, AT_FDCWD, hard_path, AT_FDCWD, dst_exist, RENAME_NOREPLACE) == 0 || + errno != EEXIST) { + printf("[FAIL] renameat2 NOREPLACE expected EEXIST, errno=%d (%s)\n", errno, + strerror(errno)); + goto fail; + } + + snprintf(renamed, sizeof(renamed), "%s/p2_renamed.txt", mp); + if (syscall(SYS_renameat2, AT_FDCWD, hard_path, AT_FDCWD, renamed, RENAME_NOREPLACE) != 0) { + printf("[FAIL] renameat2 NOREPLACE success path: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + + dfd = open(mp, O_RDONLY | O_DIRECTORY); + if (dfd < 0) { + printf("[FAIL] open mountpoint dirfd: %s (errno=%d)\n", strerror(errno), errno); + goto fail; + } + if (fsync(dfd) != 0) { + printf("[FAIL] fsync(dirfd): %s (errno=%d)\n", strerror(errno), errno); + close(dfd); + goto fail; + } + close(dfd); + + usleep(100 * 1000); + + if (access_count < 2 || flush_count == 0 || fsync_count == 0 || fsyncdir_count == 0 || + create_count == 0 || rename2_count < 2) { + printf("[FAIL] counters access=%u flush=%u fsync=%u fsyncdir=%u create=%u rename2=%u\n", + access_count, flush_count, fsync_count, fsyncdir_count, create_count, + rename2_count); + goto fail; + } + + if (umount(mp) != 0) { + printf("[FAIL] umount(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + goto fail_no_umount; + } + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return 0; + +fail: + umount(mp); +fail_no_umount: + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; +} + +static void ext_sigusr1_handler(int signo) { + (void)signo; +} + +struct ext_reader_ctx { + char path[256]; + volatile int done; + ssize_t nread; + int err; +}; + +static void *ext_reader_thread(void *arg) { + struct ext_reader_ctx *ctx = (struct ext_reader_ctx *)arg; + int fd = open(ctx->path, O_RDONLY); + if (fd < 0) { + ctx->nread = -1; + ctx->err = errno; + ctx->done = 1; + return NULL; + } + + char buf[64]; + ssize_t n = read(fd, buf, sizeof(buf)); + if (n < 0) { + ctx->nread = -1; + ctx->err = errno; + } else { + ctx->nread = n; + ctx->err = 0; + } + close(fd); + ctx->done = 1; + return NULL; +} + +static int ext_test_p3_interrupt() { + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = ext_sigusr1_handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + + struct sigaction old_sa; + if (sigaction(SIGUSR1, &sa, &old_sa) != 0) { + printf("[FAIL] sigaction(SIGUSR1): %s (errno=%d)\n", strerror(errno), errno); + return -1; + } + + const char *mp = "/tmp/test_fuse_p3_interrupt"; + if (ensure_dir(mp) != 0) { + printf("[FAIL] ensure_dir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + sigaction(SIGUSR1, &old_sa, NULL); + return -1; + } + + int fd = open("/dev/fuse", O_RDWR); + if (fd < 0) { + printf("[FAIL] open(/dev/fuse): %s (errno=%d)\n", strerror(errno), errno); + rmdir(mp); + sigaction(SIGUSR1, &old_sa, NULL); + return -1; + } + + volatile int stop = 0; + volatile int init_done = 0; + volatile uint32_t interrupt_count = 0; + volatile uint64_t blocked_read_unique = 0; + volatile uint64_t last_interrupt_target = 0; + + struct fuse_daemon_args args; + memset(&args, 0, sizeof(args)); + args.fd = fd; + args.stop = &stop; + args.init_done = &init_done; + args.enable_write_ops = 0; + args.stop_on_destroy = 1; + args.block_read_until_interrupt = 1000; + args.interrupt_count = &interrupt_count; + args.blocked_read_unique = &blocked_read_unique; + args.last_interrupt_target = &last_interrupt_target; + + pthread_t daemon_th; + if (pthread_create(&daemon_th, NULL, fuse_daemon_thread, &args) != 0) { + printf("[FAIL] pthread_create(daemon)\n"); + close(fd); + rmdir(mp); + sigaction(SIGUSR1, &old_sa, NULL); + return -1; + } + + char opts[256]; + snprintf(opts, sizeof(opts), "fd=%d,rootmode=040755,user_id=0,group_id=0", fd); + if (mount("none", mp, "fuse", 0, opts) != 0) { + printf("[FAIL] mount(fuse): %s (errno=%d)\n", strerror(errno), errno); + stop = 1; + close(fd); + pthread_join(daemon_th, NULL); + rmdir(mp); + sigaction(SIGUSR1, &old_sa, NULL); + return -1; + } + if (fuseg_wait_init(&init_done) != 0) { + printf("[FAIL] init handshake timeout\n"); + goto fail; + } + + struct ext_reader_ctx rctx; + memset(&rctx, 0, sizeof(rctx)); + snprintf(rctx.path, sizeof(rctx.path), "%s/hello.txt", mp); + + pthread_t reader_th; + if (pthread_create(&reader_th, NULL, ext_reader_thread, &rctx) != 0) { + printf("[FAIL] pthread_create(reader)\n"); + goto fail; + } + + for (int i = 0; i < 200; i++) { + if (blocked_read_unique != 0) { + break; + } + usleep(5 * 1000); + } + if (blocked_read_unique == 0) { + printf("[FAIL] timed out waiting for blocked read request\n"); + stop = 1; + pthread_join(reader_th, NULL); + goto fail; + } + + if (pthread_kill(reader_th, SIGUSR1) != 0) { + printf("[FAIL] pthread_kill(SIGUSR1)\n"); + stop = 1; + pthread_join(reader_th, NULL); + goto fail; + } + pthread_join(reader_th, NULL); + + if (rctx.nread != -1 || rctx.err != EINTR) { + printf("[FAIL] reader expected EINTR, nread=%zd err=%d (%s)\n", rctx.nread, rctx.err, + strerror(rctx.err)); + goto fail; + } + + for (int i = 0; i < 500; i++) { + if (interrupt_count > 0) { + break; + } + usleep(5 * 1000); + } + + if (interrupt_count == 0) { + printf("[FAIL] expected FUSE_INTERRUPT request\n"); + goto fail; + } + if (last_interrupt_target == 0 || last_interrupt_target != blocked_read_unique) { + printf("[FAIL] interrupt target mismatch: blocked=%llu interrupt_target=%llu\n", + (unsigned long long)blocked_read_unique, (unsigned long long)last_interrupt_target); + goto fail; + } + + if (umount(mp) != 0) { + printf("[FAIL] umount(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + goto fail_no_umount; + } + stop = 1; + close(fd); + pthread_join(daemon_th, NULL); + rmdir(mp); + sigaction(SIGUSR1, &old_sa, NULL); + return 0; + +fail: + umount(mp); +fail_no_umount: + stop = 1; + close(fd); + pthread_join(daemon_th, NULL); + rmdir(mp); + sigaction(SIGUSR1, &old_sa, NULL); + return -1; +} + +static int ext_test_p3_noopen_readdirplus_notify() { + const char *mp = "/tmp/test_fuse_p3_noopen"; + ssize_t wn = -1; + if (ensure_dir(mp) != 0) { + printf("[FAIL] ensure_dir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + return -1; + } + + int fd = open("/dev/fuse", O_RDWR); + if (fd < 0) { + printf("[FAIL] open(/dev/fuse): %s (errno=%d)\n", strerror(errno), errno); + rmdir(mp); + return -1; + } + + volatile int stop = 0; + volatile int init_done = 0; + volatile uint32_t open_count = 0; + volatile uint32_t opendir_count = 0; + volatile uint32_t release_count = 0; + volatile uint32_t releasedir_count = 0; + volatile uint32_t readdirplus_count = 0; + + struct fuse_daemon_args args; + memset(&args, 0, sizeof(args)); + args.fd = fd; + args.stop = &stop; + args.init_done = &init_done; + args.enable_write_ops = 0; + args.stop_on_destroy = 1; + args.open_count = &open_count; + args.opendir_count = &opendir_count; + args.release_count = &release_count; + args.releasedir_count = &releasedir_count; + args.readdirplus_count = &readdirplus_count; + args.force_open_enosys = 1; + args.force_opendir_enosys = 1; + args.init_out_flags_override = FUSE_INIT_EXT | FUSE_MAX_PAGES | FUSE_NO_OPEN_SUPPORT | + FUSE_NO_OPENDIR_SUPPORT | FUSE_DO_READDIRPLUS; + + pthread_t th; + if (pthread_create(&th, NULL, fuse_daemon_thread, &args) != 0) { + printf("[FAIL] pthread_create\n"); + close(fd); + rmdir(mp); + return -1; + } + + char opts[256]; + snprintf(opts, sizeof(opts), "fd=%d,rootmode=040755,user_id=0,group_id=0", fd); + if (mount("none", mp, "fuse", 0, opts) != 0) { + printf("[FAIL] mount(fuse): %s (errno=%d)\n", strerror(errno), errno); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + if (fuseg_wait_init(&init_done) != 0) { + printf("[FAIL] init handshake timeout\n"); + goto fail; + } + + char file_path[256]; + snprintf(file_path, sizeof(file_path), "%s/hello.txt", mp); + for (int i = 0; i < 2; i++) { + int f = open(file_path, O_RDONLY); + if (f < 0) { + printf("[FAIL] open(%s): %s (errno=%d)\n", file_path, strerror(errno), errno); + goto fail; + } + char buf[64]; + ssize_t n = read(f, buf, sizeof(buf) - 1); + close(f); + if (n <= 0) { + printf("[FAIL] read(%s): %s (errno=%d)\n", file_path, strerror(errno), errno); + goto fail; + } + } + + for (int i = 0; i < 2; i++) { + DIR *dir = opendir(mp); + if (!dir) { + printf("[FAIL] opendir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + goto fail; + } + int saw = 0; + struct dirent *de; + while ((de = readdir(dir)) != NULL) { + if (strcmp(de->d_name, "hello.txt") == 0) { + saw = 1; + } + } + closedir(dir); + if (!saw) { + printf("[FAIL] readdir didn't see hello.txt\n"); + goto fail; + } + } + + struct { + struct fuse_out_header out; + struct fuse_notify_inval_inode_out inval; + } notify_msg; + memset(¬ify_msg, 0, sizeof(notify_msg)); + notify_msg.out.len = sizeof(notify_msg); + notify_msg.out.error = FUSE_NOTIFY_INVAL_INODE; + notify_msg.out.unique = 0; + notify_msg.inval.ino = 2; + notify_msg.inval.off = 0; + notify_msg.inval.len = -1; + wn = write(fd, ¬ify_msg, sizeof(notify_msg)); + if (wn != (ssize_t)sizeof(notify_msg)) { + printf("[FAIL] write notify: wn=%zd errno=%d (%s)\n", wn, errno, strerror(errno)); + goto fail; + } + + usleep(100 * 1000); + + if (open_count != 1 || opendir_count != 1 || release_count != 0 || releasedir_count != 0 || + readdirplus_count == 0) { + printf("[FAIL] counters open=%u opendir=%u release=%u releasedir=%u readdirplus=%u\n", + open_count, opendir_count, release_count, releasedir_count, readdirplus_count); + goto fail; + } + + if (umount(mp) != 0) { + printf("[FAIL] umount(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + goto fail_no_umount; + } + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return 0; + +fail: + umount(mp); +fail_no_umount: + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; +} + +static int ext_test_p4_subtype_mount() { + const char *mp = "/tmp/test_fuse_p4_subtype"; + if (ensure_dir(mp) != 0) { + printf("[FAIL] ensure_dir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + return -1; + } + + int fd = open("/dev/fuse", O_RDWR); + if (fd < 0) { + printf("[FAIL] open(/dev/fuse): %s (errno=%d)\n", strerror(errno), errno); + rmdir(mp); + return -1; + } + + volatile int stop = 0; + volatile int init_done = 0; + struct fuse_daemon_args args; + memset(&args, 0, sizeof(args)); + args.fd = fd; + args.stop = &stop; + args.init_done = &init_done; + args.enable_write_ops = 0; + args.stop_on_destroy = 1; + + pthread_t th; + if (pthread_create(&th, NULL, fuse_daemon_thread, &args) != 0) { + printf("[FAIL] pthread_create\n"); + close(fd); + rmdir(mp); + return -1; + } + + char opts[256]; + snprintf(opts, sizeof(opts), "fd=%d,rootmode=040755,user_id=0,group_id=0", fd); + if (mount("none", mp, "fuse.fuse3_demo", 0, opts) != 0) { + printf("[FAIL] mount(fuse.fuse3_demo): %s (errno=%d)\n", strerror(errno), errno); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + for (int i = 0; i < 200; i++) { + if (init_done) { + break; + } + usleep(10 * 1000); + } + if (!init_done) { + printf("[FAIL] init handshake timeout\n"); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + char file_path[256]; + snprintf(file_path, sizeof(file_path), "%s/hello.txt", mp); + + char buf[128]; + if (fuseg_read_file_cstr(file_path, buf, sizeof(buf)) < 0) { + printf("[FAIL] read(%s): %s (errno=%d)\n", file_path, strerror(errno), errno); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + if (strcmp(buf, "hello from fuse\n") != 0) { + printf("[FAIL] content mismatch: got='%s'\n", buf); + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + if (umount(mp) != 0) { + printf("[FAIL] umount(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return 0; +} + +static int ext_run_child_drop_priv_and_stat(const char *mp, int expect_errno, int expect_success) { + pid_t pid = fork(); + if (pid < 0) { + return -1; + } + if (pid == 0) { + if (setgid(1000) != 0) { + _exit(30); + } + if (setuid(1000) != 0) { + _exit(31); + } + + struct stat st; + int r = stat(mp, &st); + if (expect_success) { + if (r != 0) + _exit(10); + char p[256]; + snprintf(p, sizeof(p), "%s/hello.txt", mp); + int fd = open(p, O_RDONLY); + if (fd < 0) + _exit(11); + char buf[64]; + ssize_t n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n < 0) + _exit(12); + buf[n] = '\0'; + if (strcmp(buf, "hello from fuse\n") != 0) + _exit(13); + _exit(0); + } + + if (r != 0 && errno == expect_errno) { + _exit(0); + } + if (r != 0) { + _exit(21); + } + + /* + * Linux 语义下,目录本身的 stat 可能成功;真正的拒绝点通常体现在 + * 访问目录内对象(例如 open/stat 子路径)。 + */ + char p[256]; + snprintf(p, sizeof(p), "%s/hello.txt", mp); + int fd = open(p, O_RDONLY); + if (fd >= 0) { + close(fd); + _exit(22); + } + if (errno != expect_errno) { + _exit(23); + } + _exit(0); + } + + int status = 0; + if (waitpid(pid, &status, 0) < 0) { + return -1; + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + errno = ECHILD; + return -1; + } + return 0; +} + +static int ext_run_permission_case(const char *mp, const char *opts, uint32_t root_mode_override, + uint32_t hello_mode_override, int expect_errno, + int expect_success) { + if (ensure_dir(mp) != 0) { + return -1; + } + + int fd = open("/dev/fuse", O_RDWR); + if (fd < 0) { + rmdir(mp); + return -1; + } + + volatile int stop = 0; + volatile int init_done = 0; + struct fuse_daemon_args args; + memset(&args, 0, sizeof(args)); + args.fd = fd; + args.stop = &stop; + args.init_done = &init_done; + args.enable_write_ops = 0; + args.exit_after_init = 0; + args.root_mode_override = root_mode_override; + args.hello_mode_override = hello_mode_override; + + pthread_t th; + if (pthread_create(&th, NULL, fuse_daemon_thread, &args) != 0) { + close(fd); + rmdir(mp); + return -1; + } + + char full_opts[512]; + snprintf(full_opts, sizeof(full_opts), "fd=%d,%s", fd, opts); + if (mount("none", mp, "fuse", 0, full_opts) != 0) { + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + if (fuseg_wait_init(&init_done) != 0) { + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + if (ext_run_child_drop_priv_and_stat(mp, expect_errno, expect_success) != 0) { + umount(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + rmdir(mp); + return -1; + } + + umount(mp); + rmdir(mp); + stop = 1; + close(fd); + pthread_join(th, NULL); + return 0; +} + +static int ext_test_permissions() { + const uint32_t DIR_NO_PERM = 0040000; + const uint32_t REG_NO_PERM = 0100000; + + { + const char *mp = "/tmp/test_fuse_perm_owner"; + if (ext_run_permission_case(mp, "rootmode=040755,user_id=0,group_id=0", 0, 0, EACCES, 0) != + 0) { + printf("[FAIL] mount owner restriction\n"); + return -1; + } + } + + { + const char *mp = "/tmp/test_fuse_perm_default"; + if (ext_run_permission_case( + mp, "rootmode=040000,user_id=0,group_id=0,allow_other,default_permissions", + DIR_NO_PERM, REG_NO_PERM, EACCES, 0) != 0) { + printf("[FAIL] default_permissions deny\n"); + return -1; + } + } + + { + const char *mp = "/tmp/test_fuse_perm_remote"; + if (ext_run_permission_case(mp, "rootmode=040000,user_id=0,group_id=0,allow_other", + DIR_NO_PERM, REG_NO_PERM, 0, 1) != 0) { + printf("[FAIL] remote permission model allow\n"); + return -1; + } + } + + return 0; +} + +static int ext_test_clone() { + const char *mp = "/tmp/test_fuse_clone"; + DIR *d = NULL; + int found = 0; + struct dirent *de = NULL; + char p[256]; + struct stat st; + char buf[128]; + int n = -1; + if (ensure_dir(mp) != 0) { + printf("[FAIL] ensure_dir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + return -1; + } + + int master_fd = open("/dev/fuse", O_RDWR); + if (master_fd < 0) { + printf("[FAIL] open(/dev/fuse master): %s (errno=%d)\n", strerror(errno), errno); + rmdir(mp); + return -1; + } + + volatile int stop = 0; + volatile int init_done = 0; + + struct fuse_daemon_args master_args; + memset(&master_args, 0, sizeof(master_args)); + master_args.fd = master_fd; + master_args.stop = &stop; + master_args.init_done = &init_done; + master_args.enable_write_ops = 0; + master_args.exit_after_init = 1; + + pthread_t master_th; + if (pthread_create(&master_th, NULL, fuse_daemon_thread, &master_args) != 0) { + printf("[FAIL] pthread_create(master)\n"); + close(master_fd); + rmdir(mp); + return -1; + } + + char opts[256]; + snprintf(opts, sizeof(opts), "fd=%d,rootmode=040755,user_id=0,group_id=0", master_fd); + if (mount("none", mp, "fuse", 0, opts) != 0) { + printf("[FAIL] mount(fuse): %s (errno=%d)\n", strerror(errno), errno); + stop = 1; + close(master_fd); + pthread_join(master_th, NULL); + rmdir(mp); + return -1; + } + + for (int i = 0; i < 100; i++) { + if (init_done) + break; + usleep(10 * 1000); + } + if (!init_done) { + printf("[FAIL] init handshake timeout\n"); + umount(mp); + stop = 1; + close(master_fd); + pthread_join(master_th, NULL); + rmdir(mp); + return -1; + } + + pthread_join(master_th, NULL); + + int clone_fd = open("/dev/fuse", O_RDWR); + if (clone_fd < 0) { + printf("[FAIL] open(/dev/fuse clone): %s (errno=%d)\n", strerror(errno), errno); + umount(mp); + close(master_fd); + rmdir(mp); + return -1; + } + + uint32_t oldfd_u32 = (uint32_t)master_fd; + if (ioctl(clone_fd, FUSE_DEV_IOC_CLONE, &oldfd_u32) != 0) { + printf("[FAIL] ioctl(FUSE_DEV_IOC_CLONE): %s (errno=%d)\n", strerror(errno), errno); + umount(mp); + close(clone_fd); + close(master_fd); + rmdir(mp); + return -1; + } + + struct fuse_daemon_args clone_args; + memset(&clone_args, 0, sizeof(clone_args)); + clone_args.fd = clone_fd; + clone_args.stop = &stop; + clone_args.init_done = &init_done; + clone_args.enable_write_ops = 0; + clone_args.exit_after_init = 0; + + pthread_t clone_th; + if (pthread_create(&clone_th, NULL, fuse_daemon_thread, &clone_args) != 0) { + printf("[FAIL] pthread_create(clone)\n"); + umount(mp); + close(clone_fd); + close(master_fd); + rmdir(mp); + return -1; + } + + d = opendir(mp); + if (!d) { + printf("[FAIL] opendir(%s): %s (errno=%d)\n", mp, strerror(errno), errno); + goto fail; + } + found = 0; + while ((de = readdir(d)) != NULL) { + if (strcmp(de->d_name, "hello.txt") == 0) { + found = 1; + break; + } + } + closedir(d); + if (!found) { + printf("[FAIL] readdir: hello.txt not found\n"); + goto fail; + } + + snprintf(p, sizeof(p), "%s/hello.txt", mp); + if (stat(p, &st) != 0) { + printf("[FAIL] stat(%s): %s (errno=%d)\n", p, strerror(errno), errno); + goto fail; + } + if (!S_ISREG(st.st_mode)) { + printf("[FAIL] stat: expected regular file\n"); + goto fail; + } + + n = fuseg_read_file_cstr(p, buf, sizeof(buf)); + if (n < 0) { + printf("[FAIL] read(%s): %s (errno=%d)\n", p, strerror(errno), errno); + goto fail; + } + if (strcmp(buf, "hello from fuse\n") != 0) { + printf("[FAIL] content mismatch: got='%s'\n", buf); + goto fail; + } + + umount(mp); + rmdir(mp); + stop = 1; + close(clone_fd); + close(master_fd); + pthread_join(clone_th, NULL); + return 0; + +fail: + umount(mp); + stop = 1; + close(clone_fd); + close(master_fd); + pthread_join(clone_th, NULL); + rmdir(mp); + return -1; +} + +TEST(FuseExtended, OpsAccessCreateSymlinkLinkRename2FlushFsync) { + ASSERT_EQ(0, ext_test_p2_ops()); +} + +TEST(FuseExtended, InterruptDeliversFuseInterrupt) { + ASSERT_EQ(0, ext_test_p3_interrupt()); +} + +TEST(FuseExtended, NoOpenNoOpendirReaddirplusNotify) { + ASSERT_EQ(0, ext_test_p3_noopen_readdirplus_notify()); +} + +TEST(FuseExtended, SubtypeMountFuseDotSubtype) { + ASSERT_EQ(0, ext_test_p4_subtype_mount()); +} + +TEST(FuseExtended, PermissionModelAllowOtherDefaultPermissions) { + if (geteuid() != 0) { + GTEST_SKIP() << "requires root to execute setuid/setgid permission cases"; + } + ASSERT_EQ(0, ext_test_permissions()); +} + +TEST(FuseExtended, DevCloneAttachAndServe) { + ASSERT_EQ(0, ext_test_clone()); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/user/apps/tests/dunitest/suites/fuse/fuse_gtest_common.h b/user/apps/tests/dunitest/suites/fuse/fuse_gtest_common.h new file mode 100644 index 000000000..684b5acba --- /dev/null +++ b/user/apps/tests/dunitest/suites/fuse/fuse_gtest_common.h @@ -0,0 +1,154 @@ +#pragma once + +#include + +#include "fuse_test_simplefs_local.h" + +static inline int fuseg_wait_flag(volatile int *flag, int retries, int sleep_us) { + for (int i = 0; i < retries; i++) { + if (*flag) { + return 0; + } + usleep(sleep_us); + } + errno = ETIMEDOUT; + return -1; +} + +static inline int fuseg_wait_init(volatile int *init_done) { + return fuseg_wait_flag(init_done, 200, 10 * 1000); +} + +static inline int fuseg_wait_readable(int fd, int timeout_ms) { + struct pollfd pfd; + memset(&pfd, 0, sizeof(pfd)); + pfd.fd = fd; + pfd.events = POLLIN; + int pr = poll(&pfd, 1, timeout_ms); + if (pr < 0) { + return -1; + } + if (pr == 0) { + errno = ETIMEDOUT; + return -1; + } + if ((pfd.revents & POLLIN) == 0) { + errno = EIO; + return -1; + } + return 0; +} + +static inline int fuseg_write_all_fd(int fd, const char *s) { + size_t left = strlen(s); + const char *p = s; + while (left > 0) { + ssize_t n = write(fd, p, left); + if (n <= 0) { + return -1; + } + p += n; + left -= (size_t)n; + } + return 0; +} + +static inline int fuseg_write_file(const char *path, const char *s) { + int fd = open(path, O_CREAT | O_TRUNC | O_RDWR, 0644); + if (fd < 0) { + return -1; + } + int rc = fuseg_write_all_fd(fd, s); + close(fd); + return rc; +} + +static inline int fuseg_read_file(const char *path, char *buf, size_t cap) { + int fd = open(path, O_RDONLY); + if (fd < 0) { + return -1; + } + ssize_t n = read(fd, buf, cap); + close(fd); + if (n < 0) { + return -1; + } + return (int)n; +} + +static inline int fuseg_read_file_cstr(const char *path, char *buf, size_t cap) { + int fd = open(path, O_RDONLY); + if (fd < 0) { + return -1; + } + ssize_t n = read(fd, buf, cap - 1); + int saved_errno = errno; + close(fd); + if (n < 0) { + errno = saved_errno; + return -1; + } + buf[n] = '\0'; + return (int)n; +} + +static inline int fuseg_do_init_handshake_basic(int fd) { + if (fuseg_wait_readable(fd, 1000) != 0) { + return -1; + } + + unsigned char *buf = (unsigned char *)malloc(FUSE_TEST_BUF_SIZE); + if (!buf) { + errno = ENOMEM; + return -1; + } + memset(buf, 0, FUSE_TEST_BUF_SIZE); + + ssize_t n = read(fd, buf, FUSE_TEST_BUF_SIZE); + if (n < (ssize_t)(sizeof(struct fuse_in_header) + sizeof(struct fuse_init_in))) { + free(buf); + return -1; + } + + struct fuse_in_header in_hdr; + memcpy(&in_hdr, buf, sizeof(in_hdr)); + if (in_hdr.opcode != FUSE_INIT || in_hdr.len != (uint32_t)n) { + free(buf); + errno = EPROTO; + return -1; + } + + struct fuse_init_in init_in; + memcpy(&init_in, buf + sizeof(struct fuse_in_header), sizeof(init_in)); + free(buf); + if (init_in.major != 7 || init_in.minor == 0 || (init_in.flags == 0 && init_in.flags2 == 0)) { + errno = EPROTO; + return -1; + } + + struct fuse_out_header out_hdr; + memset(&out_hdr, 0, sizeof(out_hdr)); + out_hdr.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_init_out); + out_hdr.error = 0; + out_hdr.unique = in_hdr.unique; + + struct fuse_init_out init_out; + memset(&init_out, 0, sizeof(init_out)); + init_out.major = 7; + init_out.minor = 39; + init_out.flags = FUSE_INIT_EXT | FUSE_MAX_PAGES; + init_out.flags2 = 0; + init_out.max_write = 1024 * 1024; + init_out.max_pages = 256; + + unsigned char reply[sizeof(out_hdr) + sizeof(init_out)]; + memcpy(reply, &out_hdr, sizeof(out_hdr)); + memcpy(reply + sizeof(out_hdr), &init_out, sizeof(init_out)); + + ssize_t wn = write(fd, reply, sizeof(reply)); + if (wn != (ssize_t)sizeof(reply)) { + return -1; + } + + return 0; +} diff --git a/user/apps/tests/dunitest/suites/fuse/fuse_test_simplefs_local.h b/user/apps/tests/dunitest/suites/fuse/fuse_test_simplefs_local.h new file mode 100644 index 000000000..f85dcb4c8 --- /dev/null +++ b/user/apps/tests/dunitest/suites/fuse/fuse_test_simplefs_local.h @@ -0,0 +1,1415 @@ +/* + * Minimal FUSE userspace daemon for DragonOS kernel tests (no libfuse). + * + * This header provides a tiny in-memory filesystem and request handlers for + * a subset of FUSE opcodes used by Phase C/D tests. + */ + +#pragma once + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FUSE_TEST_LOG_PREFIX "[fuse-test] " +#define FUSE_SIMPLEFS_REV "statfs-v1" + +static inline int fuse_test_log_enabled(void) { + static int inited = 0; + static int enabled = 0; + if (!inited) { + const char *v = getenv("FUSE_TEST_LOG"); + enabled = (v && v[0] && strcmp(v, "0") != 0); + inited = 1; + } + return enabled; +} + +#define FUSE_TEST_LOG(fmt, ...) \ + do { \ + if (fuse_test_log_enabled()) { \ + fprintf(stderr, FUSE_TEST_LOG_PREFIX fmt "\n", ##__VA_ARGS__); \ + } \ + } while (0) + +#ifndef DT_DIR +#define DT_DIR 4 +#endif +#ifndef DT_REG +#define DT_REG 8 +#endif +#ifndef DT_LNK +#define DT_LNK 10 +#endif + +/* Keep test buffers off small thread stacks. */ +#define FUSE_TEST_BUF_SIZE (64 * 1024) + +/* Opcodes (subset) */ +#ifndef FUSE_LOOKUP +#define FUSE_LOOKUP 1 +#endif +#ifndef FUSE_FORGET +#define FUSE_FORGET 2 +#endif +#ifndef FUSE_GETATTR +#define FUSE_GETATTR 3 +#endif +#ifndef FUSE_SETATTR +#define FUSE_SETATTR 4 +#endif +#ifndef FUSE_READLINK +#define FUSE_READLINK 5 +#endif +#ifndef FUSE_SYMLINK +#define FUSE_SYMLINK 6 +#endif +#ifndef FUSE_MKNOD +#define FUSE_MKNOD 8 +#endif +#ifndef FUSE_MKDIR +#define FUSE_MKDIR 9 +#endif +#ifndef FUSE_UNLINK +#define FUSE_UNLINK 10 +#endif +#ifndef FUSE_RMDIR +#define FUSE_RMDIR 11 +#endif +#ifndef FUSE_RENAME +#define FUSE_RENAME 12 +#endif +#ifndef FUSE_LINK +#define FUSE_LINK 13 +#endif +#ifndef FUSE_OPEN +#define FUSE_OPEN 14 +#endif +#ifndef FUSE_READ +#define FUSE_READ 15 +#endif +#ifndef FUSE_WRITE +#define FUSE_WRITE 16 +#endif +#ifndef FUSE_STATFS +#define FUSE_STATFS 17 +#endif +#ifndef FUSE_RELEASE +#define FUSE_RELEASE 18 +#endif +#ifndef FUSE_FSYNC +#define FUSE_FSYNC 20 +#endif +#ifndef FUSE_FLUSH +#define FUSE_FLUSH 25 +#endif +#ifndef FUSE_INIT +#define FUSE_INIT 26 +#endif +#ifndef FUSE_OPENDIR +#define FUSE_OPENDIR 27 +#endif +#ifndef FUSE_READDIR +#define FUSE_READDIR 28 +#endif +#ifndef FUSE_RELEASEDIR +#define FUSE_RELEASEDIR 29 +#endif +#ifndef FUSE_FSYNCDIR +#define FUSE_FSYNCDIR 30 +#endif +#ifndef FUSE_ACCESS +#define FUSE_ACCESS 34 +#endif +#ifndef FUSE_CREATE +#define FUSE_CREATE 35 +#endif +#ifndef FUSE_INTERRUPT +#define FUSE_INTERRUPT 36 +#endif +#ifndef FUSE_DESTROY +#define FUSE_DESTROY 38 +#endif +#ifndef FUSE_READDIRPLUS +#define FUSE_READDIRPLUS 44 +#endif +#ifndef FUSE_RENAME2 +#define FUSE_RENAME2 45 +#endif + +#ifndef FUSE_MIN_READ_BUFFER +#define FUSE_MIN_READ_BUFFER 8192 +#endif + +/* INIT flags (subset) */ +#ifndef FUSE_INIT_EXT +#define FUSE_INIT_EXT (1u << 30) +#endif +#ifndef FUSE_MAX_PAGES +#define FUSE_MAX_PAGES (1u << 22) +#endif +#ifndef FUSE_DO_READDIRPLUS +#define FUSE_DO_READDIRPLUS (1u << 13) +#endif +#ifndef FUSE_READDIRPLUS_AUTO +#define FUSE_READDIRPLUS_AUTO (1u << 14) +#endif +#ifndef FUSE_NO_OPEN_SUPPORT +#define FUSE_NO_OPEN_SUPPORT (1u << 17) +#endif +#ifndef FUSE_NO_OPENDIR_SUPPORT +#define FUSE_NO_OPENDIR_SUPPORT (1u << 24) +#endif +#ifndef FUSE_FSYNC_FDATASYNC +#define FUSE_FSYNC_FDATASYNC (1u << 0) +#endif + +#ifndef FUSE_NOTIFY_INVAL_INODE +#define FUSE_NOTIFY_INVAL_INODE 2 +#endif + +#ifndef RENAME_NOREPLACE +#define RENAME_NOREPLACE (1u << 0) +#endif +#ifndef RENAME_EXCHANGE +#define RENAME_EXCHANGE (1u << 1) +#endif +#ifndef RENAME_WHITEOUT +#define RENAME_WHITEOUT (1u << 2) +#endif + +/* setattr valid bits (subset) */ +#ifndef FATTR_MODE +#define FATTR_MODE (1u << 0) +#endif +#ifndef FATTR_UID +#define FATTR_UID (1u << 1) +#endif +#ifndef FATTR_GID +#define FATTR_GID (1u << 2) +#endif +#ifndef FATTR_SIZE +#define FATTR_SIZE (1u << 3) +#endif + +struct fuse_in_header { + uint32_t len; + uint32_t opcode; + uint64_t unique; + uint64_t nodeid; + uint32_t uid; + uint32_t gid; + uint32_t pid; + uint16_t total_extlen; + uint16_t padding; +}; + +struct fuse_out_header { + uint32_t len; + int32_t error; /* -errno */ + uint64_t unique; +}; + +struct fuse_init_in { + uint32_t major; + uint32_t minor; + uint32_t max_readahead; + uint32_t flags; + uint32_t flags2; + uint32_t unused[11]; +}; + +struct fuse_init_out { + uint32_t major; + uint32_t minor; + uint32_t max_readahead; + uint32_t flags; + uint16_t max_background; + uint16_t congestion_threshold; + uint32_t max_write; + uint32_t time_gran; + uint16_t max_pages; + uint16_t map_alignment; + uint32_t flags2; + uint32_t unused[7]; +}; + +struct fuse_attr { + uint64_t ino; + uint64_t size; + uint64_t blocks; + uint64_t atime; + uint64_t mtime; + uint64_t ctime; + uint32_t atimensec; + uint32_t mtimensec; + uint32_t ctimensec; + uint32_t mode; + uint32_t nlink; + uint32_t uid; + uint32_t gid; + uint32_t rdev; + uint32_t blksize; + uint32_t flags; +}; + +struct fuse_entry_out { + uint64_t nodeid; + uint64_t generation; + uint64_t entry_valid; + uint64_t attr_valid; + uint32_t entry_valid_nsec; + uint32_t attr_valid_nsec; + struct fuse_attr attr; +}; + +struct fuse_forget_in { + uint64_t nlookup; +}; + +struct fuse_interrupt_in { + uint64_t unique; +}; + +struct fuse_getattr_in { + uint32_t getattr_flags; + uint32_t dummy; + uint64_t fh; +}; + +struct fuse_attr_out { + uint64_t attr_valid; + uint32_t attr_valid_nsec; + uint32_t dummy; + struct fuse_attr attr; +}; + +struct fuse_open_in { + uint32_t flags; + uint32_t open_flags; +}; + +struct fuse_create_in { + uint32_t flags; + uint32_t mode; + uint32_t umask; + uint32_t open_flags; +}; + +struct fuse_open_out { + uint64_t fh; + uint32_t open_flags; + uint32_t padding; +}; + +struct fuse_read_in { + uint64_t fh; + uint64_t offset; + uint32_t size; + uint32_t read_flags; + uint64_t lock_owner; + uint32_t flags; + uint32_t padding; +}; + +struct fuse_write_in { + uint64_t fh; + uint64_t offset; + uint32_t size; + uint32_t write_flags; + uint64_t lock_owner; + uint32_t flags; + uint32_t padding; +}; + +struct fuse_write_out { + uint32_t size; + uint32_t padding; +}; + +struct fuse_kstatfs { + uint64_t blocks; + uint64_t bfree; + uint64_t bavail; + uint64_t files; + uint64_t ffree; + uint32_t bsize; + uint32_t namelen; + uint32_t frsize; + uint32_t padding; + uint32_t spare[6]; +}; + +struct fuse_statfs_out { + struct fuse_kstatfs st; +}; + +struct fuse_release_in { + uint64_t fh; + uint32_t flags; + uint32_t release_flags; + uint64_t lock_owner; +}; + +struct fuse_flush_in { + uint64_t fh; + uint32_t unused; + uint32_t padding; + uint64_t lock_owner; +}; + +struct fuse_fsync_in { + uint64_t fh; + uint32_t fsync_flags; + uint32_t padding; +}; + +struct fuse_access_in { + uint32_t mask; + uint32_t padding; +}; + +struct fuse_mknod_in { + uint32_t mode; + uint32_t rdev; + uint32_t umask; + uint32_t padding; +}; + +struct fuse_mkdir_in { + uint32_t mode; + uint32_t umask; +}; + +struct fuse_rename_in { + uint64_t newdir; +}; + +struct fuse_rename2_in { + uint64_t newdir; + uint32_t flags; + uint32_t padding; +}; + +struct fuse_link_in { + uint64_t oldnodeid; +}; + +struct fuse_setattr_in { + uint32_t valid; + uint32_t padding; + uint64_t fh; + uint64_t size; + uint64_t lock_owner; + uint64_t atime; + uint64_t mtime; + uint64_t ctime; + uint32_t atimensec; + uint32_t mtimensec; + uint32_t ctimensec; + uint32_t mode; + uint32_t unused4; + uint32_t uid; + uint32_t gid; + uint32_t unused5; +}; + +struct fuse_dirent { + uint64_t ino; + uint64_t off; + uint32_t namelen; + uint32_t type; + /* char name[]; */ +}; + +struct fuse_direntplus { + struct fuse_entry_out entry_out; + struct fuse_dirent dirent; + /* char name[]; */ +}; + +struct fuse_notify_inval_inode_out { + uint64_t ino; + int64_t off; + int64_t len; +}; + +static inline size_t fuse_dirent_rec_len(size_t namelen) { + size_t unaligned = sizeof(struct fuse_dirent) + namelen; + return (unaligned + 8 - 1) & ~(size_t)(8 - 1); +} + +static inline size_t fuse_direntplus_rec_len(size_t namelen) { + size_t unaligned = sizeof(struct fuse_direntplus) + namelen; + return (unaligned + 8 - 1) & ~(size_t)(8 - 1); +} + +/* ===== in-memory FS ===== */ + +#define SIMPLEFS_MAX_NODES 64 +#define SIMPLEFS_NAME_MAX 64 +#define SIMPLEFS_DATA_MAX 8192 + +struct simplefs_node { + int used; + uint64_t nodeid; + uint64_t ino; + uint64_t parent; + int is_dir; + int is_symlink; + uint32_t mode; /* includes type bits */ + char name[SIMPLEFS_NAME_MAX]; + unsigned char data[SIMPLEFS_DATA_MAX]; + size_t size; +}; + +struct simplefs { + struct simplefs_node nodes[SIMPLEFS_MAX_NODES]; + uint64_t next_nodeid; + uint64_t next_ino; +}; + +static inline void simplefs_init(struct simplefs *fs) { + memset(fs, 0, sizeof(*fs)); + fs->next_nodeid = 2; + fs->next_ino = 2; + + /* root nodeid=1 */ + fs->nodes[0].used = 1; + fs->nodes[0].nodeid = 1; + fs->nodes[0].ino = 1; + fs->nodes[0].parent = 1; + fs->nodes[0].is_dir = 1; + fs->nodes[0].is_symlink = 0; + fs->nodes[0].mode = 0040755; + strcpy(fs->nodes[0].name, ""); + fs->nodes[0].size = 0; + + /* hello.txt under root */ + fs->nodes[1].used = 1; + fs->nodes[1].nodeid = 2; + fs->nodes[1].ino = 2; + fs->nodes[1].parent = 1; + fs->nodes[1].is_dir = 0; + fs->nodes[1].is_symlink = 0; + fs->nodes[1].mode = 0100644; + strcpy(fs->nodes[1].name, "hello.txt"); + const char *msg = "hello from fuse\n"; + fs->nodes[1].size = strlen(msg); + memcpy(fs->nodes[1].data, msg, fs->nodes[1].size); + + fs->next_nodeid = 3; + fs->next_ino = 3; +} + +static inline int simplefs_mode_is_dir(uint32_t mode) { + return (mode & 0170000u) == 0040000u; +} + +static inline int simplefs_mode_is_symlink(uint32_t mode) { + return (mode & 0170000u) == 0120000u; +} + +static inline struct simplefs_node *simplefs_find_node(struct simplefs *fs, uint64_t nodeid) { + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + if (fs->nodes[i].used && fs->nodes[i].nodeid == nodeid) { + return &fs->nodes[i]; + } + } + return NULL; +} + +static inline struct simplefs_node *simplefs_find_child(struct simplefs *fs, uint64_t parent, + const char *name) { + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + if (!fs->nodes[i].used) + continue; + if (fs->nodes[i].parent != parent) + continue; + if (strcmp(fs->nodes[i].name, name) == 0) + return &fs->nodes[i]; + } + return NULL; +} + +static inline int simplefs_has_children(struct simplefs *fs, uint64_t parent) { + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + if (!fs->nodes[i].used) + continue; + if (fs->nodes[i].parent == parent) + return 1; + } + return 0; +} + +static inline struct simplefs_node *simplefs_alloc(struct simplefs *fs) { + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + if (!fs->nodes[i].used) { + struct simplefs_node *n = &fs->nodes[i]; + memset(n, 0, sizeof(*n)); + n->used = 1; + n->nodeid = fs->next_nodeid++; + n->ino = fs->next_ino++; + return n; + } + } + return NULL; +} + +static inline void simplefs_fill_attr(const struct simplefs_node *n, struct fuse_attr *a) { + memset(a, 0, sizeof(*a)); + a->ino = n->ino; + a->size = n->size; + a->blocks = (n->size + 511) / 512; + a->mode = n->mode; + a->nlink = simplefs_mode_is_dir(n->mode) ? 2 : 1; + a->uid = getuid(); + a->gid = getgid(); + a->blksize = 4096; +} + +static inline int fuse_write_reply(int fd, uint64_t unique, int err_neg, const void *payload, + size_t payload_len) { + struct fuse_out_header out; + memset(&out, 0, sizeof(out)); + out.len = sizeof(out) + (uint32_t)payload_len; + out.error = err_neg; + out.unique = unique; + + size_t total = sizeof(out) + payload_len; + unsigned char *buf = (unsigned char *)malloc(total); + if (!buf) { + errno = ENOMEM; + return -1; + } + memcpy(buf, &out, sizeof(out)); + if (payload_len) { + memcpy(buf + sizeof(out), payload, payload_len); + } + ssize_t wn = write(fd, buf, total); + free(buf); + if (wn == (ssize_t)total) { + FUSE_TEST_LOG("reply unique=%llu err=%d len=%zu", + (unsigned long long)unique, (int)err_neg, total); + } + if (wn != (ssize_t)total) { + return -1; + } + return 0; +} + +struct fuse_daemon_args { + int fd; + volatile int *stop; + volatile int *init_done; + int enable_write_ops; + int exit_after_init; + int stop_on_destroy; + uint32_t root_mode_override; + uint32_t hello_mode_override; + volatile uint32_t *forget_count; + volatile uint64_t *forget_nlookup_sum; + volatile uint32_t *destroy_count; + volatile uint32_t *init_in_flags; + volatile uint32_t *init_in_flags2; + volatile uint32_t *init_in_max_readahead; + volatile uint32_t *access_count; + volatile uint32_t *flush_count; + volatile uint32_t *fsync_count; + volatile uint32_t *fsyncdir_count; + volatile uint32_t *create_count; + volatile uint32_t *rename2_count; + volatile uint32_t *open_count; + volatile uint32_t *opendir_count; + volatile uint32_t *release_count; + volatile uint32_t *releasedir_count; + volatile uint32_t *readdirplus_count; + volatile uint32_t *interrupt_count; + volatile uint64_t *blocked_read_unique; + volatile uint64_t *last_interrupt_target; + uint32_t access_deny_mask; + uint32_t init_out_flags_override; + int force_open_enosys; + int force_opendir_enosys; + int block_read_until_interrupt; + struct simplefs fs; +}; + +static inline int simplefs_node_is_dir(const struct simplefs_node *n) { + return n && (n->is_dir || simplefs_mode_is_dir(n->mode)); +} + +static inline int simplefs_node_is_symlink(const struct simplefs_node *n) { + return n && (n->is_symlink || simplefs_mode_is_symlink(n->mode)); +} + +static inline uint32_t simplefs_dirent_type(const struct simplefs_node *n) { + if (simplefs_node_is_dir(n)) { + return DT_DIR; + } + if (simplefs_node_is_symlink(n)) { + return DT_LNK; + } + return DT_REG; +} + +static inline int simplefs_fill_entry_reply(struct fuse_daemon_args *a, const struct fuse_in_header *h, + const struct simplefs_node *node) { + struct fuse_entry_out out; + memset(&out, 0, sizeof(out)); + out.nodeid = node->nodeid; + simplefs_fill_attr(node, &out.attr); + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); +} + +static inline int simplefs_parse_two_names(const unsigned char *payload, size_t payload_len, + size_t fixed_len, const char **oldname_out, + const char **newname_out) { + if (payload_len < fixed_len + 3) { + return -1; + } + const char *names = (const char *)(payload + fixed_len); + size_t names_len = payload_len - fixed_len; + const char *oldname = names; + size_t oldlen = strnlen(oldname, names_len); + if (oldlen == names_len) { + return -1; + } + const char *newname = names + oldlen + 1; + size_t remain = names_len - oldlen - 1; + if (remain == 0) { + return -1; + } + size_t newlen = strnlen(newname, remain); + if (newlen == remain) { + return -1; + } + *oldname_out = oldname; + *newname_out = newname; + return 0; +} + +static inline int simplefs_do_rename(struct fuse_daemon_args *a, const struct fuse_in_header *h, + uint64_t newdir, uint32_t flags, const char *oldname, + const char *newname) { + if ((flags & (RENAME_EXCHANGE | RENAME_WHITEOUT)) != 0) { + return fuse_write_reply(a->fd, h->unique, -EINVAL, NULL, 0); + } + struct simplefs_node *src = simplefs_find_child(&a->fs, h->nodeid, oldname); + if (!src) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + struct simplefs_node *dst_parent = simplefs_find_node(&a->fs, newdir); + if (!dst_parent || !simplefs_node_is_dir(dst_parent)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + struct simplefs_node *dst = simplefs_find_child(&a->fs, newdir, newname); + if (dst) { + if (flags & RENAME_NOREPLACE) { + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + src->parent = newdir; + strncpy(src->name, newname, sizeof(src->name) - 1); + src->name[sizeof(src->name) - 1] = '\0'; + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); +} + +static inline int fuse_handle_one(struct fuse_daemon_args *a, const unsigned char *req, size_t n) { + if (n < sizeof(struct fuse_in_header)) { + return -1; + } + const struct fuse_in_header *h = (const struct fuse_in_header *)req; + const unsigned char *payload = req + sizeof(*h); + size_t payload_len = n - sizeof(*h); + FUSE_TEST_LOG("handle opcode=%u unique=%llu nodeid=%llu len=%u payload=%zu", + h->opcode, (unsigned long long)h->unique, (unsigned long long)h->nodeid, + h->len, payload_len); + + switch (h->opcode) { + case FUSE_INIT: { + if (payload_len < sizeof(struct fuse_init_in)) { + return -1; + } + const struct fuse_init_in *in = (const struct fuse_init_in *)payload; + if (a->init_in_flags) + *a->init_in_flags = in->flags; + if (a->init_in_flags2) + *a->init_in_flags2 = in->flags2; + if (a->init_in_max_readahead) + *a->init_in_max_readahead = in->max_readahead; + + struct fuse_init_out out; + memset(&out, 0, sizeof(out)); + out.major = 7; + out.minor = 39; + uint32_t init_flags = a->init_out_flags_override; + if (init_flags == 0) { + init_flags = FUSE_INIT_EXT | FUSE_MAX_PAGES; + } + out.flags = init_flags; + out.flags2 = 0; + out.max_write = 4096; + out.max_pages = 32; + if (fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)) != 0) { + return -1; + } + *a->init_done = 1; + return 0; + } + case FUSE_FORGET: { + if (payload_len < sizeof(struct fuse_forget_in)) + return -1; + const struct fuse_forget_in *in = (const struct fuse_forget_in *)payload; + if (a->forget_count) + (*a->forget_count)++; + if (a->forget_nlookup_sum) + (*a->forget_nlookup_sum) += in->nlookup; + return 0; + } + case FUSE_LOOKUP: { + const char *name = (const char *)payload; + if (payload_len == 0 || name[payload_len - 1] != '\0') { + return -1; + } + struct simplefs_node *parent = simplefs_find_node(&a->fs, h->nodeid); + if (!parent || !simplefs_node_is_dir(parent)) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + struct simplefs_node *child = simplefs_find_child(&a->fs, h->nodeid, name); + if (!child) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + struct fuse_entry_out out; + memset(&out, 0, sizeof(out)); + out.nodeid = child->nodeid; + simplefs_fill_attr(child, &out.attr); + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_GETATTR: { + (void)payload; + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + struct fuse_attr_out out; + memset(&out, 0, sizeof(out)); + simplefs_fill_attr(node, &out.attr); + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_OPENDIR: + case FUSE_OPEN: { + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + if (h->opcode == FUSE_OPEN && a->open_count) { + (*a->open_count)++; + } + if (h->opcode == FUSE_OPENDIR && a->opendir_count) { + (*a->opendir_count)++; + } + if (h->opcode == FUSE_OPEN && a->force_open_enosys) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (h->opcode == FUSE_OPENDIR && a->force_opendir_enosys) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (h->opcode == FUSE_OPENDIR && !simplefs_node_is_dir(node)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + if (h->opcode == FUSE_OPEN && simplefs_node_is_dir(node)) { + return fuse_write_reply(a->fd, h->unique, -EISDIR, NULL, 0); + } + struct fuse_open_out out; + memset(&out, 0, sizeof(out)); + out.fh = node->nodeid; + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_READLINK: { + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + if (!simplefs_node_is_symlink(node)) { + return fuse_write_reply(a->fd, h->unique, -EINVAL, NULL, 0); + } + return fuse_write_reply(a->fd, h->unique, 0, node->data, node->size); + } + case FUSE_READ: { + if (payload_len < sizeof(struct fuse_read_in)) { + return -1; + } + const struct fuse_read_in *in = (const struct fuse_read_in *)payload; + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node || simplefs_node_is_dir(node) || simplefs_node_is_symlink(node)) { + return fuse_write_reply(a->fd, h->unique, -EINVAL, NULL, 0); + } + if (a->block_read_until_interrupt > 0) { + if (a->blocked_read_unique && *a->blocked_read_unique == 0) { + *a->blocked_read_unique = h->unique; + } + usleep((useconds_t)a->block_read_until_interrupt * 1000); + } + if (in->offset >= node->size) { + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + } + size_t remain = node->size - (size_t)in->offset; + size_t to_copy = in->size; + if (to_copy > remain) { + to_copy = remain; + } + return fuse_write_reply(a->fd, h->unique, 0, node->data + in->offset, to_copy); + } + case FUSE_READDIR: + case FUSE_READDIRPLUS: { + if (payload_len < sizeof(struct fuse_read_in)) { + return -1; + } + const struct fuse_read_in *in = (const struct fuse_read_in *)payload; + (void)in; + int is_plus = (h->opcode == FUSE_READDIRPLUS); + if (is_plus && a->readdirplus_count) { + (*a->readdirplus_count)++; + } + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node || !simplefs_node_is_dir(node)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + + /* offset is an entry index: 0=".", 1="..", then children */ + uint64_t idx = in->offset; + unsigned char *outbuf = (unsigned char *)malloc(FUSE_TEST_BUF_SIZE); + if (!outbuf) { + return fuse_write_reply(a->fd, h->unique, -ENOMEM, NULL, 0); + } + size_t outlen = 0; + + const char *fixed_names[2] = {".", ".."}; + for (; idx < 2; idx++) { + const char *nm = fixed_names[idx]; + size_t nmlen = strlen(nm); + size_t reclen = is_plus ? fuse_direntplus_rec_len(nmlen) : fuse_dirent_rec_len(nmlen); + if (outlen + reclen > FUSE_TEST_BUF_SIZE) + break; + if (is_plus) { + struct fuse_direntplus dp; + memset(&dp, 0, sizeof(dp)); + dp.entry_out.nodeid = 1; + simplefs_fill_attr(&a->fs.nodes[0], &dp.entry_out.attr); + dp.dirent.ino = 1; + dp.dirent.off = idx + 1; + dp.dirent.namelen = (uint32_t)nmlen; + dp.dirent.type = DT_DIR; + memcpy(outbuf + outlen, &dp, sizeof(dp)); + memcpy(outbuf + outlen + sizeof(dp), nm, nmlen); + } else { + struct fuse_dirent de; + memset(&de, 0, sizeof(de)); + de.ino = 1; + de.off = idx + 1; + de.namelen = (uint32_t)nmlen; + de.type = DT_DIR; + memcpy(outbuf + outlen, &de, sizeof(de)); + memcpy(outbuf + outlen + sizeof(de), nm, nmlen); + } + outlen += reclen; + } + + /* children in insertion order */ + uint64_t child_base = 2; + uint64_t cur = idx; + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + struct simplefs_node *c = &a->fs.nodes[i]; + if (!c->used || c->parent != h->nodeid) + continue; + if (cur < child_base) { + cur = child_base; + } + if (cur > child_base) { + /* skip until we reach this entry index */ + child_base++; + continue; + } + + size_t nmlen = strlen(c->name); + size_t reclen = is_plus ? fuse_direntplus_rec_len(nmlen) : fuse_dirent_rec_len(nmlen); + if (outlen + reclen > FUSE_TEST_BUF_SIZE) + break; + if (is_plus) { + struct fuse_direntplus dp; + memset(&dp, 0, sizeof(dp)); + dp.entry_out.nodeid = c->nodeid; + simplefs_fill_attr(c, &dp.entry_out.attr); + dp.dirent.ino = c->ino; + dp.dirent.off = child_base + 1; + dp.dirent.namelen = (uint32_t)nmlen; + dp.dirent.type = simplefs_dirent_type(c); + memcpy(outbuf + outlen, &dp, sizeof(dp)); + memcpy(outbuf + outlen + sizeof(dp), c->name, nmlen); + } else { + struct fuse_dirent de; + memset(&de, 0, sizeof(de)); + de.ino = c->ino; + de.off = child_base + 1; + de.namelen = (uint32_t)nmlen; + de.type = simplefs_dirent_type(c); + memcpy(outbuf + outlen, &de, sizeof(de)); + memcpy(outbuf + outlen + sizeof(de), c->name, nmlen); + } + outlen += reclen; + + child_base++; + cur++; + } + + int ret = fuse_write_reply(a->fd, h->unique, 0, outbuf, outlen); + free(outbuf); + return ret; + } + case FUSE_STATFS: { + struct fuse_statfs_out out; + memset(&out, 0, sizeof(out)); + + unsigned used = 0; + for (int i = 0; i < SIMPLEFS_MAX_NODES; i++) { + if (a->fs.nodes[i].used) { + used++; + } + } + + out.st.blocks = 1024; + out.st.bfree = 512; + out.st.bavail = 512; + out.st.files = SIMPLEFS_MAX_NODES; + out.st.ffree = (used > SIMPLEFS_MAX_NODES) ? 0 : (SIMPLEFS_MAX_NODES - used); + out.st.bsize = 4096; + out.st.frsize = 4096; + out.st.namelen = SIMPLEFS_NAME_MAX - 1; + FUSE_TEST_LOG("statfs reply ok blocks=%llu bfree=%llu bavail=%llu", + (unsigned long long)out.st.blocks, + (unsigned long long)out.st.bfree, + (unsigned long long)out.st.bavail); + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_RELEASE: + if (a->release_count) { + (*a->release_count)++; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + case FUSE_RELEASEDIR: + if (a->releasedir_count) { + (*a->releasedir_count)++; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + case FUSE_INTERRUPT: { + if (payload_len < sizeof(struct fuse_interrupt_in)) { + return -1; + } + const struct fuse_interrupt_in *in = (const struct fuse_interrupt_in *)payload; + if (a->interrupt_count) { + (*a->interrupt_count)++; + } + if (a->last_interrupt_target) { + *a->last_interrupt_target = in->unique; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + } + case FUSE_FLUSH: + if (a->flush_count) { + (*a->flush_count)++; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + case FUSE_FSYNC: + if (a->fsync_count) { + (*a->fsync_count)++; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + case FUSE_FSYNCDIR: + if (a->fsyncdir_count) { + (*a->fsyncdir_count)++; + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + case FUSE_ACCESS: { + if (payload_len < sizeof(struct fuse_access_in)) { + return -1; + } + const struct fuse_access_in *in = (const struct fuse_access_in *)payload; + if (a->access_count) { + (*a->access_count)++; + } + if ((in->mask & a->access_deny_mask) != 0) { + return fuse_write_reply(a->fd, h->unique, -EACCES, NULL, 0); + } + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + } + case FUSE_DESTROY: + if (a->destroy_count) + (*a->destroy_count)++; + if (a->stop_on_destroy && a->stop) + *a->stop = 1; + return 0; + case FUSE_WRITE: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (payload_len < sizeof(struct fuse_write_in)) { + return -1; + } + const struct fuse_write_in *in = (const struct fuse_write_in *)payload; + const unsigned char *data = payload + sizeof(*in); + size_t data_len = payload_len - sizeof(*in); + if (data_len < in->size) { + return -1; + } + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node || simplefs_node_is_dir(node) || simplefs_node_is_symlink(node)) { + return fuse_write_reply(a->fd, h->unique, -EINVAL, NULL, 0); + } + if (in->offset >= SIMPLEFS_DATA_MAX) { + return fuse_write_reply(a->fd, h->unique, -EFBIG, NULL, 0); + } + size_t to_copy = in->size; + if (in->offset + to_copy > SIMPLEFS_DATA_MAX) { + to_copy = SIMPLEFS_DATA_MAX - (size_t)in->offset; + } + memcpy(node->data + in->offset, data, to_copy); + if (node->size < in->offset + to_copy) { + node->size = (size_t)in->offset + to_copy; + } + struct fuse_write_out out; + memset(&out, 0, sizeof(out)); + out.size = (uint32_t)to_copy; + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_CREATE: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (payload_len < sizeof(struct fuse_create_in) + 1) { + return -1; + } + const struct fuse_create_in *in = (const struct fuse_create_in *)payload; + const char *name = (const char *)(payload + sizeof(*in)); + if (name[payload_len - sizeof(*in) - 1] != '\0') { + return -1; + } + if (a->create_count) { + (*a->create_count)++; + } + struct simplefs_node *p = simplefs_find_node(&a->fs, h->nodeid); + if (!p || !simplefs_node_is_dir(p)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + if (simplefs_find_child(&a->fs, h->nodeid, name)) { + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + struct simplefs_node *nnode = simplefs_alloc(&a->fs); + if (!nnode) { + return fuse_write_reply(a->fd, h->unique, -ENOSPC, NULL, 0); + } + nnode->parent = h->nodeid; + nnode->is_dir = 0; + nnode->is_symlink = 0; + nnode->mode = in->mode; + strncpy(nnode->name, name, sizeof(nnode->name) - 1); + nnode->name[sizeof(nnode->name) - 1] = '\0'; + nnode->size = 0; + + struct { + struct fuse_entry_out entry; + struct fuse_open_out open_out; + } out; + memset(&out, 0, sizeof(out)); + out.entry.nodeid = nnode->nodeid; + simplefs_fill_attr(nnode, &out.entry.attr); + out.open_out.fh = nnode->nodeid; + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + case FUSE_SYMLINK: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + const char *target = (const char *)payload; + size_t target_len = strnlen(target, payload_len); + if (target_len == payload_len) { + return -1; + } + const char *name = target + target_len + 1; + size_t remain = payload_len - target_len - 1; + if (remain == 0) { + return -1; + } + size_t name_len = strnlen(name, remain); + if (name_len == remain) { + return -1; + } + struct simplefs_node *p = simplefs_find_node(&a->fs, h->nodeid); + if (!p || !simplefs_node_is_dir(p)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + if (simplefs_find_child(&a->fs, h->nodeid, name)) { + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + struct simplefs_node *nnode = simplefs_alloc(&a->fs); + if (!nnode) { + return fuse_write_reply(a->fd, h->unique, -ENOSPC, NULL, 0); + } + nnode->parent = h->nodeid; + nnode->is_dir = 0; + nnode->is_symlink = 1; + nnode->mode = 0120777; + strncpy(nnode->name, name, sizeof(nnode->name) - 1); + nnode->name[sizeof(nnode->name) - 1] = '\0'; + nnode->size = (target_len < SIMPLEFS_DATA_MAX) ? target_len : SIMPLEFS_DATA_MAX; + memcpy(nnode->data, target, nnode->size); + return simplefs_fill_entry_reply(a, h, nnode); + } + case FUSE_LINK: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (payload_len < sizeof(struct fuse_link_in) + 1) { + return -1; + } + const struct fuse_link_in *in = (const struct fuse_link_in *)payload; + const char *name = (const char *)(payload + sizeof(*in)); + if (name[payload_len - sizeof(*in) - 1] != '\0') { + return -1; + } + struct simplefs_node *src = simplefs_find_node(&a->fs, in->oldnodeid); + if (!src) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + if (simplefs_node_is_dir(src)) { + return fuse_write_reply(a->fd, h->unique, -EPERM, NULL, 0); + } + struct simplefs_node *dst_parent = simplefs_find_node(&a->fs, h->nodeid); + if (!dst_parent || !simplefs_node_is_dir(dst_parent)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + if (simplefs_find_child(&a->fs, h->nodeid, name)) { + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + struct simplefs_node *nnode = simplefs_alloc(&a->fs); + if (!nnode) { + return fuse_write_reply(a->fd, h->unique, -ENOSPC, NULL, 0); + } + nnode->parent = h->nodeid; + nnode->is_dir = 0; + nnode->is_symlink = src->is_symlink; + nnode->mode = src->mode; + strncpy(nnode->name, name, sizeof(nnode->name) - 1); + nnode->name[sizeof(nnode->name) - 1] = '\0'; + nnode->size = src->size; + if (nnode->size > SIMPLEFS_DATA_MAX) { + nnode->size = SIMPLEFS_DATA_MAX; + } + memcpy(nnode->data, src->data, nnode->size); + return simplefs_fill_entry_reply(a, h, nnode); + } + case FUSE_MKDIR: + case FUSE_MKNOD: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + const char *name = NULL; + size_t name_off = 0; + int is_dir = (h->opcode == FUSE_MKDIR); + uint32_t mode = 0; + if (is_dir) { + if (payload_len < sizeof(struct fuse_mkdir_in) + 1) + return -1; + const struct fuse_mkdir_in *in = (const struct fuse_mkdir_in *)payload; + mode = in->mode; + name_off = sizeof(*in); + } else { + if (payload_len < sizeof(struct fuse_mknod_in) + 1) + return -1; + const struct fuse_mknod_in *in = (const struct fuse_mknod_in *)payload; + mode = in->mode; + name_off = sizeof(*in); + } + name = (const char *)(payload + name_off); + if (name[payload_len - name_off - 1] != '\0') + return -1; + if (simplefs_find_child(&a->fs, h->nodeid, name)) { + return fuse_write_reply(a->fd, h->unique, -EEXIST, NULL, 0); + } + struct simplefs_node *p = simplefs_find_node(&a->fs, h->nodeid); + if (!p || !simplefs_node_is_dir(p)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + struct simplefs_node *nnode = simplefs_alloc(&a->fs); + if (!nnode) { + return fuse_write_reply(a->fd, h->unique, -ENOSPC, NULL, 0); + } + nnode->parent = h->nodeid; + nnode->is_dir = is_dir; + nnode->is_symlink = 0; + nnode->mode = mode; + strncpy(nnode->name, name, sizeof(nnode->name) - 1); + nnode->name[sizeof(nnode->name) - 1] = '\0'; + nnode->size = 0; + + return simplefs_fill_entry_reply(a, h, nnode); + } + case FUSE_UNLINK: + case FUSE_RMDIR: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + const char *name = (const char *)payload; + if (payload_len == 0 || name[payload_len - 1] != '\0') { + return -1; + } + struct simplefs_node *child = simplefs_find_child(&a->fs, h->nodeid, name); + if (!child) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + if (h->opcode == FUSE_RMDIR) { + if (!simplefs_node_is_dir(child)) { + return fuse_write_reply(a->fd, h->unique, -ENOTDIR, NULL, 0); + } + if (simplefs_has_children(&a->fs, child->nodeid)) { + return fuse_write_reply(a->fd, h->unique, -ENOTEMPTY, NULL, 0); + } + } else { + if (simplefs_node_is_dir(child)) { + return fuse_write_reply(a->fd, h->unique, -EISDIR, NULL, 0); + } + } + child->used = 0; + return fuse_write_reply(a->fd, h->unique, 0, NULL, 0); + } + case FUSE_RENAME: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + const struct fuse_rename_in *in = (const struct fuse_rename_in *)payload; + const char *oldname = NULL; + const char *newname = NULL; + if (simplefs_parse_two_names(payload, payload_len, sizeof(*in), &oldname, &newname) != 0) { + return -1; + } + return simplefs_do_rename(a, h, in->newdir, 0, oldname, newname); + } + case FUSE_RENAME2: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + const struct fuse_rename2_in *in = (const struct fuse_rename2_in *)payload; + const char *oldname = NULL; + const char *newname = NULL; + if (simplefs_parse_two_names(payload, payload_len, sizeof(*in), &oldname, &newname) != 0) { + return -1; + } + if (a->rename2_count) { + (*a->rename2_count)++; + } + return simplefs_do_rename(a, h, in->newdir, in->flags, oldname, newname); + } + case FUSE_SETATTR: { + if (!a->enable_write_ops) { + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } + if (payload_len < sizeof(struct fuse_setattr_in)) { + return -1; + } + const struct fuse_setattr_in *in = (const struct fuse_setattr_in *)payload; + struct simplefs_node *node = simplefs_find_node(&a->fs, h->nodeid); + if (!node) { + return fuse_write_reply(a->fd, h->unique, -ENOENT, NULL, 0); + } + if (simplefs_node_is_dir(node) || simplefs_node_is_symlink(node)) { + return fuse_write_reply(a->fd, h->unique, -EINVAL, NULL, 0); + } + if (in->valid & FATTR_SIZE) { + if (in->size > SIMPLEFS_DATA_MAX) { + return fuse_write_reply(a->fd, h->unique, -EFBIG, NULL, 0); + } + node->size = (size_t)in->size; + } + if (in->valid & FATTR_MODE) { + node->mode = in->mode; + } + struct fuse_attr_out out; + memset(&out, 0, sizeof(out)); + simplefs_fill_attr(node, &out.attr); + return fuse_write_reply(a->fd, h->unique, 0, &out, sizeof(out)); + } + default: + return fuse_write_reply(a->fd, h->unique, -ENOSYS, NULL, 0); + } +} + +static inline void *fuse_daemon_thread(void *arg) { + struct fuse_daemon_args *a = (struct fuse_daemon_args *)arg; + unsigned char *buf = (unsigned char *)malloc(FUSE_TEST_BUF_SIZE); + if (!buf) { + return NULL; + } + + simplefs_init(&a->fs); + if (a->root_mode_override) { + a->fs.nodes[0].mode = a->root_mode_override; + } + if (a->hello_mode_override) { + a->fs.nodes[1].mode = a->hello_mode_override; + } + + while (!*a->stop) { + FUSE_TEST_LOG("daemon read start"); + ssize_t n = read(a->fd, buf, FUSE_TEST_BUF_SIZE); + if (n < 0) { + FUSE_TEST_LOG("daemon read error n=%zd errno=%d", n, errno); + if (errno == EINTR) + continue; + if (errno == ENOTCONN) + break; + continue; + } + if (n == 0) { + FUSE_TEST_LOG("daemon read EOF"); + break; + } + FUSE_TEST_LOG("daemon read n=%zd", n); + struct fuse_in_header *h = (struct fuse_in_header *)buf; + if ((size_t)n != h->len) { + FUSE_TEST_LOG("daemon short read n=%zd hdr.len=%u", n, h->len); + continue; + } + (void)fuse_handle_one(a, buf, (size_t)n); + if (a->exit_after_init && a->init_done && *a->init_done) { + break; + } + } + free(buf); + return NULL; +} + +static inline int ensure_dir(const char *path) { + struct stat st; + if (stat(path, &st) == 0) { + if (S_ISDIR(st.st_mode)) + return 0; + errno = ENOTDIR; + return -1; + } + return mkdir(path, 0755); +} diff --git a/user/apps/tests/dunitest/whitelist.txt b/user/apps/tests/dunitest/whitelist.txt index 1dd8a5429..22c2e88a3 100644 --- a/user/apps/tests/dunitest/whitelist.txt +++ b/user/apps/tests/dunitest/whitelist.txt @@ -2,3 +2,5 @@ # 格式: 相对于 bin/ 的用例名(去除可执行文件的 _test 后缀) demo/gtest_demo normal/capability +fuse/fuse_core +fuse/fuse_extended diff --git a/user/apps/tests/syscall/gvisor/whitelist.txt b/user/apps/tests/syscall/gvisor/whitelist.txt index 5ab44904b..5ddb852db 100644 --- a/user/apps/tests/syscall/gvisor/whitelist.txt +++ b/user/apps/tests/syscall/gvisor/whitelist.txt @@ -16,6 +16,7 @@ pause_test chroot_test creat_test dup_test +fuse_test write_test mkdir_test mknod_test diff --git a/user/dadk/config/all/fuse3_demo.toml b/user/dadk/config/all/fuse3_demo.toml new file mode 100644 index 000000000..16b855128 --- /dev/null +++ b/user/dadk/config/all/fuse3_demo.toml @@ -0,0 +1,36 @@ +# 用户程序名称 +name = "fuse3_demo" +# 版本号 +version = "0.1.0" +# 用户程序描述信息 +description = "libfuse3 static demo and regression test" +# (可选)是否只构建一次,如果为true,DADK会在构建成功后,将构建结果缓存起来,下次构建时,直接使用缓存的构建结果 +build-once = false +# (可选) 是否只安装一次,如果为true,DADK会在安装成功后,不再重复安装 +install-once = false +# 目标架构 +# 可选值:"x86_64", "aarch64", "riscv64" +target-arch = ["x86_64"] +# 任务源 +[task-source] +# 构建类型 +# 可选值:"build-from_source", "install-from-prebuilt" +type = "build-from-source" +# 构建来源 +# "build_from_source" 可选值:"git", "local", "archive" +# "install_from_prebuilt" 可选值:"local", "archive" +source = "local" +# 路径或URL +source-path = "user/apps/fuse3_demo" +# 构建相关信息 +[build] +# (可选)构建命令 +build-command = "make install -j8" +# 安装相关信息 +[install] +# (可选)安装到DragonOS的路径 +in-dragonos-path = "/bin" +# 清除相关信息 +[clean] +# (可选)清除命令 +clean-command = "make clean" diff --git a/user/dadk/config/all/fuse_demo.toml b/user/dadk/config/all/fuse_demo.toml new file mode 100644 index 000000000..e305bab84 --- /dev/null +++ b/user/dadk/config/all/fuse_demo.toml @@ -0,0 +1,36 @@ +# 用户程序名称 +name = "fuse_demo" +# 版本号 +version = "0.1.0" +# 用户程序描述信息 +description = "" +# (可选)是否只构建一次,如果为true,DADK会在构建成功后,将构建结果缓存起来,下次构建时,直接使用缓存的构建结果 +build-once = false +# (可选) 是否只安装一次,如果为true,DADK会在安装成功后,不再重复安装 +install-once = false +# 目标架构 +# 可选值:"x86_64", "aarch64", "riscv64" +target-arch = ["x86_64"] +# 任务源 +[task-source] +# 构建类型 +# 可选值:"build-from_source", "install-from-prebuilt" +type = "build-from-source" +# 构建来源 +# "build_from_source" 可选值:"git", "local", "archive" +# "install_from_prebuilt" 可选值:"local", "archive" +source = "local" +# 路径或URL +source-path = "user/apps/fuse_demo" +# 构建相关信息 +[build] +# (可选)构建命令 +build-command = "make install -j8" +# 安装相关信息 +[install] +# (可选)安装到DragonOS的路径 +in-dragonos-path = "/bin" +# 清除相关信息 +[clean] +# (可选)清除命令 +clean-command = "make clean" diff --git a/user/dadk/config/sets/default/fuse3_demo.toml b/user/dadk/config/sets/default/fuse3_demo.toml new file mode 120000 index 000000000..41167e83c --- /dev/null +++ b/user/dadk/config/sets/default/fuse3_demo.toml @@ -0,0 +1 @@ +../../all/fuse3_demo.toml \ No newline at end of file diff --git a/user/dadk/config/sets/default/fuse_demo.toml b/user/dadk/config/sets/default/fuse_demo.toml new file mode 120000 index 000000000..88df73dd6 --- /dev/null +++ b/user/dadk/config/sets/default/fuse_demo.toml @@ -0,0 +1 @@ +../../all/fuse_demo.toml \ No newline at end of file