diff --git a/rust/alloc/vec/mod.rs b/rust/alloc/vec/mod.rs
index e8344c77939e..b755cf0b936c 100644
--- a/rust/alloc/vec/mod.rs
+++ b/rust/alloc/vec/mod.rs
@@ -2499,6 +2499,15 @@ unsafe fn split_at_spare_mut_with_len(
     }
 }
 
+impl<T: Clone> Vec<T> {
+    /// Try to clone the vector using the global allocator
+    #[inline]
+    #[stable(feature = "kernel", since = "1.0.0")]
+    pub fn try_clone(&self) -> Result<Self, TryReserveError> {
+        self.try_clone_in(Global)
+    }
+}
+
 impl<T: Clone, A: Allocator> Vec<T, A> {
     /// Resizes the `Vec` in-place so that `len` is equal to `new_len`.
     ///
@@ -2623,6 +2632,14 @@ pub fn try_extend_from_slice(&mut self, other: &[T]) -> Result<(), TryReserveErr
         self.try_spec_extend(other.iter())
     }
 
+    /// Tries to clone the vector using the given allocator
+    #[stable(feature = "kernel", since = "1.0.0")]
+    pub fn try_clone_in(&self, allocator: A) -> Result<Self, TryReserveError> {
+        let mut new_vec = Vec::try_with_capacity_in(self.len(), allocator)?;
+        new_vec.try_extend_from_slice(&self)?;
+        Ok(new_vec)
+    }
+
     /// Copies elements from `src` range to the end of the vector.
     ///
     /// # Panics
