1#![allow(clippy::unnecessary_cast)]
2use async_trait::async_trait;
3use rfuse3::raw::reply::{FileAttr, ReplyCreated, ReplyXAttr};
4use rfuse3::raw::{ObjectSafeFilesystem, Request, reply::ReplyEntry};
5use rfuse3::{Inode, Result};
6use std::ffi::OsStr;
7use std::io::Error;
8use std::time::Duration;
9
10use crate::context::OperationContext;
11use crate::passthrough::PassthroughFs;
12use crate::util::whiteout::{OCI_OPAQUE_MARKER, WhiteoutFormat, oci_whiteout_name};
13pub const OPAQUE_XATTR_LEN: u32 = 16;
14pub const OPAQUE_XATTR: &str = "user.fuseoverlayfs.opaque";
15pub const UNPRIVILEGED_OPAQUE_XATTR: &str = "user.overlay.opaque";
16pub const PRIVILEGED_OPAQUE_XATTR: &str = "trusted.overlay.opaque";
17
18#[cfg(target_os = "macos")]
19type Stat64 = libc::stat;
20#[cfg(target_os = "linux")]
21type Stat64 = libc::stat64;
22
23#[async_trait]
25pub trait Layer: ObjectSafeFilesystem {
26 fn root_inode(&self) -> Inode;
28
29 fn whiteout_format(&self) -> WhiteoutFormat {
32 WhiteoutFormat::default()
33 }
34
35 #[cfg_attr(not(target_os = "macos"), allow(dead_code))]
39 async fn host_path_of(&self, _inode: Inode) -> Option<std::path::PathBuf> {
40 None
41 }
42 async fn create_whiteout(
47 &self,
48 ctx: Request,
49 parent: Inode,
50 name: &OsStr,
51 ) -> Result<ReplyEntry> {
52 let ino: u64 = parent;
53 match self.whiteout_format() {
54 WhiteoutFormat::CharDev => {
55 match self.lookup(ctx, ino, name).await {
56 Ok(v) => {
57 if is_whiteout(&v.attr) {
58 return Ok(v);
59 }
60 if v.attr.ino != 0 {
61 self.forget(ctx, v.attr.ino, 1).await;
62 return Err(Error::from_raw_os_error(libc::EEXIST).into());
63 }
64 }
65 Err(e) => {
66 let e: std::io::Error = e.into();
67 match e.raw_os_error() {
68 Some(raw_error) => {
69 if raw_error != libc::ENOENT {
70 return Err(e.into());
71 }
72 }
73 None => return Err(e.into()),
74 }
75 }
76 }
77 let dev = libc::makedev(0, 0);
78 let mode = (libc::S_IFCHR as u32) | 0o777;
79 self.mknod(ctx, ino, name, mode, dev as u32).await
80 }
81 WhiteoutFormat::OciWhiteout => {
82 oci_create_marker(self, ctx, ino, &oci_whiteout_name(name)).await
83 }
84 }
85 }
86
87 async fn delete_whiteout(&self, ctx: Request, parent: Inode, name: &OsStr) -> Result<()> {
89 let ino: u64 = parent;
90 match self.whiteout_format() {
91 WhiteoutFormat::CharDev => {
92 match self.lookup(ctx, ino, name).await {
93 Ok(v) => {
94 if v.attr.ino != 0 {
95 self.forget(ctx, v.attr.ino, 1).await;
96 }
97 if is_whiteout(&v.attr) {
98 return match self.unlink(ctx, ino, name).await {
99 Ok(()) => Ok(()),
100 Err(e) => {
101 let ie: std::io::Error = e.into();
102 if ie.raw_os_error() == Some(libc::ENOENT) {
103 Ok(())
104 } else {
105 Err(ie.into())
106 }
107 }
108 };
109 }
110 if v.attr.ino != 0 {
111 return Err(Error::from_raw_os_error(libc::EINVAL).into());
112 }
113 }
114 Err(e) => {
115 let ie: std::io::Error = e.into();
116 if ie.raw_os_error() != Some(libc::ENOENT) {
117 return Err(ie.into());
118 }
119 }
120 }
121 Ok(())
122 }
123 WhiteoutFormat::OciWhiteout => {
124 let wh = oci_whiteout_name(name);
125 match self.unlink(ctx, ino, &wh).await {
126 Ok(()) => Ok(()),
127 Err(e) => {
128 let ie: std::io::Error = e.into();
129 if ie.raw_os_error() == Some(libc::ENOENT) {
130 Ok(())
131 } else {
132 Err(ie.into())
133 }
134 }
135 }
136 }
137 }
138 }
139
140 async fn is_whiteout(&self, ctx: Request, inode: Inode) -> Result<bool> {
146 match self.whiteout_format() {
147 WhiteoutFormat::CharDev => {
148 let rep = self.getattr(ctx, inode, None, 0).await?;
149 Ok(is_whiteout(&rep.attr))
150 }
151 WhiteoutFormat::OciWhiteout => Ok(false),
152 }
153 }
154
155 async fn set_opaque(&self, ctx: Request, inode: Inode) -> Result<()> {
157 let ino: u64 = inode;
158
159 let rep = self.getattr(ctx, ino, None, 0).await?;
160 if !is_dir(&rep.attr) {
161 return Err(Error::from_raw_os_error(libc::ENOTDIR).into());
162 }
163
164 match self.whiteout_format() {
165 WhiteoutFormat::CharDev => {
166 self.setxattr(ctx, ino, OsStr::new(OPAQUE_XATTR), b"y", 0, 0)
167 .await
168 }
169 WhiteoutFormat::OciWhiteout => {
170 oci_create_marker(self, ctx, ino, OsStr::new(OCI_OPAQUE_MARKER))
171 .await
172 .map(|_| ())
173 }
174 }
175 }
176
177 async fn is_opaque(&self, ctx: Request, inode: Inode) -> Result<bool> {
179 let ino: u64 = inode;
180
181 let attr: rfuse3::raw::prelude::ReplyAttr = self.getattr(ctx, ino, None, 0).await?;
182 if !is_dir(&attr.attr) {
183 return Err(Error::from_raw_os_error(libc::ENOTDIR).into());
184 }
185
186 if matches!(self.whiteout_format(), WhiteoutFormat::OciWhiteout) {
187 let marker = OsStr::new(OCI_OPAQUE_MARKER);
188 return match self.lookup(ctx, ino, marker).await {
189 Ok(v) => {
190 if v.attr.ino == 0 {
191 Ok(false)
192 } else {
193 self.forget(ctx, v.attr.ino, 1).await;
194 Ok(true)
195 }
196 }
197 Err(e) => {
198 let ie: std::io::Error = e.into();
199 if ie.raw_os_error() == Some(libc::ENOENT) {
200 Ok(false)
201 } else {
202 Err(ie.into())
203 }
204 }
205 };
206 }
207
208 let check_attr = |inode: Inode, attr_name: &'static str, attr_size: u32| async move {
209 let cname = OsStr::new(attr_name);
210 match self.getxattr(ctx, inode, cname, attr_size).await {
211 Ok(v) => {
212 if let ReplyXAttr::Data(bufs) = v
214 && bufs.len() == 1
215 && bufs[0].eq_ignore_ascii_case(&b'y')
216 {
217 return Ok(true);
218 }
219 Ok(false)
221 }
222 Err(e) => {
223 let ioerror: std::io::Error = e.into();
224 if ioerror.raw_os_error() == Some(libc::ENODATA) {
225 return Ok(false);
226 }
227 #[cfg(target_os = "macos")]
228 if ioerror.raw_os_error() == Some(libc::ENOATTR) {
229 return Ok(false);
230 }
231
232 Err(e)
233 }
234 }
235 };
236
237 let is_opaque = check_attr(ino, OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
242 if is_opaque {
243 return Ok(true);
244 }
245
246 let is_opaque = check_attr(ino, PRIVILEGED_OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
248 if is_opaque {
249 return Ok(true);
250 }
251
252 let is_opaque = check_attr(ino, UNPRIVILEGED_OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
254 if is_opaque {
255 return Ok(true);
256 }
257
258 Ok(false)
259 }
260
261 async fn create_with_context(
264 &self,
265 _ctx: OperationContext,
266 _parent: Inode,
267 _name: &OsStr,
268 _mode: u32,
269 _flags: u32,
270 ) -> Result<ReplyCreated> {
271 Err(Error::from_raw_os_error(libc::ENOSYS).into())
272 }
273
274 async fn mkdir_with_context(
277 &self,
278 _ctx: OperationContext,
279 _parent: Inode,
280 _name: &OsStr,
281 _mode: u32,
282 _umask: u32,
283 ) -> Result<ReplyEntry> {
284 Err(Error::from_raw_os_error(libc::ENOSYS).into())
285 }
286
287 async fn symlink_with_context(
290 &self,
291 _ctx: OperationContext,
292 _parent: Inode,
293 _name: &OsStr,
294 _link: &OsStr,
295 ) -> Result<ReplyEntry> {
296 Err(Error::from_raw_os_error(libc::ENOSYS).into())
297 }
298
299 async fn getattr_with_mapping(
304 &self,
305 _inode: Inode,
306 _handle: Option<u64>,
307 _mapping: bool,
308 ) -> std::io::Result<(Stat64, Duration)> {
309 Err(std::io::Error::from_raw_os_error(libc::ENOSYS))
310 }
311}
312
313#[async_trait]
314impl Layer for PassthroughFs {
315 fn root_inode(&self) -> Inode {
316 1
317 }
318
319 fn whiteout_format(&self) -> WhiteoutFormat {
320 self.config().whiteout_format
321 }
322
323 async fn host_path_of(&self, inode: Inode) -> Option<std::path::PathBuf> {
324 self.passthrough_host_path(inode).await
325 }
326
327 async fn create_with_context(
328 &self,
329 ctx: OperationContext,
330 parent: Inode,
331 name: &OsStr,
332 mode: u32,
333 flags: u32,
334 ) -> Result<ReplyCreated> {
335 PassthroughFs::do_create_helper(
336 self,
337 ctx.req,
338 parent,
339 name,
340 mode,
341 flags,
342 ctx.uid.unwrap_or(ctx.req.uid),
343 ctx.gid.unwrap_or(ctx.req.gid),
344 )
345 .await
346 }
347
348 async fn mkdir_with_context(
349 &self,
350 ctx: OperationContext,
351 parent: Inode,
352 name: &OsStr,
353 mode: u32,
354 umask: u32,
355 ) -> Result<ReplyEntry> {
356 PassthroughFs::do_mkdir_helper(
357 self,
358 ctx.req,
359 parent,
360 name,
361 mode,
362 umask,
363 ctx.uid.unwrap_or(ctx.req.uid),
364 ctx.gid.unwrap_or(ctx.req.gid),
365 )
366 .await
367 }
368
369 async fn symlink_with_context(
370 &self,
371 ctx: OperationContext,
372 parent: Inode,
373 name: &OsStr,
374 link: &OsStr,
375 ) -> Result<ReplyEntry> {
376 PassthroughFs::do_symlink_helper(
377 self,
378 ctx.req,
379 parent,
380 name,
381 link,
382 ctx.uid.unwrap_or(ctx.req.uid),
383 ctx.gid.unwrap_or(ctx.req.gid),
384 )
385 .await
386 }
387
388 async fn getattr_with_mapping(
389 &self,
390 inode: Inode,
391 handle: Option<u64>,
392 mapping: bool,
393 ) -> std::io::Result<(Stat64, Duration)> {
394 PassthroughFs::do_getattr_inner(self, inode, handle, mapping).await
395 }
396}
397pub(crate) fn is_dir(st: &FileAttr) -> bool {
398 st.kind.const_into_mode_t() & libc::S_IFMT == libc::S_IFDIR
399}
400
401pub(crate) fn is_chardev(st: &FileAttr) -> bool {
402 st.kind.const_into_mode_t() & libc::S_IFMT == libc::S_IFCHR
403}
404
405pub(crate) fn is_whiteout(st: &FileAttr) -> bool {
406 let major = libc::major(st.rdev as libc::dev_t);
409 let minor = libc::minor(st.rdev as libc::dev_t);
410 is_chardev(st) && major == 0 && minor == 0
411}
412
413async fn oci_create_marker<F: ObjectSafeFilesystem + ?Sized>(
419 fs: &F,
420 ctx: Request,
421 parent: Inode,
422 marker: &OsStr,
423) -> Result<ReplyEntry> {
424 match fs.lookup(ctx, parent, marker).await {
425 Ok(v) if v.attr.ino != 0 => return Ok(v),
426 Ok(_) => {}
427 Err(e) => {
428 let ie: std::io::Error = e.into();
429 if ie.raw_os_error() != Some(libc::ENOENT) {
430 return Err(ie.into());
431 }
432 }
433 }
434 let flags = (libc::O_CREAT | libc::O_EXCL | libc::O_WRONLY) as u32;
435 let created = fs.create(ctx, parent, marker, 0o000, flags).await?;
436 let _ = fs
437 .release(ctx, created.attr.ino, created.fh, flags, 0, false)
438 .await;
439 Ok(ReplyEntry {
440 ttl: created.ttl,
441 attr: created.attr,
442 generation: created.generation,
443 })
444}
445
446#[cfg(test)]
447mod test {
448 use std::{ffi::OsStr, path::PathBuf};
449
450 use rfuse3::raw::{Filesystem as _, Request};
451
452 use crate::{
453 passthrough::{PassthroughArgs, PassthroughFs, config::Config, new_passthroughfs_layer},
454 unionfs::layer::Layer,
455 unwrap_or_skip_eperm,
456 util::whiteout::WhiteoutFormat,
457 };
458
459 #[tokio::test]
460 async fn delete_missing_oci_whiteout_is_idempotent() {
461 let temp_dir = tempfile::tempdir().unwrap();
462 let fs = PassthroughFs::<()>::new(Config {
463 root_dir: temp_dir.path().to_path_buf(),
464 do_import: true,
465 whiteout_format: WhiteoutFormat::OciWhiteout,
466 ..Default::default()
467 })
468 .unwrap();
469 unwrap_or_skip_eperm!(fs.init(Request::default()).await, "fs init");
470 unwrap_or_skip_eperm!(
471 fs.delete_whiteout(Request::default(), 1, OsStr::new("missing"))
472 .await,
473 "delete_whiteout missing OCI marker"
474 );
475 }
476
477 #[ignore]
479 #[tokio::test]
480 async fn test_whiteout_create_delete() {
481 let temp_dir = "/tmp/test_whiteout/t2";
482 let rootdir = PathBuf::from(temp_dir);
483 std::fs::create_dir_all(&rootdir).unwrap();
484 if std::env::var("RUN_PRIVILEGED_TESTS").ok().as_deref() != Some("1") {
485 eprintln!("skip test_whiteout_create_delete: RUN_PRIVILEGED_TESTS!=1");
486 return;
487 }
488 let fs = unwrap_or_skip_eperm!(
489 new_passthroughfs_layer(PassthroughArgs {
490 root_dir: rootdir,
491 mapping: None::<&str>
492 })
493 .await,
494 "init passthrough layer"
495 );
496 let _ = unwrap_or_skip_eperm!(fs.init(Request::default()).await, "fs init");
497 let white_name = OsStr::new(&"test");
498 let res = unwrap_or_skip_eperm!(
499 fs.create_whiteout(Request::default(), 1, white_name).await,
500 "create whiteout"
501 );
502
503 print!("{res:?}");
504 let res = fs.delete_whiteout(Request::default(), 1, white_name).await;
505 if res.is_err() {
506 panic!("{res:?}");
507 }
508 let _ = fs.destroy(Request::default()).await;
509 }
510
511 #[tokio::test]
512 async fn test_is_opaque_on_non_directory() {
513 let temp_dir = "/tmp/test_opaque_non_dir/t2";
514 let rootdir = PathBuf::from(temp_dir);
515 std::fs::create_dir_all(&rootdir).unwrap();
516 if std::env::var("RUN_PRIVILEGED_TESTS").ok().as_deref() != Some("1") {
517 eprintln!("skip test_is_opaque_on_non_directory: RUN_PRIVILEGED_TESTS!=1");
518 return;
519 }
520 let fs = unwrap_or_skip_eperm!(
521 new_passthroughfs_layer(PassthroughArgs {
522 root_dir: rootdir,
523 mapping: None::<&str>
524 })
525 .await,
526 "init passthrough layer"
527 );
528 let _ = unwrap_or_skip_eperm!(fs.init(Request::default()).await, "fs init");
529
530 let file_name = OsStr::new("not_a_dir");
532 let _ = unwrap_or_skip_eperm!(
533 fs.create(Request::default(), 1, file_name, 0o644, 0).await,
534 "create file"
535 );
536
537 let entry = unwrap_or_skip_eperm!(
539 fs.lookup(Request::default(), 1, file_name).await,
540 "lookup file"
541 );
542 let file_inode = entry.attr.ino;
543
544 let res = fs.is_opaque(Request::default(), file_inode).await;
546 assert!(res.is_err());
547 let err = res.err().unwrap();
548 let ioerr: std::io::Error = err.into();
549 assert_eq!(ioerr.raw_os_error(), Some(libc::ENOTDIR));
550
551 let _ = fs.unlink(Request::default(), 1, file_name).await;
553 let _ = fs.destroy(Request::default()).await;
554 }
555
556 #[tokio::test]
557 async fn test_set_opaque_on_non_directory() {
558 let temp_dir = "/tmp/test_set_opaque_non_dir/t2";
559 let rootdir = PathBuf::from(temp_dir);
560 std::fs::create_dir_all(&rootdir).unwrap();
561 if std::env::var("RUN_PRIVILEGED_TESTS").ok().as_deref() != Some("1") {
562 eprintln!("skip test_set_opaque_on_non_directory: RUN_PRIVILEGED_TESTS!=1");
563 return;
564 }
565 let fs = unwrap_or_skip_eperm!(
566 new_passthroughfs_layer(PassthroughArgs {
567 root_dir: rootdir,
568 mapping: None::<&str>
569 })
570 .await,
571 "init passthrough layer"
572 );
573 let _ = unwrap_or_skip_eperm!(fs.init(Request::default()).await, "fs init");
574
575 let file_name = OsStr::new("not_a_dir2");
577 let _ = unwrap_or_skip_eperm!(
578 fs.create(Request::default(), 1, file_name, 0o644, 0).await,
579 "create file"
580 );
581
582 let entry = unwrap_or_skip_eperm!(
584 fs.lookup(Request::default(), 1, file_name).await,
585 "lookup file"
586 );
587 let file_inode = entry.attr.ino;
588
589 let res = fs.set_opaque(Request::default(), file_inode).await;
591 assert!(res.is_err());
592 let err = res.err().unwrap();
593 let ioerr: std::io::Error = err.into();
594 assert_eq!(ioerr.raw_os_error(), Some(libc::ENOTDIR));
595
596 let _ = fs.unlink(Request::default(), 1, file_name).await;
598 let _ = fs.destroy(Request::default()).await;
599 }
600}