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
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,6 @@ name = "ioctl"

[[example]]
name = "passthrough"

[[example]]
name = "passthrough_fork"
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ build: pre
cargo build --examples --features=experimental

format:
cargo fmt --all
cargo +nightly fmt --all

pre:
cargo fmt --all -- --check
cargo +nightly fmt --all -- --check
cargo deny check licenses
cargo clippy --all-targets
cargo clippy --all-targets --no-default-features
Expand Down Expand Up @@ -63,8 +63,9 @@ mount_tests:
fuser:mount_tests_libfuse3 bash -c "cd /code/fuser && cargo run -p fuser-tests -- linux-mount-libfuse3"

test_passthrough:
cargo build --example passthrough
cargo build --example passthrough --example passthrough_fork
sudo tests/test_passthrough.sh target/debug/examples/passthrough
sudo tests/test_passthrough.sh target/debug/examples/passthrough_fork

test: pre mount_tests pjdfs_tests xfstests
cargo test
Expand Down
335 changes: 335 additions & 0 deletions examples/passthrough_fork.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
// This example requires fuse 7.40 or later. Run with:
//
// cargo run --example passthrough_fork /tmp/foobar

mod common;

use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::File;
use std::os::fd::AsFd;
use std::os::fd::AsRawFd;
use std::os::fd::FromRawFd;
use std::os::fd::OwnedFd;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::net::UnixDatagram;
use std::sync::Arc;
use std::sync::Weak;
use std::time::Duration;
use std::time::UNIX_EPOCH;

use clap::Parser;
use fuser::BackingId;
use fuser::Errno;
use fuser::FileAttr;
use fuser::FileHandle;
use fuser::FileType;
use fuser::Filesystem;
use fuser::FopenFlags;
use fuser::INodeNo;
use fuser::InitFlags;
use fuser::KernelConfig;
use fuser::LockOwner;
use fuser::MountOption;
use fuser::OpenFlags;
use fuser::ReplyAttr;
use fuser::ReplyDirectory;
use fuser::ReplyEmpty;
use fuser::ReplyEntry;
use fuser::ReplyOpen;
use fuser::Request;
use nix::sys::socket;
use nix::sys::socket::MsgFlags;
use parking_lot::Mutex;

#[derive(Parser)]
#[command(version)]
struct Args {
#[clap(flatten)]
common_args: CommonArgs,
}

const TTL: Duration = Duration::from_secs(1); // 1 second

use crate::common::args::CommonArgs;

// See [./passthrough.rs]
#[derive(Debug, Default)]
struct BackingCache {
by_handle: HashMap<u64, Arc<BackingId>>,
by_inode: HashMap<INodeNo, Weak<BackingId>>,
next_fh: u64,
}

impl BackingCache {
fn next_fh(&mut self) -> u64 {
self.next_fh += 1;
self.next_fh
}

fn get_or(
&mut self,
ino: INodeNo,
callback: impl Fn() -> std::io::Result<BackingId>,
) -> std::io::Result<(u64, Arc<BackingId>)> {
let fh = self.next_fh();

let id = if let Some(id) = self.by_inode.get(&ino).and_then(Weak::upgrade) {
eprintln!("HIT! reusing {id:?}");
id
} else {
let id = Arc::new(callback()?);
self.by_inode.insert(ino, Arc::downgrade(&id));
eprintln!("MISS! new {id:?}");
id
};

self.by_handle.insert(fh, Arc::clone(&id));
Ok((fh, id))
}

fn put(&mut self, fh: u64) {
eprintln!("Put fh {fh}");
match self.by_handle.remove(&fh) {
None => eprintln!("ERROR: Put fh {fh} but it wasn't found in cache!!\n"),
Some(id) => eprintln!("Put fh {fh}, was {id:?}\n"),
}
}
}

#[derive(Debug)]
struct ForkPassthroughFs {
root_attr: FileAttr,
passthrough_file_attr: FileAttr,
socket: UnixDatagram,
backing_cache: Mutex<BackingCache>,
}

impl ForkPassthroughFs {
fn new(socket: UnixDatagram) -> Self {
let uid = nix::unistd::getuid().into();
let gid = nix::unistd::getgid().into();

let root_attr = FileAttr {
ino: INodeNo::ROOT,
size: 0,
blocks: 0,
atime: UNIX_EPOCH, // 1970-01-01 00:00:00
mtime: UNIX_EPOCH,
ctime: UNIX_EPOCH,
crtime: UNIX_EPOCH,
kind: FileType::Directory,
perm: 0o755,
nlink: 2,
uid,
gid,
rdev: 0,
flags: 0,
blksize: 512,
};

let passthrough_file_attr = FileAttr {
ino: INodeNo(2),
size: 123_456,
blocks: 1,
atime: UNIX_EPOCH, // 1970-01-01 00:00:00
mtime: UNIX_EPOCH,
ctime: UNIX_EPOCH,
crtime: UNIX_EPOCH,
kind: FileType::RegularFile,
perm: 0o644,
nlink: 1,
uid: 333,
gid: 333,
rdev: 0,
flags: 0,
blksize: 512,
};

Self {
root_attr,
passthrough_file_attr,
socket,
backing_cache: Mutex::default(),
}
}
}

