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;
12pub const OPAQUE_XATTR_LEN: u32 = 16;
13pub const OPAQUE_XATTR: &str = "user.fuseoverlayfs.opaque";
14pub const UNPRIVILEGED_OPAQUE_XATTR: &str = "user.overlay.opaque";
15pub const PRIVILEGED_OPAQUE_XATTR: &str = "trusted.overlay.opaque";
16
17#[cfg(target_os = "macos")]
18type Stat64 = libc::stat;
19#[cfg(target_os = "linux")]
20type Stat64 = libc::stat64;
21
22/// A filesystem must implement Layer trait, or it cannot be used as an OverlayFS layer.
23#[async_trait]
24pub trait Layer: ObjectSafeFilesystem {
25    /// Return the root inode number
26    fn root_inode(&self) -> Inode;
27    /// Create whiteout file with name <name>.
28    ///
29    /// If this call is successful then the lookup count of the `Inode` associated with the returned
30    /// `Entry` must be increased by 1.
31    async fn create_whiteout(
32        &self,
33        ctx: Request,
34        parent: Inode,
35        name: &OsStr,
36    ) -> Result<ReplyEntry> {
37        // Use temp value to avoid moved 'parent'.
38        let ino: u64 = parent;
39        match self.lookup(ctx, ino, name).await {
40            Ok(v) => {
41                // Find whiteout char dev.
42                if is_whiteout(&v.attr) {
43                    return Ok(v);
44                }
45                // Non-negative entry with inode larger than 0 indicates file exists.
46                if v.attr.ino != 0 {
47                    // Decrease the refcount.
48                    self.forget(ctx, v.attr.ino, 1).await;
49                    // File exists with same name, create whiteout file is not allowed.
50                    return Err(Error::from_raw_os_error(libc::EEXIST).into());
51                }
52            }
53            Err(e) => {
54                let e: std::io::Error = e.into();
55                match e.raw_os_error() {
56                    Some(raw_error) => {
57                        // We expect ENOENT error.
58                        if raw_error != libc::ENOENT {
59                            return Err(e.into());
60                        }
61                    }
62                    None => return Err(e.into()),
63                }
64            }
65        }
66
67        // Try to create whiteout char device with 0/0 device number.
68        let dev = libc::makedev(0, 0);
69        let mode = (libc::S_IFCHR as u32) | 0o777;
70        self.mknod(ctx, ino, name, mode, dev as u32).await
71    }
72
73    /// Delete whiteout file with name <name>.
74    async fn delete_whiteout(&self, ctx: Request, parent: Inode, name: &OsStr) -> Result<()> {
75        // Use temp value to avoid moved 'parent'.
76        let ino: u64 = parent;
77        match self.lookup(ctx, ino, name).await {
78            Ok(v) => {
79                if v.attr.ino != 0 {
80                    // Decrease the refcount since we make a lookup call.
81                    self.forget(ctx, v.attr.ino, 1).await;
82                }
83
84                // Find whiteout so we can safely delete it.
85                if is_whiteout(&v.attr) {
86                    return self.unlink(ctx, ino, name).await;
87                }
88                //  Non-negative entry with inode larger than 0 indicates file exists.
89                if v.attr.ino != 0 {
90                    // File exists but not whiteout file.
91                    return Err(Error::from_raw_os_error(libc::EINVAL).into());
92                }
93            }
94            Err(e) => return Err(e),
95        }
96        Ok(())
97    }
98
99    /// Check if the Inode is a whiteout file
100    async fn is_whiteout(&self, ctx: Request, inode: Inode) -> Result<bool> {
101        let rep = self.getattr(ctx, inode, None, 0).await?;
102
103        // Check attributes of the inode to see if it's a whiteout char device.
104        Ok(is_whiteout(&rep.attr))
105    }
106
107    /// Set the directory to opaque.
108    async fn set_opaque(&self, ctx: Request, inode: Inode) -> Result<()> {
109        // Use temp value to avoid moved 'parent'.
110        let ino: u64 = inode;
111
112        // Get attributes and check if it's directory.
113        let rep = self.getattr(ctx, ino, None, 0).await?;
114        if !is_dir(&rep.attr) {
115            // Only directory can be set to opaque.
116            return Err(Error::from_raw_os_error(libc::ENOTDIR).into());
117        }
118        // A directory is made opaque by setting the xattr "trusted.overlay.opaque" to "y".
119        // See ref: https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
120        self.setxattr(ctx, ino, OsStr::new(OPAQUE_XATTR), b"y", 0, 0)
121            .await
122    }
123
124    /// Check if the directory is opaque.
125    async fn is_opaque(&self, ctx: Request, inode: Inode) -> Result<bool> {
126        // Use temp value to avoid moved 'parent'.
127        let ino: u64 = inode;
128
129        // Get attributes of the directory.
130        let attr: rfuse3::raw::prelude::ReplyAttr = self.getattr(ctx, ino, None, 0).await?;
131        if !is_dir(&attr.attr) {
132            return Err(Error::from_raw_os_error(libc::ENOTDIR).into());
133        }
134
135        // Return Result<is_opaque>.
136        let check_attr = |inode: Inode, attr_name: &'static str, attr_size: u32| async move {
137            let cname = OsStr::new(attr_name);
138            match self.getxattr(ctx, inode, cname, attr_size).await {
139                Ok(v) => {
140                    // xattr name exists and we get value.
141                    if let ReplyXAttr::Data(bufs) = v
142                        && bufs.len() == 1
143                        && bufs[0].eq_ignore_ascii_case(&b'y')
144                    {
145                        return Ok(true);
146                    }
147                    // No value found, go on to next check.
148                    Ok(false)
149                }
150                Err(e) => {
151                    let ioerror: std::io::Error = e.into();
152                    if ioerror.raw_os_error() == Some(libc::ENODATA) {
153                        return Ok(false);
154                    }
155                    #[cfg(target_os = "macos")]
156                    if ioerror.raw_os_error() == Some(libc::ENOATTR) {
157                        return Ok(false);
158                    }
159
160                    Err(e)
161                }
162            }
163        };
164
165        // A directory is made opaque by setting some specific xattr to "y".
166        // See ref: https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
167
168        // Check our customized version of the xattr "user.fuseoverlayfs.opaque".
169        let is_opaque = check_attr(ino, OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
170        if is_opaque {
171            return Ok(true);
172        }
173
174        // Also check for the unprivileged version of the xattr "trusted.overlay.opaque".
175        let is_opaque = check_attr(ino, PRIVILEGED_OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
176        if is_opaque {
177            return Ok(true);
178        }
179
180        // Also check for the unprivileged version of the xattr "user.overlay.opaque".
181        let is_opaque = check_attr(ino, UNPRIVILEGED_OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
182        if is_opaque {
183            return Ok(true);
184        }
185
186        Ok(false)
187    }
188
189    /// Create a file with explicit ownership context.
190    /// Uses OperationContext to allow overriding UID/GID for internal operations like copy-up.
191    async fn create_with_context(
192        &self,
193        _ctx: OperationContext,
194        _parent: Inode,
195        _name: &OsStr,
196        _mode: u32,
197        _flags: u32,
198    ) -> Result<ReplyCreated> {
199        Err(Error::from_raw_os_error(libc::ENOSYS).into())
200    }
201
202    /// Create a directory with explicit ownership context.
203    /// Uses OperationContext to allow overriding UID/GID for internal operations like copy-up.
204    async fn mkdir_with_context(
205        &self,
206        _ctx: OperationContext,
207        _parent: Inode,
208        _name: &OsStr,
209        _mode: u32,
210        _umask: u32,
211    ) -> Result<ReplyEntry> {
212        Err(Error::from_raw_os_error(libc::ENOSYS).into())
213    }
214
215    /// Create a symbolic link with explicit ownership context.
216    /// Uses OperationContext to allow overriding UID/GID for internal operations like copy-up.
217    async fn symlink_with_context(
218        &self,
219        _ctx: OperationContext,
220        _parent: Inode,
221        _name: &OsStr,
222        _link: &OsStr,
223    ) -> Result<ReplyEntry> {
224        Err(Error::from_raw_os_error(libc::ENOSYS).into())
225    }
226
227    /// Retrieve metadata with optional ID mapping control.
228    ///
229    /// - `mapping: true`: Returns attributes as seen inside the container (mapped).
230    /// - `mapping: false`: Returns raw attributes on the host filesystem (unmapped).
231    async fn getattr_with_mapping(
232        &self,
233        _inode: Inode,
234        _handle: Option<u64>,
235        _mapping: bool,
236    ) -> std::io::Result<(Stat64, Duration)> {
237        Err(std::io::Error::from_raw_os_error(libc::ENOSYS))
238    }
239}
240
241#[async_trait]
242impl Layer for PassthroughFs {
243    fn root_inode(&self) -> Inode {
244        1
245    }
246
247    async fn create_with_context(
248        &self,
249        ctx: OperationContext,
250        parent: Inode,
251        name: &OsStr,
252        mode: u32,
253        flags: u32,
254    ) -> Result<ReplyCreated> {
255        PassthroughFs::do_create_helper(
256            self,
257            ctx.req,
258            parent,
259            name,
260            mode,
261            flags,
262            ctx.uid.unwrap_or(ctx.req.uid),
263            ctx.gid.unwrap_or(ctx.req.gid),
264        )
265        .await
266    }
267
268    async fn mkdir_with_context(
269        &self,
270        ctx: OperationContext,
271        parent: Inode,
272        name: &OsStr,
273        mode: u32,
274        umask: u32,
275    ) -> Result<ReplyEntry> {
276        PassthroughFs::do_mkdir_helper(
277            self,
278            ctx.req,
279            parent,
280            name,
281            mode,
282            umask,
283            ctx.uid.unwrap_or(ctx.req.uid),
284            ctx.gid.unwrap_or(ctx.req.gid),
285        )
286        .await
287    }
288
289    async fn symlink_with_context(
290        &self,
291        ctx: OperationContext,
292        parent: Inode,
293        name: &OsStr,
294        link: &OsStr,
295    ) -> Result<ReplyEntry> {
296        PassthroughFs::do_symlink_helper(
297            self,
298            ctx.req,
299            parent,
300            name,
301            link,
302            ctx.uid.unwrap_or(ctx.req.uid),
303            ctx.gid.unwrap_or(ctx.req.gid),
304        )
305        .await
306    }
307
308    async fn getattr_with_mapping(
309        &self,
310        inode: Inode,
311        handle: Option<u64>,
312        mapping: bool,
313    ) -> std::io::Result<(Stat64, Duration)> {
314        PassthroughFs::do_getattr_inner(self, inode, handle, mapping).await
315    }
316}
317pub(crate) fn is_dir(st: &FileAttr) -> bool {
318    st.kind.const_into_mode_t() & libc::S_IFMT == libc::S_IFDIR
319}
320
321pub(crate) fn is_chardev(st: &FileAttr) -> bool {
322    st.kind.const_into_mode_t() & libc::S_IFMT == libc::S_IFCHR
323}
324
325pub(crate) fn is_whiteout(st: &FileAttr) -> bool {
326    // A whiteout is created as a character device with 0/0 device number.
327    // See ref: https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
328    let major = libc::major(st.rdev as libc::dev_t);
329    let minor = libc::minor(st.rdev as libc::dev_t);
330    is_chardev(st) && major == 0 && minor == 0
331}
332
333#[cfg(test)]
334mod test {
335    use std::{ffi::OsStr, path::PathBuf};
336
337    use rfuse3::raw::{Filesystem as _, Request};
338
339    use crate::{
340        passthrough::{PassthroughArgs, new_passthroughfs_layer},
341        unionfs::layer::Layer,
342        unwrap_or_skip_eperm,
343    };
344
345    // Mark as ignored by default; run with: RUN_PRIVILEGED_TESTS=1 cargo test -- --ignored
346    #[ignore]
347    #[tokio::test]
348    async fn test_whiteout_create_delete() {
349        let temp_dir = "/tmp/test_whiteout/t2";
350        let rootdir = PathBuf::from(temp_dir);
351        std::fs::create_dir_all(&rootdir).unwrap();
352        if std::env::var("RUN_PRIVILEGED_TESTS").ok().as_deref() != Some("1") {
353            eprintln!("skip test_whiteout_create_delete: RUN_PRIVILEGED_TESTS!=1");
354            return;
355        }
356        let fs = unwrap_or_skip_eperm!(
357            new_passthroughfs_layer(PassthroughArgs {
358                root_dir: rootdir,
359                mapping: None::<&str>
360            })
361            .await,
362            "init passthrough layer"
363        );
364        let _ = unwrap_or_skip_eperm!(fs.init(Request::default()).await, "fs init");
365        let white_name = OsStr::new(&"test");
366        let res = unwrap_or_skip_eperm!(
367            fs.create_whiteout(Request::default(), 1, white_name).await,
368            "create whiteout"
369        );
370
371        print!("{res:?}");
372        let res = fs.delete_whiteout(Request::default(), 1, white_name).await;
373        if res.is_err() {
374            panic!("{res:?}");
375        }
376        let _ = fs.destroy(Request::default()).await;
377    }
378
379    #[tokio::test]
380    async fn test_is_opaque_on_non_directory() {
381        let temp_dir = "/tmp/test_opaque_non_dir/t2";
382        let rootdir = PathBuf::from(temp_dir);
383        std::fs::create_dir_all(&rootdir).unwrap();
384        if std::env::var("RUN_PRIVILEGED_TESTS").ok().as_deref() != Some("1") {
385            eprintln!("skip test_is_opaque_on_non_directory: RUN_PRIVILEGED_TESTS!=1");
386            return;
387        }
388        let fs = unwrap_or_skip_eperm!(
389            new_passthroughfs_layer(PassthroughArgs {
390                root_dir: rootdir,
391                mapping: None::<&str>
392            })
393            .await,
394            "init passthrough layer"
395        );
396        let _ = unwrap_or_skip_eperm!(fs.init(Request::default()).await, "fs init");
397
398        // Create a file
399        let file_name = OsStr::new("not_a_dir");
400        let _ = unwrap_or_skip_eperm!(
401            fs.create(Request::default(), 1, file_name, 0o644, 0).await,
402            "create file"
403        );
404
405        // Lookup to get the inode of the file
406        let entry = unwrap_or_skip_eperm!(
407            fs.lookup(Request::default(), 1, file_name).await,
408            "lookup file"
409        );
410        let file_inode = entry.attr.ino;
411
412        // is_opaque should return ENOTDIR error
413        let res = fs.is_opaque(Request::default(), file_inode).await;
414        assert!(res.is_err());
415        let err = res.err().unwrap();
416        let ioerr: std::io::Error = err.into();
417        assert_eq!(ioerr.raw_os_error(), Some(libc::ENOTDIR));
418
419        // Clean up
420        let _ = fs.unlink(Request::default(), 1, file_name).await;
421        let _ = fs.destroy(Request::default()).await;
422    }
423
424    #[tokio::test]
425    async fn test_set_opaque_on_non_directory() {
426        let temp_dir = "/tmp/test_set_opaque_non_dir/t2";
427        let rootdir = PathBuf::from(temp_dir);
428        std::fs::create_dir_all(&rootdir).unwrap();
429        if std::env::var("RUN_PRIVILEGED_TESTS").ok().as_deref() != Some("1") {
430            eprintln!("skip test_set_opaque_on_non_directory: RUN_PRIVILEGED_TESTS!=1");
431            return;
432        }
433        let fs = unwrap_or_skip_eperm!(
434            new_passthroughfs_layer(PassthroughArgs {
435                root_dir: rootdir,
436                mapping: None::<&str>
437            })
438            .await,
439            "init passthrough layer"
440        );
441        let _ = unwrap_or_skip_eperm!(fs.init(Request::default()).await, "fs init");
442
443        // Create a file
444        let file_name = OsStr::new("not_a_dir2");
445        let _ = unwrap_or_skip_eperm!(
446            fs.create(Request::default(), 1, file_name, 0o644, 0).await,
447            "create file"
448        );
449
450        // Lookup to get the inode of the file
451        let entry = unwrap_or_skip_eperm!(
452            fs.lookup(Request::default(), 1, file_name).await,
453            "lookup file"
454        );
455        let file_inode = entry.attr.ino;
456
457        // set_opaque should return ENOTDIR error
458        let res = fs.set_opaque(Request::default(), file_inode).await;
459        assert!(res.is_err());
460        let err = res.err().unwrap();
461        let ioerr: std::io::Error = err.into();
462        assert_eq!(ioerr.raw_os_error(), Some(libc::ENOTDIR));
463
464        // Clean up
465        let _ = fs.unlink(Request::default(), 1, file_name).await;
466        let _ = fs.destroy(Request::default()).await;
467    }
468}