Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ keywords = ["fuse", "filesystem", "branching", "copy-on-write"]
categories = ["filesystem"]

[dependencies]
fuser = { version = "0.16", features = ["abi-7-40"] }
[target.'cfg(target_os = "linux")'.dependencies]
fuser = { version = "0.16", features = ["abi-7-40", "libfuse"] }

[target.'cfg(not(target_os = "linux"))'.dependencies]
fuser = { version = "0.16", features = ["abi-7-31", "libfuse"] }
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ FUSE adds userspace-kernel context switches per operation, which is slower than

## Prerequisites

- Linux with FUSE support
- libfuse3 development libraries
- Linux with FUSE support or macOS with macFUSE
- libfuse3 development libraries (Linux) or macFUSE (macOS)
- Rust toolchain (1.70 or later)

### Installing Dependencies
Expand All @@ -57,6 +57,24 @@ sudo dnf install fuse3-devel pkg-config
sudo pacman -S fuse3 pkg-config
```

**macOS:**
```bash
brew install macfuse pkg-config
```

### macOS Support

BranchFS supports macOS via **macFUSE**.

1. **Install macFUSE**: `brew install macfuse pkg-config`.
2. **System Extension**: You must approve the `macFUSE` system extension in System Settings. On Apple Silicon Macs, you may need to enable third-party kernel extensions in Recovery Mode.
3. **Control Interface**: Since `ioctl` support can be inconsistent on macOS, BranchFS provides a reliable write-based interface. You can send commands to `.branchfs_ctl` via direct writes:
- `echo "create:name" > .branchfs_ctl`
- `echo "commit" > .branchfs_ctl`
- `echo "abort" > .branchfs_ctl`
4. **FUSE ABI**: On macOS, BranchFS targets FUSE ABI 7.31 for maximum compatibility and to resolve path resolution issues.
5. **Advanced Features**: Linux-specific features like FUSE passthrough and `RENAME_EXCHANGE` are currently disabled on macOS.

## Building

```bash
Expand Down
8 changes: 4 additions & 4 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,10 @@ impl Daemon {
self.manager.set_mount_branch(mountpoint, branch_name);

let fs = BranchFs::new(self.manager.clone(), mountpoint.to_path_buf(), passthrough);
let options = vec![
let mut options = vec![
MountOption::FSName("branchfs".to_string()),
MountOption::DefaultPermissions,
Comment thread
nitin966 marked this conversation as resolved.
];
options.extend(crate::platform::get_mount_options());

log::info!(
"Spawning mount for branch '{}' at {:?}",
Expand Down Expand Up @@ -407,8 +407,8 @@ pub fn start_daemon_background(
cmd.args(["--max-storage", &max.to_string()]);
}
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
Comment thread
nitin966 marked this conversation as resolved.
Outdated
.spawn()?;

// Wait for daemon to be ready
Expand Down
97 changes: 31 additions & 66 deletions src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use fuser::{
BackingId, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry,
FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry,
ReplyIoctl, ReplyOpen, ReplyStatfs, ReplyWrite, Request, TimeOrNow,
};
use parking_lot::RwLock;
Expand All @@ -18,17 +18,14 @@ use crate::error::BranchError;
use crate::fs_path::{classify_path, PathContext};
use crate::inode::{InodeManager, ROOT_INO};
use crate::storage;
use crate::platform::{FS_IOC_BRANCH_ABORT, FS_IOC_BRANCH_COMMIT, FS_IOC_BRANCH_CREATE};

// Zero TTL forces the kernel to always revalidate with FUSE, ensuring consistent
// behavior after branch switches. This is important for speculative execution
// where branches can change at any time.
pub(crate) const TTL: Duration = Duration::from_secs(0);
pub(crate) const BLOCK_SIZE: u32 = 512;

pub const FS_IOC_BRANCH_CREATE: u32 = 0x8080_6200; // _IOR('b', 0, [u8; 128])
pub const FS_IOC_BRANCH_COMMIT: u32 = 0x0000_6201; // _IO ('b', 1)
pub const FS_IOC_BRANCH_ABORT: u32 = 0x0000_6202; // _IO ('b', 2)

pub(crate) const CTL_FILE: &str = ".branchfs_ctl";
pub(crate) const CTL_INO: u64 = u64::MAX - 1;

Expand Down Expand Up @@ -132,10 +129,8 @@ pub struct BranchFs {
write_cache: WriteFileCache,
/// Whether FUSE passthrough mode is enabled (--passthrough flag).
passthrough_enabled: bool,
/// Monotonically increasing file handle counter for passthrough opens.
next_fh: AtomicU64,
/// BackingId objects kept alive until release() — one per passthrough open().
backing_ids: HashMap<u64, BackingId>,
/// FUSE passthrough state (fh counter, backing_ids)
passthrough_state: crate::platform::PassthroughState,
}

impl BranchFs {
Expand All @@ -155,8 +150,7 @@ impl BranchFs {
open_cache: OpenFileCache::new(),
write_cache: WriteFileCache::new(),
passthrough_enabled: passthrough,
next_fh: AtomicU64::new(1),
backing_ids: HashMap::new(),
passthrough_state: crate::platform::PassthroughState::new(),
}
}

Expand Down Expand Up @@ -228,7 +222,6 @@ impl BranchFs {
/// Attempt to open a file with FUSE passthrough. Falls back to non-passthrough on failure.
fn try_open_passthrough(
&mut self,
_ino: u64,
flags: i32,
branch: &str,
rel_path: &str,
Expand All @@ -237,13 +230,10 @@ impl BranchFs {
) {
let is_writable = (flags & libc::O_ACCMODE) != libc::O_RDONLY;

// For writable opens, do eager COW — the kernel will write directly to
// the backing file, bypassing our write() callback.
let backing_path = if is_writable {
match self.ensure_cow_for_branch(branch, rel_path) {
Ok(p) => p,
Err(_) => {
// Fallback to non-passthrough
reply.opened(0, 0);
return;
}
Expand All @@ -252,7 +242,6 @@ impl BranchFs {
resolved.to_path_buf()
};

// Open the backing file
let open_result = if is_writable {
std::fs::OpenOptions::new()
.read(true)
Expand All @@ -261,26 +250,11 @@ impl BranchFs {
} else {
File::open(&backing_path)
};
let file = match open_result {
Ok(f) => f,
Err(_) => {
reply.opened(0, 0);
return;
}
};

// Register the fd with the kernel
let backing_id = match reply.open_backing(&file) {
Ok(id) => id,
Err(_) => {
reply.opened(0, 0);
return;
}
};

let fh = self.next_fh.fetch_add(1, Ordering::Relaxed);
reply.opened_passthrough(fh, 0, &backing_id);
self.backing_ids.insert(fh, backing_id);

match open_result {
Ok(f) => crate::platform::try_open_passthrough(&mut self.passthrough_state, f, reply),
Err(_) => reply.opened(0, 0),
}
}

/// Classify an inode number. Returns None for root and CTL_INO (handled separately).
Expand Down Expand Up @@ -311,21 +285,7 @@ impl Filesystem for BranchFs {
}

if self.passthrough_enabled {
if let Err(e) = config.add_capabilities(fuser::consts::FUSE_PASSTHROUGH) {
log::warn!(
"Kernel does not support FUSE_PASSTHROUGH (unsupported bits: {:#x}), disabling",
e
);
self.passthrough_enabled = false;
} else if let Err(e) = config.set_max_stack_depth(2) {
log::warn!(
"Failed to set max_stack_depth (max: {}), disabling passthrough",
e
);
self.passthrough_enabled = false;
} else {
log::info!("FUSE passthrough enabled");
}
crate::platform::setup_capabilities(config, &mut self.passthrough_enabled);
}

Ok(())
Expand Down Expand Up @@ -1164,11 +1124,11 @@ impl Filesystem for BranchFs {
name: &OsStr,
newparent: u64,
newname: &OsStr,
flags: u32,
_flags: u32,
reply: ReplyEmpty,
) {
if flags & libc::RENAME_EXCHANGE != 0 {
reply.error(libc::EINVAL);
if let Err(e) = crate::platform::check_rename_flags(_flags) {
reply.error(e);
return;
}

Expand Down Expand Up @@ -1254,7 +1214,7 @@ impl Filesystem for BranchFs {
}

// RENAME_NOREPLACE
if flags & libc::RENAME_NOREPLACE != 0
if crate::platform::check_rename_noreplace(_flags)
&& self.resolve_for_branch(&branch, &dst_rel).is_some()
{
reply.error(libc::EEXIST);
Expand Down Expand Up @@ -1330,7 +1290,7 @@ impl Filesystem for BranchFs {
reply.ok();
}

fn open(&mut self, _req: &Request, ino: u64, flags: i32, reply: ReplyOpen) {
fn open(&mut self, _req: &Request, ino: u64, _flags: i32, reply: ReplyOpen) {
// Control file is always openable (no epoch check)
if ino == CTL_INO {
reply.opened(0, 0);
Expand Down Expand Up @@ -1363,7 +1323,7 @@ impl Filesystem for BranchFs {
reply.error(libc::ENOENT);
return;
}
let resolved = match self.resolve_for_branch(&branch, &rel_path) {
let _resolved = match self.resolve_for_branch(&branch, &rel_path) {
Some(p) => p,
None => {
reply.error(libc::ENOENT);
Expand All @@ -1373,7 +1333,7 @@ impl Filesystem for BranchFs {
self.manager.register_opened_inode(&branch, ino);

if self.passthrough_enabled {
self.try_open_passthrough(ino, flags, &branch, &rel_path, &resolved, reply);
self.try_open_passthrough(_flags, &branch, &rel_path, &_resolved, reply);
} else {
reply.opened(0, 0);
}
Expand All @@ -1384,7 +1344,7 @@ impl Filesystem for BranchFs {
reply.error(libc::ESTALE);
return;
}
let resolved = match self.resolve(&path) {
let _resolved = match self.resolve(&path) {
Some(p) => p,
None => {
reply.error(libc::ENOENT);
Expand All @@ -1395,7 +1355,7 @@ impl Filesystem for BranchFs {
self.manager.register_opened_inode(&branch_name, ino);

if self.passthrough_enabled {
self.try_open_passthrough(ino, flags, &branch_name, &path, &resolved, reply);
self.try_open_passthrough(_flags, &branch_name, &path, &_resolved, reply);
} else {
reply.opened(0, 0);
}
Expand All @@ -1414,7 +1374,7 @@ impl Filesystem for BranchFs {
reply: ReplyEmpty,
) {
if fh != 0 {
self.backing_ids.remove(&fh);
crate::platform::release_passthrough(&mut self.passthrough_state, fh);
}
reply.ok();
}
Expand Down Expand Up @@ -1589,6 +1549,7 @@ impl Filesystem for BranchFs {
_out_size: u32,
reply: ReplyIoctl,
) {
log::info!("ioctl: ino={}, cmd={:#x}", ino, cmd);
Comment thread
nitin966 marked this conversation as resolved.
Outdated
// Resolve ino to the branch name this ctl fd refers to.
let branch_name = if ino == CTL_INO {
self.get_branch_name()
Expand Down Expand Up @@ -1794,6 +1755,10 @@ impl Filesystem for BranchFs {
}
}

fn access(&mut self, _req: &Request, _ino: u64, _mask: i32, reply: ReplyEmpty) {
crate::platform::handle_access(reply);
}

fn symlink(
&mut self,
_req: &Request,
Expand Down Expand Up @@ -1909,11 +1874,11 @@ impl Filesystem for BranchFs {
match nix::sys::statvfs::statvfs(storage_path) {
Ok(stat) => {
reply.statfs(
stat.blocks(),
stat.blocks_free(),
stat.blocks_available(),
stat.files(),
stat.files_free(),
stat.blocks().into(),
stat.blocks_free().into(),
stat.blocks_available().into(),
stat.files().into(),
stat.files_free().into(),
stat.block_size() as u32,
stat.name_max() as u32,
stat.fragment_size() as u32,
Expand Down
Loading
Loading