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#[async_trait]
24pub trait Layer: ObjectSafeFilesystem {
25 fn root_inode(&self) -> Inode;
27 async fn create_whiteout(
32 &self,
33 ctx: Request,
34 parent: Inode,
35 name: &OsStr,
36 ) -> Result<ReplyEntry> {
37 let ino: u64 = parent;
39 match self.lookup(ctx, ino, name).await {
40 Ok(v) => {
41 if is_whiteout(&v.attr) {
43 return Ok(v);
44 }
45 if v.attr.ino != 0 {
47 self.forget(ctx, v.attr.ino, 1).await;
49 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 if raw_error != libc::ENOENT {
59 return Err(e.into());
60 }
61 }
62 None => return Err(e.into()),
63 }
64 }
65 }
66
67 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 async fn delete_whiteout(&self, ctx: Request, parent: Inode, name: &OsStr) -> Result<()> {
75 let ino: u64 = parent;
77 match self.lookup(ctx, ino, name).await {
78 Ok(v) => {
79 if v.attr.ino != 0 {
80 self.forget(ctx, v.attr.ino, 1).await;
82 }
83
84 if is_whiteout(&v.attr) {
86 return self.unlink(ctx, ino, name).await;
87 }
88 if v.attr.ino != 0 {
90 return Err(Error::from_raw_os_error(libc::EINVAL).into());
92 }
93 }
94 Err(e) => return Err(e),
95 }
96 Ok(())
97 }
98
99 async fn is_whiteout(&self, ctx: Request, inode: Inode) -> Result<bool> {
101 let rep = self.getattr(ctx, inode, None, 0).await?;
102
103 Ok(is_whiteout(&rep.attr))
105 }
106
107 async fn set_opaque(&self, ctx: Request, inode: Inode) -> Result<()> {
109 let ino: u64 = inode;
111
112 let rep = self.getattr(ctx, ino, None, 0).await?;
114 if !is_dir(&rep.attr) {
115 return Err(Error::from_raw_os_error(libc::ENOTDIR).into());
117 }
118 self.setxattr(ctx, ino, OsStr::new(OPAQUE_XATTR), b"y", 0, 0)
121 .await
122 }
123
124 async fn is_opaque(&self, ctx: Request, inode: Inode) -> Result<bool> {
126 let ino: u64 = inode;
128
129 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 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 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 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 let is_opaque = check_attr(ino, OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
170 if is_opaque {
171 return Ok(true);
172 }
173
174 let is_opaque = check_attr(ino, PRIVILEGED_OPAQUE_XATTR, OPAQUE_XATTR_LEN).await?;
176 if is_opaque {
177 return Ok(true);
178 }
179
180 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 let _ = fs.unlink(Request::default(), 1, file_name).await;
466 let _ = fs.destroy(Request::default()).await;
467 }
468}