c4a49e5
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
c4a49e5
From: Jon DeVree <nuxi@vault24.org>
c4a49e5
Date: Tue, 17 Oct 2023 23:03:47 -0400
c4a49e5
Subject: [PATCH] fs/xfs: Fix XFS directory extent parsing
c4a49e5
c4a49e5
The XFS directory entry parsing code has never been completely correct
c4a49e5
for extent based directories. The parser correctly handles the case
c4a49e5
where the directory is contained in a single extent, but then mistakenly
c4a49e5
assumes the data blocks for the multiple extent case are each identical
c4a49e5
to the single extent case. The difference in the format of the data
c4a49e5
blocks between the two cases is tiny enough that its gone unnoticed for
c4a49e5
a very long time.
c4a49e5
c4a49e5
A recent change introduced some additional bounds checking into the XFS
c4a49e5
parser. Like GRUB's existing parser, it is correct for the single extent
c4a49e5
case but incorrect for the multiple extent case. When parsing a directory
c4a49e5
with multiple extents, this new bounds checking is sometimes (but not
c4a49e5
always) tripped and triggers an "invalid XFS directory entry" error. This
c4a49e5
probably would have continued to go unnoticed but the /boot/grub/<arch>
c4a49e5
directory is large enough that it often has multiple extents.
c4a49e5
c4a49e5
The difference between the two cases is that when there are multiple
c4a49e5
extents, the data blocks do not contain a trailer nor do they contain
c4a49e5
any leaf information. That information is stored in a separate set of
c4a49e5
extents dedicated to just the leaf information. These extents come after
c4a49e5
the directory entry extents and are not included in the inode size. So
c4a49e5
the existing parser already ignores the leaf extents.
c4a49e5
c4a49e5
The only reason to read the trailer/leaf information at all is so that
c4a49e5
the parser can avoid misinterpreting that data as directory entries. So
c4a49e5
this updates the parser as follows:
c4a49e5
c4a49e5
For the single extent case the parser doesn't change much:
c4a49e5
1. Read the size of the leaf information from the trailer
c4a49e5
2. Set the end pointer for the parser to the start of the leaf
c4a49e5
   information. (The previous bounds checking set the end pointer to the
c4a49e5
   start of the trailer, so this is actually a small improvement.)
c4a49e5
3. Set the entries variable to the expected number of directory entries.
c4a49e5
c4a49e5
For the multiple extent case:
c4a49e5
1. Set the end pointer to the end of the block.
c4a49e5
2. Do not set up the entries variable. Figuring out how many entries are
c4a49e5
   in each individual block is complex and does not seem worth it when
c4a49e5
   it appears to be safe to just iterate over the entire block.
c4a49e5
c4a49e5
The bounds check itself was also dependent upon the faulty XFS parser
c4a49e5
because it accidentally used "filename + length - 1". Presumably this
c4a49e5
was able to pass the fuzzer because in the old parser there was always
c4a49e5
8 bytes of slack space between the tail pointer and the actual end of
c4a49e5
the block. Since this is no longer the case the bounds check needs to be
c4a49e5
updated to "filename + length + 1" in order to prevent a regression in
c4a49e5
the handling of corrupt fliesystems.
c4a49e5
c4a49e5
Notes:
c4a49e5
* When there is only one extent there will only ever be one block. If
c4a49e5
  more than one block is required then XFS will always switch to holding
c4a49e5
  leaf information in a separate extent.
c4a49e5
* B-tree based directories seems to be parsed properly by the same code
c4a49e5
  that handles multiple extents. This is unlikely to ever occur within
c4a49e5
  /boot though because its only used when there are an extremely large
c4a49e5
  number of directory entries.
c4a49e5
c4a49e5
Fixes: ef7850c75 (fs/xfs: Fix issues found while fuzzing the XFS filesystem)
c4a49e5
Fixes: b2499b29c (Adds support for the XFS filesystem.)
c4a49e5
Fixes: https://savannah.gnu.org/bugs/?64376
c4a49e5
c4a49e5
Signed-off-by: Jon DeVree <nuxi@vault24.org>
c4a49e5
Reviewed-by: Daniel Kiper <daniel.kiper@oracle.com>
c4a49e5
Tested-by: Sebastian Andrzej Siewior <sebastian@breakpoint.cc>
c4a49e5
Tested-by: Marta Lewandowska <mlewando@redhat.com>
c4a49e5
---
c4a49e5
 grub-core/fs/xfs.c | 52 ++++++++++++++++++++++++++++++++++++++--------------
c4a49e5
 1 file changed, 38 insertions(+), 14 deletions(-)
c4a49e5
c4a49e5
diff --git a/grub-core/fs/xfs.c b/grub-core/fs/xfs.c
c4a49e5
index ebf962793fa7..18edfcff486c 100644
c4a49e5
--- a/grub-core/fs/xfs.c
c4a49e5
+++ b/grub-core/fs/xfs.c
c4a49e5
@@ -223,6 +223,12 @@ struct grub_xfs_inode
c4a49e5
 /* Size of struct grub_xfs_inode v2, up to unused4 member included. */
