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 metadata with optional ID mapping control.
220    ///
221    /// - `mapping: true`: Returns attributes as seen inside the container (mapped).
222    /// - `mapping: false`: Returns raw attributes on the host filesystem (unmapped).
223    async fn getattr_with_mapping(
224        &self,
225        _inode: Inode,
226        _handle: Option<u64>,
227        _mapping: bool,
228    ) -> std::io::Result<(libc::stat64, Duration)> {
229        Err(std::io::Error::from_raw_os_error(libc::ENOSYS))
230    }
231}
232
233#[async_trait]
234impl Layer for PassthroughFs {
235    fn root_inode(&self) -> Inode {
236        1
237    }
238
239    async fn create_with_context(
240        &self,
241        ctx: OperationContext,
242        parent: Inode,
243        name: &OsStr,
244        mode: u32,
245        flags: u32,
246    ) -> Result<ReplyCreated> {
247        PassthroughFs::do_create_helper(
248            self,
249            ctx.req,
250            parent,
251            name,
252            mode,
253            flags,
254            ctx.uid.unwrap_or(ctx.req.uid),
255            ctx.gid.unwrap_or(ctx.req.gid),
256        )
257        .await
258    }
259
260    async fn mkdir_with_context(
261        &self,
262        ctx: OperationContext,
263        parent: Inode,
264        name: &OsStr,
265        mode: u32,
266        umask: u32,
267    ) -> Result<ReplyEntry> {
268        PassthroughFs::do_mkdir_helper(
269            self,
270            ctx.req,
271            parent,
272            name,
273            mode,
274            umask,
275            ctx.uid.unwrap_or(ctx.req.uid),
276            ctx.gid.unwrap_or(ctx.req.gid),
277        )
278        .await
279    }
280
281    async fn symlink_with_context(
282        &self,
283        ctx: OperationContext,
284        parent: Inode,
285        name: &OsStr,
286        link: &OsStr,
287    ) -> Result<ReplyEntry> {
288        PassthroughFs::do_symlink_helper(
289            self,
290            ctx.req,
291            parent,
292            name,
293            link,
294            ctx.uid.unwrap_or(ctx.req.uid),
295            ctx.gid.unwrap_or(ctx.req.gid),
296        )
297        .await
298    }
299
300    async fn getattr_with_mapping(
301        &self,
302        inode: Inode,
303        handle: Option<u64>,
304        mapping: bool,
305    ) -> std::io::Result<(libc::stat64, Duration)> {
306        PassthroughFs::do_getattr_inner(self, inode, handle, mapping).await
307    }
308}
309pub(crate) fn is_dir(st: &FileAttr) -> bool {
310    st.kind.const_into_mode_t() & libc::S_IFMT == libc::S_IFDIR
311}
312
313pub(crate) fn is_chardev(st: &FileAttr) -> bool {
314    st.kind.const_into_mode_t() & libc::S_IFMT == libc::S_IFCHR
315}
316
317pub(crate) fn is_whiteout(st: &FileAttr) -> bool {
318    // A whiteout is created as a character device with 0/0 device number.
319    // See ref: https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
320    let major = libc::major(st.rdev.into());
321    let minor = libc::minor(st.rdev.into());
322    is_chardev(st) && major == 0 && minor == 0
323}
324
325#[cfg(test)]
326mod test {
327    use std::{ffi::OsStr, path::PathBuf};
328
329    use rfuse3::raw::{Filesystem as _, Request};
330
331    use crate::{
332        passthrough::{PassthroughArgs, new_passthroughfs_layer},
333        unionfs::layer::Layer,
334        unwrap_or_skip_eperm,
335    };
336
337    // Mark as ignored by default; run with: RUN_PRIVILEGED_TESTS=1 cargo test -- --ignored
338    #[ignore]
339    #[tokio::test]
340    async fn test_whiteout_create_delete() {
341        let temp_dir = "/tmp/test_whiteout/t2";
342        let rootdir = PathBuf::from(temp_dir);
343        std::fs::create_dir_all(&rootdir).unwrap();
344        if std::env::var("RUN_PRIVILEGED_TESTS").ok().as_deref() != Some("1") {
345            eprintln!("skip test_whiteout_create_delete: RUN_PRIVILEGED_TESTS!=1");
346            return;
347        }
348        let fs = unwrap_or_skip_eperm!(
349            new_passthroughfs_layer(PassthroughArgs {
350                root_dir: rootdir,
351                mapping: None::<&str>
352            })
353            .await,
354            "init passthrough layer"
355        );
356        let _ = unwrap_or_skip_eperm!(fs.init(Request::default()).await, "fs init");
357        let white_name = OsStr::new(&"test");
358        let res = unwrap_or_skip_eperm!(
359            fs.create_whiteout(Request::default(), 1, white_name).await,
360            "create whiteout"
361        );
362
363        print!("{res:?}");
364        let res = fs.delete_whiteout(Request::default(), 1, white_name).await;
365        if res.is_err() {
366            panic!("{res:?}");
367        }
368        let _ = fs.destroy(Request::default()).await;
369    }
370
371    #[tokio::test]
372    async fn test_is_opaque_on_non_directory() {
373        let temp_dir = "/tmp/test_opaque_non_dir/t2";
374        let rootdir = PathBuf::from(temp_dir);
375        std::fs::create_dir_all(&rootdir).unwrap();
376        if std::env::var("RUN_PRIVILEGED_TESTS").ok().as_deref() != Some("1") {
377            eprintln!("skip test_is_opaque_on_non_directory: RUN_PRIVILEGED_TESTS!=1");
378            return;
379        }
380        let fs = unwrap_or_skip_eperm!(
381            new_passthroughfs_layer(PassthroughArgs {
382                root_dir: rootdir,
383                mapping: None::<&str>
384            })
385            .await,
386            "init passthrough layer"
387        );
388        let _ = unwrap_or_skip_eperm!(fs.init(Request::default()).await, "fs init");
389
390        // Create a file
391        let file_name = OsStr::new("not_a_dir");
392        let _ = unwrap_or_skip_eperm!(
393            fs.create(Request::default(), 1, file_name, 0o644, 0).await,
394            "create file"
395        );
396
397        // Lookup to get the inode of the file
398        let entry = unwrap_or_skip_eperm!(
399            fs.lookup(Request::default(), 1, file_name).await,
400            "lookup file"
401        );
402        let file_inode = entry.attr.ino;
403
404        // is_opaque should return ENOTDIR error
405        let res = fs.is_opaque(Request::default(), file_inode).await;
406        assert!(res.is_err());
407        let err = res.err().unwrap();
408        let ioerr: std::io::Error = err.into();
409        assert_eq!(ioerr.raw_os_error(), Some(libc::ENOTDIR));
410
411        // Clean up
412        let _ = fs.unlink(Request::default(), 1, file_name).await;
413        let _ = fs.destroy(Request::default()).await;
414    }
415
416    #[tokio::test]
417    async fn test_set_opaque_on_non_directory() {
418        let temp_dir = "/tmp/test_set_opaque_non_dir/t2";
419        let rootdir = PathBuf::from(temp_dir);
420        std::fs::create_dir_all(&rootdir).unwrap();
421        if std::env::var("RUN_PRIVILEGED_TESTS").ok().as_deref() != Some("1") {
422            eprintln!("skip test_set_opaque_on_non_directory: RUN_PRIVILEGED_TESTS!=1");
423            return;
424        }
425        let fs = unwrap_or_skip_eperm!(
426            new_passthroughfs_layer(PassthroughArgs {
427                root_dir: rootdir,
428                mapping: None::<&str>
429            })
430            .await,
431            "init passthrough layer"
432        );
433        let _ = unwrap_or_skip_eperm!(fs.init(Request::default()).await, "fs init");
434
435        // Create a file
436        let file_name = OsStr::new("not_a_dir2");
437        let _ = unwrap_or_skip_eperm!(
438            fs.create(Request::default(), 1, file_name, 0o644, 0).await,
439            "create file"
440        );
441
442        // Lookup to get the inode of the file
443        let entry = unwrap_or_skip_eperm!(
444            fs.lookup(Request::default(), 1, file_name).await,
445            "lookup file"
446        );
447        let file_inode = entry.attr.ino;
448
449        // set_opaque should return ENOTDIR error
450        let res = fs.set_opaque(Request::default(), file_inode).await;
451        assert!(res.is_err());
452        let err = res.err().unwrap();
453        let ioerr: std::io::Error = err.into();
454        assert_eq!(ioerr.raw_os_error(), Some(libc::ENOTDIR));
455
456        // Clean up
457        let _ = fs.unlink(Request::default(), 1, file_name).await;
458        let _ = fs.destroy(Request::default()).await;
459    }
460}