diff --git a/rust/kernel/fs.rs b/rust/kernel/fs.rs
index 10f90f78fc54..709b79fa0030 100644
--- a/rust/kernel/fs.rs
+++ b/rust/kernel/fs.rs
@@ -781,7 +781,9 @@ impl SuperParams {
 ///
 /// The superblock is a newly-created one and this is the only active pointer to it.
 pub struct NewSuperBlock<'a, T: Type + ?Sized, S = NeedsInit> {
-    sb: &'a mut SuperBlock<T>,
+    /// Pointer to the superblock; this fields is public so puzzlefs can call
+    /// try_new_dcache_dir_inode when populating the directory hierarchy
+    pub sb: &'a mut SuperBlock<T>,
 
     // This also forces `'a` to be invariant.
     _p: PhantomData<&'a mut &'a S>,
diff --git a/samples/rust/puzzlefs.rs b/samples/rust/puzzlefs.rs
index 8a64e0bd437d..1f0073716d91 100644
--- a/samples/rust/puzzlefs.rs
+++ b/samples/rust/puzzlefs.rs
@@ -6,16 +6,19 @@
 use kernel::mount::Vfsmount;
 use kernel::prelude::*;
 use kernel::{
-    c_str, file, fmt, fs,
+    c_str, file, fs,
     io_buffer::IoBufferWriter,
-    str::CString,
     sync::{Arc, ArcBorrow},
 };
 
 mod puzzle;
 // Required by the autogenerated '_capnp.rs' files
+use puzzle::inode::PuzzleFS;
+use puzzle::types::{Inode, InodeMode, MetadataBlob};
 use puzzle::{manifest_capnp, metadata_capnp};
 
+use kernel::fs::{DEntry, INodeParams, NeedsRoot, NewSuperBlock, RootDEntry};
+
 module_fs! {
     type: PuzzleFsModule,
     name: "puzzlefs",
@@ -27,7 +30,6 @@
 
 #[derive(Debug)]
 struct PuzzlefsInfo {
-    base_path: CString,
     vfs_mount: Arc<Vfsmount>,
 }
 
@@ -55,9 +57,81 @@ fn try_new() -> Result {
     }
 }
 
+fn puzzlefs_populate_dir(
+    sb: &NewSuperBlock<'_, PuzzleFsModule, NeedsRoot>,
+    pfs: &PuzzleFS,
+    parent: &DEntry<PuzzleFsModule>,
+    ino: u64,
+    name: &CStr,
+    recursion: usize,
+) -> Result {
+    if recursion == 0 {
+        return Err(E2BIG);
+    }
+
+    let inode = Arc::try_new(pfs.find_inode(ino).map_err(|_| EINVAL)?)?;
+    match &inode.mode {
+        InodeMode::File { chunks: _ } => {
+            let params = INodeParams {
+                mode: inode.permissions,
+                ino: inode.ino,
+                value: inode.clone(),
+            };
+            let creator = fs::file_creator::<_, FsFile>();
+            let inode = creator(sb, params)?;
+            sb.try_new_dentry(inode, parent, name)?;
+        }
+        InodeMode::Dir { dir_list } => {
+            let params = INodeParams {
+                mode: inode.permissions,
+                ino: inode.ino,
+                value: inode.clone(),
+            };
+
+            let new_dentry;
+            let new_parent = if name.as_bytes() != c_str!("").as_bytes() {
+                let dcache_inode = sb.sb.try_new_dcache_dir_inode(params)?;
+                new_dentry = sb.try_new_dentry(dcache_inode, parent, name)?;
+                &new_dentry
+            } else {
+                parent
+            };
+
+            for entry in &dir_list.entries {
+                let mut name = entry.name.try_clone()?;
+                // append NUL terminator
+                name.try_push(0)?;
+                let name = CStr::from_bytes_with_nul(&name)?;
+                puzzlefs_populate_dir(sb, pfs, new_parent, entry.ino, name, recursion - 1)?;
+            }
+        }
+        _ => todo!(),
+    }
+
+    Ok(())
+}
+
+/// Creates a new root dentry populated with the given entries.
+fn try_new_populated_root_puzzlefs_dentry(
+    sb: &NewSuperBlock<'_, PuzzleFsModule, NeedsRoot>,
+    pfs: &PuzzleFS,
+    root_value: <PuzzleFsModule as fs::Type>::INodeData,
+) -> Result<RootDEntry<PuzzleFsModule>> {
+    let root_inode = sb.sb.try_new_dcache_dir_inode(INodeParams {
+        mode: 0o755,
+        ino: root_value.ino,
+        value: root_value,
+    })?;
+    let root = sb.try_new_root_dentry(root_inode)?;
+    let ino = 1u64;
+    puzzlefs_populate_dir(sb, pfs, &root, ino, c_str!(""), 10)?;
+    Ok(root)
+}
+
 impl fs::Type for PuzzleFsModule {
     type Context = Self;
-    type INodeData = &'static [u8];
+    // this is Arc so it can be cloned in puzzlefs_populate_dir
+    type INodeData = Arc<Inode>;
     type Data = Box<PuzzlefsInfo>;
     const SUPER_TYPE: fs::Super = fs::Super::Independent;
     const NAME: &'static CStr = c_str!("puzzlefs");
@@ -65,41 +139,35 @@ impl fs::Type for PuzzleFsModule {
     const DCACHE_BASED: bool = true;
 
     fn fill_super(_data: (), sb: fs::NewSuperBlock<'_, Self>) -> Result<&fs::SuperBlock<Self>> {
-        let base_path = CString::try_from_fmt(fmt!("hello world"))?;
-        pr_info!("base_path {:?}\n", base_path);
         let vfs_mount = Vfsmount::new_private_mount(c_str!("/home/puzzlefs_oci"))?;
         pr_info!("vfs_mount {:?}\n", vfs_mount);
 
+        let arc_vfs_mount = Arc::try_new(vfs_mount)?;
+
         let sb = sb.init(
             Box::try_new(PuzzlefsInfo {
-                base_path,
-                vfs_mount: Arc::try_new(vfs_mount)?,
+                vfs_mount: arc_vfs_mount.clone(),
             })?,
             &fs::SuperParams {
                 magic: 0x72757374,
                 ..fs::SuperParams::DEFAULT
             },
         )?;
-        let root = sb.try_new_populated_root_dentry(
-            &[],
-            kernel::fs_entries![
-                file("test1", 0o600, "abc\n".as_bytes(), FsFile),
-                file("test2", 0o600, "def\n".as_bytes(), FsFile),
-                char("test3", 0o600, [].as_slice(), (10, 125)),
-                sock("test4", 0o755, [].as_slice()),
-                fifo("test5", 0o755, [].as_slice()),
-                block("test6", 0o755, [].as_slice(), (1, 1)),
-                dir(
-                    "dir1",
-                    0o755,
-                    [].as_slice(),
-                    [
-                        file("test1", 0o600, "abc\n".as_bytes(), FsFile),
-                        file("test2", 0o600, "def\n".as_bytes(), FsFile),
-                    ]
-                ),
-            ],
+
+        let file = file::RegularFile::from_path_in_root_mnt(
+            &arc_vfs_mount,
+            c_str!("997eed138af30d187e87d682dd2ae9f240fae78f668907a0519460b397c82467"),
+            file::flags::O_RDONLY.try_into().unwrap(),
+            0,
         )?;
+
+        // TODO: figure out how to go from WireFormatError to kernel::error::Error
+        let metadata = MetadataBlob::new(file).map_err(|_| EINVAL)?;
+
+        let mut puzzlefs = PuzzleFS::new(metadata).map_err(|_| EINVAL)?;
+        let root_inode = Arc::try_new(puzzlefs.find_inode(1).map_err(|_| EINVAL)?)?;
+
+        let root = try_new_populated_root_puzzlefs_dentry(&sb, &mut puzzlefs, root_inode)?;
         let sb = sb.init_root(root)?;
         Ok(sb)
     }
@@ -110,7 +178,7 @@ fn fill_super(_data: (), sb: fs::NewSuperBlock<'_, Self>) -> Result<&fs::SuperBl
 #[vtable]
 impl file::Operations for FsFile {
     // must be the same as INodeData
-    type OpenData = &'static [u8];
+    type OpenData = Arc<Inode>;
     type Filesystem = PuzzleFsModule;
     // this is an Arc because Data must be ForeignOwnable and the only implementors of it are Box,
     // Arc and (); we cannot pass a reference to read, so we share Vfsmount using and Arc
@@ -126,14 +194,19 @@ fn open(
 
     fn read(
         data: ArcBorrow<'_, Vfsmount>,
-        file: &file::File,
+        _file: &file::File,
         writer: &mut impl IoBufferWriter,
         offset: u64,
     ) -> Result<usize> {
-        file::read_from_slice(
-            file.inode::<PuzzleFsModule>().ok_or(EINVAL)?.fs_data(),
-            writer,
-            offset,
-        )
+        let mut buf = Vec::try_with_capacity(writer.len())?;
+        buf.try_resize(writer.len(), 0)?;
+        let file = file::RegularFile::from_path_in_root_mnt(
+            &data,
+            c_str!("data"),
+            file::flags::O_RDONLY.try_into().unwrap(),
+            0,
+        )?;
+        let nr_bytes_read = file.read_with_offset(&mut buf[..], offset)?;
+        file::read_from_slice(&buf[..nr_bytes_read], writer, 0)
     }
 }
