[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH 4/4] libfsimage/xfs: bound inline extent count and directory btree traversals


  • To: Anthony PERARD <anthony.perard@xxxxxxxxxx>
  • From: SounLabs <holyting07@xxxxxxxxx>
  • Date: Thu, 12 Nov 2026 20:49:49 +0900
  • Authentication-results: eu.smtp.expurgate.cloud; dkim=pass header.s=20260707 header.d=gmail.com header.i="@gmail.com" header.h="Content-Transfer-Encoding:MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:Cc:To:From"
  • Cc: xen-devel@xxxxxxxxxxxxxxxxxxxx, SounLabs <holyting07@xxxxxxxxx>
  • Delivery-date: Thu, 12 Nov 2026 11:57:00 +0000
  • List-id: Xen developer discussion <xen-devel.lists.xenproject.org>

libfsimage is used by pygrub to parse the filesystem of an untrusted guest
disk image from dom0.  Several xfs on-disk fields are used without validation,
leading to an out-of-bounds read and to unbounded tree descents that loop
forever:

 - init_extents(), XFS_DINODE_FMT_EXTENTS: di_nextents is taken as the number
   of inline extent records; next_extent() then iterates that many records from
   the inode data fork without checking they fit (heap OOB read).

 - init_extents(), XFS_DINODE_FMT_BTREE: the root-to-leaf descent follows
   bb_level/ptr0 with no depth limit, so a node whose bb_level does not
   decrease (e.g. one pointing to itself) loops forever.

 - first_dentry(), directory da-btree: the root-to-leaf descent assigns
   xfs.dablk = btree[0].before with no depth limit or cycle detection, so a
   self-referential or cyclic directory node loops forever.  This is the loop
   reached by the di_nextents reproducer as well, which therefore triggers
   both the OOB read above and this unbounded da-descent hang.  When the
   descent is bounded and the directory yields no entries, first_dentry()
   returns NULL and the caller in xfs_dir() dereferenced that NULL name via
   substring(dirname, NULL) (NULL read).

Cap di_nextents to the number of records that fit in the inode data fork,
bound both the bmap btree and the directory da-btree descents to a fixed
maximum depth, and have xfs_dir() fail the lookup gracefully when a directory
yields no first entry.

Same class of issue as XSA-497 (iso9660); the superblock sanity check added
by XSA-443 does not cover these inode-level and btree-level fields.  pygrub is
only security supported when run de-privileged (XSA-508), so this is hardening
rather than a security fix.

Signed-off-by: SounLabs <holyting07@xxxxxxxxx>
---
 tools/libfsimage/xfs/fsys_xfs.c | 57 +++++++++++++++++++++++++++++++--
 1 file changed, 54 insertions(+), 3 deletions(-)

diff --git a/tools/libfsimage/xfs/fsys_xfs.c b/tools/libfsimage/xfs/fsys_xfs.c
index e98b367901..7fbd78a122 100644
--- a/tools/libfsimage/xfs/fsys_xfs.c
+++ b/tools/libfsimage/xfs/fsys_xfs.c
@@ -201,20 +201,42 @@ di_read (fsi_file_t *ffi, xfs_ino_t ino)
        return 1;
 }
 