impl Filesystem for ForkPassthroughFs {
fn init(&mut self, _req: &Request, config: &mut KernelConfig) -> std::io::Result<()> {
config
.add_capabilities(InitFlags::FUSE_PASSTHROUGH)
.unwrap();
config.set_max_stack_depth(2).unwrap();
Ok(())
}

fn destroy(&mut self) {
// Tell the parent process to shut down
self.socket.send(&[]).unwrap();
}

fn lookup(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEntry) {
if parent == INodeNo::ROOT && name.to_str() == Some("passthrough") {
reply.entry(&TTL, &self.passthrough_file_attr, fuser::Generation(0));
} else {
reply.error(Errno::ENOENT);
}
}

fn getattr(&self, _req: &Request, ino: INodeNo, _fh: Option<FileHandle>, reply: ReplyAttr) {
match ino.0 {
1 => reply.attr(&TTL, &self.root_attr),
2 => reply.attr(&TTL, &self.passthrough_file_attr),
_ => reply.error(Errno::ENOENT),
}
}

fn open(&self, _req: &Request, ino: INodeNo, _flags: OpenFlags, reply: ReplyOpen) {
if ino != INodeNo(2) {
reply.error(Errno::ENOENT);
return;
}

let (fh, id) = self
.backing_cache
.lock()
.get_or(ino, || {
// Ask the parent process to open a backing ID for us (concurrency is left as an exercise for the reader)
const FILE: &str = "/etc/profile";
eprintln!("Asking server to open backing file for {FILE:?}");

let mut buf = [0u8; 4];
self.socket.send(FILE.as_bytes())?;
self.socket.recv(&mut buf)?;
Ok(unsafe { reply.wrap_backing(u32::from_ne_bytes(buf)) })
})
.unwrap();

eprintln!(" -> opened_passthrough({fh:?}, 0, {id:?});\n");
reply.opened_passthrough(FileHandle(fh), FopenFlags::empty(), &id);
}

fn release(
&self,
_req: &Request,
_ino: INodeNo,
fh: FileHandle,
_flags: OpenFlags,
_lock_owner: Option<LockOwner>,
_flush: bool,
reply: ReplyEmpty,
) {
self.backing_cache.lock().put(fh.into());
reply.ok();
}

fn readdir(
&self,
_req: &Request,
ino: INodeNo,
_fh: FileHandle,
offset: u64,
mut reply: ReplyDirectory,
) {
if ino != INodeNo::ROOT {
reply.error(Errno::ENOENT);
return;
}

let entries = vec![
(1, FileType::Directory, "."),
(1, FileType::Directory, ".."),
(2, FileType::RegularFile, "passthrough"),
];

for (i, entry) in entries.into_iter().enumerate().skip(offset as usize) {
// i + 1 means the index of the next entry
if reply.add(INodeNo(entry.0), (i + 1) as u64, entry.1, entry.2) {
break;
}
}
reply.ok();
}
}

fn main() {
// Fork and handle opening backing IDs in the parent process
// Do this early since forking isn't guaranteed to be a safe operation once e.g. libraries get involved
// You may also choose to use `std::process::Command` / etc instead
let (parent_sock, child_sock) = UnixDatagram::pair().unwrap();
match unsafe { nix::unistd::fork().unwrap() } {
nix::unistd::ForkResult::Parent { .. } => {
drop(child_sock);
backing_id_server(parent_sock);
return;
}
nix::unistd::ForkResult::Child => {
drop(parent_sock);
}
}

// You may wish to drop privileges (i.e. CAP_SYS_ADMIN / root) here

// Mount the FS as usual
let args = Args::parse();
env_logger::init();

let mut cfg = args.common_args.config();
cfg.mount_options
.extend([MountOption::FSName("passthrough".to_string())]);
let fs = ForkPassthroughFs::new(child_sock.try_clone().unwrap());
let session = fuser::Session::new(fs, &args.common_args.mount_point, &cfg).unwrap();

// Send the FUSE FD to the parent process so it may open backing files
let fds = [session.as_fd().as_raw_fd()];

socket::sendmsg::<()>(
child_sock.as_raw_fd(),
&[],
&[socket::ControlMessage::ScmRights(&fds)],
MsgFlags::empty(),
None,
)
.unwrap();

// Run the FS
session.run().unwrap();
}

fn backing_id_server(socket: UnixDatagram) {
let mut buf = [0u8; 1024];

// Receive the FUSE FD over the Unix socket
let msg = socket::recvmsg::<()>(
socket.as_fd().as_raw_fd(),
&mut [],
Some(&mut buf),
MsgFlags::empty(),
)
.unwrap();

let fuse_fd = 'fd: {
for cmsg in msg.cmsgs().unwrap() {
if let socket::ControlMessageOwned::ScmRights(fds) = cmsg {
let fd = unsafe { OwnedFd::from_raw_fd(fds[0]) };
break 'fd fd;
}
}
unreachable!();
};

// Handle backing ID requests (in real world scenarios, you may wish to perform input validation / etc here)
loop {
let sz = socket.recv(&mut buf).unwrap();
if sz == 0 {
return;
}

let path = OsStr::from_bytes(&buf[..sz]);
eprintln!("[SERVER] opening backing file for {path:?}");

let id = BackingId::create_raw(&fuse_fd, File::open(path).unwrap()).unwrap();
socket.send(&u32::to_le_bytes(id)).unwrap();
}
}
4 changes: 4 additions & 0 deletions src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,8 @@ impl ChannelSender {
pub(crate) fn open_backing(&self, fd: BorrowedFd<'_>) -> std::io::Result<BackingId> {
BackingId::create(&self.0, fd)
}

pub(crate) unsafe fn wrap_backing(&self, id: u32) -> BackingId {
unsafe { BackingId::wrap_raw(&self.0, id) }
}
}
Loading
Loading