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
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"
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
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)) })
Comment thread
Popax21 marked this conversation as resolved.
})
.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_ne_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) }
}
}
5 changes: 1 addition & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ pub use crate::ll::request::LockOwner;
pub use crate::ll::request::Version;
pub use crate::mnt::mount_options::Config;
pub use crate::mnt::mount_options::MountOption;
use crate::mnt::mount_options::check_option_conflicts;
pub use crate::notify::Notifier;
pub use crate::notify::PollHandle;
pub use crate::notify::PollNotifier;
Expand Down Expand Up @@ -1054,8 +1053,7 @@ pub fn mount<FS: Filesystem, P: AsRef<Path>>(
mountpoint: P,
options: &Config,
) -> io::Result<()> {
check_option_conflicts(options)?;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Please submit this in a separate PR, since it isn't related to the BackingId feature, as far as I can tell

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was mainly a drive-by change while implementing the other session fixes / improvements, since currently one can bypass this option conflict check by calling Session::new() directly. Moving this call there cleans up the module-level entrypoints while also patching this minor blind spot.

It is however true that this is unrelated to the actual purpose of the PR; as such I can factor out this commit (which also includes the run() visibility change) into a separate PR. My reasoning for not doing so initially was that this was a rather minor change, so the bureaucratic overhead of making a new PR for it instead of just doing this is a drive-by seemed unreasonable to me.

Session::new(filesystem, mountpoint.as_ref(), options).and_then(|se| se.run())
Session::new(filesystem, mountpoint.as_ref(), options).and_then(session::Session::run)
}

/// Mount the given filesystem to the given mountpoint. This function spawns
Expand All @@ -1072,6 +1070,5 @@ pub fn spawn_mount<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef<Path>>(
mountpoint: P,
options: &Config,
) -> io::Result<BackgroundSession> {
check_option_conflicts(options)?;
Session::new(filesystem, mountpoint.as_ref(), options).and_then(session::Session::spawn)
}
Loading