libfuse_fs/unionfs/
layer.rs

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