diff --git a/fs/puzzlefs/puzzlefs.rs b/fs/puzzlefs/puzzlefs.rs
index a062bf0249f6..9622ea71eda0 100644
--- a/fs/puzzlefs/puzzlefs.rs
+++ b/fs/puzzlefs/puzzlefs.rs
@@ -107,8 +107,8 @@ fn fill_super(
         _: Option<inode::Mapper>,
     ) -> Result<Box<PuzzleFS>> {
         let puzzlefs = PuzzleFS::open(
-            c_str!("/home/puzzlefs_oci"),
-            c_str!("83aa96c40a20671edc4490cfefadbb487b2ab23dfc0570049b56f0cc49b56eaf"),
+            c_str!("/home/puzzlefs_xattr"),
+            c_str!("ed63ace21eccceabab08d89afb75e94dae47973f82a17a172396a19ea953c8ab"),
         );
 
         if let Err(ref e) = puzzlefs {
@@ -124,6 +124,36 @@ fn init_root(sb: &sb::SuperBlock<Self>) -> Result<dentry::Root<Self>> {
         let inode = Self::iget(sb, 1)?;
         dentry::Root::try_new(inode)
     }
+
+    fn read_xattr(
+        _dentry: &DEntry<Self>,
+        inode: &INode<Self>,
+        name: &CStr,
+        outbuf: &mut [u8],
+    ) -> Result<usize> {
+        let inode = inode.data();
+        let readonly = outbuf.len() == 0;
+        // pr_info!("outbuf len {}\n", outbuf.len());
+
+        if let Some(add) = &inode.additional {
+            let xattr = add
+                .xattrs
+                .iter()
+                .find(|elem| elem.key == name.as_bytes())
+                .ok_or(ENODATA)?;
+            if readonly {
+                return Ok(xattr.val.len());
+            }
+
+            if xattr.val.len() > outbuf.len() {
+                return Err(ERANGE);
+            }
+
+            outbuf[0..xattr.val.len()].copy_from_slice(xattr.val.as_slice());
+            return Ok(xattr.val.len());
+        }
+        Err(ENODATA)
+    }
 }
 
 #[vtable]
@@ -143,6 +173,22 @@ fn lookup(
         }
     }
 
+    fn listxattr(
+        inode: &INode<Self>,
+        mut add_entry: impl FnMut(&[i8]) -> Result<()>,
+    ) -> Result<()> {
+        let inode = inode.data();
+
+        if let Some(add) = &inode.additional {
+            for xattr in &add.xattrs {
+                // convert a u8 slice into an i8 slice
+                let i8slice = unsafe { &*(xattr.key.as_slice() as *const _ as *const [i8]) };
+                add_entry(i8slice)?;
+            }
+        }
+        Ok(())
+    }
+
     fn get_link<'a>(
         dentry: Option<&DEntry<PuzzleFsModule>>,
         inode: &'a INode<PuzzleFsModule>,
diff --git a/rust/kernel/fs/inode.rs b/rust/kernel/fs/inode.rs
index b2b7d000080e..a092ee150d43 100644
--- a/rust/kernel/fs/inode.rs
+++ b/rust/kernel/fs/inode.rs
@@ -10,7 +10,7 @@
     address_space, dentry, dentry::DEntry, file, mode, sb::SuperBlock, FileSystem, Offset,
     PageOffset, UnspecifiedFS,
 };
-use crate::error::{code::*, from_err_ptr, Result};
+use crate::error::{code::*, from_err_ptr, from_result, Result};
 use crate::types::{ARef, AlwaysRefCounted, Either, ForeignOwnable, Lockable, Locked, Opaque};
 use crate::{
     bindings, block, build_error, container_of, folio, folio::Folio, mem_cache::MemCache,
@@ -48,6 +48,14 @@ fn lookup(
     ) -> Result<Option<ARef<DEntry<Self::FileSystem>>>> {
         Err(ENOTSUPP)
     }
+
+    /// Get extended attributes list
+    fn listxattr<'a>(
+        _inode: &'a INode<Self::FileSystem>,
+        mut _add_entry: impl FnMut(&[i8]) -> Result<()>,
+    ) -> Result<()> {
+        Err(ENOSYS)
+    }
 }
 
 /// A node (inode) in the file index.
@@ -615,7 +623,7 @@ impl<T: Operations + ?Sized> Table<T> {
                 rename: None,
                 setattr: None,
                 getattr: None,
-                listxattr: None,
+                listxattr: Some(Self::listxattr_callback),
                 fiemap: None,
                 update_time: None,
                 atomic_open: None,
@@ -688,6 +696,60 @@ extern "C" fn drop_cstring(ptr: *mut core::ffi::c_void) {
                     }
                 }
             }
+
+            extern "C" fn listxattr_callback(
+                dentry: *mut bindings::dentry,
+                buffer: *mut core::ffi::c_char,
+                buffer_size: usize,
+            ) -> isize {
+                from_result(|| {
+                    // SAFETY: The C API guarantees that `dentry` is valid for read.
+                    let inode = unsafe { bindings::d_inode(dentry) };
+                    // SAFETY: The C API guarantees that `d_inode` inside `dentry` is valid for read.
+                    let inode = unsafe { INode::from_raw(inode) };
+
+                    // `buffer_size` should be 0 when `buffer` is NULL, but we enforce it
+                    let (mut buffer_ptr, buffer_size) = match ptr::NonNull::new(buffer) {
+                        Some(buf) => (buf, buffer_size),
+                        None => (ptr::NonNull::dangling(), 0),
+                    };
+
+                    // SAFETY: The C API guarantees that `buffer` is at least `buffer_size` bytes in
+                    // length. Also, when `buffer_size` is 0, `buffer_ptr` is NonNull::dangling, as
+                    // suggested by `from_raw_parts_mut` documentation
+                    let outbuf = unsafe {
+                        core::slice::from_raw_parts_mut(buffer_ptr.as_mut(), buffer_size)
+                    };
+
+                    let mut offset = 0;
+                    let mut total_len = 0;
+
+                    //  The extended attributes keys must be placed into the output buffer sequentially,
+                    //  separated by the NUL character. We do this in the callback because it simplifies
+                    //  the implementation of the `listxattr` abstraction: the user just calls the
+                    //  add_entry function for each extended attribute key, passing a slice.
+                    T::listxattr(inode, |xattr_key| {
+                        let len = xattr_key.len();
+                        total_len += isize::try_from(len)? + 1;
+
+                        if buffer_size == 0 {
+                            return Ok(());
+                        }
+
+                        let max = offset + len + 1;
+                        if max > buffer_size {
+                            return Err(ERANGE);
+                        }
+
+                        outbuf[offset..max - 1].copy_from_slice(xattr_key);
+                        outbuf[max - 1] = 0;
+                        offset = max;
+                        Ok(())
+                    })?;
+
+                    Ok(total_len)
+                })
+            }
         }
         Self(&Table::<U>::TABLE, PhantomData)
     }
