e2fsck: make insert_dirent_tail more robust

Fix the routine that adds dirent checksum structures to the directory
block to handle oddball situations a bit more robustly.

First, when we're walking the entry array, we might encounter an
entry that ends exactly one byte before where the checksum entry needs
to start, i.e. there's space for the tail entry, but it needs to be
reinitialized.  When that happens, we should proceed until d points to
that space so that the tail entry can be initialized.

Second, it's possible that we've been fed a directory block where the
entries end just short of the end of the block.  In this case, we need
to adjust the size of the last entry to point exactly to where the
dirent tail starts.  The current code requires that entries end
exactly on the block boundary, but this is not always the case with
damaged filesystems.

Signed-off-by: Darrick J. Wong <darrick.wong@oracle.com>
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
diff --git a/e2fsck/pass2.c b/e2fsck/pass2.c
index 4774f06..69ebab6 100644
--- a/e2fsck/pass2.c
+++ b/e2fsck/pass2.c
@@ -740,6 +740,7 @@
 		return (offset < fs->blocksize - csum_size);
 }
 
+#define NEXT_DIRENT(d)	((void *)((char *)(d) + (d)->rec_len))
 static errcode_t insert_dirent_tail(ext2_filsys fs, void *dirbuf)
 {
 	struct ext2_dir_entry *d;
@@ -750,20 +751,15 @@
 	d = dirbuf;
 	top = EXT2_DIRENT_TAIL(dirbuf, fs->blocksize);
 
-	rec_len = d->rec_len;
-	while (rec_len && !(rec_len & 0x3)) {
-		d = (struct ext2_dir_entry *)(((char *)d) + rec_len);
-		if (((void *)d) + d->rec_len >= top)
-			break;
-		rec_len = d->rec_len;
-	}
+	while (d->rec_len && !(d->rec_len & 0x3) && NEXT_DIRENT(d) <= top)
+		d = NEXT_DIRENT(d);
 
 	if (d != top) {
 		size_t min_size = EXT2_DIR_REC_LEN(
 				ext2fs_dirent_name_len(dirbuf));
-		if (min_size > d->rec_len - sizeof(struct ext2_dir_entry_tail))
+		if (min_size > top - (void *)d)
 			return EXT2_ET_DIR_NO_SPACE_FOR_CSUM;
-		d->rec_len -= sizeof(struct ext2_dir_entry_tail);
+		d->rec_len = top - (void *)d;
 	}
 
 	t = (struct ext2_dir_entry_tail *)top;
@@ -774,6 +770,7 @@
 
 	return 0;
 }
+#undef NEXT_DIRENT
 
 static int check_dir_block(ext2_filsys fs,
 			   struct ext2_db_entry2 *db,
diff --git a/tests/f_corrupt_dirent_tail/expect.1 b/tests/f_corrupt_dirent_tail/expect.1
new file mode 100644
index 0000000..0813755
--- /dev/null
+++ b/tests/f_corrupt_dirent_tail/expect.1
@@ -0,0 +1,16 @@
+Pass 1: Checking inodes, blocks, and sizes
+Pass 2: Checking directory structure
+Directory inode 2, block #0, offset 0: directory has no checksum.
+Fix? yes
+
+Directory inode 2, block #0, offset 1012: directory corrupted
+Salvage? yes
+
+Pass 3: Checking directory connectivity
+Pass 3A: Optimizing directories
+Pass 4: Checking reference counts
+Pass 5: Checking group summary information
+
+test_filesys: ***** FILE SYSTEM WAS MODIFIED *****
+test_filesys: 11/128 files (9.1% non-contiguous), 1090/2048 blocks
+Exit status is 1
diff --git a/tests/f_corrupt_dirent_tail/expect.2 b/tests/f_corrupt_dirent_tail/expect.2
new file mode 100644
index 0000000..c42466d
--- /dev/null
+++ b/tests/f_corrupt_dirent_tail/expect.2
@@ -0,0 +1,7 @@
+Pass 1: Checking inodes, blocks, and sizes
+Pass 2: Checking directory structure
+Pass 3: Checking directory connectivity
+Pass 4: Checking reference counts
+Pass 5: Checking group summary information
+test_filesys: 11/128 files (9.1% non-contiguous), 1090/2048 blocks
+Exit status is 0
diff --git a/tests/f_corrupt_dirent_tail/image.gz b/tests/f_corrupt_dirent_tail/image.gz
new file mode 100644
index 0000000..f275308
--- /dev/null
+++ b/tests/f_corrupt_dirent_tail/image.gz
Binary files differ
diff --git a/tests/f_corrupt_dirent_tail/name b/tests/f_corrupt_dirent_tail/name
new file mode 100644
index 0000000..08259a3
--- /dev/null
+++ b/tests/f_corrupt_dirent_tail/name
@@ -0,0 +1 @@
+rebuild a directory with corrupt dirent tail