Skip to main content

libfuse_fs/unionfs/
layer.rs

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/// A filesystem must implement Layer trait, or it cannot be used as an OverlayFS layer.
24#[async_trait]
25pub trait Layer: ObjectSafeFilesystem {
26    /// Return the root inode number
27    fn root_inode(&self) -> Inode;
28
29    /// Whiteout format used by this layer. Default is `CharDev` on Linux and
30    /// `OciWhiteout` on macOS; backends may override via config.
31    fn whiteout_format(&self) -> WhiteoutFormat {
32        WhiteoutFormat::default()
33    }
34
35    /// Resolve `inode` to the absolute host filesystem path that backs it,
36    /// if such a mapping exists. Returns `None` for layers that lack a 1:1
37    /// host-fs mapping. See `overlayfs::Layer::host_path_of` for details.
38    #[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    /// Create whiteout file with name <name>.
43    ///
44    /// If this call is successful then the lookup count of the `Inode` associated with the returned
45    /// `Entry` must be increased by 1.
46    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    /// Delete whiteout file with name <name>.
88    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    /// Check if the Inode is a whiteout file.
141    ///
142    /// **Note**: this overload is `CharDev`-only by design — see the trait
143    /// definition for explanation. In `OciWhiteout` mode this returns
144    /// `Ok(false)`.
145    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    /// Set the directory to opaque.
156    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    /// Check if the directory is opaque.
178    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                    // xattr name exists and we get value.
213                    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                    // No value found, go on to next check.
220                    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        // A directory is made opaque by setting some specific xattr to "y".
238        // See ref: https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
239
240        // Check our customized version of the xattr "user.fuseoverlayfs.opaque".
241        let is_opaque = check_attr(ino, OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
242        if is_opaque {
243            return Ok(true);
244        }
245
246        // Also check for the unprivileged version of the xattr "trusted.overlay.opaque".
247        let is_opaque = check_attr(ino, PRIVILEGED_OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
248        if is_opaque {
249            return Ok(true);
250        }
251
252        // Also check for the unprivileged version of the xattr "user.overlay.opaque".
253        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    /// Create a file with explicit ownership context.
262    /// Uses OperationContext to allow overriding UID/GID for internal operations like copy-up.
263    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    /// Create a directory with explicit ownership context.
275    /// Uses OperationContext to allow overriding UID/GID for internal operations like copy-up.
276    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    /// Create a symbolic link with explicit ownership context.
288    /// Uses OperationContext to allow overriding UID/GID for internal operations like copy-up.
289    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    /// Retrieve metadata with optional ID mapping control.
300    ///
301    /// - `mapping: true`: Returns attributes as seen inside the container (mapped).
302    /// - `mapping: false`: Returns raw attributes on the host filesystem (unmapped).
303    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    // A whiteout is created as a character device with 0/0 device number.
407    // See ref: https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
408    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
413/// Create an OCI whiteout / opaque-marker file (regular file, mode 0).
414///
415/// Uses `ObjectSafeFilesystem::create` rather than `mknod(S_IFREG, ..)`
416/// because Darwin's `mknod(2)` rejects regular-file modes with EINVAL. The fh
417/// from `create` is released immediately — callers only want the entry attrs.
418async 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    // Mark as ignored by default; run with: RUN_PRIVILEGED_TESTS=1 cargo test -- --ignored
478    #[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        // Create a file
531        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        // Lookup to get the inode of the file
538        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        // is_opaque should return ENOTDIR error
545        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        // Clean up
552        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        // Create a file
576        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        // Lookup to get the inode of the file
583        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        // set_opaque should return ENOTDIR error
590        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        // Clean up
597        let _ = fs.unlink(Request::default(), 1, file_name).await;
598        let _ = fs.destroy(Request::default()).await;
599    }
600}