[PATCH v5 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
Nithurshen
nithurshen.dev at gmail.com
Fri Apr 3 11:34:52 AEDT 2026
This patch introduces a regression test (erofs/099) to verify that
the FUSE daemon gracefully handles corrupted inodes without crashing
or violating the FUSE protocol.
Recently, a bug was identified where erofs_read_inode_from_disk()
would fail, but erofsfuse_getattr() lacked a return statement
after sending an error reply. This caused a fall-through, sending
a second reply via fuse_reply_attr() and triggering a libfuse
segmentation fault.
To prevent future regressions, this test:
1. Creates a valid EROFS image.
2. Uses dump.erofs to dynamically determine the root directory's
inode NID and metadata block address.
3. Deterministically corrupts the root inode by injecting 32 bytes
of 0xFF, invalidating its layout while leaving the superblock intact.
4. Mounts the image in the foreground to capture daemon stderr.
5. Runs 'stat' on the root directory to trigger the inode read failure.
6. Evaluates the stderr log to ensure no segfaults, aborts, or
"multiple replies" warnings are emitted by libfuse.
Signed-off-by: Nithurshen <nithurshen.dev at gmail.com>
---
Changes in v5:
- Removed hardcoded offset 1152 and /dev/urandom logic.
- Implemented robust dump.erofs parsing to dynamically calculate
the exact root inode offset.
- Replaced the corruption payload with a deterministic 32-byte
0xFF sequence to guarantee i_format invalidation across all shells.
---
tests/Makefile.am | 3 ++
tests/erofs/099 | 106 ++++++++++++++++++++++++++++++++++++++++++++
tests/erofs/099.out | 2 +
3 files changed, 111 insertions(+)
create mode 100755 tests/erofs/099
create mode 100644 tests/erofs/099.out
diff --git a/tests/Makefile.am b/tests/Makefile.am
index e376d6a..c0f117c 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -122,6 +122,9 @@ TESTS += erofs/027
# 028 - test inode page cache sharing functionality
TESTS += erofs/028
+# 099 - test fuse error handling on truncated images
+TESTS += erofs/099
+
EXTRA_DIST = common/rc erofs
clean-local: clean-local-check
diff --git a/tests/erofs/099 b/tests/erofs/099
new file mode 100755
index 0000000..f34403d
--- /dev/null
+++ b/tests/erofs/099
@@ -0,0 +1,106 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Test FUSE daemon and kernel error handling on corrupted inodes
+#
+seq=`basename $0`
+seqres=$RESULT_DIR/$(echo $0 | awk '{print $((NF-1))"/"$NF}' FS="/")
+
+# get standard environment, filters and checks
+. "${srcdir}/common/rc"
+
+_cleanup()
+{
+ cd /
+ rm -rf $tmp.*
+ # Ensure we kill our background daemon if it's still alive
+ [ -n "$fuse_pid" ] && kill -9 $fuse_pid 2>/dev/null
+}
+
+# remove previous $seqres.full before test
+rm -f $seqres.full
+
+# real QA test starts here
+echo "QA output created by $seq"
+
+# Default to erofs (kernel) if FSTYP is not set
+[ -z "$FSTYP" ] && FSTYP="erofs"
+[ -z "$EROFSDUMP_PROG" ] && EROFSDUMP_PROG="../dump/dump.erofs"
+
+if [ -z "$SCRATCH_DEV" ]; then
+ SCRATCH_DEV=$tmp/erofs_$seq.img
+ rm -f $SCRATCH_DEV
+fi
+
+localdir="$tmp/$seq"
+rm -rf $localdir
+mkdir -p $localdir
+
+echo "test data" > $localdir/testfile
+
+_scratch_mkfs -Enosbcrc $localdir >> $seqres.full 2>&1 || _fail "failed to mkfs"
+
+# Dynamically determine the inode offset of the ROOT directory (/) using dump.erofs.
+META_BLKADDR=0
+BLOCK_SIZE=4096
+
+META_STR=$($EROFSDUMP_PROG $SCRATCH_DEV | grep -i "meta_blkaddr" | grep -oE '[0-9]+' | head -n 1)
+[ -n "$META_STR" ] && META_BLKADDR=$META_STR
+
+BLK_STR=$($EROFSDUMP_PROG $SCRATCH_DEV | grep -i "block size" | grep -oE '[0-9]+' | head -n 1)
+[ -n "$BLK_STR" ] && BLOCK_SIZE=$BLK_STR
+
+# Extract the NID of the root directory
+NID=$($EROFSDUMP_PROG --path=/ $SCRATCH_DEV | grep -iE 'nid\s*[:=]?\s*[0-9]+' -o | grep -oE '[0-9]+' | head -n 1)
+
+if [ -z "$NID" ]; then
+ _fail "Could not parse NID from dump.erofs output"
+fi
+
+OFFSET=$(( META_BLKADDR * BLOCK_SIZE + NID * 32 ))
+
+# Deterministically corrupt the root inode's layout by writing 32 bytes of 0xFF.
+awk 'BEGIN { for(i=0;i<32;i++) printf "\377" }' | dd of=$SCRATCH_DEV bs=1 seek=$OFFSET count=32 conv=notrunc >> $seqres.full 2>&1
+
+if [ "$FSTYP" = "erofsfuse" ]; then
+ [ -z "$EROFSFUSE_PROG" ] && _notrun "erofsfuse is not available"
+ # Run erofsfuse in the foreground to capture libfuse's internal stderr
+ $EROFSFUSE_PROG -f $SCRATCH_DEV $SCRATCH_MNT > $tmp/fuse_err.log 2>&1 &
+ fuse_pid=$!
+ # Wait for the mount to establish
+ sleep 1
+else
+ _require_erofs
+ _scratch_mount >> $seqres.full 2>&1
+fi
+
+# Attempt to stat the root directory to directly trigger getattr without a lookup.
+timeout 5 stat $SCRATCH_MNT >> $seqres.full 2>&1
+res=$?
+
+if [ "$FSTYP" = "erofsfuse" ]; then
+ # Clean up the mount
+ _scratch_unmount >> $seqres.full 2>&1
+ # Wait for the daemon to cleanly exit, or kill it if stuck
+ kill $fuse_pid 2>/dev/null
+ wait $fuse_pid 2>/dev/null
+ cat $tmp/fuse_err.log >> $seqres.full
+
+ # Evaluate results based on captured stderr and timeout
+ if [ $res -eq 124 ]; then
+ _fail "stat command timed out (FUSE daemon likely hung due to double reply)"
+ elif grep -q -i "multiple replies" $tmp/fuse_err.log; then
+ _fail "Bug detected: libfuse reported multiple replies to request"
+ elif grep -q -i "segmentation fault\|aborted" $tmp/fuse_err.log; then
+ _fail "Bug detected: FUSE daemon crashed"
+ fi
+else
+ # Kernel check: ensure no hang and error is returned
+ [ $res -eq 124 ] && _fail "stat command timed out (kernel hung?)"
+ [ $res -eq 0 ] && _fail "stat unexpectedly succeeded on a corrupted image"
+ _scratch_unmount >> $seqres.full 2>&1
+fi
+
+echo Silence is golden
+status=0
+exit 0
diff --git a/tests/erofs/099.out b/tests/erofs/099.out
new file mode 100644
index 0000000..4f36820
--- /dev/null
+++ b/tests/erofs/099.out
@@ -0,0 +1,2 @@
+QA output created by 099
+Silence is golden
--
2.52.0
More information about the Linux-erofs
mailing list