From: Nithurshen <nithurshen.dev@gmail.com>
To: nithurshen.dev@gmail.com
Cc: hsiangkao@linux.alibaba.com, linux-erofs@lists.ozlabs.org,
newajay.11r@gmail.com, xiang@kernel.org
Subject: [PATCH v3 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
Date: Wed, 1 Apr 2026 12:40:18 +0530 [thread overview]
Message-ID: <20260401071018.86191-1-nithurshen.dev@gmail.com> (raw)
In-Reply-To: <20260330103051.26877-1-nithurshen.dev@gmail.com>
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 with a test file.
2. Uses dump.erofs to dynamically determine the test file's inode offset.
3. Deterministically corrupts the 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 corrupted file 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@gmail.com>
---
Changes in v3:
- Disabled superblock checksums using `-Enosbcrc` in _scratch_mkfs.
- Used `_scratch_unmount` instead of standard `umount`.
- Replaced the hardcoded root offset with a dynamic offset
calculation for `/testfile` using `dump.erofs` as suggested.
Note regarding the corruption payload:
While implementing the dynamic offset for `/testfile`, I found
that injecting random garbage via `/dev/urandom` made the test
slightly flaky. If the random bytes happen to form a layout that
erofs_read_inode_from_disk() does not immediately reject as
invalid, the function returns success and the buggy FUSE error
path is bypassed.
To ensure the test is 100% deterministic, I changed the payload
to inject exactly 32 bytes of `0xFF`. This guarantees an invalid
`i_format`, reliably forcing the exact inode read error needed
to exercise the FUSE regression.
---
tests/Makefile.am | 3 ++
tests/erofs/099 | 90 +++++++++++++++++++++++++++++++++++++++++++++
tests/erofs/099.out | 2 +
3 files changed, 95 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..0189813
--- /dev/null
+++ b/tests/erofs/099
@@ -0,0 +1,90 @@
+#!/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"
+
+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"
+
+# Corrupt the root inode to force erofs_read_inode_from_disk to fail.
+# The EROFS superblock is at offset 1024 and is 128 bytes long.
+# The metadata (including the root inode) starts immediately after (offset 1152).
+# We inject 1024 bytes of random garbage starting at offset 1152. This leaves
+# the SB intact so the mount succeeds, but guarantees the inode read will fail.
+dd if=/dev/urandom of=$SCRATCH_DEV bs=1 seek=1152 count=1024 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. We expect this to fail with an error.
+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
next prev parent reply other threads:[~2026-04-01 7:10 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-30 4:28 [PATCH experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes Nithurshen
2026-03-30 5:43 ` Gao Xiang
2026-03-30 10:30 ` [PATCH v2 " Nithurshen
2026-03-31 2:33 ` Gao Xiang
2026-04-01 7:10 ` Nithurshen [this message]
2026-04-01 7:19 ` [PATCH v3 " Gao Xiang
2026-04-01 7:55 ` [PATCH v4 " Nithurshen
2026-04-01 8:05 ` Gao Xiang
2026-04-01 8:09 ` Gao Xiang
2026-04-03 0:34 ` [PATCH v5 " Nithurshen
2026-04-07 3:01 ` [PATCH v6 " Gao Xiang
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260401071018.86191-1-nithurshen.dev@gmail.com \
--to=nithurshen.dev@gmail.com \
--cc=hsiangkao@linux.alibaba.com \
--cc=linux-erofs@lists.ozlabs.org \
--cc=newajay.11r@gmail.com \
--cc=xiang@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox