Description: restore of file and directory attributes fails if split across blocks
 Many years ago I reported a bug with verifying attributes on files where
 the count was sometimes wrong.
 However, it turns out the same bug is present for directories too.
 Unfortunately, the fix for files meant that directories *verified*
 correctly but did not restore correctly.
 The attached patch *replaces* the xattr_verify patch currently in the
 package and includes fix for the directory attributes which now both
 restore and verify successfully.
Author: Tim Woodall <debianbugs@woodall.me.uk>
Bug: https://bugs.debian.org/1081011
Bug: https://bugs.debian.org/940473

--- a/restore/dirs.c
+++ b/restore/dirs.c
@@ -181,7 +181,7 @@ extractdirs(int genmode)
 	struct inotab *itp;
 	struct direct nulldir;
 	int fd;
-	char xattr[XATTR_MAXSIZE];
+	char xattr[XATTR_MAXSIZE*2] = {0};
 	int xattr_found = 0;
 	dump_ino_t ino;
 
@@ -251,7 +251,7 @@ extractdirs(int genmode)
 				skipfile();
 				break;
 			case EXT_XATTR:
-				if (readxattr(xattr) == GOOD)
+				if (readxattr(xattr+xattr_found*XATTR_MAXSIZE) == GOOD)
 					xattr_found = 1;
 				break;
 			}
