Skip to content
Open
Show file tree
Hide file tree
Changes from all 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this also removed for Linux?

];
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())
.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);
// 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