c4a49e5
 #define XFS_V2_INODE_SIZE	(XFS_V3_INODE_SIZE - 76)
c4a49e5
 
c4a49e5
+struct grub_xfs_dir_leaf_entry
c4a49e5
+{
c4a49e5
+  grub_uint32_t hashval;
c4a49e5
+  grub_uint32_t address;
c4a49e5
+} GRUB_PACKED;
c4a49e5
+
c4a49e5
 struct grub_xfs_dirblock_tail
c4a49e5
 {
c4a49e5
   grub_uint32_t leaf_count;
c4a49e5
@@ -874,9 +880,8 @@ grub_xfs_iterate_dir (grub_fshelp_node_t dir,
c4a49e5
 	  {
c4a49e5
 	    struct grub_xfs_dir2_entry *direntry =
c4a49e5
 					grub_xfs_first_de(dir->data, dirblock);
c4a49e5
-	    int entries;
c4a49e5
-	    struct grub_xfs_dirblock_tail *tail =
c4a49e5
-					grub_xfs_dir_tail(dir->data, dirblock);
c4a49e5
+	    int entries = -1;
c4a49e5
+	    char *end = dirblock + dirblk_size;
c4a49e5
 
c4a49e5
 	    numread = grub_xfs_read_file (dir, 0, 0,
c4a49e5
 					  blk << dirblk_log2,
c4a49e5
@@ -887,14 +892,27 @@ grub_xfs_iterate_dir (grub_fshelp_node_t dir,
c4a49e5
 	        return 0;
c4a49e5
 	      }
c4a49e5
 
c4a49e5
-	    entries = (grub_be_to_cpu32 (tail->leaf_count)
c4a49e5
-		       - grub_be_to_cpu32 (tail->leaf_stale));
c4a49e5
+	    /*
c4a49e5
+	     * Leaf and tail information are only in the data block if the number
c4a49e5
+	     * of extents is 1.
c4a49e5
+	     */
c4a49e5
+	    if (dir->inode.nextents == grub_cpu_to_be32_compile_time (1))
c4a49e5
+	      {
c4a49e5
+		struct grub_xfs_dirblock_tail *tail = grub_xfs_dir_tail (dir->data, dirblock);
c4a49e5
 
c4a49e5
-	    if (!entries)
c4a49e5
-	      continue;
c4a49e5
+		end = (char *) tail;
c4a49e5
+
c4a49e5
+		/* Subtract the space used by leaf nodes. */
c4a49e5
+		end -= grub_be_to_cpu32 (tail->leaf_count) * sizeof (struct grub_xfs_dir_leaf_entry);
c4a49e5
+
c4a49e5
+		entries = grub_be_to_cpu32 (tail->leaf_count) - grub_be_to_cpu32 (tail->leaf_stale);
c4a49e5
+
c4a49e5
+		if (!entries)
c4a49e5
+		  continue;
c4a49e5
+	      }
c4a49e5
 
c4a49e5
 	    /* Iterate over all entries within this block.  */
c4a49e5
-	    while ((char *)direntry < (char *)tail)
c4a49e5
+	    while ((char *) direntry < (char *) end)
c4a49e5
 	      {
c4a49e5
 		grub_uint8_t *freetag;
c4a49e5
 		char *filename;
c4a49e5
@@ -914,7 +932,7 @@ grub_xfs_iterate_dir (grub_fshelp_node_t dir,
c4a49e5
 		  }
c4a49e5
 
c4a49e5
 		filename = (char *)(direntry + 1);
c4a49e5
-		if (filename + direntry->len - 1 > (char *) tail)
c4a49e5
+		if (filename + direntry->len + 1 > (char *) end)
c4a49e5
 		  return grub_error (GRUB_ERR_BAD_FS, "invalid XFS directory entry");
c4a49e5
 
c4a49e5
 		/* The byte after the filename is for the filetype, padding, or
c4a49e5
@@ -928,11 +946,17 @@ grub_xfs_iterate_dir (grub_fshelp_node_t dir,
c4a49e5
 		    return 1;
c4a49e5
 		  }
c4a49e5
 
c4a49e5
-		/* Check if last direntry in this block is
c4a49e5
-		   reached.  */
c4a49e5
-		entries--;
c4a49e5
-		if (!entries)
c4a49e5
-		  break;
c4a49e5
+		/*
c4a49e5
+		 * The expected number of directory entries is only tracked for the
c4a49e5
+		 * single extent case.
c4a49e5
+		 */
c4a49e5
+		if (dir->inode.nextents == grub_cpu_to_be32_compile_time (1))
c4a49e5
+		  {
c4a49e5
+		    /* Check if last direntry in this block is reached. */
c4a49e5
+		    entries--;
c4a49e5
+		    if (!entries)
c4a49e5
+		      break;
c4a49e5
+		  }
c4a49e5
 
c4a49e5
 		/* Select the next directory entry.  */
c4a49e5
 		direntry = grub_xfs_next_de(dir->data, direntry);