]> git.proxmox.com Git - proxmox-backup.git/blob - src/pxar/fuse.rs
src/pxar/fuse.rs: impl opendir callback for fuse
[proxmox-backup.git] / src / pxar / fuse.rs
1 //! Low level FUSE implementation for pxar.
2 //!
3 //! Allows to mount the archive as read-only filesystem to inspect its contents.
4
5 use std::collections::HashMap;
6 use std::convert::TryFrom;
7 use std::ffi::{CStr, CString, OsStr};
8 use std::fs::File;
9 use std::os::unix::ffi::OsStrExt;
10 use std::io::{BufReader, Read, Seek};
11 use std::path::Path;
12 use std::sync::Mutex;
13
14 use failure::{bail, format_err, Error};
15 use lazy_static::lazy_static;
16 use libc;
17 use libc::{c_char, c_int, c_void, size_t};
18
19 use super::decoder::Decoder;
20 use super::format_definition::{PxarAttributes, PxarGoodbyeItem};
21
22 /// Node ID of the root i-node
23 ///
24 /// Offsets in the archive are used as i-node for the fuse implementation, as
25 /// they are unique and enough to reference each item in the pxar archive.
26 /// The only exception to this is the `FUSE_ROOT_ID`, which is defined as 1 by
27 /// the fuse library.
28 /// This is okay since offset 1 is part of the root directory entry header and
29 /// will therefore not occur again, but remapping to the correct offset of 0 is
30 /// required.
31 const FUSE_ROOT_ID: u64 = 1;
32
33 const GOODBYE_ITEM_SIZE: u64 = std::mem::size_of::<PxarGoodbyeItem>() as u64;
34 lazy_static! {
35 /// HashMap holding the mapping from the child offsets to their parent
36 /// offsets.
37 ///
38 /// In order to include the parent directory entry '..' in the response for
39 /// readdir callback, this mapping is needed.
40 /// Calling the lookup callback will insert the offsets into the HashMap.
41 static ref CHILD_PARENT: Mutex<HashMap<u64, u64>> = Mutex::new(HashMap::new());
42 }
43
44 /// Callback function for `super::decoder::Decoder`.
45 ///
46 /// At the moment, this is only needed to satisfy the `SequentialDecoder`.
47 fn decoder_callback(_path: &Path) -> Result<(), Error> {
48 Ok(())
49 }
50
51 /// FFI types for easier readability
52 type Request = *mut c_void;
53 type MutPtr = *mut c_void;
54 type ConstPtr = *const c_void;
55 type StrPtr = *const c_char;
56 type MutStrPtr = *mut c_char;
57
58 #[rustfmt::skip]
59 #[link(name = "fuse3")]
60 extern "C" {
61 fn fuse_session_new(args: Option<&FuseArgs>, oprs: Option<&Operations>, size: size_t, op: ConstPtr) -> MutPtr;
62 fn fuse_set_signal_handlers(session: ConstPtr) -> c_int;
63 fn fuse_remove_signal_handlers(session: ConstPtr);
64 fn fuse_daemonize(foreground: c_int) -> c_int;
65 fn fuse_session_mount(session: ConstPtr, mountpoint: StrPtr) -> c_int;
66 fn fuse_session_unmount(session: ConstPtr);
67 fn fuse_session_loop(session: ConstPtr) -> c_int;
68 fn fuse_session_loop_mt_31(session: ConstPtr, clone_fd: c_int) -> c_int;
69 fn fuse_session_destroy(session: ConstPtr);
70 fn fuse_reply_attr(req: Request, attr: Option<&libc::stat>, timeout: f64) -> c_int;
71 fn fuse_reply_err(req: Request, errno: c_int) -> c_int;
72 fn fuse_reply_open(req: Request, fileinfo: ConstPtr) -> c_int;
73 fn fuse_reply_buf(req: Request, buf: MutStrPtr, size: size_t) -> c_int;
74 fn fuse_reply_entry(req: Request, entry: Option<&EntryParam>) -> c_int;
75 fn fuse_req_userdata(req: Request) -> MutPtr;
76 }
77
78 /// Command line arguments passed to fuse.
79 #[repr(C)]
80 #[derive(Debug)]
81 struct FuseArgs {
82 argc: c_int,
83 argv: *const StrPtr,
84 allocated: c_int,
85 }
86
87 /// `Session` stores a pointer to the session context and is used to mount the
88 /// archive to the given mountpoint.
89 pub struct Session {
90 ptr: MutPtr,
91 verbose: bool,
92 }
93
94 /// `Operations` defines the callback function table of supported operations.
95 #[repr(C)]
96 #[derive(Default)]
97 #[rustfmt::skip]
98 struct Operations {
99 // The order in which the functions are listed matters, as the offset in the
100 // struct defines what function the fuse driver uses.
101 // It should therefore not be altered!
102 init: Option<extern fn(userdata: MutPtr)>,
103 destroy: Option<extern fn(userdata: MutPtr)>,
104 lookup: Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
105 forget: Option<extern fn(req: Request, inode: u64, nlookup: u64)>,
106 getattr: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
107 setattr: Option<extern fn(req: Request, inode: u64, attr: MutPtr, to_set: c_int, fileinfo: MutPtr)>,
108 readlink: Option<extern fn(req: Request, inode: u64)>,
109 mknod: Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: c_int, rdev: c_int)>,
110 mkdir: Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: c_int)>,
111 unlink: Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
112 rmdir: Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
113 symlink: Option<extern fn(req: Request, link: StrPtr, parent: u64, name: StrPtr)>,
114 rename: Option<extern fn(req: Request, parent: u64, name: StrPtr, newparent: u64, newname: StrPtr, flags: c_int)>,
115 link: Option<extern fn(req: Request, inode: u64, newparent: u64, newname: StrPtr)>,
116 open: Option<extern fn(req: Request, indoe: u64, fileinfo: MutPtr)>,
117 read: Option<extern fn(req: Request, inode: u64, size: size_t, offset: c_int, fileinfo: MutPtr)>,
118 write: Option<extern fn(req: Request, inode: u64, buffer: StrPtr, size: size_t, offset: c_void, fileinfo: MutPtr)>,
119 flush: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
120 release: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
121 fsync: Option<extern fn(req: Request, inode: u64, datasync: c_int, fileinfo: MutPtr)>,
122 opendir: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
123 readdir: Option<extern fn(req: Request, inode: u64, size: size_t, offset: c_int, fileinfo: MutPtr)>,
124 releasedir: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
125 fsyncdir: Option<extern fn(req: Request, inode: u64, datasync: c_int, fileinfo: MutPtr)>,
126 statfs: Option<extern fn(req: Request, inode: u64)>,
127 setxattr: Option<extern fn(req: Request, inode: u64, name: StrPtr, value: StrPtr, size: size_t, flags: c_int)>,
128 getxattr: Option<extern fn(req: Request, inode: u64, name: StrPtr, size: size_t)>,
129 listxattr: Option<extern fn(req: Request, inode: u64, size: size_t)>,
130 removexattr: Option<extern fn(req: Request, inode: u64, name: StrPtr)>,
131 access: Option<extern fn(req: Request, inode: u64, mask: i32)>,
132 create: Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: c_int, fileinfo: MutPtr)>,
133 getlk: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, lock: MutPtr)>,
134 setlk: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, lock: MutPtr, sleep: c_int)>,
135 bmap: Option<extern fn(req: Request, inode: u64, blocksize: size_t, idx: u64)>,
136 ioctl: Option<extern fn(req: Request, inode: u64, cmd: c_int, arg: MutPtr, fileinfo: MutPtr, flags: c_int, in_buf: ConstPtr, in_bufsz: size_t, out_bufsz: size_t)>,
137 poll: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, pollhandle: MutPtr)>,
138 write_buf: Option<extern fn(req: Request, inode: u64, bufv: MutPtr, offset: c_int, fileinfo: MutPtr)>,
139 retrieve_reply: Option<extern fn(req: Request, cookie: ConstPtr, inode: u64, offset: c_int, bufv: MutPtr)>,
140 forget_multi: Option<extern fn(req: Request, count: size_t, forgets: MutPtr)>,
141 flock: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, op: c_int)>,
142 fallocate: Option<extern fn(req: Request, inode: u64, mode: c_int, offset: c_int, length: c_int, fileinfo: MutPtr)>,
143 readdirplus: Option<extern fn(req: Request, inode: u64, size: size_t, offset: c_int, fileinfo: MutPtr)>,
144 copy_file_range: Option<extern fn(req: Request, ino_in: u64, off_in: c_int, fi_in: MutPtr, ino_out: u64, off_out: c_int, fi_out: MutPtr, len: size_t, flags: c_int)>,
145 }
146
147 impl Session {
148 /// Create a new low level fuse session.
149 ///
150 /// `Session` is created using the provided mount options and sets the
151 /// default signal handlers.
152 /// Options have to be provided as comma separated OsStr, e.g.
153 /// ("ro,default_permissions").
154 pub fn new(archive_path: &Path, options: &OsStr, verbose: bool) -> Result<Self, Error> {
155 let file = File::open(archive_path)?;
156 // First argument should be the executable name
157 let mut arguments = vec![
158 CString::new("pxar-mount").unwrap(),
159 CString::new("-o").unwrap(),
160 CString::new(options.as_bytes())?,
161 ];
162 if verbose {
163 arguments.push(CString::new("--debug").unwrap());
164 }
165
166 let arg_ptrs: Vec<_> = arguments.iter().map(|opt| opt.as_ptr()).collect();
167 let args = FuseArgs {
168 argc: arg_ptrs.len() as i32,
169 argv: arg_ptrs.as_ptr(),
170 allocated: 0,
171 };
172
173 // Register the callback funcitons for the session
174 let mut oprs = Operations::default();
175 oprs.init = Some(init);
176 oprs.destroy = Some(destroy);
177 oprs.lookup = Some(lookup);
178 oprs.getattr = Some(getattr);
179 oprs.open = Some(open);
180 oprs.read = Some(read);
181 oprs.opendir = Some(opendir);
182 oprs.readdir = Some(readdir);
183
184 // By storing the decoder as userdata of the session, each request may
185 // access it.
186 let reader = BufReader::new(file);
187 let decoder = Decoder::new(reader, decoder_callback as fn(&Path) -> Result<(), Error>)?;
188 let session_decoder = Box::new(Mutex::new(decoder));
189 let session_ptr = unsafe {
190 fuse_session_new(
191 Some(&args),
192 Some(&oprs),
193 std::mem::size_of::<Operations>(),
194 // Ownership of session_decoder is passed to the session here.
195 // It has to be reclaimed before dropping the session to free
196 // the decoder and close the underlying file. This is done inside
197 // the destroy callback function.
198 Box::into_raw(session_decoder) as ConstPtr,
199 )
200 };
201
202 if session_ptr.is_null() {
203 bail!("error while creating new fuse session");
204 }
205
206 if unsafe { fuse_set_signal_handlers(session_ptr) } != 0 {
207 bail!("error while setting signal handlers");
208 }
209
210 Ok(Self {
211 ptr: session_ptr,
212 verbose: verbose,
213 })
214 }
215
216 /// Mount the filesystem on the given mountpoint.
217 ///
218 /// Actually mount the filesystem for this session on the provided mountpoint
219 /// and daemonize process.
220 pub fn mount(&mut self, mountpoint: &Path) -> Result<(), Error> {
221 if self.verbose {
222 println!("Mounting archive to {:#?}", mountpoint);
223 }
224 let mountpoint = mountpoint.canonicalize()?;
225 let path_cstr = CString::new(mountpoint.as_os_str().as_bytes())
226 .map_err(|err| format_err!("invalid mountpoint - {}", err))?;
227 if unsafe { fuse_session_mount(self.ptr, path_cstr.as_ptr()) } != 0 {
228 bail!("mounting on {:#?} failed", mountpoint);
229 }
230
231 // Do not send process to background if verbose flag is set
232 if !self.verbose && unsafe { fuse_daemonize(0) } != 0 {
233 bail!("could not send process to background");
234 }
235
236 Ok(())
237 }
238
239 /// Execute session loop which handles requests from kernel.
240 ///
241 /// The multi_threaded flag controls if the session loop runs in
242 /// single-threaded or multi-threaded mode.
243 /// Single-threaded mode is intended for debugging only.
244 pub fn run_loop(&mut self, multi_threaded: bool) -> Result<(), Error> {
245 if self.verbose {
246 println!("Executing fuse session loop");
247 }
248 let result = match multi_threaded {
249 true => unsafe { fuse_session_loop_mt_31(self.ptr, 1) },
250 false => unsafe { fuse_session_loop(self.ptr) },
251 };
252 if result < 0 {
253 bail!("fuse session loop exited with - {}", result);
254 }
255 if result > 0 {
256 eprintln!("fuse session loop received signal - {}", result);
257 }
258
259 Ok(())
260 }
261 }
262
263 impl Drop for Session {
264 fn drop(&mut self) {
265 unsafe {
266 fuse_session_unmount(self.ptr);
267 fuse_remove_signal_handlers(self.ptr);
268 fuse_session_destroy(self.ptr);
269 }
270 }
271 }
272
273 /// Creates a context providing an exclusive mutable reference to the decoder.
274 ///
275 /// Each callback function needing access to the decoder can easily get an
276 /// exclusive handle by running the code inside this context.
277 /// Responses with error code can easily be generated by returning with the
278 /// error code.
279 /// The error code will be used to reply to libfuse.
280 fn run_in_context<F>(req: Request, inode: u64, code: F)
281 where
282 F: FnOnce(
283 &mut Decoder<BufReader<File>, fn(&Path) -> Result<(), Error>>,
284 u64,
285 ) -> Result<(), i32>,
286 {
287 let ptr = unsafe {
288 fuse_req_userdata(req)
289 as *mut Mutex<Decoder<BufReader<File>, fn(&Path) -> Result<(), Error>>>
290 };
291 let boxed_decoder = unsafe { Box::from_raw(ptr) };
292 let result = boxed_decoder
293 .lock()
294 .map(|mut decoder| {
295 let ino_offset = match inode {
296 FUSE_ROOT_ID => decoder.root_end_offset() - GOODBYE_ITEM_SIZE,
297 _ => inode,
298 };
299 code(&mut decoder, ino_offset)
300 })
301 .unwrap_or(Err(libc::EIO));
302
303 if let Err(err) = result {
304 unsafe {
305 let _res = fuse_reply_err(req, err);
306 }
307 }
308
309 // Release ownership of boxed decoder, do not drop it.
310 let _ = Box::into_raw(boxed_decoder);
311 }
312
313 /// Callback functions for fuse kernel driver.
314 extern "C" fn init(_decoder: MutPtr) {
315 // Notting to do here for now
316 }
317
318 /// Cleanup the userdata created while creating the session, which is the decoder
319 extern "C" fn destroy(decoder: MutPtr) {
320 // Get ownership of the decoder and drop it when Box goes out of scope.
321 unsafe {
322 Box::from_raw(decoder);
323 }
324 }
325
326 /// FUSE entry for fuse_reply_entry in lookup callback
327 #[repr(C)]
328 struct EntryParam {
329 inode: u64,
330 generation: u64,
331 attr: libc::stat,
332 attr_timeout: f64,
333 entry_timeout: f64,
334 }
335
336 /// Lookup `name` in the directory referenced by `parent` inode.
337 ///
338 /// Inserts also the child and parent file offset in the hashmap to quickly
339 /// obtain the parent offset based on the child offset.
340 extern "C" fn lookup(req: Request, parent: u64, name: StrPtr) {
341 let filename = unsafe { CStr::from_ptr(name) };
342 let hash = super::format_definition::compute_goodbye_hash(filename.to_bytes());
343
344 run_in_context(req, parent, |mut decoder, ino_offset| {
345 let goodbye_table = decoder.goodbye_table(None, ino_offset + GOODBYE_ITEM_SIZE).map_err(|_| libc::EIO)?;
346
347 let (_item, start, end) = goodbye_table
348 .iter()
349 .find(|(e, _, _)| e.hash == hash)
350 .ok_or(libc::ENOENT)?;
351
352 let (mut attr, _) = stat(&mut decoder, *start)?;
353 let offset = if attr.st_mode & libc::S_IFMT == libc::S_IFDIR {
354 *end - GOODBYE_ITEM_SIZE
355 } else {
356 *start
357 };
358 let inode = if offset == decoder.root_end_offset() - GOODBYE_ITEM_SIZE {
359 FUSE_ROOT_ID
360 } else {
361 offset
362 };
363 attr.st_ino = inode;
364
365 let e = EntryParam {
366 inode,
367 generation: 1,
368 attr,
369 attr_timeout: std::f64::MAX,
370 entry_timeout: std::f64::MAX,
371 };
372
373 // Update the parent for this child entry. Used to get parent offset if
374 // only child offset is known.
375 CHILD_PARENT
376 .lock()
377 .map_err(|_| libc::EIO)?
378 .insert(offset, ino_offset);
379 let _res = unsafe { fuse_reply_entry(req, Some(&e)) };
380
381 Ok(())
382 });
383 }
384
385 /// Get attr and xattr from the decoder and update stat according to the fuse
386 /// implementation before returning
387 fn stat<R, F>(decoder: &mut Decoder<R, F>, offset: u64) -> Result<(libc::stat, PxarAttributes), i32>
388 where
389 R: Read + Seek,
390 F: Fn(&Path) -> Result<(), Error>,
391 {
392 let (entry, xattr, payload_size) = decoder.attributes(offset).map_err(|_| libc::EIO)?;
393 let inode = if offset == decoder.root_end_offset() - GOODBYE_ITEM_SIZE {
394 FUSE_ROOT_ID
395 } else {
396 offset
397 };
398 let nlink = match (entry.mode as u32) & libc::S_IFMT {
399 libc::S_IFDIR => 2,
400 _ => 1,
401 };
402 let time = i64::try_from(entry.mtime).map_err(|_| libc::EIO)? / 1_000_000_000;
403
404 let mut attr: libc::stat = unsafe { std::mem::zeroed() };
405 attr.st_ino = inode;
406 attr.st_nlink = nlink;
407 attr.st_mode = u32::try_from(entry.mode).map_err(|_| libc::EIO)?;
408 attr.st_size = i64::try_from(payload_size).map_err(|_| libc::EIO)?;
409 attr.st_uid = entry.uid;
410 attr.st_gid = entry.gid;
411 attr.st_atime = time;
412 attr.st_mtime = time;
413 attr.st_ctime = time;
414
415 Ok((attr, xattr))
416 }
417
418 extern "C" fn getattr(req: Request, inode: u64, _fileinfo: MutPtr) {
419 run_in_context(req, inode, |mut decoder, ino_offset| {
420 let (attr, _) = stat(&mut decoder, ino_offset)?;
421 let _res = unsafe {
422 // Since fs is read-only, the timeout can be max.
423 let timeout = std::f64::MAX;
424 fuse_reply_attr(req, Some(&attr), timeout)
425 };
426
427 Ok(())
428 });
429 }
430
431 extern "C" fn open(req: Request, inode: u64, fileinfo: MutPtr) {
432 run_in_context(req, inode, |decoder, ino_offset| {
433 decoder.open(ino_offset).map_err(|_| libc::ENOENT)?;
434 let _ret = unsafe { fuse_reply_open(req, fileinfo) };
435
436 Ok(())
437 });
438 }
439
440 extern "C" fn read(req: Request, inode: u64, size: size_t, offset: c_int, _fileinfo: MutPtr) {
441 run_in_context(req, inode, |decoder, ino_offset| {
442 let mut data = decoder
443 .read(ino_offset, size, offset as u64)
444 .map_err(|_| libc::EIO)?;
445
446 let _res = unsafe {
447 let len = data.len();
448 let dptr = data.as_mut_ptr() as *mut c_char;
449 fuse_reply_buf(req, dptr, len)
450 };
451
452 Ok(())
453 });
454 }
455
456 /// Open the directory referenced by the given inode for reading.
457 ///
458 /// This simply checks if the inode references a valid directory, no internal
459 /// state identifies the directory as opened.
460 extern "C" fn opendir(req: Request, inode: u64, fileinfo: MutPtr) {
461 run_in_context(req, inode, |mut decoder, ino_offset| {
462 let (attr, _) = stat(&mut decoder, ino_offset).map_err(|_| libc::ENOENT)?;
463 if attr.st_mode & libc::S_IFMT != libc::S_IFDIR {
464 return Err(libc::ENOENT);
465 }
466 let _ret = unsafe { fuse_reply_open(req, fileinfo) };
467
468 Ok(())
469 });
470 }
471
472 extern "C" fn readdir(req: Request, inode: u64, _size: size_t, _offset: c_int, _fileinfo: MutPtr) {
473 run_in_context(req, inode, |_decoder, _ino_offset| {
474 // code goes here
475
476 Err(libc::ENOENT)
477 });
478 }