[PATCH v2] erofs-utils: mkfs: emit an inode's xattrs in a canonical order

Martin Pitt <[email protected]> Mon, 3 Aug 2026 13:52:37 +0200
Newsgroups org.ozlabs.lists.linux-erofs
Message-ID <[email protected]>
listxattr(2) makes no promise about the order it reports, and
filesystems disagree: tmpfs reports them in insertion order on recent
kernels (or in a random order on older ones), while ext4 and btrfs
report their own on-disk order.

mkfs.erofs stored an inode's attributes in exactly the order it received
them, so staging the same tree on different filesystems (or on older
kernels merely twice in the same place) produced images that differed
byte for byte. That made EROFS images unreproducible.

Insert into the inode's list ordered by attribute name instead, and move
inline attributes onto the on-stack list with list_add_tail() so the
emitted order matches. The shared attribute pool already sorts by the
same key, so generalize comp_shared_xattritem() into
erofs_comp_xattritem() and use it for both.

The length tiebreak previously returned only 0 or 1, never a negative
value; make it a proper three-way comparison with cmpsgn().

Suggested-by: Gao Xiang <[email protected]>
Signed-off-by: Martin Pitt <[email protected]>
---
v1: https://lore.kernel.org/linux-erofs/[email protected]

v2:
 - Reuse the shared pool's comparator for the per-inode list, as suggested by
   Gao Xiang, instead of adding a separate comp_inode_xattritem().
 - Use cmpsgn() for the length tiebreak, which returned only 0 or 1 before.
 - Reword the commit message: the order differs between filesystems, it is not
   specific to tmpfs varying it per inode. Measured with Fedora kernels,
   7.1.4-204.fc44 reports a random order per inode on tmpfs while
   7.1.5-201.fc44 reports insertion order.

Verified with a script that stages the same tree, with ten user.validatefs.*
attributes on the root inode, in /tmp (tmpfs) and /var/tmp (disk backed), and
compares the resulting images: 1.9.2 produces four distinct images from six
identical trees, this patch produces one. Three attributes are not enough to
expose an ordering bug, which is why the script sets ten.

 lib/xattr.c | 54 ++++++++++++++++++++++++++++++-----------------------
 1 file changed, 31 insertions(+), 23 deletions(-)

diff --git a/lib/xattr.c b/lib/xattr.c
index 051fdd8..f6bd8de 100644
--- a/lib/xattr.c
+++ b/lib/xattr.c
@@ -400,17 +400,43 @@ static struct erofs_xattritem *erofs_get_selabel_xattr(struct erofs_sb_info *sbi
 	return NULL;
 }
 
+static int erofs_comp_xattritem(const void *a, const void *b)
+{
+	const struct erofs_xattritem *ia, *ib;
+	unsigned int la, lb;
+	int ret;
+
+	ia = *((const struct erofs_xattritem **)a);
+	ib = *((const struct erofs_xattritem **)b);
+	la = EROFS_XATTR_KVSIZE(ia->len);
+	lb = EROFS_XATTR_KVSIZE(ib->len);
+
+	ret = memcmp(ia->kvbuf, ib->kvbuf, min(la, lb));
+	if (ret != 0)
+		return ret;
+	return cmpsgn(la, lb);
+}
+
 static int erofs_inode_xattr_add(struct list_head *hlist,
 				 struct erofs_xattritem *item)
 {
-	struct erofs_inode_xattr_node *node;
+	struct erofs_inode_xattr_node *node, *pos;
 
 	node = malloc(sizeof(*node));
 	if (!node)
 		return -ENOMEM;
 	init_list_head(&node->list);
 	node->item = item;
-	list_add(&node->list, hlist);
+
+	/*
+	 * Order each inode's xattrs by name so that images stay reproducible.
+	 * listxattr(2) makes no promise about the order it reports, and
+	 * filesystems disagree: insertion order, on-disk order, or random.
+	 */
+	list_for_each_entry(pos, hlist, list)
+		if (erofs_comp_xattritem(&item, &pos->item) < 0)
+			break;
+	list_add_tail(&node->list, &pos->list);
 	return 0;
 }
 
@@ -843,24 +869,6 @@ static unsigned int erofs_cleanxattrs(struct erofs_xattrmgr *xamgr,
 	return count;
 }
 
-static int comp_shared_xattritem(const void *a, const void *b)
-{
-	const struct erofs_xattritem *ia, *ib;
-	unsigned int la, lb;
-	int ret;
-
-	ia = *((const struct erofs_xattritem **)a);
-	ib = *((const struct erofs_xattritem **)b);
-	la = EROFS_XATTR_KVSIZE(ia->len);
-	lb = EROFS_XATTR_KVSIZE(ib->len);
-
-	ret = memcmp(ia->kvbuf, ib->kvbuf, min(la, lb));
-	if (ret != 0)
-		return ret;
-
-	return la > lb;
-}
-
 int erofs_xattr_flush_name_prefixes(struct erofs_importer *im, bool plain)
 {
 	const struct erofs_importer_params *params = im->params;
@@ -1010,7 +1018,7 @@ int erofs_load_shared_xattrs_from_path(struct erofs_sb_info *sbi, const char *pa
 	}
 	DBG_BUGON(i != sharedxattr_count);
 	sorted_n[i] = NULL;
-	qsort(sorted_n, sharedxattr_count, sizeof(n), comp_shared_xattritem);
+	qsort(sorted_n, sharedxattr_count, sizeof(n), erofs_comp_xattritem);
 
 	buf = calloc(1, shared_xattrs_size);
 	if (!buf) {
@@ -1091,10 +1099,10 @@ char *erofs_export_xattr_ibody(struct erofs_inode *inode)
 		item = node->item;
 		list_del(&node->list);
 
-		/* move inline xattrs to the onstack list */
+		/* move inline xattrs to the onstack list, order preserved */
 		if (item->shared_xattr_id < 0 ||
 		    header->h_shared_count >= UCHAR_MAX) {
-			list_add(&node->list, &ilst);
+			list_add_tail(&node->list, &ilst);
 			continue;
 		}
 
-- 
2.55.0