Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ keywords = ["fuse", "filesystem", "branching", "copy-on-write"]
categories = ["filesystem"]

[dependencies]
fuser = "0.16"
fuser = { version = "0.16", features = ["abi-7-40"] }
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
9 changes: 7 additions & 2 deletions bench/branchfs_bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,15 @@ def _start_daemon(self, base_dir: Path, storage: Path, mountpoint: Path, log_fil
env = os.environ.copy()
env["RUST_LOG"] = "debug"

cmd = [self.branchfs, "mount", "--base", str(base_dir),
"--storage", str(storage)]
if os.geteuid() == 0:
cmd.append("--passthrough")
cmd.append(str(mountpoint))

with open(log_file, "w") as f:
proc = subprocess.Popen(
[self.branchfs, "mount", "--base", str(base_dir),
"--storage", str(storage), str(mountpoint)],
cmd,
env=env,
stdout=f,
stderr=subprocess.STDOUT
Expand Down
18 changes: 13 additions & 5 deletions bench/quick_bench.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ fi
TMPDIR=$(mktemp -d)
trap "rm -rf $TMPDIR" EXIT

PASSTHROUGH_FLAG=""
if [ "$(id -u)" = "0" ]; then
PASSTHROUGH_FLAG="--passthrough"
fi

echo "Using branchfs: $BRANCHFS"
echo "Temp dir: $TMPDIR"
if [ -n "$PASSTHROUGH_FLAG" ]; then
echo "Passthrough: enabled (root)"
fi

# Helper to measure time in microseconds using high-res timer
time_us() {
Expand Down Expand Up @@ -65,7 +73,7 @@ for num_files in 100 1000 10000; do
done

# Mount
$BRANCHFS mount --base "$BASE" --storage "$STORAGE" "$MNT"
$BRANCHFS mount --base "$BASE" --storage "$STORAGE" $PASSTHROUGH_FLAG "$MNT"
sleep 0.2

# Measure branch creation (average of 5)
Expand Down Expand Up @@ -100,7 +108,7 @@ for mod_kb in 1 10 100 1000; do
STORAGE="$TMPDIR/commit_storage_$mod_kb"
mkdir -p "$MNT" "$STORAGE"

$BRANCHFS mount --base "$BASE" --storage "$STORAGE" "$MNT"
$BRANCHFS mount --base "$BASE" --storage "$STORAGE" $PASSTHROUGH_FLAG "$MNT"
sleep 0.1

$BRANCHFS create "bench" "$MNT" --storage "$STORAGE"
Expand Down Expand Up @@ -128,7 +136,7 @@ for mod_kb in 1 100 1000; do
STORAGE="$TMPDIR/abort_storage_$mod_kb"
mkdir -p "$MNT" "$STORAGE"

$BRANCHFS mount --base "$BASE" --storage "$STORAGE" "$MNT"
$BRANCHFS mount --base "$BASE" --storage "$STORAGE" $PASSTHROUGH_FLAG "$MNT"
sleep 0.1

$BRANCHFS create "bench" "$MNT" --storage "$STORAGE"
Expand Down Expand Up @@ -158,7 +166,7 @@ mkdir -p "$MNT" "$STORAGE"
# Create test file
dd if=/dev/urandom of="$BASE/testfile.bin" bs=1M count=1 2>/dev/null

$BRANCHFS mount --base "$BASE" --storage "$STORAGE" "$MNT"
$BRANCHFS mount --base "$BASE" --storage "$STORAGE" $PASSTHROUGH_FLAG "$MNT"
sleep 0.1
$BRANCHFS create "bench" "$MNT" --storage "$STORAGE"

Expand Down Expand Up @@ -186,7 +194,7 @@ MNT="$TMPDIR/switch_mnt"
STORAGE="$TMPDIR/switch_storage"
mkdir -p "$MNT" "$STORAGE"

$BRANCHFS mount --base "$BASE" --storage "$STORAGE" "$MNT"
$BRANCHFS mount --base "$BASE" --storage "$STORAGE" $PASSTHROUGH_FLAG "$MNT"
sleep 0.1

# Create multiple branches
Expand Down
42 changes: 33 additions & 9 deletions src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,26 @@ use crate::fs::BranchFs;
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum Request {
Mount { branch: String, mountpoint: String },
Unmount { mountpoint: String },
Create { name: String, parent: String },
NotifySwitch { mountpoint: String, branch: String },
GetMountBranch { mountpoint: String },
Mount {
branch: String,
mountpoint: String,
#[serde(default)]
passthrough: bool,
},
Unmount {
mountpoint: String,
},
Create {
name: String,
parent: String,
},
NotifySwitch {
mountpoint: String,
branch: String,
},
GetMountBranch {
mountpoint: String,
},
List,
Shutdown,
}
Expand Down Expand Up @@ -124,8 +139,13 @@ impl Daemon {
&self.socket_path
}

pub fn spawn_mount(&self, branch_name: &str, mountpoint: &Path) -> Result<()> {
let fs = BranchFs::new(self.manager.clone(), branch_name.to_string());
pub fn spawn_mount(
&self,
branch_name: &str,
mountpoint: &Path,
passthrough: bool,
) -> Result<()> {
let fs = BranchFs::new(self.manager.clone(), branch_name.to_string(), passthrough);
let options = vec![
MountOption::FSName("branchfs".to_string()),
MountOption::DefaultPermissions,
Expand Down Expand Up @@ -284,12 +304,16 @@ impl Daemon {

fn handle_request(&self, request: Request) -> Response {
match request {
Request::Mount { branch, mountpoint } => {
Request::Mount {
branch,
mountpoint,
passthrough,
} => {
let path = PathBuf::from(&mountpoint);
if let Err(e) = fs::create_dir_all(&path) {
return Response::error(&format!("Failed to create mountpoint: {}", e));
}
match self.spawn_mount(&branch, &path) {
match self.spawn_mount(&branch, &path, passthrough) {
Ok(()) => Response::success(),
Err(e) => Response::error(&format!("{}", e)),
}
Expand Down
149 changes: 131 additions & 18 deletions src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use fuser::{
FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyIoctl,
ReplyOpen, ReplyWrite, Request, TimeOrNow,
BackingId, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry,
ReplyIoctl, ReplyOpen, ReplyWrite, Request, TimeOrNow,
};
use parking_lot::RwLock;

Expand Down Expand Up @@ -129,10 +129,16 @@ pub struct BranchFs {
/// Cached write fd — avoids re-open on consecutive writes to the same
/// delta file (after COW).
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>,
}

impl BranchFs {
pub fn new(manager: Arc<BranchManager>, branch_name: String) -> Self {
pub fn new(manager: Arc<BranchManager>, branch_name: String, passthrough: bool) -> Self {
let current_epoch = manager.get_epoch();
Self {
manager,
Expand All @@ -147,6 +153,9 @@ impl BranchFs {
gid: AtomicU32::new(nix::unistd::getgid().as_raw()),
open_cache: OpenFileCache::new(),
write_cache: WriteFileCache::new(),
passthrough_enabled: passthrough,
next_fh: AtomicU64::new(1),
backing_ids: HashMap::new(),
}
}

Expand Down Expand Up @@ -212,6 +221,64 @@ 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,
resolved: &std::path::Path,
reply: ReplyOpen,
) {
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;
}
}
} else {
resolved.to_path_buf()
};

// Open the backing file
let open_result = if is_writable {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&backing_path)
} 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);
}

/// Classify an inode number. Returns None for root and CTL_INO (handled separately).
fn classify_ino(&self, ino: u64) -> Option<PathContext> {
if ino == ROOT_INO {
Expand All @@ -230,11 +297,7 @@ impl BranchFs {
}

impl Filesystem for BranchFs {
fn init(
&mut self,
req: &Request,
_config: &mut fuser::KernelConfig,
) -> Result<(), libc::c_int> {
fn init(&mut self, req: &Request, config: &mut fuser::KernelConfig) -> Result<(), libc::c_int> {
// The init request may come from the kernel (uid=0) rather than the
// mounting user, so only override the process-derived defaults when
// the request carries a real (non-root) uid.
Expand All @@ -243,6 +306,24 @@ impl Filesystem for BranchFs {
self.gid.store(req.gid(), Ordering::Relaxed);
}

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");
}
}

Ok(())
}

Expand Down Expand Up @@ -1037,7 +1118,7 @@ impl Filesystem for BranchFs {
self.unlink(_req, parent, name, reply);
}

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 @@ -1070,11 +1151,19 @@ impl Filesystem for BranchFs {
reply.error(libc::ENOENT);
return;
}
if self.resolve_for_branch(&branch, &rel_path).is_some() {
self.manager.register_opened_inode(&branch, ino);
reply.opened(0, 0);
let resolved = match self.resolve_for_branch(&branch, &rel_path) {
Some(p) => p,
None => {
reply.error(libc::ENOENT);
return;
}
};
self.manager.register_opened_inode(&branch, ino);

if self.passthrough_enabled {
self.try_open_passthrough(ino, flags, &branch, &rel_path, &resolved, reply);
} else {
reply.error(libc::ENOENT);
reply.opened(0, 0);
}
}
_ => {
Expand All @@ -1083,17 +1172,41 @@ impl Filesystem for BranchFs {
reply.error(libc::ESTALE);
return;
}
if self.resolve(&path).is_some() {
self.manager
.register_opened_inode(&self.get_branch_name(), ino);
reply.opened(0, 0);
let resolved = match self.resolve(&path) {
Some(p) => p,
None => {
reply.error(libc::ENOENT);
return;
}
};
let branch_name = self.get_branch_name();
self.manager.register_opened_inode(&branch_name, ino);

if self.passthrough_enabled {
self.try_open_passthrough(ino, flags, &branch_name, &path, &resolved, reply);
} else {
reply.error(libc::ENOENT);
reply.opened(0, 0);
}
}
}
}

fn release(
&mut self,
_req: &Request<'_>,
_ino: u64,
fh: u64,
_flags: i32,
_lock_owner: Option<u64>,
_flush: bool,
reply: ReplyEmpty,
) {
if fh != 0 {
self.backing_ids.remove(&fh);
}
reply.ok();
}

fn setattr(
&mut self,
_req: &Request,
Expand Down
Loading