1use crate::device::mem::new_null_file;
6use crate::execution::{
7 create_init_child_process, create_init_process, create_system_task,
8 execute_task_with_prerun_result,
9};
10use crate::fs::fuchsia::RemoteFs;
11use crate::fs::tmpfs::TmpFs;
12use crate::mm::syscalls::{do_mmap, sys_mremap};
13use crate::mm::{MemoryAccessor, MemoryAccessorExt, MemoryManager, PAGE_SIZE};
14use crate::security;
15use crate::task::container_namespace::ContainerNamespace;
16use crate::task::{
17 CurrentTask, ExitStatus, Kernel, KernelFeatures, KernelOrTask, SchedulerManager, SystemLimits,
18 Task, TaskBuilder,
19};
20use crate::vfs::buffers::{InputBuffer, OutputBuffer};
21use crate::vfs::{
22 Anon, CacheMode, DirEntry, FdNumber, FileHandle, FileObject, FileOps, FileSystem,
23 FileSystemHandle, FileSystemOps, FileSystemOptions, FsContext, FsNode, FsNodeHandle,
24 FsNodeInfo, FsNodeOps, FsStr, Namespace, NamespaceNode, fileops_impl_nonseekable,
25 fileops_impl_noop_sync, fs_node_impl_not_dir,
26};
27use fidl_fuchsia_io as fio;
28use fuchsia_async as fasync;
29use fuchsia_async::LocalExecutor;
30use selinux::SecurityServer;
31use starnix_sync::{FileOpsCore, LockEqualOrBefore, Locked, Unlocked};
32use starnix_syscalls::{SyscallArg, SyscallResult};
33use starnix_task_command::TaskCommand;
34use starnix_types::arch::ArchWidth;
35use starnix_types::vfs::default_statfs;
36use starnix_uapi::auth::{Credentials, FsCred};
37use starnix_uapi::errors::Errno;
38use starnix_uapi::file_mode::mode;
39use starnix_uapi::open_flags::OpenFlags;
40use starnix_uapi::user_address::{ArchSpecific, UserAddress};
41use starnix_uapi::{MAP_ANONYMOUS, MAP_PRIVATE, PROT_READ, PROT_WRITE, errno, error, statfs};
42use std::ffi::CString;
43use std::future::Future;
44use std::mem::MaybeUninit;
45use std::ops::Deref;
46use std::sync::{Arc, mpsc};
47use zerocopy::{Immutable, IntoBytes};
48
49fn create_pkgfs<L>(locked: &mut Locked<L>, kernel: &Kernel) -> FileSystemHandle
53where
54 L: LockEqualOrBefore<FileOpsCore>,
55{
56 let rights = fio::PERM_READABLE | fio::PERM_EXECUTABLE;
57 let (server, client) = zx::Channel::create();
58 fdio::open("/pkg", rights, server).expect("failed to open /pkg");
59 RemoteFs::new_fs(
60 locked,
61 kernel,
62 client,
63 FileSystemOptions { source: "/pkg".into(), ..Default::default() },
64 rights,
65 )
66 .unwrap()
67}
68
69pub fn spawn_kernel_and_run<F, R>(callback: F) -> impl Future<Output = R>
74where
75 F: AsyncFnOnce(&mut Locked<Unlocked>, &mut CurrentTask) -> R + Send + Sync + 'static,
76 R: Send + Sync + 'static,
77{
78 spawn_kernel_and_run_internal(callback, None, TmpFs::new_fs, KernelFeatures::default())
79}
80
81pub fn spawn_kernel_with_features_and_run<F, R>(
83 callback: F,
84 features: KernelFeatures,
85) -> impl Future<Output = R>
86where
87 F: AsyncFnOnce(&mut Locked<Unlocked>, &mut CurrentTask) -> R + Send + Sync + 'static,
88 R: Send + Sync + 'static,
89{
90 spawn_kernel_and_run_internal(callback, None, TmpFs::new_fs, features)
91}
92
93pub fn spawn_kernel_and_run_sync<F, R>(callback: F) -> impl Future<Output = R>
98where
99 F: FnOnce(&mut Locked<Unlocked>, &mut CurrentTask) -> R + Send + Sync + 'static,
100 R: Send + Sync + 'static,
101{
102 spawn_kernel_and_run_internal_sync(callback, None, TmpFs::new_fs, KernelFeatures::default())
103}
104
105pub fn spawn_kernel_and_run_with_pkgfs<F, R>(callback: F) -> impl Future<Output = R>
111where
112 F: AsyncFnOnce(&mut Locked<Unlocked>, &mut CurrentTask) -> R + Send + Sync + 'static,
113 R: Send + Sync + 'static,
114{
115 spawn_kernel_and_run_internal(callback, None, create_pkgfs, KernelFeatures::default())
116}
117
118pub async fn spawn_kernel_with_selinux_and_run<F, R>(callback: F) -> R
124where
125 F: AsyncFnOnce(&mut Locked<Unlocked>, &mut CurrentTask, &Arc<SecurityServer>) -> R
126 + Send
127 + Sync
128 + 'static,
129 R: Send + Sync + 'static,
130{
131 let security_server = SecurityServer::new_default();
132 let security_server_for_callback = security_server.clone();
133 spawn_kernel_and_run_internal(
134 async move |unlocked, current_task| {
135 security::selinuxfs_init_null(
136 current_task,
137 &new_null_file(unlocked, current_task, OpenFlags::empty()),
138 );
139 callback(unlocked, current_task, &security_server_for_callback).await
140 },
141 Some(security_server),
142 TmpFs::new_fs,
143 KernelFeatures::default(),
144 )
145 .await
146}
147
148fn spawn_kernel_and_run_internal<F, FS, R>(
151 callback: F,
152 security_server: Option<Arc<SecurityServer>>,
153 fs_factory: FS,
154 features: KernelFeatures,
155) -> impl Future<Output = R>
156where
157 R: Send + Sync + 'static,
158 F: AsyncFnOnce(&mut Locked<Unlocked>, &mut CurrentTask) -> R + Send + Sync + 'static,
159 FS: FnOnce(&mut Locked<Unlocked>, &Kernel) -> FileSystemHandle,
160{
161 spawn_kernel_and_run_internal_sync(
162 move |locked, current_task| {
163 LocalExecutor::default().run_singlethreaded(callback(locked, current_task))
164 },
165 security_server,
166 fs_factory,
167 features,
168 )
169}
170
171fn spawn_kernel_and_run_internal_sync<F, FS, R>(
174 callback: F,
175 security_server: Option<Arc<SecurityServer>>,
176 fs_factory: FS,
177 features: KernelFeatures,
178) -> impl Future<Output = R>
179where
180 R: Send + Sync + 'static,
181 F: FnOnce(&mut Locked<Unlocked>, &mut CurrentTask) -> R + Send + Sync + 'static,
182 FS: FnOnce(&mut Locked<Unlocked>, &Kernel) -> FileSystemHandle,
183{
184 #[allow(
185 clippy::undocumented_unsafe_blocks,
186 reason = "Force documented unsafe blocks in Starnix"
187 )]
188 let locked = unsafe { Unlocked::new() };
189 let kernel = create_test_kernel(locked, security_server, features);
190 let fs = create_test_fs_context(locked, &kernel, fs_factory);
191 let init_task = create_test_init_task(locked, &kernel, fs);
192 fasync::unblock(move || {
193 let (sender, receiver) = mpsc::sync_channel(1);
194 let error = execute_task_with_prerun_result(
195 locked,
196 init_task,
197 move |locked, current_task| -> Result<(), Errno> {
198 let result = callback(locked, current_task);
199 current_task.write().set_exit_status_if_not_already(ExitStatus::Exit(0));
200 sender.send(result).map_err(|e| errno!(EIO, e))?;
201 error!(EHWPOISON)
202 },
203 |_| {},
204 None,
205 )
206 .unwrap_err();
207 assert_eq!(error, errno!(EHWPOISON));
209 receiver.recv().expect("recv")
210 })
211}
212
213#[deprecated = "Do not add new callers, use spawn_kernel_and_run() instead."]
214pub fn create_kernel_task_and_unlocked()
215-> (Arc<Kernel>, AutoReleasableTask, &'static mut Locked<Unlocked>) {
216 create_kernel_task_and_unlocked_with_fs(TmpFs::new_fs)
217}
218
219fn create_test_kernel(
220 _locked: &mut Locked<Unlocked>,
221 security_server: Option<Arc<SecurityServer>>,
222 features: KernelFeatures,
223) -> Arc<Kernel> {
224 Kernel::new(
225 b"".into(),
226 features,
227 SystemLimits::default(),
228 ContainerNamespace::new(),
229 SchedulerManager::empty_for_tests(),
230 None,
231 fuchsia_inspect::Node::default(),
232 security::testing::kernel_state(security_server),
233 None,
234 None,
235 )
236 .expect("failed to create kernel")
237}
238
239fn create_test_fs_context(
240 locked: &mut Locked<Unlocked>,
241 kernel: &Kernel,
242 create_fs: impl FnOnce(&mut Locked<Unlocked>, &Kernel) -> FileSystemHandle,
243) -> Arc<FsContext> {
244 FsContext::new(Namespace::new(create_fs(locked, kernel)))
245}
246
247fn create_test_mm(task: &Task) -> Result<Arc<MemoryManager>, Errno> {
249 let arch_width = ArchWidth::Arch64;
250 let mm =
251 MemoryManager::new_for_test(task.thread_group().root_vmar.unowned(), ArchWidth::Arch64);
252 let fake_executable_addr = mm.get_random_base_for_executable(arch_width, 0)?;
253 mm.initialize_brk_origin(arch_width, fake_executable_addr)?;
254 task.live()?.mm.update(Some(mm.clone()));
255 Ok(mm)
256}
257
258fn create_test_init_task(
259 locked: &mut Locked<Unlocked>,
260 kernel: &Kernel,
261 fs: Arc<FsContext>,
262) -> TaskBuilder {
263 let init_pid = kernel.pids.write().allocate_pid();
264 assert_eq!(init_pid, 1);
265 let init_task = create_init_process(
266 locked,
267 &kernel.weak_self.upgrade().unwrap(),
268 init_pid,
269 TaskCommand::new(b"test-task"),
270 fs.fork(),
271 &[],
272 )
273 .expect("failed to create first task");
274 create_test_mm(&init_task).expect("failed to create MM");
275
276 let system_task = create_system_task(locked, &kernel.weak_self.upgrade().unwrap(), fs)
277 .expect("create system task");
278 kernel.kthreads.init(system_task).expect("failed to initialize kthreads");
279
280 let system_task = kernel.kthreads.system_task();
281 kernel.hrtimer_manager.init(&system_task).expect("init hrtimer manager worker thread");
282
283 {
286 let _l1 = init_task.thread_group().read();
287 let _l2 = init_task.read();
288 }
289 init_task
290}
291
292fn create_kernel_task_and_unlocked_with_fs(
293 create_fs: impl FnOnce(&mut Locked<Unlocked>, &Kernel) -> FileSystemHandle,
294) -> (Arc<Kernel>, AutoReleasableTask, &'static mut Locked<Unlocked>) {
295 #[allow(
296 clippy::undocumented_unsafe_blocks,
297 reason = "Force documented unsafe blocks in Starnix"
298 )]
299 let locked = unsafe { Unlocked::new() };
300 let kernel = create_test_kernel(locked, None, KernelFeatures::default());
301 let fs = create_fs(locked, &kernel);
302 let fs_context = create_test_fs_context(locked, &kernel, |_, _| fs.clone());
303 let init_task = create_test_init_task(locked, &kernel, fs_context);
304 (kernel, init_task.into(), locked)
305}
306
307pub fn create_task(
317 locked: &mut Locked<Unlocked>,
318 kernel: &Kernel,
319 task_name: &str,
320) -> AutoReleasableTask {
321 create_task_with_security_context(locked, kernel, task_name, &CString::new("#kernel").unwrap())
322}
323
324pub fn create_task_with_security_context(
328 locked: &mut Locked<Unlocked>,
329 kernel: &Kernel,
330 task_name: &str,
331 security_context: &CString,
332) -> AutoReleasableTask {
333 let task = create_init_child_process(
334 locked,
335 &kernel.weak_self.upgrade().unwrap(),
336 TaskCommand::new(task_name.as_bytes()),
337 Credentials::with_ids(0, 0),
338 Some(security_context),
339 )
340 .expect("failed to create second task");
341 create_test_mm(&task).expect("failed to create MM");
342
343 {
346 let _l1 = task.thread_group().read();
347 let _l2 = task.read();
348 }
349
350 task.into()
351}
352
353pub fn map_memory_anywhere<L>(
356 locked: &mut Locked<L>,
357 current_task: &CurrentTask,
358 len: u64,
359) -> UserAddress
360where
361 L: LockEqualOrBefore<FileOpsCore>,
362{
363 map_memory(locked, current_task, UserAddress::NULL, len)
364}
365
366pub fn map_object_anywhere<L, T>(
371 locked: &mut Locked<L>,
372 current_task: &CurrentTask,
373 object: &T,
374) -> UserAddress
375where
376 L: LockEqualOrBefore<FileOpsCore>,
377 T: IntoBytes + Immutable,
378{
379 let addr = map_memory_anywhere(locked, current_task, std::mem::size_of::<T>() as u64);
380 current_task.write_object(addr.into(), object).expect("could not write object");
381 addr
382}
383
384pub fn map_memory<L>(
388 locked: &mut Locked<L>,
389 current_task: &CurrentTask,
390 address: UserAddress,
391 length: u64,
392) -> UserAddress
393where
394 L: LockEqualOrBefore<FileOpsCore>,
395{
396 map_memory_with_flags(locked, current_task, address, length, MAP_ANONYMOUS | MAP_PRIVATE)
397}
398
399pub fn map_memory_with_flags<L>(
403 locked: &mut Locked<L>,
404 current_task: &CurrentTask,
405 address: UserAddress,
406 length: u64,
407 flags: u32,
408) -> UserAddress
409where
410 L: LockEqualOrBefore<FileOpsCore>,
411{
412 do_mmap(
413 locked,
414 current_task,
415 address,
416 length as usize,
417 PROT_READ | PROT_WRITE,
418 flags,
419 FdNumber::from_raw(-1),
420 0,
421 )
422 .expect("Could not map memory")
423}
424
425pub fn remap_memory(
428 locked: &mut Locked<Unlocked>,
429 current_task: &CurrentTask,
430 old_addr: UserAddress,
431 old_length: u64,
432 new_length: u64,
433 flags: u32,
434 new_addr: UserAddress,
435) -> Result<UserAddress, Errno> {
436 sys_mremap(
437 locked,
438 current_task,
439 old_addr,
440 old_length as usize,
441 new_length as usize,
442 flags,
443 new_addr,
444 )
445}
446
447#[track_caller]
453pub fn fill_page(current_task: &CurrentTask, addr: UserAddress, data: char) {
454 let data = [data as u8].repeat(*PAGE_SIZE as usize);
455 if let Err(err) = current_task.write_memory(addr, &data) {
456 panic!("write page: failed to fill page @ {addr:?} with {data:?}: {err:?}");
457 }
458}
459
460#[track_caller]
466pub fn check_page_eq(current_task: &CurrentTask, addr: UserAddress, data: char) {
467 let buf = match current_task.read_memory_to_vec(addr, *PAGE_SIZE as usize) {
468 Ok(b) => b,
469 Err(err) => panic!("read page: failed to read page @ {addr:?}: {err:?}"),
470 };
471 assert!(
472 buf.into_iter().all(|c| c == data as u8),
473 "unexpected payload: page @ {addr:?} should be filled with {data:?}"
474 );
475}
476
477#[track_caller]
483pub fn check_page_ne(current_task: &CurrentTask, addr: UserAddress, data: char) {
484 let buf = match current_task.read_memory_to_vec(addr, *PAGE_SIZE as usize) {
485 Ok(b) => b,
486 Err(err) => panic!("read page: failed to read page @ {addr:?}: {err:?}"),
487 };
488 assert!(
489 !buf.into_iter().all(|c| c == data as u8),
490 "unexpected payload: page @ {addr:?} should not be filled with {data:?}"
491 );
492}
493
494#[track_caller]
500pub fn check_unmapped(current_task: &CurrentTask, addr: UserAddress) {
501 match current_task.read_memory_to_vec(addr, *PAGE_SIZE as usize) {
502 Ok(_) => panic!("read page: page @ {addr:?} should be unmapped"),
503 Err(err) if err == starnix_uapi::errors::EFAULT => {}
504 Err(err) => {
505 panic!("read page: expected EFAULT reading page @ {addr:?} but got {err:?} instead")
506 }
507 }
508}
509
510pub struct PanickingFsNode;
513
514impl FsNodeOps for PanickingFsNode {
515 fs_node_impl_not_dir!();
516
517 fn create_file_ops(
518 &self,
519 _locked: &mut Locked<FileOpsCore>,
520 _node: &FsNode,
521 _current_task: &CurrentTask,
522 _flags: OpenFlags,
523 ) -> Result<Box<dyn FileOps>, Errno> {
524 panic!("should not be called")
525 }
526}
527
528pub struct PanickingFile;
530
531impl PanickingFile {
532 pub fn new_file(locked: &mut Locked<Unlocked>, current_task: &CurrentTask) -> FileHandle {
534 anon_test_file(locked, current_task, Box::new(PanickingFile), OpenFlags::RDWR)
535 }
536}
537
538impl FileOps for PanickingFile {
539 fileops_impl_nonseekable!();
540 fileops_impl_noop_sync!();
541
542 fn write(
543 &self,
544 _locked: &mut Locked<FileOpsCore>,
545 _file: &FileObject,
546 _current_task: &CurrentTask,
547 _offset: usize,
548 _data: &mut dyn InputBuffer,
549 ) -> Result<usize, Errno> {
550 panic!("write called on TestFile")
551 }
552
553 fn read(
554 &self,
555 _locked: &mut Locked<FileOpsCore>,
556 _file: &FileObject,
557 _current_task: &CurrentTask,
558 _offset: usize,
559 _data: &mut dyn OutputBuffer,
560 ) -> Result<usize, Errno> {
561 panic!("read called on TestFile")
562 }
563
564 fn ioctl(
565 &self,
566 _locked: &mut Locked<Unlocked>,
567 _file: &FileObject,
568 _current_task: &CurrentTask,
569 _request: u32,
570 _arg: SyscallArg,
571 ) -> Result<SyscallResult, Errno> {
572 panic!("ioctl called on TestFile")
573 }
574}
575
576pub fn anon_test_file<L>(
578 locked: &mut Locked<L>,
579 current_task: &CurrentTask,
580 ops: Box<dyn FileOps>,
581 flags: OpenFlags,
582) -> FileHandle
583where
584 L: LockEqualOrBefore<FileOpsCore>,
585{
586 Anon::new_private_file(locked, current_task, ops, flags, "[fuchsia:test_file]")
588}
589
590pub struct UserMemoryWriter<'a> {
592 mm: &'a Task,
594 current_addr: UserAddress,
596}
597
598impl<'a> UserMemoryWriter<'a> {
599 pub fn new(task: &'a Task, addr: UserAddress) -> Self {
601 Self { mm: task, current_addr: addr }
602 }
603
604 pub fn write(&mut self, data: &[u8]) -> UserAddress {
608 let bytes_written = self.mm.write_memory(self.current_addr, data).unwrap();
609 assert_eq!(bytes_written, data.len());
610 let start_addr = self.current_addr;
611 self.current_addr = (self.current_addr + bytes_written).unwrap();
612 start_addr
613 }
614
615 pub fn write_object<T: IntoBytes + Immutable>(&mut self, object: &T) -> UserAddress {
619 self.write(object.as_bytes())
620 }
621
622 pub fn current_address(&self) -> UserAddress {
624 self.current_addr
625 }
626}
627
628#[derive(Debug)]
629pub struct AutoReleasableTask(Option<CurrentTask>);
630
631impl AutoReleasableTask {
632 fn as_ref(this: &Self) -> &CurrentTask {
633 this.0.as_ref().unwrap()
634 }
635
636 fn as_mut(this: &mut Self) -> &mut CurrentTask {
637 this.0.as_mut().unwrap()
638 }
639}
640
641impl From<CurrentTask> for AutoReleasableTask {
642 fn from(task: CurrentTask) -> Self {
643 Self(Some(task))
644 }
645}
646
647impl From<TaskBuilder> for AutoReleasableTask {
648 fn from(builder: TaskBuilder) -> Self {
649 CurrentTask::from(builder).into()
650 }
651}
652
653impl<'a> KernelOrTask<'a> for &'a AutoReleasableTask {
654 fn kernel(&self) -> &'a Kernel {
655 (self as &Task).kernel()
656 }
657 fn maybe_task(&self) -> Option<&'a CurrentTask> {
658 Some(&self)
659 }
660}
661
662impl Drop for AutoReleasableTask {
663 fn drop(&mut self) {
664 #[allow(
666 clippy::undocumented_unsafe_blocks,
667 reason = "Force documented unsafe blocks in Starnix"
668 )]
669 let locked = unsafe { Unlocked::new() };
670 self.0.take().unwrap().release(locked);
671 }
672}
673
674impl std::ops::Deref for AutoReleasableTask {
675 type Target = CurrentTask;
676
677 fn deref(&self) -> &Self::Target {
678 AutoReleasableTask::as_ref(self)
679 }
680}
681
682impl std::ops::DerefMut for AutoReleasableTask {
683 fn deref_mut(&mut self) -> &mut Self::Target {
684 AutoReleasableTask::as_mut(self)
685 }
686}
687
688impl std::borrow::Borrow<CurrentTask> for AutoReleasableTask {
689 fn borrow(&self) -> &CurrentTask {
690 AutoReleasableTask::as_ref(self)
691 }
692}
693
694impl std::convert::AsRef<CurrentTask> for AutoReleasableTask {
695 fn as_ref(&self) -> &CurrentTask {
696 AutoReleasableTask::as_ref(self)
697 }
698}
699
700impl ArchSpecific for AutoReleasableTask {
701 fn is_arch32(&self) -> bool {
702 self.deref().is_arch32()
703 }
704}
705
706impl MemoryAccessor for AutoReleasableTask {
707 fn read_memory<'a>(
708 &self,
709 addr: UserAddress,
710 bytes: &'a mut [MaybeUninit<u8>],
711 ) -> Result<&'a mut [u8], Errno> {
712 (**self).read_memory(addr, bytes)
713 }
714 fn read_memory_partial_until_null_byte<'a>(
715 &self,
716 addr: UserAddress,
717 bytes: &'a mut [MaybeUninit<u8>],
718 ) -> Result<&'a mut [u8], Errno> {
719 (**self).read_memory_partial_until_null_byte(addr, bytes)
720 }
721 fn read_memory_partial<'a>(
722 &self,
723 addr: UserAddress,
724 bytes: &'a mut [MaybeUninit<u8>],
725 ) -> Result<&'a mut [u8], Errno> {
726 (**self).read_memory_partial(addr, bytes)
727 }
728 fn write_memory(&self, addr: UserAddress, bytes: &[u8]) -> Result<usize, Errno> {
729 (**self).write_memory(addr, bytes)
730 }
731 fn write_memory_partial(&self, addr: UserAddress, bytes: &[u8]) -> Result<usize, Errno> {
732 (**self).write_memory_partial(addr, bytes)
733 }
734 fn zero(&self, addr: UserAddress, length: usize) -> Result<usize, Errno> {
735 (**self).zero(addr, length)
736 }
737}
738
739struct TestFs;
740impl FileSystemOps for TestFs {
741 fn statfs(
742 &self,
743 _locked: &mut Locked<FileOpsCore>,
744 _fs: &FileSystem,
745 _current_task: &CurrentTask,
746 ) -> Result<statfs, Errno> {
747 Ok(default_statfs(0))
748 }
749 fn name(&self) -> &'static FsStr {
750 "test".into()
751 }
752}
753
754pub fn create_testfs(locked: &mut Locked<Unlocked>, kernel: &Kernel) -> FileSystemHandle {
755 FileSystem::new(locked, &kernel, CacheMode::Uncached, TestFs, Default::default())
756 .expect("testfs constructed with valid options")
757}
758
759pub fn create_testfs_with_root(
760 locked: &mut Locked<Unlocked>,
761 kernel: &Kernel,
762 ops: impl FsNodeOps,
763) -> FileSystemHandle {
764 let test_fs = create_testfs(locked, kernel);
765 let root_ino = test_fs.allocate_ino();
766 test_fs.create_root(root_ino, ops);
767 test_fs
768}
769
770pub fn create_fs_node_for_testing(fs: &FileSystemHandle, ops: impl FsNodeOps) -> FsNodeHandle {
771 let ino = fs.allocate_ino();
772 let info = FsNodeInfo::new(mode!(IFDIR, 0o777), FsCred::root());
773 FsNode::new_uncached(ino, ops, fs, info)
774}
775
776pub fn create_namespace_node_for_testing(
777 fs: &FileSystemHandle,
778 ops: impl FsNodeOps,
779) -> NamespaceNode {
780 let node = create_fs_node_for_testing(fs, ops);
781 NamespaceNode::new_anonymous(DirEntry::new_unrooted(node))
782}