1use crate::mm::PAGE_SIZE;
6use crate::security;
7use crate::task::{CurrentTask, Kernel};
8use crate::vfs::memory_directory::MemoryDirectoryFile;
9use crate::vfs::{
10 CacheMode, DirEntry, DirEntryHandle, FileOps, FileSystem, FileSystemHandle, FileSystemOps,
11 FileSystemOptions, FsNode, FsNodeHandle, FsNodeInfo, FsNodeOps, FsStr, FsString,
12 MemoryRegularNode, MemoryXattrStorage, RenameContext, SymlinkNode, XattrStorage as _, fs_args,
13 fs_node_impl_not_dir, fs_node_impl_xattr_delegate,
14};
15use starnix_logging::{log_warn, track_stub};
16use starnix_sync::{FileOpsCore, LockEqualOrBefore, Locked, Unlocked};
17use starnix_types::vfs::default_statfs;
18use starnix_uapi::auth::FsCred;
19use starnix_uapi::device_id::DeviceId;
20use starnix_uapi::errors::Errno;
21use starnix_uapi::file_mode::{FileMode, mode};
22use starnix_uapi::open_flags::OpenFlags;
23use starnix_uapi::seal_flags::SealFlags;
24use starnix_uapi::{TMPFS_MAGIC, error, gid_t, statfs, uid_t};
25use std::collections::BTreeMap;
26use std::sync::Arc;
27use std::sync::atomic::{AtomicU32, Ordering};
28
29pub struct TmpFs(&'static FsStr);
30
31impl FileSystemOps for Arc<TmpFs> {
32 fn statfs(
33 &self,
34 _locked: &mut Locked<FileOpsCore>,
35 _fs: &FileSystem,
36 _current_task: &CurrentTask,
37 ) -> Result<statfs, Errno> {
38 Ok(statfs {
39 f_blocks: 0x100000000,
41 f_bavail: 0x100000000,
42 f_bfree: 0x100000000,
43 ..default_statfs(TMPFS_MAGIC)
44 })
45 }
46 fn name(&self) -> &'static FsStr {
47 self.0
48 }
49
50 fn rename(
51 &self,
52 _locked: &mut Locked<FileOpsCore>,
53 _fs: &FileSystem,
54 _current_task: &CurrentTask,
55 context: &mut RenameContext<'_>,
56 _old_name: &FsStr,
57 _new_name: &FsStr,
58 ) -> Result<(), Errno> {
59 fn child_count(node: &FsNodeHandle) -> &AtomicU32 {
60 &node.downcast_ops::<TmpFsDirectory>().unwrap().child_count
65 }
66 let replaced = context.replaced.map(|r| &r.node);
67 let renamed_is_dir = context.renamed_is_dir();
68 let replaced_is_dir = context.replaced_is_dir();
69
70 if let Some(replaced) = replaced {
71 if replaced_is_dir {
72 if child_count(replaced).load(Ordering::Acquire) != 0 {
74 return error!(ENOTEMPTY);
75 }
76 }
77 }
78
79 {
82 let old_parent = &context.old_parent().node;
83 let new_parent = &context.new_parent().node;
84 if !Arc::ptr_eq(old_parent, new_parent) {
85 child_count(new_parent).fetch_add(1, Ordering::Release);
86 child_count(old_parent).fetch_sub(1, Ordering::Release);
87 }
88 if replaced.is_some() {
89 child_count(new_parent).fetch_sub(1, Ordering::Release);
90 }
91 }
92
93 let (old_parent_info, mut new_parent_info) = context.parent_infos_mut();
94 if renamed_is_dir {
95 if let Some(new_info) = new_parent_info.as_deref_mut() {
96 new_info.link_count += 1;
97 old_parent_info.link_count -= 1;
98 }
99 }
100 if replaced_is_dir {
103 if let Some(new_info) = new_parent_info.as_deref_mut() {
104 new_info.link_count -= 1;
105 } else {
106 old_parent_info.link_count -= 1;
107 }
108 }
109 Ok(())
110 }
111
112 fn exchange(
113 &self,
114 _fs: &FileSystem,
115 _current_task: &CurrentTask,
116 context: &mut RenameContext<'_>,
117 _name1: &FsStr,
118 _name2: &FsStr,
119 ) -> Result<(), Errno> {
120 let is_dir1 = context.renamed_is_dir();
121 let is_dir2 = context.replaced_is_dir();
122 let (parent1_info, mut parent2_info) = context.parent_infos_mut();
123 if let Some(parent2_info) = parent2_info.as_deref_mut() {
124 if is_dir1 != is_dir2 {
125 if is_dir1 {
126 parent1_info.link_count -= 1;
127 parent2_info.link_count += 1;
128 } else {
129 parent1_info.link_count += 1;
130 parent2_info.link_count -= 1;
131 }
132 }
133 }
134
135 Ok(())
136 }
137}
138
139pub fn tmp_fs(
140 locked: &mut Locked<Unlocked>,
141 current_task: &CurrentTask,
142 options: FileSystemOptions,
143) -> Result<FileSystemHandle, Errno> {
144 TmpFs::new_fs_with_options(locked, ¤t_task.kernel(), options)
145}
146
147impl TmpFs {
148 pub fn new_fs<L>(locked: &mut Locked<L>, kernel: &Kernel) -> FileSystemHandle
149 where
150 L: LockEqualOrBefore<FileOpsCore>,
151 {
152 Self::new_fs_with_options(locked, kernel, Default::default())
153 .expect("empty options cannot fail")
154 }
155
156 pub fn new_fs_with_name<L>(
157 locked: &mut Locked<L>,
158 kernel: &Kernel,
159 name: &'static FsStr,
160 ) -> FileSystemHandle
161 where
162 L: LockEqualOrBefore<FileOpsCore>,
163 {
164 Self::new_fs_with_options_and_name(locked, kernel, Default::default(), name)
165 .expect("empty options cannot fail")
166 }
167
168 pub fn new_fs_with_options<L>(
169 locked: &mut Locked<L>,
170 kernel: &Kernel,
171 options: FileSystemOptions,
172 ) -> Result<FileSystemHandle, Errno>
173 where
174 L: LockEqualOrBefore<FileOpsCore>,
175 {
176 Self::new_fs_with_options_and_name(locked, kernel, options, "tmpfs".into())
177 }
178
179 fn new_fs_with_options_and_name<L>(
180 locked: &mut Locked<L>,
181 kernel: &Kernel,
182 options: FileSystemOptions,
183 name: &'static FsStr,
184 ) -> Result<FileSystemHandle, Errno>
185 where
186 L: LockEqualOrBefore<FileOpsCore>,
187 {
188 let fs =
189 FileSystem::new(locked, kernel, CacheMode::Permanent, Arc::new(TmpFs(name)), options)?;
190 let mut mount_options = fs.options.params.clone();
191 let mode = if let Some(mode) = mount_options.remove(b"mode") {
192 FileMode::from_string(mode.as_ref())?
193 } else {
194 mode!(IFDIR, 0o777)
195 };
196 let uid = if let Some(uid) = mount_options.remove(b"uid") {
197 fs_args::parse::<uid_t>(uid.as_ref())?
198 } else {
199 0
200 };
201 let gid = if let Some(gid) = mount_options.remove(b"gid") {
202 fs_args::parse::<gid_t>(gid.as_ref())?
203 } else {
204 0
205 };
206 let root_ino = fs.allocate_ino();
207 let mut info = FsNodeInfo::new(mode!(IFDIR, 0o777), FsCred { uid, gid });
208 info.chmod(mode);
209 fs.create_root_with_info(root_ino, TmpFsDirectory::new(), info);
210
211 if !mount_options.is_empty() {
212 track_stub!(
213 TODO("https://fxbug.dev/322873419"),
214 "unknown tmpfs options, see logs for strings"
215 );
216 log_warn!("Unknown tmpfs options: {}", mount_options);
217 }
218
219 Ok(fs)
220 }
221
222 pub fn set_initial_content(kernel: &Kernel, fs: &FileSystemHandle, data: TmpFsData) {
223 fn create_dir_entry_from_data(
224 kernel: &Kernel,
225 fs: &FileSystemHandle,
226 data: TmpFsData,
227 this: Option<DirEntryHandle>,
228 name: FsString,
229 ) -> DirEntryHandle {
230 let new_direntry = |node, parent, name| {
233 let dir_entry = DirEntry::new(node, parent, name);
234 security::fs_node_init_with_dentry_deferred(kernel, &dir_entry);
235 dir_entry
236 };
237
238 match data.node_type {
239 TmpFsNodeType::Link(target) => {
240 assert!(this.is_none());
241 let node = TmpFsDirectory::new_symlink(fs, target.as_ref(), data.owner);
242 new_direntry(node, None, name)
243 }
244 TmpFsNodeType::Directory(children) => {
245 let this = this.unwrap_or_else(|| {
246 let info = FsNodeInfo::new(mode!(IFDIR, data.perm), data.owner);
247 let node = fs.create_node_and_allocate_node_id(TmpFsDirectory::new(), info);
248 new_direntry(node, None, name.clone())
249 });
250 this.node
251 .downcast_ops::<TmpFsDirectory>()
252 .expect("directory must be from tmpfs")
253 .child_count
254 .fetch_add(children.len() as u32, Ordering::Release);
255 let children = children
256 .into_iter()
257 .map(|(name, data)| {
258 let child =
259 create_dir_entry_from_data(kernel, fs, data, None, name.clone());
260 (name, child)
261 })
262 .collect::<BTreeMap<_, _>>();
263 this.set_children(children);
264 this
265 }
266 }
267 }
268
269 create_dir_entry_from_data(kernel, fs, data, Some(Arc::clone(fs.root())), "".into());
270 }
271}
272
273pub enum TmpFsNodeType {
274 Link(FsString),
275 Directory(BTreeMap<FsString, TmpFsData>),
276}
277
278pub struct TmpFsData {
279 pub owner: FsCred,
280 pub perm: u32,
281 pub node_type: TmpFsNodeType,
282}
283
284pub struct TmpFsDirectory {
285 xattrs: MemoryXattrStorage,
286 child_count: AtomicU32,
287}
288
289impl TmpFsDirectory {
290 pub fn new() -> Self {
291 Self { xattrs: MemoryXattrStorage::default(), child_count: AtomicU32::new(0) }
292 }
293
294 fn new_symlink(fs: &Arc<FileSystem>, target: &FsStr, owner: FsCred) -> FsNodeHandle {
295 let (link, info) = SymlinkNode::new(target, owner);
296 fs.create_node_and_allocate_node_id(link, info)
297 }
298}
299
300fn create_child_node(
301 parent: &FsNode,
302 mode: FileMode,
303 dev: DeviceId,
304 owner: FsCred,
305) -> Result<FsNodeHandle, Errno> {
306 let ops: Box<dyn FsNodeOps> = match mode.fmt() {
307 FileMode::IFREG => Box::new(MemoryRegularNode::new()?),
308 FileMode::IFIFO | FileMode::IFBLK | FileMode::IFCHR | FileMode::IFSOCK => {
309 Box::new(TmpFsSpecialNode::new())
310 }
311 _ => return error!(EACCES),
312 };
313 let mut info = FsNodeInfo::new(mode, owner);
314 info.rdev = dev;
315 info.blksize = *PAGE_SIZE as usize;
317 let child = parent.fs().create_node_and_allocate_node_id(ops, info);
318 if mode.fmt() == FileMode::IFREG {
319 child.write_guard_state.lock().enable_sealing(SealFlags::SEAL);
321 }
322 Ok(child)
323}
324
325impl FsNodeOps for TmpFsDirectory {
326 fs_node_impl_xattr_delegate!(self, self.xattrs);
327
328 fn create_file_ops(
329 &self,
330 _locked: &mut Locked<FileOpsCore>,
331 _node: &FsNode,
332 _current_task: &CurrentTask,
333 _flags: OpenFlags,
334 ) -> Result<Box<dyn FileOps>, Errno> {
335 Ok(Box::new(MemoryDirectoryFile::new()))
336 }
337
338 fn mkdir(
339 &self,
340 _locked: &mut Locked<FileOpsCore>,
341 node: &FsNode,
342 _current_task: &CurrentTask,
343 _name: &FsStr,
344 mode: FileMode,
345 owner: FsCred,
346 ) -> Result<FsNodeHandle, Errno> {
347 node.update_info(|info| {
348 info.link_count += 1;
349 });
350 self.child_count.fetch_add(1, Ordering::Release);
351 Ok(node
352 .fs()
353 .create_node_and_allocate_node_id(TmpFsDirectory::new(), FsNodeInfo::new(mode, owner)))
354 }
355
356 fn mknod(
357 &self,
358 _locked: &mut Locked<FileOpsCore>,
359 node: &FsNode,
360 _current_task: &CurrentTask,
361 _name: &FsStr,
362 mode: FileMode,
363 dev: DeviceId,
364 owner: FsCred,
365 ) -> Result<FsNodeHandle, Errno> {
366 let child = create_child_node(node, mode, dev, owner)?;
367 self.child_count.fetch_add(1, Ordering::Release);
368 Ok(child)
369 }
370
371 fn create_symlink(
372 &self,
373 _locked: &mut Locked<FileOpsCore>,
374 node: &FsNode,
375 _current_task: &CurrentTask,
376 _name: &FsStr,
377 target: &FsStr,
378 owner: FsCred,
379 ) -> Result<FsNodeHandle, Errno> {
380 self.child_count.fetch_add(1, Ordering::Release);
381 Ok(Self::new_symlink(&node.fs(), target, owner))
382 }
383
384 fn create_tmpfile(
385 &self,
386 node: &FsNode,
387 _current_task: &CurrentTask,
388 mode: FileMode,
389 owner: FsCred,
390 ) -> Result<FsNodeHandle, Errno> {
391 assert!(mode.is_reg());
392 create_child_node(node, mode, DeviceId::NONE, owner)
393 }
394
395 fn link(
396 &self,
397 _locked: &mut Locked<FileOpsCore>,
398 _node: &FsNode,
399 _current_task: &CurrentTask,
400 _name: &FsStr,
401 child: &FsNodeHandle,
402 ) -> Result<(), Errno> {
403 child.update_info(|info| {
404 info.link_count += 1;
405 });
406 self.child_count.fetch_add(1, Ordering::Release);
407 Ok(())
408 }
409
410 fn unlink(
411 &self,
412 _locked: &mut Locked<FileOpsCore>,
413 node: &FsNode,
414 _current_task: &CurrentTask,
415 _name: &FsStr,
416 child_to_unlink: &FsNodeHandle,
417 ) -> Result<(), Errno> {
418 if child_to_unlink.is_dir() {
419 let child_count =
424 &child_to_unlink.downcast_ops::<TmpFsDirectory>().unwrap().child_count;
425 if child_count.load(Ordering::Relaxed) != 0 {
426 return error!(ENOTEMPTY);
427 }
428
429 node.update_info(|info| {
430 info.link_count -= 1;
431 });
432 }
433 child_to_unlink.update_info(|info| {
434 info.link_count -= 1;
435 });
436 self.child_count.fetch_sub(1, Ordering::Release);
437 Ok(())
438 }
439}
440
441struct TmpFsSpecialNode {
442 xattrs: MemoryXattrStorage,
443}
444
445impl TmpFsSpecialNode {
446 pub fn new() -> Self {
447 Self { xattrs: MemoryXattrStorage::default() }
448 }
449}
450
451impl FsNodeOps for TmpFsSpecialNode {
452 fs_node_impl_not_dir!();
453 fs_node_impl_xattr_delegate!(self, self.xattrs);
454
455 fn create_file_ops(
456 &self,
457 _locked: &mut Locked<FileOpsCore>,
458 _node: &FsNode,
459 _current_task: &CurrentTask,
460 _flags: OpenFlags,
461 ) -> Result<Box<dyn FileOps>, Errno> {
462 unreachable!("Special nodes cannot be opened.");
463 }
464}
465
466#[cfg(test)]
467mod test {
468 use super::*;
469 use crate::testing::spawn_kernel_and_run;
470 use crate::vfs::buffers::{VecInputBuffer, VecOutputBuffer};
471 use crate::vfs::fs_args::MountParams;
472 use crate::vfs::{FdNumber, UnlinkKind};
473 use starnix_uapi::errno;
474 use starnix_uapi::file_mode::AccessCheck;
475 use starnix_uapi::mount_flags::FileSystemFlags;
476 use starnix_uapi::vfs::ResolveFlags;
477 use zerocopy::IntoBytes;
478
479 #[::fuchsia::test]
480 async fn test_tmpfs() {
481 spawn_kernel_and_run(async |locked, current_task| {
482 let kernel = current_task.kernel();
483 let fs = TmpFs::new_fs(locked, &kernel);
484 let root = fs.root();
485 let usr = root.create_dir(locked, ¤t_task, "usr".into()).unwrap();
486 let _etc = root.create_dir(locked, ¤t_task, "etc".into()).unwrap();
487 let _usr_bin = usr.create_dir(locked, ¤t_task, "bin".into()).unwrap();
488 let mut names = root.copy_child_names();
489 names.sort();
490 assert!(names.iter().eq(["etc", "usr"].iter()));
491 })
492 .await;
493 }
494
495 #[::fuchsia::test]
496 async fn test_write_read() {
497 spawn_kernel_and_run(async |locked, current_task| {
498 let path = "test.bin";
499 let _file = current_task
500 .fs()
501 .root()
502 .create_node(
503 locked,
504 ¤t_task,
505 path.into(),
506 mode!(IFREG, 0o777),
507 DeviceId::NONE,
508 )
509 .unwrap();
510
511 let wr_file = current_task.open_file(locked, path.into(), OpenFlags::RDWR).unwrap();
512
513 let test_seq = 0..10000u16;
514 let test_vec = test_seq.collect::<Vec<_>>();
515 let test_bytes = test_vec.as_slice().as_bytes();
516
517 let written =
518 wr_file.write(locked, ¤t_task, &mut VecInputBuffer::new(test_bytes)).unwrap();
519 assert_eq!(written, test_bytes.len());
520
521 let mut read_buffer = VecOutputBuffer::new(test_bytes.len() + 1);
522 let read = wr_file.read_at(locked, ¤t_task, 0, &mut read_buffer).unwrap();
523 assert_eq!(read, test_bytes.len());
524 assert_eq!(test_bytes, read_buffer.data());
525 })
526 .await;
527 }
528
529 #[::fuchsia::test]
530 async fn test_read_past_eof() {
531 spawn_kernel_and_run(async |locked, current_task| {
532 let path = "test.bin";
534 let _file = current_task
535 .fs()
536 .root()
537 .create_node(
538 locked,
539 ¤t_task,
540 path.into(),
541 mode!(IFREG, 0o777),
542 DeviceId::NONE,
543 )
544 .unwrap();
545 let rd_file = current_task.open_file(locked, path.into(), OpenFlags::RDONLY).unwrap();
546
547 let buffer_size = 0x10000;
549 let mut output_buffer = VecOutputBuffer::new(buffer_size);
550 let test_offset = 100;
551 let result =
552 rd_file.read_at(locked, ¤t_task, test_offset, &mut output_buffer).unwrap();
553 assert_eq!(result, 0);
554 })
555 .await;
556 }
557
558 #[::fuchsia::test]
559 async fn test_permissions() {
560 spawn_kernel_and_run(async |locked, current_task| {
561 let path = "test.bin";
562 let file = current_task
563 .open_file_at(
564 locked,
565 FdNumber::AT_FDCWD,
566 path.into(),
567 OpenFlags::CREAT | OpenFlags::RDONLY,
568 FileMode::from_bits(0o777),
569 ResolveFlags::empty(),
570 AccessCheck::default(),
571 )
572 .expect("failed to create file");
573 assert_eq!(
574 0,
575 file.read(locked, ¤t_task, &mut VecOutputBuffer::new(0))
576 .expect("failed to read")
577 );
578
579 assert!(file.write(locked, ¤t_task, &mut VecInputBuffer::new(&[])).is_err());
580
581 let file = current_task
582 .open_file_at(
583 locked,
584 FdNumber::AT_FDCWD,
585 path.into(),
586 OpenFlags::WRONLY,
587 FileMode::EMPTY,
588 ResolveFlags::empty(),
589 AccessCheck::default(),
590 )
591 .expect("failed to open file WRONLY");
592
593 assert!(file.read(locked, ¤t_task, &mut VecOutputBuffer::new(0)).is_err());
594
595 assert_eq!(
596 0,
597 file.write(locked, ¤t_task, &mut VecInputBuffer::new(&[]))
598 .expect("failed to write")
599 );
600
601 let file = current_task
602 .open_file_at(
603 locked,
604 FdNumber::AT_FDCWD,
605 path.into(),
606 OpenFlags::RDWR,
607 FileMode::EMPTY,
608 ResolveFlags::empty(),
609 AccessCheck::default(),
610 )
611 .expect("failed to open file RDWR");
612
613 assert_eq!(
614 0,
615 file.read(locked, ¤t_task, &mut VecOutputBuffer::new(0))
616 .expect("failed to read")
617 );
618
619 assert_eq!(
620 0,
621 file.write(locked, ¤t_task, &mut VecInputBuffer::new(&[]))
622 .expect("failed to write")
623 );
624 })
625 .await;
626 }
627
628 #[::fuchsia::test]
629 async fn test_persistence() {
630 spawn_kernel_and_run(async |locked, current_task| {
631 {
632 let root = ¤t_task.fs().root().entry;
633 let usr = root
634 .create_dir(locked, ¤t_task, "usr".into())
635 .expect("failed to create usr");
636 root.create_dir(locked, ¤t_task, "etc".into())
637 .expect("failed to create usr/etc");
638 usr.create_dir(locked, ¤t_task, "bin".into())
639 .expect("failed to create usr/bin");
640 }
641
642 current_task
645 .open_file(locked, "/usr/bin".into(), OpenFlags::RDONLY | OpenFlags::DIRECTORY)
646 .expect("failed to open /usr/bin");
647 assert_eq!(
648 errno!(ENOENT),
649 current_task
650 .open_file(locked, "/usr/bin/test.txt".into(), OpenFlags::RDWR)
651 .unwrap_err()
652 );
653 current_task
654 .open_file_at(
655 locked,
656 FdNumber::AT_FDCWD,
657 "/usr/bin/test.txt".into(),
658 OpenFlags::RDWR | OpenFlags::CREAT,
659 FileMode::from_bits(0o777),
660 ResolveFlags::empty(),
661 AccessCheck::default(),
662 )
663 .expect("failed to create test.txt");
664 let txt = current_task
665 .open_file(locked, "/usr/bin/test.txt".into(), OpenFlags::RDWR)
666 .expect("failed to open test.txt");
667
668 let usr_bin = current_task
669 .open_file(locked, "/usr/bin".into(), OpenFlags::RDONLY)
670 .expect("failed to open /usr/bin");
671 usr_bin
672 .name
673 .unlink(locked, ¤t_task, "test.txt".into(), UnlinkKind::NonDirectory, false)
674 .expect("failed to unlink test.text");
675 assert_eq!(
676 errno!(ENOENT),
677 current_task
678 .open_file(locked, "/usr/bin/test.txt".into(), OpenFlags::RDWR)
679 .unwrap_err()
680 );
681 assert_eq!(
682 errno!(ENOENT),
683 usr_bin
684 .name
685 .unlink(
686 locked,
687 ¤t_task,
688 "test.txt".into(),
689 UnlinkKind::NonDirectory,
690 false
691 )
692 .unwrap_err()
693 );
694
695 assert_eq!(
696 0,
697 txt.read(locked, ¤t_task, &mut VecOutputBuffer::new(0))
698 .expect("failed to read")
699 );
700 std::mem::drop(txt);
701 assert_eq!(
702 errno!(ENOENT),
703 current_task
704 .open_file(locked, "/usr/bin/test.txt".into(), OpenFlags::RDWR)
705 .unwrap_err()
706 );
707 std::mem::drop(usr_bin);
708
709 let usr = current_task
710 .open_file(locked, "/usr".into(), OpenFlags::RDONLY)
711 .expect("failed to open /usr");
712 assert_eq!(
713 errno!(ENOENT),
714 current_task.open_file(locked, "/usr/foo".into(), OpenFlags::RDONLY).unwrap_err()
715 );
716 usr.name
717 .unlink(locked, ¤t_task, "bin".into(), UnlinkKind::Directory, false)
718 .expect("failed to unlink /usr/bin");
719 })
720 .await;
721 }
722
723 #[::fuchsia::test]
724 async fn test_data() {
725 spawn_kernel_and_run(async |locked, current_task| {
726 let kernel = current_task.kernel();
727 let fs = TmpFs::new_fs_with_options(
728 locked,
729 &kernel,
730 FileSystemOptions {
731 source: Default::default(),
732 flags: FileSystemFlags::empty().into(),
733 params: MountParams::parse(b"mode=0123,uid=42,gid=84".into())
734 .expect("parsed correctly"),
735 },
736 )
737 .expect("new_fs");
738 let info = fs.root().node.info();
739 assert_eq!(info.mode, mode!(IFDIR, 0o123));
740 assert_eq!(info.uid, 42);
741 assert_eq!(info.gid, 84);
742 })
743 .await;
744 }
745}