From a154cfc53a2f0486808c8433419cae4fe807a5af Mon Sep 17 00:00:00 2001 From: "bootc-bot[bot]" <225049296+bootc-bot[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:50:31 +0000 Subject: [PATCH 1/2] fix(deps): update rust Signed-off-by: bootc-bot[bot] <225049296+bootc-bot[bot]@users.noreply.github.com> --- crates/composefs-fuse/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/composefs-fuse/Cargo.toml b/crates/composefs-fuse/Cargo.toml index 05f6fcfe..18b46cb1 100644 --- a/crates/composefs-fuse/Cargo.toml +++ b/crates/composefs-fuse/Cargo.toml @@ -13,6 +13,6 @@ version.workspace = true [dependencies] anyhow = { version = "1.0.98", default-features = false } composefs = { workspace = true } -fuser = { version = "0.15.1", default-features = false, features = ["abi-7-31"] } +fuser = { version = "0.17.0", default-features = false, features = ["abi-7-31"] } log = { version = "0.4.8", default-features = false } rustix = { version = "1.0.0", default-features = false, features = ["fs", "mount"] } From 1e6c221b630c26adfcfca6c4b8df08b25fc57a15 Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Mon, 30 Mar 2026 11:51:47 -0400 Subject: [PATCH 2/2] fix(deps): Adapt code to fuser 0.17 breaking changes fuser 0.17 requires Filesystem: Send + Sync + 'static, which means the composefs core types need to be thread-safe. This commit: - Changes Rc> to Arc> and Box> to Arc> in the generic_tree Inode enum, making tree nodes shareable across threads. - Changes RefCell> to RwLock> in Stat.xattrs, replacing .borrow()/.borrow_mut() with .read().unwrap()/.write().unwrap(). - Rewrites composefs-fuse to adapt to fuser 0.17's API changes: - &mut self -> &self on all Filesystem trait methods (mutable state moved behind a Mutex) - u64 -> INodeNo/FileHandle/LockOwner newtype wrappers - i32 -> OpenFlags, reply errors use fuser::Errno constants - Session::run() replaced with Session::spawn()?.join() - Session::from_fd() takes a Config parameter - serve_tree_fuse() now takes Arc and Arc by value to satisfy the 'static bound - #![forbid(unsafe_code)] is preserved throughout Assisted-by: OpenCode (claude-opus-4-6) --- crates/composefs-boot/src/selabel.rs | 5 +- crates/composefs-fuse/src/lib.rs | 312 ++++++++++++-------- crates/composefs-oci/src/image.rs | 12 +- crates/composefs-oci/src/tar.rs | 4 +- crates/composefs/fuzz/Cargo.lock | 358 ++++++++++++++++++++--- crates/composefs/fuzz/generate_corpus.rs | 18 +- crates/composefs/src/dumpfile.rs | 52 ++-- crates/composefs/src/erofs/reader.rs | 26 +- crates/composefs/src/erofs/writer.rs | 13 +- crates/composefs/src/fs.rs | 18 +- crates/composefs/src/generic_tree.rs | 94 +++--- crates/composefs/src/repository.rs | 4 +- crates/composefs/src/test.rs | 22 +- crates/composefs/src/tree.rs | 20 +- crates/composefs/tests/mkfs.rs | 10 +- 15 files changed, 653 insertions(+), 315 deletions(-) diff --git a/crates/composefs-boot/src/selabel.rs b/crates/composefs-boot/src/selabel.rs index 1914b1de..59e855fe 100644 --- a/crates/composefs-boot/src/selabel.rs +++ b/crates/composefs-boot/src/selabel.rs @@ -211,7 +211,7 @@ impl Policy { } fn relabel(stat: &Stat, path: &Path, ifmt: u8, policy: &mut Policy) { - let mut xattrs = stat.xattrs.borrow_mut(); + let mut xattrs = stat.xattrs.write().unwrap(); let key = OsStr::new(XATTR_SECURITY_SELINUX); if let Some(label) = policy.lookup(path.as_os_str(), ifmt) { @@ -268,7 +268,8 @@ fn parse_config(file: impl Read) -> Result> { fn strip_selinux_labels(fs: &FileSystem) { fs.for_each_stat(|stat| { stat.xattrs - .borrow_mut() + .write() + .unwrap() .remove(OsStr::new(XATTR_SECURITY_SELINUX)); }); } diff --git a/crates/composefs-fuse/src/lib.rs b/crates/composefs-fuse/src/lib.rs index 44993fac..4bfedbe3 100644 --- a/crates/composefs-fuse/src/lib.rs +++ b/crates/composefs-fuse/src/lib.rs @@ -13,19 +13,19 @@ use std::{ fd::{AsFd, AsRawFd, OwnedFd}, unix::ffi::OsStrExt, }, - rc::Rc, + sync::{Arc, Mutex}, time::{Duration, SystemTime}, }; use anyhow::Context; use fuser::{ - FileAttr, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, ReplyOpen, - Request, Session, SessionACL, + Config, FileAttr, FileHandle, FileType, Filesystem, FopenFlags, Generation, INodeNo, OpenFlags, + ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, ReplyOpen, Request, Session, }; use rustix::{ buffer::spare_capacity, fs::{open, Mode, OFlags}, - io::{pread, Errno}, + io::pread, mount::{ fsconfig_create, fsconfig_set_flag, fsconfig_set_string, fsmount, FsMountFlags, MountAttrFlags, @@ -42,9 +42,9 @@ use composefs::{ const TTL: Duration = Duration::from_secs(1_000_000); #[derive(Debug, Clone)] -enum InodeRef<'a, ObjectID: FsVerityHashValue> { - Directory(&'a Directory, u64), - Leaf(&'a Rc>), +enum InodeRef { + Directory(Arc>, u64), + Leaf(Arc>), } #[derive(Debug)] @@ -53,18 +53,18 @@ enum OpenHandle { Data(Box<[u8]>), } -impl<'a, ObjectID: FsVerityHashValue> InodeRef<'a, ObjectID> { - fn new(inode: &'a Inode, parent: u64) -> Self { +impl InodeRef { + fn new(inode: &Inode, parent: u64) -> Self { match inode { - Inode::Directory(dir) => InodeRef::Directory(dir, parent), - Inode::Leaf(leaf) => InodeRef::Leaf(leaf), + Inode::Directory(dir) => InodeRef::Directory(Arc::clone(dir), parent), + Inode::Leaf(leaf) => InodeRef::Leaf(Arc::clone(leaf)), } } fn ino(&self) -> u64 { match self { - InodeRef::Directory(dir, ..) => *dir as *const Directory as u64, - InodeRef::Leaf(leaf) => Rc::as_ptr(leaf) as u64, + InodeRef::Directory(dir, ..) => Arc::as_ptr(dir) as u64, + InodeRef::Leaf(leaf) => Arc::as_ptr(leaf) as u64, } } @@ -76,7 +76,7 @@ impl<'a, ObjectID: FsVerityHashValue> InodeRef<'a, ObjectID> { .filter(|i| matches!(i, Inode::Directory(..))) .count() } - InodeRef::Leaf(leaf) => Rc::strong_count(leaf), + InodeRef::Leaf(leaf) => Arc::strong_count(leaf), }) as u32 } @@ -108,7 +108,7 @@ impl<'a, ObjectID: FsVerityHashValue> InodeRef<'a, ObjectID> { } } - fn stat(&self) -> &'a Stat { + fn stat(&self) -> &Stat { match self { InodeRef::Directory(dir, ..) => &dir.stat, InodeRef::Leaf(leaf) => &leaf.stat, @@ -131,7 +131,7 @@ impl<'a, ObjectID: FsVerityHashValue> InodeRef<'a, ObjectID> { let mtime = SystemTime::UNIX_EPOCH + Duration::from_secs(stat.st_mtim_sec as u64); FileAttr { - ino: self.ino(), + ino: INodeNo(self.ino()), size: self.size(), blocks: 1, atime: mtime, @@ -150,120 +150,156 @@ impl<'a, ObjectID: FsVerityHashValue> InodeRef<'a, ObjectID> { } } +/// Mutable state for the FUSE filesystem, protected by a Mutex since fuser 0.17 +/// requires `Filesystem` methods to take `&self` (not `&mut self`). #[derive(Debug)] -struct TreeFuse<'a, ObjectID: FsVerityHashValue> { - repo: &'a Repository, - inodes: HashMap>, +struct TreeFuseState { + inodes: HashMap>, attrs: HashMap, handles: HashMap, next_fh: u64, } -impl<'a, ObjectID: FsVerityHashValue> TreeFuse<'a, ObjectID> { - fn inode_ref(&mut self, inode: &'a Inode, parent: u64) -> InodeRef<'a, ObjectID> { +#[derive(Debug)] +struct TreeFuse { + repo: Arc>, + state: Mutex>, +} + +impl TreeFuse { + fn inode_ref( + state: &mut TreeFuseState, + inode: &Inode, + parent: u64, + ) -> InodeRef { let iref = InodeRef::new(inode, parent); - self.inodes.insert(iref.ino(), iref.clone()); + state.inodes.insert(iref.ino(), iref.clone()); iref } - fn iref_fileattr(&mut self, iref: &InodeRef) -> &FileAttr { - self.attrs.insert(iref.ino(), iref.fileattr()); - self.attrs.get(&iref.ino()).unwrap() + fn iref_fileattr(state: &mut TreeFuseState, iref: &InodeRef) -> FileAttr { + let attr = iref.fileattr(); + state.attrs.insert(iref.ino(), attr); + attr } - fn inode_fileattr(&mut self, inode: &'a Inode, parent: u64) -> &FileAttr { - let iref = self.inode_ref(inode, parent); - self.attrs.insert(iref.ino(), iref.fileattr()); - self.attrs.get(&iref.ino()).unwrap() + fn inode_fileattr( + state: &mut TreeFuseState, + inode: &Inode, + parent: u64, + ) -> FileAttr { + let iref = Self::inode_ref(state, inode, parent); + let attr = iref.fileattr(); + state.attrs.insert(iref.ino(), attr); + attr } } -impl Filesystem for TreeFuse<'_, ObjectID> { - fn statfs(&mut self, _req: &Request<'_>, _ino: u64, reply: fuser::ReplyStatfs) { +impl Filesystem for TreeFuse { + fn statfs(&self, _req: &Request, _ino: INodeNo, reply: fuser::ReplyStatfs) { reply.statfs(0, 0, 0, 0, 0, 4096, 255, 4096); } - fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { + fn lookup(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEntry) { + let parent = parent.0; log::trace!("lookup {parent} {name:?}"); - let Some(InodeRef::Directory(dir, ..)) = self.inodes.get(&parent) else { - log::error!("lookup({parent}, {name:?}) parent does not exist"); - return reply.error(Errno::BADF.raw_os_error()); - }; + let mut state = self.state.lock().unwrap(); - // dir is &&Directory which means it holds a reference to the image and also a reference to - // self. Dereference to drop the spurious self-reference to allow further mutability. - let dir = *dir; + // Clone the directory Arc to release the borrow on state + let dir = match state.inodes.get(&parent) { + Some(InodeRef::Directory(dir, ..)) => Arc::clone(dir), + _ => { + log::error!( + "lookup({parent}, {name:?}) parent does not exist or is not a directory" + ); + return reply.error(fuser::Errno::EBADF); + } + }; match dir.lookup(name) { - Some(inode) => reply.entry(&TTL, self.inode_fileattr(inode, parent), 0), - None => reply.error(Errno::NOENT.raw_os_error()), + Some(inode) => { + let attr = Self::inode_fileattr(&mut state, inode, parent); + reply.entry(&TTL, &attr, Generation(0)); + } + None => reply.error(fuser::Errno::ENOENT), } } - fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option, reply: ReplyAttr) { - if let Some(attrs) = self.attrs.get(&ino) { + fn getattr(&self, _req: &Request, ino: INodeNo, _fh: Option, reply: ReplyAttr) { + let ino = ino.0; + let mut state = self.state.lock().unwrap(); + if let Some(attrs) = state.attrs.get(&ino) { return reply.attr(&TTL, attrs); } - let Some(iref) = self.inodes.get(&ino) else { + let Some(iref) = state.inodes.get(&ino) else { log::error!("getattr({ino}) inode does not exist"); - return reply.error(Errno::BADF.raw_os_error()); + return reply.error(fuser::Errno::EBADF); }; - // iref is &InodeRef which means it holds a reference to self. Drop that. + // Clone to release the borrow on state let iref = iref.clone(); - - reply.attr(&TTL, self.iref_fileattr(&iref)); + let attr = Self::iref_fileattr(&mut state, &iref); + reply.attr(&TTL, &attr); } - fn readlink(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyData) { - let Some(InodeRef::Leaf(leaf, ..)) = self.inodes.get(&ino) else { - return reply.error(Errno::INVAL.raw_os_error()); + fn readlink(&self, _req: &Request, ino: INodeNo, reply: ReplyData) { + let ino = ino.0; + let state = self.state.lock().unwrap(); + let Some(InodeRef::Leaf(leaf)) = state.inodes.get(&ino) else { + return reply.error(fuser::Errno::EINVAL); }; let LeafContent::Symlink(target) = &leaf.content else { - return reply.error(Errno::INVAL.raw_os_error()); + return reply.error(fuser::Errno::EINVAL); }; reply.data(target.as_bytes()); } - fn opendir(&mut self, _req: &Request<'_>, _ino: u64, _flags: i32, reply: ReplyOpen) { - reply.opened(0, 0); + fn opendir(&self, _req: &Request, _ino: INodeNo, _flags: OpenFlags, reply: ReplyOpen) { + reply.opened(FileHandle(0), FopenFlags::empty()); } fn readdir( - &mut self, + &self, _req: &Request, - ino: u64, - _fh: u64, - mut offset: i64, + ino: INodeNo, + _fh: FileHandle, + mut offset: u64, mut reply: ReplyDirectory, ) { - let Some(InodeRef::Directory(dir, parent)) = self.inodes.get(&ino) else { - log::error!("readdir({ino}) inode is not a directory"); - return reply.error(Errno::BADF.raw_os_error()); + let ino = ino.0; + let mut state = self.state.lock().unwrap(); + + // Clone the directory Arc and parent to release the borrow on state + let (dir, parent) = match state.inodes.get(&ino) { + Some(InodeRef::Directory(dir, parent)) => (Arc::clone(dir), *parent), + _ => { + log::error!("readdir({ino}) inode is not a directory"); + return reply.error(fuser::Errno::EBADF); + } }; if offset == 0 { offset += 1; - if reply.add(ino, offset, FileType::Directory, ".") { + if reply.add(INodeNo(ino), offset, FileType::Directory, ".") { return reply.ok(); } } if offset == 1 { offset += 1; - if reply.add(*parent, offset, FileType::Directory, "..") { + if reply.add(INodeNo(parent), offset, FileType::Directory, "..") { return reply.ok(); } } for (name, inode) in dir.sorted_entries().skip(offset as usize - 2) { - let iref = self.inode_ref(inode, ino); + let iref = Self::inode_ref(&mut state, inode, ino); offset += 1; - if reply.add(iref.ino(), offset, iref.kind(), name) { + if reply.add(INodeNo(iref.ino()), offset, iref.kind(), name) { break; } } @@ -272,51 +308,55 @@ impl Filesystem for TreeFuse<'_, ObjectID> { } fn releasedir( - &mut self, - _req: &Request<'_>, - _ino: u64, - _fh: u64, - _flags: i32, + &self, + _req: &Request, + _ino: INodeNo, + _fh: FileHandle, + _flags: OpenFlags, reply: fuser::ReplyEmpty, ) { reply.ok(); } fn getxattr( - &mut self, - _req: &Request<'_>, - ino: u64, + &self, + _req: &Request, + ino: INodeNo, name: &OsStr, size: u32, reply: fuser::ReplyXattr, ) { - let Some(iref) = self.inodes.get(&ino) else { + let ino = ino.0; + let state = self.state.lock().unwrap(); + let Some(iref) = state.inodes.get(&ino) else { log::error!("getxattr({ino}, {name:?}, {size}) inode does not exist"); - return reply.error(Errno::BADF.raw_os_error()); + return reply.error(fuser::Errno::EBADF); }; - let xattrs = iref.stat().xattrs.borrow(); + let xattrs = iref.stat().xattrs.read().unwrap(); let Some(value) = xattrs.get(name) else { - return reply.error(Errno::NODATA.raw_os_error()); + return reply.error(fuser::Errno::ENODATA); }; if size == 0 { return reply.size(value.len() as u32); } else if value.len() > size as usize { - return reply.error(Errno::RANGE.raw_os_error()); + return reply.error(fuser::Errno::ERANGE); } reply.data(value); } - fn listxattr(&mut self, _req: &Request<'_>, ino: u64, size: u32, reply: fuser::ReplyXattr) { - let Some(iref) = self.inodes.get(&ino) else { + fn listxattr(&self, _req: &Request, ino: INodeNo, size: u32, reply: fuser::ReplyXattr) { + let ino = ino.0; + let state = self.state.lock().unwrap(); + let Some(iref) = state.inodes.get(&ino) else { log::error!("listxattr({ino}, {size}) inode does not exist"); - return reply.error(Errno::BADF.raw_os_error()); + return reply.error(fuser::Errno::EBADF); }; let mut list = vec![]; - for name in iref.stat().xattrs.borrow().keys() { + for name in iref.stat().xattrs.read().unwrap().keys() { list.extend_from_slice(name.as_bytes()); list.push(b'\0'); } @@ -324,70 +364,79 @@ impl Filesystem for TreeFuse<'_, ObjectID> { if size == 0 { return reply.size(list.len() as u32); } else if list.len() > size as usize { - return reply.error(Errno::RANGE.raw_os_error()); + return reply.error(fuser::Errno::ERANGE); } reply.data(&list); } - fn open(&mut self, _req: &Request<'_>, ino: u64, _flags: i32, reply: ReplyOpen) { + fn open(&self, _req: &Request, ino: INodeNo, _flags: OpenFlags, reply: ReplyOpen) { + let ino = ino.0; log::trace!("open({ino})"); - let Some(iref) = self.inodes.get(&ino) else { - log::error!("open({ino}) inode does not exist"); - return reply.error(Errno::BADF.raw_os_error()); - }; - - let InodeRef::Leaf(leaf) = iref else { - log::error!("open({ino}) inode is a directory"); - return reply.error(Errno::BADF.raw_os_error()); + let mut state = self.state.lock().unwrap(); + + // Clone the leaf Arc to release the borrow on state + let leaf = match state.inodes.get(&ino) { + Some(InodeRef::Leaf(leaf)) => Arc::clone(leaf), + Some(_) => { + log::error!("open({ino}) inode is a directory"); + return reply.error(fuser::Errno::EBADF); + } + None => { + log::error!("open({ino}) inode does not exist"); + return reply.error(fuser::Errno::EBADF); + } }; let handle = match &leaf.content { LeafContent::Regular(RegularFile::External(id, ..)) => { let Ok(fd) = self.repo.open_object(id) else { log::error!("open({ino}) open object failed"); - return reply.error(Errno::INVAL.raw_os_error()); + return reply.error(fuser::Errno::EINVAL); }; OpenHandle::Fd(fd) } LeafContent::Regular(RegularFile::Inline(data)) => OpenHandle::Data(data.clone()), _ => { log::error!("open({ino}) non-regular file"); - return reply.error(Errno::BADF.raw_os_error()); + return reply.error(fuser::Errno::EBADF); } }; - let fh = self.next_fh; - self.next_fh += 1; - log::debug!("self.handles.insert({fh}, {handle:?})"); - self.handles.insert(fh, handle); - reply.opened(fh, 0); + let fh = state.next_fh; + state.next_fh += 1; + log::debug!("state.handles.insert({fh}, {handle:?})"); + state.handles.insert(fh, handle); + reply.opened(FileHandle(fh), FopenFlags::empty()); } fn read( - &mut self, - _req: &Request<'_>, - _ino: u64, - fh: u64, - offset: i64, + &self, + _req: &Request, + _ino: INodeNo, + fh: FileHandle, + offset: u64, size: u32, - _flags: i32, - _lock_owner: Option, + _flags: OpenFlags, + _lock_owner: Option, reply: fuser::ReplyData, ) { - match self.handles.get(&fh) { + let fh = fh.0; + let state = self.state.lock().unwrap(); + match state.handles.get(&fh) { Some(OpenHandle::Fd(fd)) => { let mut data = Vec::with_capacity(size as usize); - match pread(fd, spare_capacity(&mut data), offset as u64) { + match pread(fd, spare_capacity(&mut data), offset) { Ok(_) => reply.data(&data), - Err(errno) => reply.error(errno.raw_os_error()), + Err(errno) => reply.error(fuser::Errno::from_i32(errno.raw_os_error())), } } Some(OpenHandle::Data(data)) => { - if offset as usize > data.len() { + let offset = offset as usize; + if offset > data.len() { reply.data(b""); } else { - let mut data = &data[offset as usize..]; + let mut data = &data[offset..]; if data.len() > size as usize { data = &data[..size as usize]; } @@ -396,26 +445,28 @@ impl Filesystem for TreeFuse<'_, ObjectID> { } None => { log::error!("Handle doesn't exist: pread({fh}, {size}, {offset})"); - reply.error(Errno::BADF.raw_os_error()); + reply.error(fuser::Errno::EBADF); } } } fn release( - &mut self, - _req: &Request<'_>, - _ino: u64, - fh: u64, - _flags: i32, - _lock_owner: Option, + &self, + _req: &Request, + _ino: INodeNo, + fh: FileHandle, + _flags: OpenFlags, + _lock_owner: Option, _flush: bool, reply: fuser::ReplyEmpty, ) { - match self.handles.remove(&fh) { + let fh = fh.0; + let mut state = self.state.lock().unwrap(); + match state.handles.remove(&fh) { Some(_) => reply.ok(), None => { log::error!("Handle doesn't exist: close({fh})"); - reply.error(Errno::BADF.raw_os_error()) + reply.error(fuser::Errno::EBADF) } } } @@ -461,17 +512,22 @@ pub fn mount_fuse(dev_fuse: impl AsFd) -> anyhow::Result { /// Serves a FUSE filesystem exposing the content of `root`, backed by `repo`. /// /// You should have called mount_fuse() on the dev_fuse fd to establish a mount point. -pub fn serve_tree_fuse<'a, ObjectID: FsVerityHashValue>( +pub fn serve_tree_fuse( dev_fuse: OwnedFd, - root: &'a Directory, - repo: &'a Repository, + root: Arc>, + repo: Arc>, ) -> std::io::Result<()> { + let root_ref = InodeRef::Directory(root, 1); + let root_ino = root_ref.ino(); let fs = TreeFuse:: { repo, - inodes: HashMap::from([(1, InodeRef::Directory(root, 1))]), - attrs: Default::default(), - handles: Default::default(), - next_fh: 1, + state: Mutex::new(TreeFuseState { + inodes: HashMap::from([(root_ino, root_ref)]), + attrs: Default::default(), + handles: Default::default(), + next_fh: 1, + }), }; - Session::from_fd(fs, dev_fuse, SessionACL::All).run() + let session = Session::from_fd(fs, dev_fuse, fuser::SessionACL::All, Config::default())?; + session.spawn()?.join() } diff --git a/crates/composefs-oci/src/image.rs b/crates/composefs-oci/src/image.rs index 866d94c7..fe00f5bd 100644 --- a/crates/composefs-oci/src/image.rs +++ b/crates/composefs-oci/src/image.rs @@ -8,7 +8,7 @@ //! and builds a complete filesystem by processing all layers in order. The `process_entry()` function //! handles individual tar entries and implements overlayfs whiteout semantics for proper layer merging. -use std::{ffi::OsStr, os::unix::ffi::OsStrExt, rc::Rc}; +use std::{ffi::OsStr, os::unix::ffi::OsStrExt, sync::Arc}; use anyhow::{ensure, Context, Result}; use composefs::util::DigestWrite; @@ -50,8 +50,8 @@ pub fn process_entry( } let inode = match entry.item { - TarItem::Directory => Inode::Directory(Box::from(Directory::new(entry.stat))), - TarItem::Leaf(content) => Inode::Leaf(Rc::new(Leaf { + TarItem::Directory => Inode::Directory(Arc::from(Directory::new(entry.stat))), + TarItem::Leaf(content) => Inode::Leaf(Arc::new(Leaf { stat: entry.stat, content, })), @@ -145,7 +145,7 @@ mod test { fsverity::Sha256HashValue, tree::{LeafContent, RegularFile, Stat}, }; - use std::{cell::RefCell, collections::BTreeMap, io::BufRead, path::PathBuf}; + use std::{collections::BTreeMap, io::BufRead, path::PathBuf, sync::RwLock}; use super::*; @@ -157,7 +157,7 @@ mod test { st_uid: 0, st_gid: 0, st_mtim_sec: 0, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), }, item: TarItem::Leaf(LeafContent::Regular(RegularFile::Inline([].into()))), } @@ -171,7 +171,7 @@ mod test { st_uid: 0, st_gid: 0, st_mtim_sec: 0, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), }, item: TarItem::Directory, } diff --git a/crates/composefs-oci/src/tar.rs b/crates/composefs-oci/src/tar.rs index 5fee3524..4bbee1e7 100644 --- a/crates/composefs-oci/src/tar.rs +++ b/crates/composefs-oci/src/tar.rs @@ -10,7 +10,6 @@ //! The `TarEntry` and `TarItem` types represent processed tar entries in composefs format. use std::{ - cell::RefCell, collections::BTreeMap, ffi::{OsStr, OsString}, fmt, @@ -18,6 +17,7 @@ use std::{ os::unix::prelude::{OsStrExt, OsStringExt}, path::PathBuf, sync::Arc, + sync::RwLock, }; use anyhow::{bail, ensure, Result}; @@ -425,7 +425,7 @@ pub fn get_entry( st_gid: entry.gid as u32, st_mode: entry.mode, st_mtim_sec: entry.mtime as i64, - xattrs: RefCell::new(xattrs), + xattrs: RwLock::new(xattrs), }, item, })); diff --git a/crates/composefs/fuzz/Cargo.lock b/crates/composefs/fuzz/Cargo.lock index c64f299e..7028c165 100644 --- a/crates/composefs/fuzz/Cargo.lock +++ b/crates/composefs/fuzz/Cargo.lock @@ -22,11 +22,11 @@ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" -version = "0.10.4" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -53,6 +53,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "composefs" version = "0.3.0" @@ -65,6 +76,7 @@ dependencies = [ "once_cell", "rand", "rustix", + "serde", "sha2", "thiserror", "tokio", @@ -91,33 +103,38 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.17" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ - "generic-array", - "typenum", + "hybrid-array", ] [[package]] name = "digest" -version = "0.10.7" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ "block-buffer", "crypto-common", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -146,14 +163,10 @@ dependencies = [ ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" @@ -163,26 +176,100 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core", "wasip2", + "wasip3", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hybrid-array" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a79f2aff40c18ab8615ddc5caa9eb5b96314aef18fe5823090f204ad988e813" +dependencies = [ + "typenum", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + [[package]] name = "jobserver" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom", + "getrandom 0.3.4", "libc", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.183" @@ -211,6 +298,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + [[package]] name = "mio" version = "1.1.1" @@ -241,12 +334,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ - "zerocopy", + "proc-macro2", + "syn", ] [[package]] @@ -274,33 +368,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand" -version = "0.9.2" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "rand_chacha" -version = "0.9.0" +name = "rand" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "ppv-lite86", + "chacha20", + "getrandom 0.4.2", "rand_core", ] [[package]] name = "rand_core" -version = "0.9.5" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom", -] +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" [[package]] name = "rustix" @@ -315,11 +403,60 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "sha2" -version = "0.10.9" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures", @@ -412,10 +549,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "version_check" -version = "0.9.5" +name = "unicode-xid" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "wasi" @@ -432,6 +569,49 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -452,6 +632,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "xxhash-rust" @@ -479,6 +741,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zstd" version = "0.13.3" diff --git a/crates/composefs/fuzz/generate_corpus.rs b/crates/composefs/fuzz/generate_corpus.rs index bb92289d..4cfc742a 100644 --- a/crates/composefs/fuzz/generate_corpus.rs +++ b/crates/composefs/fuzz/generate_corpus.rs @@ -7,12 +7,12 @@ //! Run via: `cargo run --manifest-path crates/composefs/fuzz/Cargo.toml --bin generate-corpus` //! or: `just generate-corpus` -use std::cell::RefCell; use std::collections::BTreeMap; use std::ffi::{OsStr, OsString}; use std::fs; use std::path::Path; -use std::rc::Rc; +use std::sync::Arc; +use std::sync::RwLock; use composefs::erofs::writer::mkfs_erofs; use composefs::fsverity::{FsVerityHashValue, Sha256HashValue}; @@ -31,7 +31,7 @@ fn stat(mode: u32, uid: u32, gid: u32, mtime: i64) -> Stat { st_uid: uid, st_gid: gid, st_mtim_sec: mtime, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), } } @@ -52,14 +52,14 @@ fn empty_root() -> FileSystem { /// Insert a leaf into a directory. fn insert_leaf(dir: &mut Dir, name: &str, leaf: Leaf) { - dir.insert(OsStr::new(name), Inode::Leaf(Rc::new(leaf))); + dir.insert(OsStr::new(name), Inode::Leaf(Arc::new(leaf))); } /// Insert a subdirectory into a directory, returning a mutable reference to it. fn insert_dir<'a>(parent: &'a mut Dir, name: &str, s: Stat) -> &'a mut Dir { parent.insert( OsStr::new(name), - Inode::Directory(Box::new(generic_tree::Directory::new(s))), + Inode::Directory(Arc::new(generic_tree::Directory::new(s))), ); parent.get_directory_mut(OsStr::new(name)).unwrap() } @@ -231,7 +231,7 @@ fn main() { let mut fs = empty_root(); let xattr_stat = file_stat(); { - let mut xattrs = xattr_stat.xattrs.borrow_mut(); + let mut xattrs = xattr_stat.xattrs.write().unwrap(); xattrs.insert( Box::from(OsStr::new("security.selinux")), Box::from(b"system_u:object_r:usr_t:s0".as_slice()), @@ -322,10 +322,10 @@ fn main() { seeds.push(("mixed_types", image.into())); } - // 13. Hardlink — two entries sharing the same Rc (nlink > 1) + // 13. Hardlink — two entries sharing the same Arc (nlink > 1) { let mut fs = empty_root(); - let shared = Rc::new(Leaf { + let shared = Arc::new(Leaf { stat: file_stat(), content: LeafContent::Regular(RegularFile::Inline( b"shared content".to_vec().into_boxed_slice(), @@ -333,7 +333,7 @@ fn main() { }); fs.root.insert( OsStr::new("original").into(), - Inode::Leaf(Rc::clone(&shared)), + Inode::Leaf(Arc::clone(&shared)), ); fs.root .insert(OsStr::new("hardlink").into(), Inode::Leaf(shared)); diff --git a/crates/composefs/src/dumpfile.rs b/crates/composefs/src/dumpfile.rs index 5aaa4927..f6de049a 100644 --- a/crates/composefs/src/dumpfile.rs +++ b/crates/composefs/src/dumpfile.rs @@ -7,14 +7,14 @@ //! The module handles file metadata, extended attributes, and hardlink tracking. use std::{ - cell::RefCell, collections::{BTreeMap, HashMap}, ffi::{OsStr, OsString}, fmt, io::{BufWriter, Write}, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, - rc::Rc, + sync::Arc, + sync::RwLock, }; use anyhow::{ensure, Context, Result}; @@ -131,7 +131,7 @@ fn write_entry( write_empty(writer)?; } - for (key, value) in &*stat.xattrs.borrow() { + for (key, value) in &*stat.xattrs.read().unwrap() { write!(writer, " ")?; write_escaped_raw(writer, key.as_bytes(), EscapeEquals::Yes)?; write!(writer, "=")?; @@ -336,12 +336,12 @@ impl<'a, W: Write, ObjectID: FsVerityHashValue> DumpfileWriter<'a, W, ObjectID> } #[context("Writing leaf to dumpfile: {}", path.display())] - fn write_leaf(&mut self, path: &Path, leaf: &Rc>) -> Result<()> { - let nlink = Rc::strong_count(leaf); + fn write_leaf(&mut self, path: &Path, leaf: &Arc>) -> Result<()> { + let nlink = Arc::strong_count(leaf); if nlink > 1 { // This is a hardlink. We need to handle that specially. - let ptr = Rc::as_ptr(leaf); + let ptr = Arc::as_ptr(leaf); if let Some(target) = self.hardlinks.get(&ptr) { return writeln_fmt(self.writer, |fmt| write_hardlink(fmt, path, target)); } @@ -388,7 +388,7 @@ pub fn dump_single_dir( /// Write a single file pub fn dump_single_file( writer: &mut impl Write, - file: &Rc>, + file: &Arc>, path: PathBuf, ) -> Result<()> { // default pipe capacity on Linux is 16 pages (65536 bytes), but @@ -408,7 +408,7 @@ pub fn dump_single_file( pub fn add_entry_to_filesystem( fs: &mut FileSystem, entry: Entry<'_>, - hardlinks: &mut HashMap>>, + hardlinks: &mut HashMap>>, ) -> Result<()> { let path = entry.path.as_ref(); @@ -438,7 +438,7 @@ pub fn add_entry_to_filesystem( let inode = match entry.item { Item::Directory { .. } => { let stat = entry_to_stat(&entry); - Inode::Directory(Box::new(Directory::new(stat))) + Inode::Directory(Arc::new(Directory::new(stat))) } Item::Hardlink { ref target } => { // Look up the target in our hardlinks map and clone the Rc @@ -455,7 +455,7 @@ pub fn add_entry_to_filesystem( std::borrow::Cow::Owned(d) => d.clone().into_boxed_slice(), }; let content = LeafContent::Regular(RegularFile::Inline(data)); - Inode::Leaf(Rc::new(Leaf { stat, content })) + Inode::Leaf(Arc::new(Leaf { stat, content })) } Item::Regular { size, @@ -468,7 +468,7 @@ pub fn add_entry_to_filesystem( .ok_or_else(|| anyhow::anyhow!("External file missing fsverity digest"))?; let object_id = ObjectID::from_hex(digest)?; let content = LeafContent::Regular(RegularFile::External(object_id, size)); - Inode::Leaf(Rc::new(Leaf { stat, content })) + Inode::Leaf(Arc::new(Leaf { stat, content })) } Item::Device { rdev, .. } => { let stat = entry_to_stat(&entry); @@ -478,7 +478,7 @@ pub fn add_entry_to_filesystem( } else { LeafContent::CharacterDevice(rdev) }; - Inode::Leaf(Rc::new(Leaf { stat, content })) + Inode::Leaf(Arc::new(Leaf { stat, content })) } Item::Symlink { ref target, .. } => { let stat = entry_to_stat(&entry); @@ -487,12 +487,12 @@ pub fn add_entry_to_filesystem( std::borrow::Cow::Owned(t) => Box::from(t.as_os_str()), }; let content = LeafContent::Symlink(target_os); - Inode::Leaf(Rc::new(Leaf { stat, content })) + Inode::Leaf(Arc::new(Leaf { stat, content })) } Item::Fifo { .. } => { let stat = entry_to_stat(&entry); let content = LeafContent::Fifo; - Inode::Leaf(Rc::new(Leaf { stat, content })) + Inode::Leaf(Arc::new(Leaf { stat, content })) } }; @@ -525,7 +525,7 @@ fn entry_to_stat(entry: &Entry<'_>) -> Stat { st_uid: entry.uid, st_gid: entry.gid, st_mtim_sec: entry.mtime.sec as i64, - xattrs: RefCell::new(xattrs), + xattrs: RwLock::new(xattrs), } } @@ -640,11 +640,11 @@ mod tests { }; // They should all point to the same Rc (same pointer) - assert!(Rc::ptr_eq(original_leaf, hardlink1_leaf)); - assert!(Rc::ptr_eq(original_leaf, hardlink2_leaf)); + assert!(Arc::ptr_eq(original_leaf, hardlink1_leaf)); + assert!(Arc::ptr_eq(original_leaf, hardlink2_leaf)); // Verify the strong count is 3 (original + 2 hardlinks) - assert_eq!(Rc::strong_count(original_leaf), 3); + assert_eq!(Arc::strong_count(original_leaf), 3); // Verify content if let LeafContent::Regular(RegularFile::Inline(data)) = &original_leaf.content { @@ -690,8 +690,8 @@ mod tests { /// not treat specially. #[test] fn test_xattr_empty_and_dash_values_round_trip() -> Result<()> { - use std::cell::RefCell; use std::collections::BTreeMap; + use std::sync::RwLock; let mut xattrs = BTreeMap::new(); xattrs.insert( @@ -708,15 +708,15 @@ mod tests { st_uid: 0, st_gid: 0, st_mtim_sec: 0, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), }); - let leaf = std::rc::Rc::new(Leaf { + let leaf = std::sync::Arc::new(Leaf { stat: Stat { st_mode: 0o644, st_uid: 0, st_gid: 0, st_mtim_sec: 0, - xattrs: RefCell::new(xattrs), + xattrs: RwLock::new(xattrs), }, content: LeafContent::Regular(RegularFile::Inline(b"test".to_vec().into())), }); @@ -736,22 +736,22 @@ mod tests { /// hardlinks correctly. #[test] fn test_hardlink_write_round_trip() -> Result<()> { - use std::cell::RefCell; use std::collections::BTreeMap; + use std::sync::RwLock; let stat = || Stat { st_mode: 0o644, st_uid: 0, st_gid: 0, st_mtim_sec: 0, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), }; let mut fs = FileSystem::::new(Stat { st_mode: 0o755, ..stat() }); - let leaf = std::rc::Rc::new(Leaf { + let leaf = std::sync::Arc::new(Leaf { stat: stat(), content: LeafContent::Regular(RegularFile::Inline(b"data".to_vec().into())), }); @@ -770,7 +770,7 @@ mod tests { let orig = fs2.root.lookup(OsStr::new("original")).unwrap(); let link = fs2.root.lookup(OsStr::new("link")).unwrap(); match (orig, link) { - (Inode::Leaf(a), Inode::Leaf(b)) => assert!(Rc::ptr_eq(a, b)), + (Inode::Leaf(a), Inode::Leaf(b)) => assert!(Arc::ptr_eq(a, b)), _ => panic!("expected both to be leaves"), } diff --git a/crates/composefs/src/erofs/reader.rs b/crates/composefs/src/erofs/reader.rs index b3a8439d..70986fd0 100644 --- a/crates/composefs/src/erofs/reader.rs +++ b/crates/composefs/src/erofs/reader.rs @@ -5,12 +5,12 @@ //! reference collection for garbage collection. use core::mem::size_of; -use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::ffi::OsStr; use std::ops::Range; use std::os::unix::ffi::OsStrExt; -use std::rc::Rc; +use std::sync::Arc; +use std::sync::RwLock; use anyhow::Context; use thiserror::Error; @@ -1118,7 +1118,7 @@ fn stat_from_inode_for_tree(img: &Image, inode: &InodeType) -> anyhow::Result { /// The on-disk nlink value from the inode header. expected: u32, - /// Reference to the leaf so we can check Rc::strong_count() later. - leaf: Rc>, + /// Reference to the leaf so we can check Arc::strong_count() later. + leaf: Arc>, } /// Mutable state threaded through the recursive directory traversal. struct TreeBuilder { /// Map from nid to first-seen leaf for hardlink detection. - hardlinks: HashMap>>, + hardlinks: HashMap>>, /// Map from nid to nlink tracking entry for post-traversal validation. nlink_tracker: HashMap>, } @@ -1343,7 +1343,7 @@ impl TreeBuilder { // strong_count includes: one per tree insertion + one held by // nlink_tracker + possibly one in the hardlinks map. let tracker_refs: usize = 1 + usize::from(self.hardlinks.contains_key(nid)); - let tree_refs = Rc::strong_count(&entry.leaf) + let tree_refs = Arc::strong_count(&entry.leaf) .checked_sub(tracker_refs) .expect("strong_count must be >= tracker_refs"); let tree_nlink: u32 = tree_refs @@ -1421,11 +1421,11 @@ fn populate_directory( depth + 1, ) .with_context(|| format!("reading directory {:?}", name))?; - dir.insert(name, tree::Inode::Directory(Box::new(child_dir))); + dir.insert(name, tree::Inode::Directory(Arc::new(child_dir))); } else { // Check if this is a hardlink (same nid seen before) if let Some(existing_leaf) = builder.hardlinks.get(&nid) { - dir.insert(name, tree::Inode::Leaf(Rc::clone(existing_leaf))); + dir.insert(name, tree::Inode::Leaf(Arc::clone(existing_leaf))); continue; } @@ -1476,12 +1476,12 @@ fn populate_directory( _ => anyhow::bail!("unknown file type {:#o} for {:?}", file_type, name), }; - let leaf = Rc::new(tree::Leaf { stat, content }); + let leaf = Arc::new(tree::Leaf { stat, content }); // Track for hardlink detection if nlink > 1 let on_disk_nlink = child_inode.nlink(); if on_disk_nlink > 1 { - builder.hardlinks.insert(nid, Rc::clone(&leaf)); + builder.hardlinks.insert(nid, Arc::clone(&leaf)); } // Track for post-traversal nlink validation @@ -1490,7 +1490,7 @@ fn populate_directory( .entry(nid) .or_insert_with(|| NlinkEntry { expected: on_disk_nlink, - leaf: Rc::clone(&leaf), + leaf: Arc::clone(&leaf), }); dir.insert(name, tree::Inode::Leaf(leaf)); @@ -2101,7 +2101,7 @@ mod tests { let orig_leaf = fs_rt.root.ref_leaf(OsStr::new("original")).unwrap(); let hardlink_leaf = fs_rt.root.ref_leaf(OsStr::new("hardlink")).unwrap(); assert!( - Rc::ptr_eq(&orig_leaf, &hardlink_leaf), + Arc::ptr_eq(&orig_leaf, &hardlink_leaf), "hardlink entries should share the same Rc" ); } diff --git a/crates/composefs/src/erofs/writer.rs b/crates/composefs/src/erofs/writer.rs index 33f5a09e..4afb4e9f 100644 --- a/crates/composefs/src/erofs/writer.rs +++ b/crates/composefs/src/erofs/writer.rs @@ -5,11 +5,10 @@ //! and metadata serialization. use std::{ - cell::RefCell, collections::{BTreeMap, HashMap}, mem::size_of, os::unix::ffi::OsStrExt, - rc::Rc, + sync::Arc, }; use log::trace; @@ -442,7 +441,7 @@ impl<'a, ObjectID: FsVerityHashValue> InodeCollector<'a, ObjectID> { } // Add the normal xattrs. They're already listed in sorted order. - for (name, value) in RefCell::borrow(&stat.xattrs).iter() { + for (name, value) in stat.xattrs.read().unwrap().iter() { let name = name.as_bytes(); if let Some(escapee) = name.strip_prefix(b"trusted.overlay.") { @@ -464,11 +463,11 @@ impl<'a, ObjectID: FsVerityHashValue> InodeCollector<'a, ObjectID> { inode } - fn collect_leaf(&mut self, leaf: &'a Rc>) -> usize { - let nlink = Rc::strong_count(leaf); + fn collect_leaf(&mut self, leaf: &'a Arc>) -> usize { + let nlink = Arc::strong_count(leaf); if nlink > 1 { - if let Some(inode) = self.hardlinks.get(&Rc::as_ptr(leaf)) { + if let Some(inode) = self.hardlinks.get(&Arc::as_ptr(leaf)) { return *inode; } } @@ -482,7 +481,7 @@ impl<'a, ObjectID: FsVerityHashValue> InodeCollector<'a, ObjectID> { ); if nlink > 1 { - self.hardlinks.insert(Rc::as_ptr(leaf), inode); + self.hardlinks.insert(Arc::as_ptr(leaf), inode); } inode diff --git a/crates/composefs/src/fs.rs b/crates/composefs/src/fs.rs index 750dc68b..2fc5aad8 100644 --- a/crates/composefs/src/fs.rs +++ b/crates/composefs/src/fs.rs @@ -5,7 +5,6 @@ //! handling of hardlinks, extended attributes, and repository integration. use std::{ - cell::RefCell, collections::{BTreeMap, HashMap}, ffi::{CStr, OsStr}, fs::File, @@ -13,7 +12,8 @@ use std::{ mem::MaybeUninit, os::unix::ffi::OsStrExt, path::Path, - rc::Rc, + sync::Arc, + sync::RwLock, }; use anyhow::{ensure, Context as _, Result}; @@ -167,7 +167,7 @@ pub fn write_to_path( #[derive(Debug)] pub struct FilesystemReader<'repo, ObjectID: FsVerityHashValue> { repo: Option<&'repo Repository>, - inodes: HashMap<(u64, u64), Rc>>, + inodes: HashMap<(u64, u64), Arc>>, } impl FilesystemReader<'_, ObjectID> { @@ -213,7 +213,7 @@ impl FilesystemReader<'_, ObjectID> { st_uid: buf.st_uid, st_gid: buf.st_gid, st_mtim_sec: buf.st_mtime as i64, - xattrs: RefCell::new(Self::read_xattrs(fd)?), + xattrs: RwLock::new(Self::read_xattrs(fd)?), }, )) } @@ -263,7 +263,7 @@ impl FilesystemReader<'_, ObjectID> { dirfd: &OwnedFd, name: &OsStr, ifmt: FileType, - ) -> Result>> { + ) -> Result>> { let oflags = match ifmt { FileType::RegularFile => OFlags::RDONLY, _ => OFlags::PATH, @@ -283,11 +283,11 @@ impl FilesystemReader<'_, ObjectID> { // Track all files. https://github.com/containers/fuse-overlayfs/issues/435 let key = (buf.st_dev, buf.st_ino); if let Some(leafref) = self.inodes.get(&key) { - Ok(Rc::clone(leafref)) + Ok(Arc::clone(leafref)) } else { let content = self.read_leaf_content(fd, buf)?; - let leaf = Rc::new(Leaf { stat, content }); - self.inodes.insert(key, Rc::clone(&leaf)); + let leaf = Arc::new(Leaf { stat, content }); + self.inodes.insert(key, Arc::clone(&leaf)); Ok(leaf) } } @@ -333,7 +333,7 @@ impl FilesystemReader<'_, ObjectID> { ) -> Result> { if ifmt == FileType::Directory { let dir = self.read_directory(dirfd, name)?; - Ok(Inode::Directory(Box::new(dir))) + Ok(Inode::Directory(Arc::new(dir))) } else { let leaf = self.read_leaf(dirfd, name, ifmt)?; Ok(Inode::Leaf(leaf)) diff --git a/crates/composefs/src/generic_tree.rs b/crates/composefs/src/generic_tree.rs index 6a683250..9f7a920e 100644 --- a/crates/composefs/src/generic_tree.rs +++ b/crates/composefs/src/generic_tree.rs @@ -2,11 +2,10 @@ //! however the caller wants. use std::{ - cell::RefCell, collections::BTreeMap, ffi::OsStr, path::{Component, Path}, - rc::Rc, + sync::{Arc, RwLock}, }; use thiserror::Error; @@ -23,7 +22,7 @@ pub struct Stat { /// Modification time in seconds since Unix epoch. pub st_mtim_sec: i64, /// Extended attributes as key-value pairs. - pub xattrs: RefCell, Box<[u8]>>>, + pub xattrs: RwLock, Box<[u8]>>>, } impl Stat { @@ -41,7 +40,7 @@ impl Stat { st_uid: 0, st_gid: 0, st_mtim_sec: 0, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), } } } @@ -85,9 +84,9 @@ pub struct Directory { #[derive(Debug)] pub enum Inode { /// A directory inode. - Directory(Box>), + Directory(Arc>), /// A leaf inode (reference-counted to support hardlinks). - Leaf(Rc>), + Leaf(Arc>), } /// Errors that can occur when working with filesystem images. @@ -221,7 +220,8 @@ impl Directory { return Err(ImageError::InvalidFilename(pathname.into())); } Component::Normal(filename) => match dir.entries.get_mut(filename) { - Some(Inode::Directory(subdir)) => subdir, + Some(Inode::Directory(subdir)) => Arc::get_mut(subdir) + .expect("directory Arc should be uniquely owned during tree construction"), Some(_) => return Err(ImageError::NotADirectory(filename.into())), None => return Err(ImageError::NotFound(filename.into())), }, @@ -302,13 +302,13 @@ impl Directory { /// /// # Return value /// - /// On success (the entry exists and is not a directory) the Rc is cloned and a new reference + /// On success (the entry exists and is not a directory) the Arc is cloned and a new reference /// is returned. /// /// On failure, can return any number of errors from ImageError. - pub fn ref_leaf(&self, filename: &OsStr) -> Result>, ImageError> { + pub fn ref_leaf(&self, filename: &OsStr) -> Result>, ImageError> { match self.entries.get(filename) { - Some(Inode::Leaf(leaf)) => Ok(Rc::clone(leaf)), + Some(Inode::Leaf(leaf)) => Ok(Arc::clone(leaf)), Some(Inode::Directory(..)) => Err(ImageError::IsADirectory(Box::from(filename))), None => Err(ImageError::NotFound(Box::from(filename))), } @@ -366,7 +366,12 @@ impl Directory { // keep the old entries in place. if let Inode::Directory(new_dir) = inode { if let Some(Inode::Directory(old_dir)) = self.entries.get_mut(filename) { - old_dir.stat = new_dir.stat; + let new_dir = Arc::try_unwrap(new_dir) + .ok() + .expect("directory Arc should be uniquely owned during tree construction"); + Arc::get_mut(old_dir) + .expect("directory Arc should be uniquely owned during tree construction") + .stat = new_dir.stat; } else { // Unfortunately we already deconstructed the original inode and we can't get it // back again. This is necessary because we wanted to move the stat field (above) @@ -496,14 +501,14 @@ impl FileSystem { let st_uid = usr.stat.st_uid; let st_gid = usr.stat.st_gid; let st_mtim_sec = usr.stat.st_mtim_sec; - let xattrs = usr.stat.xattrs.clone(); + let xattrs = usr.stat.xattrs.read().unwrap().clone(); // Apply copied metadata to root self.root.stat.st_mode = st_mode; self.root.stat.st_uid = st_uid; self.root.stat.st_gid = st_gid; self.root.stat.st_mtim_sec = st_mtim_sec; - self.root.stat.xattrs = xattrs; + self.root.stat.xattrs = RwLock::new(xattrs); Ok(()) } @@ -544,7 +549,7 @@ impl FileSystem { F: Fn(&OsStr) -> bool, { self.for_each_stat(|stat| { - stat.xattrs.borrow_mut().retain(|k, _| predicate(k)); + stat.xattrs.write().unwrap().retain(|k, _| predicate(k)); }); } @@ -598,10 +603,10 @@ impl FileSystem { #[cfg(test)] mod tests { use super::*; - use std::cell::RefCell; use std::collections::BTreeMap; use std::ffi::{OsStr, OsString}; - use std::rc::Rc; + use std::sync::Arc; + use std::sync::RwLock; // We never store any actual data here #[derive(Debug, Default)] @@ -614,7 +619,7 @@ mod tests { st_uid: 0, st_gid: 0, st_mtim_sec: 0, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), } } @@ -625,21 +630,21 @@ mod tests { st_uid: 1000, st_gid: 1000, st_mtim_sec: mtime, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), } } // Helper to create a simple Leaf (e.g., an empty inline file) - fn new_leaf_file(mtime: i64) -> Rc> { - Rc::new(Leaf { + fn new_leaf_file(mtime: i64) -> Arc> { + Arc::new(Leaf { stat: stat_with_mtime(mtime), content: LeafContent::Regular(FileContents::default()), }) } // Helper to create a simple Leaf (symlink) - fn new_leaf_symlink(target: &str, mtime: i64) -> Rc> { - Rc::new(Leaf { + fn new_leaf_symlink(target: &str, mtime: i64) -> Arc> { + Arc::new(Leaf { stat: stat_with_mtime(mtime), content: LeafContent::Symlink(OsString::from(target).into_boxed_os_str()), }) @@ -647,7 +652,7 @@ mod tests { // Helper to create an empty Directory Inode with a specific mtime fn new_dir_inode(mtime: i64) -> Inode { - Inode::Directory(Box::new(Directory { + Inode::Directory(Arc::new(Directory { stat: stat_with_mtime(mtime), entries: BTreeMap::new(), })) @@ -655,7 +660,7 @@ mod tests { // Helper to create a Directory Inode with specific stat fn new_dir_inode_with_stat(stat: Stat) -> Inode { - Inode::Directory(Box::new(Directory { + Inode::Directory(Arc::new(Directory { stat, entries: BTreeMap::new(), })) @@ -673,11 +678,11 @@ mod tests { fn test_insert_and_get_leaf() { let mut dir = Directory::::new(default_stat()); let leaf = new_leaf_file(10); - dir.insert(OsStr::new("file.txt"), Inode::Leaf(Rc::clone(&leaf))); + dir.insert(OsStr::new("file.txt"), Inode::Leaf(Arc::clone(&leaf))); assert_eq!(dir.entries.len(), 1); let retrieved_leaf_rc = dir.ref_leaf(OsStr::new("file.txt")).unwrap(); - assert!(Rc::ptr_eq(&retrieved_leaf_rc, &leaf)); + assert!(Arc::ptr_eq(&retrieved_leaf_rc, &leaf)); let regular_file_content = dir.get_file(OsStr::new("file.txt")).unwrap(); assert!(matches!(regular_file_content, FileContents {})); @@ -783,8 +788,10 @@ mod tests { // Merge Directory onto existing Directory let mut existing_dir_inode = new_dir_inode_with_stat(stat_with_mtime(80)); - if let Inode::Directory(ref mut ed_box) = existing_dir_inode { - ed_box.insert(OsStr::new("inner_file"), Inode::Leaf(new_leaf_file(85))); + if let Inode::Directory(ref mut ed_arc) = existing_dir_inode { + Arc::get_mut(ed_arc) + .unwrap() + .insert(OsStr::new("inner_file"), Inode::Leaf(new_leaf_file(85))); } dir.insert(OsStr::new("merged_dir"), existing_dir_inode); @@ -836,13 +843,15 @@ mod tests { assert_eq!(root.newest_file(), 10); let subdir_stat = stat_with_mtime(15); - let mut subdir = Box::new(Directory::new(subdir_stat)); + let mut subdir = Directory::new(subdir_stat); subdir.insert(OsStr::new("subfile1"), Inode::Leaf(new_leaf_file(12))); - root.insert(OsStr::new("subdir"), Inode::Directory(subdir)); + root.insert(OsStr::new("subdir"), Inode::Directory(Arc::new(subdir))); assert_eq!(root.newest_file(), 15); if let Some(Inode::Directory(sd)) = root.entries.get_mut(OsStr::new("subdir")) { - sd.insert(OsStr::new("subfile2"), Inode::Leaf(new_leaf_file(20))); + Arc::get_mut(sd) + .unwrap() + .insert(OsStr::new("subfile2"), Inode::Leaf(new_leaf_file(20))); } assert_eq!(root.newest_file(), 20); @@ -898,7 +907,7 @@ mod tests { st_uid: 42, st_gid: 43, st_mtim_sec: 1234567890, - xattrs: RefCell::new(BTreeMap::from([( + xattrs: RwLock::new(BTreeMap::from([( Box::from(OsStr::new("security.selinux")), Box::from(b"system_u:object_r:usr_t:s0".as_slice()), )])), @@ -909,7 +918,7 @@ mod tests { }; fs.root.entries.insert( Box::from(OsStr::new("usr")), - Inode::Directory(Box::new(usr_dir)), + Inode::Directory(Arc::new(usr_dir)), ); fs.copy_root_metadata_from_usr().unwrap(); @@ -922,7 +931,8 @@ mod tests { .root .stat .xattrs - .borrow() + .read() + .unwrap() .contains_key(OsStr::new("security.selinux"))); } @@ -943,7 +953,7 @@ mod tests { st_uid: 0, st_gid: 0, st_mtim_sec: 0, - xattrs: RefCell::new(BTreeMap::from([ + xattrs: RwLock::new(BTreeMap::from([ ( Box::from(OsStr::new("security.selinux")), Box::from(b"label".as_slice()), @@ -963,7 +973,7 @@ mod tests { // Filter to keep only xattrs starting with "user." fs.filter_xattrs(|name| name.as_encoded_bytes().starts_with(b"user.")); - let root_xattrs = fs.root.stat.xattrs.borrow(); + let root_xattrs = fs.root.stat.xattrs.read().unwrap(); assert_eq!(root_xattrs.len(), 1); assert!(root_xattrs.contains_key(OsStr::new("user.custom"))); } @@ -975,16 +985,16 @@ mod tests { // Create /usr with specific mtime let usr_dir = Directory::new(stat_with_mtime(12345)); fs.root - .insert(OsStr::new("usr"), Inode::Directory(Box::new(usr_dir))); + .insert(OsStr::new("usr"), Inode::Directory(Arc::new(usr_dir))); // Create /run with content and different mtime let mut run_dir = Directory::new(stat_with_mtime(99999)); run_dir.insert(OsStr::new("somefile"), Inode::Leaf(new_leaf_file(11111))); let mut subdir = Directory::new(stat_with_mtime(22222)); subdir.insert(OsStr::new("nested"), Inode::Leaf(new_leaf_file(33333))); - run_dir.insert(OsStr::new("subdir"), Inode::Directory(Box::new(subdir))); + run_dir.insert(OsStr::new("subdir"), Inode::Directory(Arc::new(subdir))); fs.root - .insert(OsStr::new("run"), Inode::Directory(Box::new(run_dir))); + .insert(OsStr::new("run"), Inode::Directory(Arc::new(run_dir))); // Verify /run has content before assert_eq!( @@ -1012,7 +1022,7 @@ mod tests { // Create /usr but no /run let usr_dir = Directory::new(stat_with_mtime(12345)); fs.root - .insert(OsStr::new("usr"), Inode::Directory(Box::new(usr_dir))); + .insert(OsStr::new("usr"), Inode::Directory(Arc::new(usr_dir))); // Should succeed without error fs.canonicalize_run().unwrap(); @@ -1028,7 +1038,7 @@ mod tests { st_uid: 100, st_gid: 200, st_mtim_sec: 54321, - xattrs: RefCell::new(BTreeMap::from([( + xattrs: RwLock::new(BTreeMap::from([( Box::from(OsStr::new("user.test")), Box::from(b"val".as_slice()), )])), @@ -1040,7 +1050,7 @@ mod tests { let mut run_dir = Directory::new(stat_with_mtime(99999)); run_dir.insert(OsStr::new("file"), Inode::Leaf(new_leaf_file(11111))); fs.root - .insert(OsStr::new("run"), Inode::Directory(Box::new(run_dir))); + .insert(OsStr::new("run"), Inode::Directory(Arc::new(run_dir))); // Transform for OCI fs.transform_for_oci().unwrap(); diff --git a/crates/composefs/src/repository.rs b/crates/composefs/src/repository.rs index b3834f25..6d272dea 100644 --- a/crates/composefs/src/repository.rs +++ b/crates/composefs/src/repository.rs @@ -2710,7 +2710,7 @@ mod tests { /// Make a test in-memory filesystem that only contains one externally referenced object fn make_test_fs(obj: &Sha512HashValue, size: u64) -> FileSystem { let mut fs: FileSystem = FileSystem::new(test_root_stat()); - let inode = Inode::Leaf(std::rc::Rc::new(Leaf { + let inode = Inode::Leaf(std::sync::Arc::new(Leaf { stat: Stat { st_mode: 0o644, st_uid: 0, @@ -2871,7 +2871,7 @@ mod tests { size2: u64, ) -> FileSystem { let mut fs = make_test_fs(obj1, size1); - let inode = Inode::Leaf(std::rc::Rc::new(Leaf { + let inode = Inode::Leaf(std::sync::Arc::new(Leaf { stat: Stat { st_mode: 0o644, st_uid: 0, diff --git a/crates/composefs/src/test.rs b/crates/composefs/src/test.rs index 7e165f76..90d7336e 100644 --- a/crates/composefs/src/test.rs +++ b/crates/composefs/src/test.rs @@ -100,7 +100,7 @@ impl Default for TestRepo { /// Proptest strategies for generating random `tree::FileSystem` instances. /// /// These strategies build the tree directly (not through dumpfile strings), -/// which means they can express things like hardlinks (shared `Rc`) +/// which means they can express things like hardlinks (shared `Arc`) /// that are awkward to generate as text. /// /// The spec types are hash-type-agnostic: external file references store @@ -110,12 +110,12 @@ impl Default for TestRepo { #[cfg(test)] pub(crate) mod proptest_strategies { use std::{ - cell::RefCell, collections::BTreeMap, ffi::{OsStr, OsString}, mem, os::unix::ffi::OsStringExt, - rc::Rc, + sync::Arc, + sync::RwLock, }; use proptest::prelude::*; @@ -170,7 +170,7 @@ pub(crate) mod proptest_strategies { st_uid: uid, st_gid: gid, st_mtim_sec: mtime, - xattrs: RefCell::new(xattrs), + xattrs: RwLock::new(xattrs), }) } @@ -446,16 +446,16 @@ pub(crate) mod proptest_strategies { ) -> tree::FileSystem { let mut fs = tree::FileSystem::new(spec.root.stat); - let mut all_leaves: Vec>> = Vec::new(); + let mut all_leaves: Vec>> = Vec::new(); let mut used_names: std::collections::HashSet = std::collections::HashSet::new(); // Insert root-level leaves for (name, leaf_spec) in spec.root.leaves { - let leaf = Rc::new(tree::Leaf { + let leaf = Arc::new(tree::Leaf { stat: leaf_spec.stat, content: build_leaf_content(leaf_spec.content), }); - all_leaves.push(Rc::clone(&leaf)); + all_leaves.push(Arc::clone(&leaf)); used_names.insert(name.clone()); fs.root.insert(&name, tree::Inode::Leaf(leaf)); } @@ -464,16 +464,16 @@ pub(crate) mod proptest_strategies { for (dir_name, dir_spec) in spec.root.subdirs { let mut subdir = tree::Directory::new(dir_spec.stat); for (name, leaf_spec) in dir_spec.leaves { - let leaf = Rc::new(tree::Leaf { + let leaf = Arc::new(tree::Leaf { stat: leaf_spec.stat, content: build_leaf_content(leaf_spec.content), }); - all_leaves.push(Rc::clone(&leaf)); + all_leaves.push(Arc::clone(&leaf)); subdir.insert(&name, tree::Inode::Leaf(leaf)); } used_names.insert(dir_name.clone()); fs.root - .insert(&dir_name, tree::Inode::Directory(Box::new(subdir))); + .insert(&dir_name, tree::Inode::Directory(Arc::new(subdir))); } // Insert hardlinks into the root directory @@ -481,7 +481,7 @@ pub(crate) mod proptest_strategies { if !all_leaves.is_empty() { let idx = hl.source_index % all_leaves.len(); if used_names.insert(hl.link_name.clone()) { - let leaf = Rc::clone(&all_leaves[idx]); + let leaf = Arc::clone(&all_leaves[idx]); fs.root.insert(&hl.link_name, tree::Inode::Leaf(leaf)); } } diff --git a/crates/composefs/src/tree.rs b/crates/composefs/src/tree.rs index 3d0f2702..9e5e5603 100644 --- a/crates/composefs/src/tree.rs +++ b/crates/composefs/src/tree.rs @@ -41,7 +41,11 @@ pub type FileSystem = generic_tree::FileSystem>; #[cfg(test)] mod tests { - use std::{cell::RefCell, collections::BTreeMap, ffi::OsStr, rc::Rc}; + use std::{ + collections::BTreeMap, + ffi::OsStr, + sync::{Arc, RwLock}, + }; use super::*; use crate::fsverity::Sha256HashValue; @@ -53,21 +57,21 @@ mod tests { st_uid: 1000, st_gid: 1000, st_mtim_sec: mtime, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), } } // Helper to create an empty Directory Inode with a specific mtime fn new_dir_inode(mtime: i64) -> Inode { - Inode::Directory(Box::new(Directory { + Inode::Directory(Arc::new(Directory { stat: stat_with_mtime(mtime), entries: BTreeMap::new(), })) } // Helper to create a simple Leaf (e.g., an empty inline file) - fn new_leaf_file(mtime: i64) -> Rc> { - Rc::new(Leaf { + fn new_leaf_file(mtime: i64) -> Arc> { + Arc::new(Leaf { stat: stat_with_mtime(mtime), content: LeafContent::Regular(super::RegularFile::Inline(Default::default())), }) @@ -80,7 +84,7 @@ mod tests { st_uid: 0, st_gid: 0, st_mtim_sec: 0, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), } } @@ -88,11 +92,11 @@ mod tests { fn test_insert_and_get_leaf() { let mut dir = Directory::::new(default_stat()); let leaf = new_leaf_file(10); - dir.insert(OsStr::new("file.txt"), Inode::Leaf(Rc::clone(&leaf))); + dir.insert(OsStr::new("file.txt"), Inode::Leaf(Arc::clone(&leaf))); assert_eq!(dir.entries.len(), 1); let retrieved_leaf_rc = dir.ref_leaf(OsStr::new("file.txt")).unwrap(); - assert!(Rc::ptr_eq(&retrieved_leaf_rc, &leaf)); + assert!(Arc::ptr_eq(&retrieved_leaf_rc, &leaf)); let regular_file_content = dir.get_file(OsStr::new("file.txt")).unwrap(); assert!(matches!( diff --git a/crates/composefs/tests/mkfs.rs b/crates/composefs/tests/mkfs.rs index a2f45e35..673a2408 100644 --- a/crates/composefs/tests/mkfs.rs +++ b/crates/composefs/tests/mkfs.rs @@ -1,12 +1,12 @@ //! Tests for mkfs use std::{ - cell::RefCell, collections::BTreeMap, ffi::OsStr, io::Write, process::{Command, Stdio}, - rc::Rc, + sync::Arc, + sync::RwLock, }; use similar_asserts::assert_eq; @@ -25,7 +25,7 @@ fn default_stat() -> Stat { st_uid: 0, st_gid: 0, st_mtim_sec: 0, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), } } @@ -52,14 +52,14 @@ fn add_leaf( ) { dir.insert( name.as_ref(), - Inode::Leaf(Rc::new(Leaf { + Inode::Leaf(Arc::new(Leaf { content, stat: Stat { st_gid: 0, st_uid: 0, st_mode: 0, st_mtim_sec: 0, - xattrs: RefCell::new(BTreeMap::new()), + xattrs: RwLock::new(BTreeMap::new()), }, })), );