Sophie

Sophie

distrib > Scientific%20Linux > 5x > x86_64 > by-pkgid > 27922b4260f65d317aabda37e42bbbff > files > 994

kernel-2.6.18-238.el5.src.rpm

From: Eric Sandeen <sandeen@redhat.com>
Subject: [PATCH RHEL5.1] ext3: ensure do_split leaves enough free space in 	both blocks
Date: Mon, 17 Sep 2007 13:28:54 -0500
Bugzilla: 286501
Message-Id: <46EEC766.2070203@redhat.com>
Changelog: [fs] ext3: ensure do_split leaves enough free space in both blocks


This is for
Bug 286501: ext3 fs corruption noted after fresh install of RHEL5.1-Server-20070906.0

I sent this upstream today, and it's acked by Andreas Dilger, though not yet
in any upstream tree.
http://marc.info/?l=linux-ext4&m=119005010917843&w=2

I tested it with the testcase at:
http://marc.info/?l=linux-ext4&m=118067140512836&w=2
(jburke, thanks for re-finding that!)

I'm not 100% certain that the corruption in the bug noted above is due
to the same root cause as the testcase (in particular, finding corruption 
at the 0-byte offset in the dir is... interesting...) but since the bug 
the patch below addresses causes memory corruption (it copies an entry 
name past the end of a buffer), all sorts of bad thing *could* happen...

-Eric

-------------


The do_split() function for htree dir blocks is intended to split a
leaf block to make room for a new entry.  It sorts the entries in the
original block by hash value, then moves the last half of the entries to 
the new block - without accounting for how much space this actually moves.  
(IOW, it moves half of the entry *count* not half of the entry *space*).
If by chance we have both large & small entries, and we move only the 
smallest entries, and we have a large new entry to insert, we may not have 
created enough space for it.

The patch below stores each record size when calculating the dx_map,
and then walks the hash-sorted dx_map, calculating how many entries must 
be moved to more evenly split the existing entries between the old block 
and the new block, guaranteeing enough space for the new entry.

The dx_map "offs" member is reduced to u16 so that the overall map
size does not change - it is temporarily stored at the end of the new 
block, and if it grows too large it may be overwritten.  By making offs
and size both u16, we won't grow the map size.

Also add a few comments to the functions involved.

This fixes the testcase reported by hooanon05@yahoo.co.jp on the 
linux-ext4 list, "ext3 dir_index causes an error"

Thanks to Andreas Dilger for discussing the problem & solution with me.

Signed-off-by: Eric Sandeen <sandeen@redhat.com>

Index: linux-2.6.18-24.el5/fs/ext3/namei.c
===================================================================
--- linux-2.6.18-24.el5.orig/fs/ext3/namei.c
+++ linux-2.6.18-24.el5/fs/ext3/namei.c
@@ -140,7 +140,8 @@ struct dx_frame
 struct dx_map_entry
 {
 	u32 hash;
-	u32 offs;
+	u16 offs;
+	u16 size;
 };
 
 #ifdef CONFIG_EXT3_INDEX
@@ -671,6 +672,10 @@ errout:
  * Directory block splitting, compacting
  */
 
+/*
+ * Create map of hash values, offsets, and sizes, stored at end of block.
+ * Returns number of entries mapped.
+ */
 static int dx_make_map (struct ext3_dir_entry_2 *de, int size,
 			struct dx_hash_info *hinfo, struct dx_map_entry *map_tail)
 {
@@ -684,7 +689,8 @@ static int dx_make_map (struct ext3_dir_
 			ext3fs_dirhash(de->name, de->name_len, &h);
 			map_tail--;
 			map_tail->hash = h.hash;
-			map_tail->offs = (u32) ((char *) de - base);
+			map_tail->offs = (u16) ((char *) de - base);
+			map_tail->size = le16_to_cpu(de->rec_len);
 			count++;
 			cond_resched();
 		}
@@ -694,6 +700,7 @@ static int dx_make_map (struct ext3_dir_
 	return count;
 }
 
+/* Sort map by hash value */
 static void dx_sort_map (struct dx_map_entry *map, unsigned count)
 {
         struct dx_map_entry *p, *q, *top = map + count - 1;
@@ -1080,6 +1087,10 @@ static inline void ext3_set_de_type(stru
 }
 
 #ifdef CONFIG_EXT3_INDEX
+/*
+ * Move count entries from end of map between two memory locations.
+ * Returns pointer to last entry moved.
+ */
 static struct ext3_dir_entry_2 *
 dx_move_dirents(char *from, char *to, struct dx_map_entry *map, int count)
 {
@@ -1098,6 +1109,10 @@ dx_move_dirents(char *from, char *to, st
 	return (struct ext3_dir_entry_2 *) (to - rec_len);
 }
 
+/*
+ * Compact each dir entry in the range to the minimal rec_len.
+ * Returns pointer to last entry in range.
+ */
 static struct ext3_dir_entry_2* dx_pack_dirents(char *base, int size)
 {
 	struct ext3_dir_entry_2 *next, *to, *prev, *de = (struct ext3_dir_entry_2 *) base;
@@ -1120,6 +1135,11 @@ static struct ext3_dir_entry_2* dx_pack_
 	return prev;
 }
 
+/*
+ * Split a full leaf block to make room for a new dir entry.
+ * Allocate a new block, and move entries so that they are approx. equally full.
+ * Returns pointer to de in block into which the new entry will be inserted.
+ */
 static struct ext3_dir_entry_2 *do_split(handle_t *handle, struct inode *dir,
 			struct buffer_head **bh,struct dx_frame *frame,
 			struct dx_hash_info *hinfo, int *error)
@@ -1131,7 +1151,7 @@ static struct ext3_dir_entry_2 *do_split
 	u32 hash2;
 	struct dx_map_entry *map;
 	char *data1 = (*bh)->b_data, *data2;
-	unsigned split;
+	unsigned split, move, size, i;
 	struct ext3_dir_entry_2 *de = NULL, *de2;
 	int	err;
 
@@ -1164,8 +1184,19 @@ static struct ext3_dir_entry_2 *do_split
 	count = dx_make_map ((struct ext3_dir_entry_2 *) data1,
 			     blocksize, hinfo, map);
 	map -= count;
-	split = count/2; // need to adjust to actual middle
 	dx_sort_map (map, count);
+	/* Split the existing block in the middle, size-wise */
+	size = 0;
+	move = 0;
+	for (i = count-1; i >= 0; i--) {
+		/* is more than half of this entry in 2nd half of the block? */
+		if (size + map[i].size/2 > blocksize/2)
+			break;
+		size += map[i].size;
+		move++;
+	}
+	/* map index at which we will split */
+	split = count - move;
 	hash2 = map[split].hash;
 	continued = hash2 == map[split - 1].hash;
 	dxtrace(printk("Split block %i at %x, %i/%i\n",