[PATCH] erofs-utils: mkfs: emit an inode's xattrs in a canonical order
Martin Pitt <[email protected]> Wed, 29 Jul 2026 21:52:08 +0200
| Newsgroups | org.ozlabs.lists.linux-erofs |
|---|---|
| Message-ID | <[email protected]> |
listxattr(2) makes no promise about the order it reports: while e.g. ext4 returns a reproducible order, tmpfs varies it from inode to inode, so building the same tree twice can lay the same set of xattrs out differently and yield images that differ byte for byte. This makes the 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. This is the same approach as the shared attribute pool already does with comp_shared_xattritem(). Signed-off-by: Martin Pitt <[email protected]> --- See the attached reproducer below (that was written by Claude Opus 5). With $TMPDIR pointing to tmpfs (most modern distros should have the default /tmp/ on tmpfs), it does 8 runs with lots of jitter: > run 1 listxattr: gpt_label gpt_type_uuid mount_point sha256: d81df9744f252cf3 > run 4 listxattr: gpt_label gpt_type_uuid mount_point sha256: 6bad887d59e1e404 > run 7 listxattr: gpt_label gpt_type_uuid mount_point sha256: a16c9aca9449e61b > [...] > > FAIL: mkfs.erofs output depends on listxattr(2) order With the fix, the shas are all identical. fsck.erofs is happy with images carrying shared, inline and multi-prefix attributes. The script needs setfattr/getfattr from attr, and a tmpfs $TMPDIR to vary the reported order; it says so when the order came out stable, so a pass on ext4 does not mean much. I have not proposed the reproducer as a patch since the repo has no tests. But if you want me to do that, let me know. Also: Kernel process newbie here, sorry if I messed up formatting/structure. Thanks, Martin #!/bin/sh # Reproducer: mkfs.erofs output depends on listxattr(2) order. # # mkfs.erofs stores an inode's extended attributes in the order listxattr(2) # reports them. That order is not specified, and tmpfs varies it from inode to # inode, so building the very same tree twice can produce different images. # # Every run below writes identical content and sets the same three xattrs on the # root inode in the same order; only the staging directory (hence its inode) # differs. That is exactly what systemd-repart does when it builds a DDI: it # stages each partition in a fresh mkdtemp() directory and sets its # user.validatefs.* xattrs there before calling mkfs.erofs. # # Usage: ./xattr-order-reproducer.sh [path-to-mkfs.erofs] [runs] # # Exit status: 0 output is independent of listxattr order, 1 it is not, # 77 prerequisites missing (autotools "skipped" convention). set -eu MKFS=${1:-mkfs.erofs} RUNS=${2:-8} # -x first: command -v does not look up a bare relative path such as mkfs/mkfs.erofs. [ -x "$MKFS" ] || command -v "$MKFS" >/dev/null 2>&1 || { echo "cannot run $MKFS"; exit 77; } command -v setfattr >/dev/null 2>&1 || { echo "need setfattr from the attr package"; exit 77; } command -v getfattr >/dev/null 2>&1 || { echo "need getfattr from the attr package"; exit 77; } WORK=$(mktemp -d "${TMPDIR:-/tmp}/erofs-xattr-order.XXXXXX") trap 'rm -rf "$WORK"' EXIT echo "mkfs: $MKFS" echo "work dir: $WORK on a $(stat -f -c %T "$WORK") filesystem" echo i=1 while [ "$i" -le "$RUNS" ]; do src=$WORK/src$i mkdir -p "$src/dir" echo payload > "$src/dir/file" # Always set in this order; listxattr(2) need not report it back this way. setfattr -n user.validatefs.gpt_label -v root-x86-64 "$src" setfattr -n user.validatefs.gpt_type_uuid \ -v 2c7357ed-ebd2-46d9-aec1-23d437ec2bf5 "$src" setfattr -n user.validatefs.mount_point -v / "$src" order=$(getfattr -d --absolute-names "$src" | sed -n 's/^user\.validatefs\.\([a-z_]*\)=.*/\1/p' | tr '\n' ' ') # -T and -U pin the timestamp and the filesystem UUID, so the xattr order # is the only thing that can differ between runs. SOURCE_DATE_EPOCH=1739577600 "$MKFS" --quiet -T1739577600 \ -U 5230d7cf-f2ce-43ed-9ae2-39e7e2fe48ca "$WORK/img$i" "$src" printf 'run %2d listxattr: %-46s sha256: %s\n' \ "$i" "$order" "$(sha256sum < "$WORK/img$i" | cut -c1-16)" i=$((i + 1)) done echo distinct=$(sha256sum "$WORK"/img* | awk '{print $1}' | sort -u | wc -l) orders=$(for f in "$WORK"/src*; do getfattr -d --absolute-names "$f" | sed -n 's/^user\.validatefs\.\([a-z_]*\)=.*/\1/p' | tr '\n' ' ' echo done | sort -u | wc -l) echo "$RUNS identical trees: $orders distinct listxattr order(s), $distinct distinct image(s)" if [ "$distinct" -ne 1 ]; then echo "FAIL: mkfs.erofs output depends on listxattr(2) order" exit 1 fi if [ "$orders" -eq 1 ]; then echo "PASS, but inconclusive: this filesystem reported one stable order." echo "Point TMPDIR at a tmpfs to vary it, e.g. TMPDIR=/dev/shm $0 $MKFS $RUNS" else echo "PASS: output is independent of listxattr(2) order" fi lib/xattr.c | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/xattr.c b/lib/xattr.c index 051fdd8..cc74e16 100644 --- a/lib/xattr.c +++ b/lib/xattr.c @@ -400,17 +400,39 @@ static struct erofs_xattritem *erofs_get_selabel_xattr(struct erofs_sb_info *sbi return NULL; } +static int comp_inode_xattritem(const struct erofs_xattritem *a, + const struct erofs_xattritem *b) +{ + int ret = memcmp(a->kvbuf, b->kvbuf, min(a->len[0], b->len[0])); + + if (ret) + return ret; + if (a->len[0] != b->len[0]) + return a->len[0] < b->len[0] ? -1 : 1; + return 0; +} + 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); + + /* + * Keep each inode's xattrs ordered by name. listxattr(2) makes no + * promise about the order it reports, and tmpfs varies it from inode + * to inode, so appending in listing order would emit the same set of + * xattrs differently from run to run and make images unreproducible. + */ + list_for_each_entry(pos, hlist, list) + if (comp_inode_xattritem(item, pos->item) < 0) + break; + list_add_tail(&node->list, &pos->list); return 0; } @@ -1091,10 +1113,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