+/* An XFS bmap btree is only a handful of levels deep; use a generous cap
+ * to bound a traversal that would otherwise loop forever on a node whose
+ * bb_level does not decrease (e.g. one that points to itself). */
+#define XFS_BM_MAXLEVELS 32
+
+/* A directory da-btree is likewise only a few levels deep; bound the
+ * root-to-leaf descent in first_dentry() so a node whose btree[0].before
+ * never reaches a leaf (e.g. one that points to itself) cannot loop forever. 
*/
+#define XFS_DA_MAXLEVELS 32
+
 static void
 init_extents (fsi_file_t *ffi)
 {
        xfs_bmbt_ptr_t ptr0;
        xfs_btree_lblock_t h;
+       int levels;
 
        switch (icore.di_format) {
-       case XFS_DINODE_FMT_EXTENTS:
+       case XFS_DINODE_FMT_EXTENTS: {
+               /* di_nextents is attacker-controlled; the inline extent
+                * records (xfs_bmbt_rec_32_t each) must fit in the inode's
+                * data fork, or next_extent() reads past the inode buffer. */
+               int fork_bytes =
+                   (icore.di_forkoff ? (icore.di_forkoff << 3) : xfs.isize)
+                   - (int) offsetof(xfs_dinode_t, di_u);
+               unsigned int maxext = fork_bytes > 0
+                   ? (unsigned int) fork_bytes / sizeof(xfs_bmbt_rec_32_t) : 0;
                xfs.xt = inode->di_u.di_bmx;
                xfs.nextents = le32 (icore.di_nextents);
+               if (xfs.nextents > maxext)
+                       xfs.nextents = maxext;
                break;
+       }
        case XFS_DINODE_FMT_BTREE:
                ptr0 = xfs.ptr0;
-               for (;;) {
+               for (levels = 0; ; levels++) {
                        xfs.daddr = fsb2daddr (le64(ptr0));
                        devread (ffi, xfs.daddr, 0,
                                 sizeof(xfs_btree_lblock_t), (char *)&h);
@@ -224,6 +246,12 @@ init_extents (fsi_file_t *ffi)
                                xfs.fpos = sizeof(xfs_btree_block_t);
                                return;
                        }
+                       if (levels >= XFS_BM_MAXLEVELS) {
+                               /* malformed btree: refuse to iterate */
+                               xfs.nextents = 0;
+                               xfs.next = 0;
+                               return;
+                       }
                        devread (ffi, xfs.daddr, xfs.btnode_ptr0_off,
                                 sizeof(xfs_bmbt_ptr_t), (char *)&ptr0);
                }
@@ -392,6 +420,8 @@ next_dentry (fsi_file_t *ffi, xfs_ino_t *ino)
 static char *
 first_dentry (fsi_file_t *ffi, xfs_ino_t *ino)
 {
+       int da_levels;
+
        xfs.forw = 0;
        switch (icore.di_format) {
        case XFS_DINODE_FMT_LOCAL:
@@ -413,7 +443,7 @@ first_dentry (fsi_file_t *ffi, xfs_ino_t *ino)
                        xfs.dablk = (1ULL << 35) >> xfs.blklog;
 #define h              ((xfs_dir2_leaf_hdr_t *)dirbuf)
 #define n              ((xfs_da_intnode_t *)dirbuf)
-                       for (;;) {
+                       for (da_levels = 0; ; da_levels++) {
                                xfs_dabread (ffi);
                                if ((n->hdr.info.magic == 
le16(XFS_DIR2_LEAFN_MAGIC))
                                    || (n->hdr.info.magic == 
le16(XFS_DIR2_LEAF1_MAGIC))) {
@@ -421,6 +451,16 @@ first_dentry (fsi_file_t *ffi, xfs_ino_t *ino)
                                        xfs.forw = le32 (h->info.forw);
                                        break;
                                }
+                               if (da_levels >= XFS_DA_MAXLEVELS) {
+                                       /* malformed directory da-btree: a node
+                                        * that never descends to a leaf (e.g. a
+                                        * btree[0].before that points to 
itself)
+                                        * would otherwise loop forever.  Treat
+                                        * the directory as empty. */
+                                       xfs.dirmax = 0;
+                                       xfs.forw = 0;
+                                       break;
+                               }
                                xfs.dablk = le32 (n->btree[0].before);
                        }
 #undef n
@@ -602,6 +642,17 @@ xfs_dir (fsi_file_t *ffi, char *dirname)
                *rest = 0;
 
                name = first_dentry (ffi, &new_ino);
+               if (name == NULL) {
+                       /* A malformed directory (e.g. one whose da-btree
+                        * descent was bounded above) yields no entries;
+                        * fail the lookup instead of dereferencing NULL. */
+                       if (print_possibilities < 0)
+                               return 1;
+
+                       errnum = ERR_FILE_NOT_FOUND;
+                       *rest = ch;
+                       return 0;
+               }
                for (;;) {
                        cmp = (!*dirname) ? -1 : substring (dirname, name);
 #ifndef STAGE1_5
-- 
2.52.0.windows.1




 


Rackspace

Lists.xenproject.org is hosted with RackSpace, monitoring our
servers 24x7x365 and backed by RackSpace's Fanatical Support®.