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#[async_trait]
18pub trait Layer: ObjectSafeFilesystem {
19 fn root_inode(&self) -> Inode;
21 async fn create_whiteout(
26 &self,
27 ctx: Request,
28 parent: Inode,
29 name: &OsStr,
30 ) -> Result<ReplyEntry> {
31 let ino: u64 = parent;
33 match self.lookup(ctx, ino, name).await {
34 Ok(v) => {
35 if is_whiteout(&v.attr) {
37 return Ok(v);
38 }
39 if v.attr.ino != 0 {
41 self.forget(ctx, v.attr.ino, 1).await;
43 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 if raw_error != libc::ENOENT {
53 return Err(e.into());
54 }
55 }
56 None => return Err(e.into()),
57 }
58 }
59 }
60
61 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 async fn delete_whiteout(&self, ctx: Request, parent: Inode, name: &OsStr) -> Result<()> {
69 let ino: u64 = parent;
71 match self.lookup(ctx, ino, name).await {
72 Ok(v) => {
73 if v.attr.ino != 0 {
74 self.forget(ctx, v.attr.ino, 1).await;
76 }
77
78 if is_whiteout(&v.attr) {
80 return self.unlink(ctx, ino, name).await;
81 }
82 if v.attr.ino != 0 {
84 return Err(Error::from_raw_os_error(libc::EINVAL).into());
86 }
87 }
88 Err(e) => return Err(e),
89 }
90 Ok(())
91 }
92
93 async fn is_whiteout(&self, ctx: Request, inode: Inode) -> Result<bool> {
95 let rep = self.getattr(ctx, inode, None, 0).await?;
96
97 Ok(is_whiteout(&rep.attr))
99 }
100
101 async fn set_opaque(&self, ctx: Request, inode: Inode) -> Result<()> {
103 let ino: u64 = inode;
105
106 let rep = self.getattr(ctx, ino, None, 0).await?;
108 if !is_dir(&rep.attr) {
109 return Err(Error::from_raw_os_error(libc::ENOTDIR).into());
111 }
112 self.setxattr(ctx, ino, OsStr::new(OPAQUE_XATTR), b"y", 0, 0)
115 .await
116 }
117
118 async fn is_opaque(&self, ctx: Request, inode: Inode) -> Result<bool> {
120 let ino: u64 = inode;
122
123 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 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 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 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 let is_opaque = check_attr(ino, OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
162 if is_opaque {
163 return Ok(true);
164 }
165
166 let is_opaque = check_attr(ino, PRIVILEGED_OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
168 if is_opaque {
169 return Ok(true);
170 }
171
172 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 let _ = fs.unlink(Request::default(), 1, file_name).await;
458 let _ = fs.destroy(Request::default()).await;
459 }
460}