@@ -676,14 +676,14 @@ setdirmodes(int flags)
 	}
 	clearerr(mf);
 	for (;;) {
-		char xattr[XATTR_MAXSIZE];
+		char xattr[XATTR_MAXSIZE*2];
 		if (fread((char *)&node, sizeof(struct modeinfo), 1, mf) != 1) {
 			if (feof(mf))
 				break;
 			err(1, "fread");
 		}
 		if (node.xattr) {
-			if (fread(xattr, XATTR_MAXSIZE, 1, mf) != 1) {
+			if (fread(xattr, XATTR_MAXSIZE*2, 1, mf) != 1) {
 				if (feof(mf))
 					break;
 				err(1, "fread");
@@ -710,8 +710,13 @@ setdirmodes(int flags)
 				warn("chown");
 			(void) chmod(cp, node.mode);
 			utimes(cp, node.timep);
-			if (node.xattr)
+			if (node.xattr) {
 				xattr_extract(cp, xattr);
+				char* xattr2 = xattr + XATTR_MAXSIZE;
+				if (*(uint32_t*)xattr2 != 0) {
+					xattr_extract(cp, xattr2);
+				}
+			}
 			ep->e_flags &= ~NEW;
 			if (node.flags)
 #ifdef	__linux__
@@ -755,14 +760,14 @@ comparedirmodes(void)
 	}
 	clearerr(mf);
 	for (;;) {
-		char xattr[XATTR_MAXSIZE];
+		char xattr[XATTR_MAXSIZE*2];
 		if (fread((char *)&node, sizeof(struct modeinfo), 1, mf) != 1) {
 			if (feof(mf))
 				break;
 			err(1, "fread");
 		}
 		if (node.xattr) {
-			if (fread(xattr, XATTR_MAXSIZE, 1, mf) != 1) {
+			if (fread(xattr, XATTR_MAXSIZE*2, 1, mf) != 1) {
 				if (feof(mf))
 					break;
 				err(1, "fread");
@@ -814,14 +819,32 @@ comparedirmodes(void)
 				}
 			}
 #endif
+			int xattr_count_all = 0;
 			if (node.xattr) {
+				int xattr_count_thisblock;
 				if (xattr_compare(cp, xattr) == FAIL)
 					do_compare_error;
+				xattr_count(xattr, &xattr_count_thisblock);
+				xattr_count_all += xattr_count_thisblock;
+				char* xattr2 = xattr + XATTR_MAXSIZE;
+				if (*(uint32_t*)xattr2 != 0) {
+					xattr_extract(cp, xattr2);
+					xattr_count(xattr2, &xattr_count_thisblock);
+					xattr_count_all += xattr_count_thisblock;
+				}
 			}
 			else {
 				if (xattr_compare(cp, NULL) == FAIL)
 					do_compare_error;
 			}
+			int count_file = xattr_count_file(cp);
+			if (count_file < 0) {
+				do_compare_error;
+			} else if (count_file != xattr_count_all) {
+				fprintf(stderr, "%s: directory does not have the same number of attributes (%d) as the tape (%d).\n",
+								cp, count_file, xattr_count_all );
+				do_compare_error;
+			}
 			ep->e_flags &= ~NEW;
 		}
 	}
@@ -934,7 +957,7 @@ savemodeinfo(dump_ino_t ino, struct dino
 	if ( fwrite((char *)&node, sizeof(struct modeinfo), 1, mf) != 1 )
 		err(1,"cannot write to file %s", modefile);
 	if (xattr)
-		if ( fwrite(xattr, XATTR_MAXSIZE, 1, mf) != 1 )
+		if ( fwrite(xattr, XATTR_MAXSIZE*2, 1, mf) != 1 )
 			err(1,"cannot write to file %s", modefile);
 }
 
--- a/restore/extern.h
+++ b/restore/extern.h
@@ -154,3 +154,5 @@ void	skipxattr (void);
 int	readxattr (char *);
 int	xattr_compare (char *, char *);
 int	xattr_extract (char *, char *);
+int xattr_count_file (char *);
+int xattr_count (char *, int *);
--- a/restore/tape.c
+++ b/restore/tape.c
@@ -1710,39 +1710,63 @@ cmpfiles(char *tapefile, char *diskfile,
 }
 #endif /* !COMPARE_ONTHEFLY */
 
+/* collect both extended attribute tape blocks, then
+	compare the attributes */
 static void
 compareattr(char *name)
 {
 	int xattr_done = 0;
+	int xattr_count_all = 0;
 
 	while (spcl.c_flags & DR_EXTATTRIBUTES) {
-		switch (spcl.c_extattributes) {
-		case EXT_MACOSFNDRINFO:
-			msg("MacOSX not supported for comparision in this version, skipping\n");
-			skipfile();
-			break;
-		case EXT_MACOSRESFORK:
-			msg("MacOSX not supported for comparision in this version, skipping\n");
-			skipfile();
-			break;
-		case EXT_XATTR: {
-			char xattr[XATTR_MAXSIZE];
+			switch (spcl.c_extattributes) {
+				case EXT_MACOSFNDRINFO:
+						msg("MacOSX not supported for comparision in this version, skipping\n");
+						skipfile();
+						break;
+				case EXT_MACOSRESFORK:
+						msg("MacOSX not supported for comparision in this version, skipping\n");
+						skipfile();
+						break;
+				case EXT_XATTR: {
+						char xattr[XATTR_MAXSIZE];
+						int xattr_count_thisblock = 0;
 
-			if (readxattr(xattr) == GOOD) {
-				if (xattr_compare(name, xattr) == FAIL)
-					do_compare_error;
-				xattr_done = 1;
-			}
-			else
-				do_compare_error;
-			break;
-		}
+						/* in-inode xattr record comes first, if present at all; then block xattr */
+						if (readxattr(xattr) == GOOD) {
+							/* this returns fail if the buffer contains different attributes
+									than the file on disk; note that it mustn't return fail if the file on disk has more attributes! */
+							if (xattr_compare(name, xattr) == FAIL)
+									do_compare_error;
+
+							xattr_count(xattr, &xattr_count_thisblock);
+							xattr_count_all += xattr_count_thisblock;
+							xattr_done = 1;
+						}
+						else
+							do_compare_error;
+						break;
+				}
 		default:
 			msg("unexpected inode extension %ld, skipping\n", spcl.c_extattributes);
 			skipfile();
 			break;
 		}
 	}
+
+	/* cover the case of two xattr sources, the contents of which matched the file on disk:
+			the only uncovered case left is if there re more attributes on disk than observed on tape */
+	if (xattr_done) {
+			int count_file = xattr_count_file(name);
+			if (count_file < 0) {
+				do_compare_error;
+			} else if (count_file != xattr_count_all) {
+				fprintf(stderr, "%s: file does not have the same number of attributes (%d) as the tape (%d).\n",
+								name, count_file, xattr_count_all );
+				do_compare_error;
+			}
+	}
+	/* no attrs whatsoever? file must have none then as well */
 	if (!xattr_done && xattr_compare(name, NULL) == FAIL)
 		do_compare_error;
 }
--- a/restore/xattr.c
+++ b/restore/xattr.c
@@ -123,7 +123,7 @@ static int xattr_cb_list (char *, char *
 static int xattr_cb_set (char *, char *, int, int, void *);
 static int xattr_cb_compare (char *, char *, int, int, void *);
 static int xattr_verify (char *);
-static int xattr_count (char *, int *);
+int xattr_count (char *, int *);
 static int xattr_walk (char *, int (*)(char *, char *, int, int, void *), void *);
 
 #define POSIX_ACL_XATTR_VERSION 0x0002
@@ -373,7 +373,7 @@ xattr_cb_compare(char *name, char *value
 #endif
 		valuesz = lgetxattr(path, name, valuef, XATTR_MAXSIZE);
 		if (valuesz < 0) {
-			warn("%s: EA compare lgetxattr failed\n", path);
+			warn("%s: EA compare lgetxattr for %s failed\n", path, name);
 			return FAIL;
 		}
 #ifdef TRANSSELINUX			/*GAN6May06 SELinux MLS */
@@ -381,8 +381,9 @@ xattr_cb_compare(char *name, char *value
 #endif
 
 	if (valuesz != valuelen || memcmp(value, valuef, valuelen)) {
+		valuef[valuesz] = '\0';
 		/* GAN24May06: show name and new value for user to compare */
-		fprintf(stderr, "%s: EA %s:%s value changed to %s\n", path, name, value, valuef);
+		fprintf(stderr, "%s: EA %s value changed from %s on tape to %s on disk\n", path, name, value, valuef);
 		return FAIL;
 	}
 
@@ -429,7 +430,7 @@ xattr_verify(char *buffer)
 	return GOOD;
 }
 
-static int
+int
 xattr_count(char *buffer, int *count)
 {
 	struct ext2_xattr_entry *entry;
@@ -493,6 +494,7 @@ xattr_walk(char *buffer, int (*xattr_cb)
 		size = entry->e_value_size;
 
 		memcpy(value, buffer + VALUE_OFFSET(buffer, entry), size);
+		value[size]='\0';
 
 		if (convertacl) {
 			struct posix_acl *acl;
@@ -540,43 +542,58 @@ xattr_walk(char *buffer, int (*xattr_cb)
 	return GOOD;
 }
 
-int
-xattr_compare(char *path, char *buffer)
+/* return count of the xattrs on the given file or FAIL if failed */
+int xattr_count_file(char *path)
 {
-	int countf, countt;
+	int size, count = 0;
 	char *names = NULL, *end_names, *name;
 
-	countf = llistxattr(path, NULL, 0);
-	if (countf < 0) {
-		warn("%s: llistxattr failed", path);
-		return FAIL;
+	/* return number of bytes a/v */
+	size = llistxattr(path, NULL, 0);
+	if (size < 0) {
+			warn("%s: llistxattr failed", path);
+			return FAIL;
 	}
 
-	names = malloc(countf + 1);
-	if (!names) {
-		warn("%s: llistxattr failed", path);
-		return FAIL;
+	names = malloc(size + 1);
+	if (!names)
+	{
+			warn("%s: malloc failed", path);
+			return FAIL;
 	}
 
-	countf = llistxattr(path, names, countf);
-	if (countf < 0) {
-		warn("%s: llistxattr failed", path);
-		free(names);
-		return FAIL;
+	/* return nr of bytes and park data in names array */
+	size = llistxattr(path, names, size);
+	if (size < 0)
+	{
+			warn("%s: llistxattr failed", path);
+			free(names);
+			return FAIL;
 	}
 
-	names[countf] = '\0';
-	end_names = names + countf;
+	names[size] = '\0';
+	end_names = names + size;
 
-	countf = 0;
-	for (name = names; name != end_names; name = strchr(name, '\0') + 1) {
-		if (!*name)
-			continue;
-		countf++;
+	for (name = names; name != end_names; name = strchr(name, '\0') + 1)
+	{
+			if (!*name)
+				continue;
+			count++;
 	}
-
 	free(names);
 
+	return count;
+}
+
+int
+xattr_compare(char *path, char *buffer)
+{
+	int countf, countt;
+
+	countf = xattr_count_file(path); /* already warns on failure */
+	if (countf < 0)
+			return FAIL;
+
 	if (buffer) {
 		if (xattr_verify(buffer) == FAIL)
 			return FAIL;
@@ -587,10 +604,20 @@ xattr_compare(char *path, char *buffer)
 	else
 		countt = 0;
 
-	if (countf != countt) {
-		fprintf(stderr, "%s: EA count changed from %d to %d\n", path, countt, countf);
+	/* with ext4 there may be two tape blocks for attrs: one from the actual inode, plus
+		one extra filesystem block. in this case countf must be at least >= countt;
+		verification of that aspect is delayed and left to compareattr
+		which loops over the different blocks */
+
+	/* nothing on tape but something on disk? sure fail. ditto for more on tape than on disk. */
+	if (countf > 0 && !countt)
+	{
+		fprintf(stderr, "%s: file does not have the same number of attributes (%d) as the tape (%d).\n",
+						path, countf, countt);
 		return FAIL;
 	}
+	if (countf < countt)
+		return FAIL;								/* warning left to compareattr */
 
 	if (!buffer)
 		return GOOD;
