* [PATCH experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
@ 2026-03-30 4:28 Nithurshen
2026-03-30 5:43 ` Gao Xiang
2026-03-30 10:30 ` [PATCH v2 " Nithurshen
0 siblings, 2 replies; 11+ messages in thread
From: Nithurshen @ 2026-03-30 4:28 UTC (permalink / raw)
To: linux-erofs; +Cc: hsiangkao, xiang, newajay.11r, Nithurshen
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. Surgically corrupts the root inode (injecting random data at
offset 1152) while leaving the superblock intact so it mounts.
3. Mounts the image in the foreground to capture daemon stderr.
4. Runs 'stat' to trigger the inode read failure.
5. 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>
---
tests/Makefile.am | 3 ++
tests/erofs/099 | 76 +++++++++++++++++++++++++++++++++++++++++++++
tests/erofs/099.out | 2 ++
3 files changed, 81 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..952bdbd
--- /dev/null
+++ b/tests/erofs/099
@@ -0,0 +1,76 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Test FUSE daemon error handling on corrupted inodes (missing return fix)
+#
+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"
+
+[ -z "$EROFSFUSE_PROG" ] && _notrun "erofsfuse is not available"
+
+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 $localdir >> $seqres.full 2>&1 || _fail "failed to mkfs"
+
+# Corrupt the root inode to force erofs_read_inode_from_disk to fail.
+dd if=/dev/urandom of=$SCRATCH_DEV bs=1 seek=1152 count=1024 conv=notrunc >> $seqres.full 2>&1
+
+# Bypass _scratch_mount to run erofsfuse in the foreground (-f)
+# This lets us capture libfuse's internal stderr warnings.
+$EROFSFUSE_PROG -f $SCRATCH_DEV $SCRATCH_MNT > $tmp/fuse_err.log 2>&1 &
+fuse_pid=$!
+
+# Wait for the mount to establish
+sleep 1
+
+# Attempt to stat the root directory.
+timeout 5 stat $SCRATCH_MNT >> $seqres.full 2>&1
+res=$?
+
+# Clean up the mount
+umount $SCRATCH_MNT >> $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 the results based on the captured stderr and timeout
+if [ $res -eq 124 ]; then
+ _fail "stat command timed out (macFUSE daemon 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
+
+echo Silence is golden
+status=0
+exit 0
\ No newline at end of file
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
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
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
1 sibling, 0 replies; 11+ messages in thread
From: Gao Xiang @ 2026-03-30 5:43 UTC (permalink / raw)
To: Nithurshen, linux-erofs; +Cc: xiang, newajay.11r
On 2026/3/30 12:28, Nithurshen wrote:
> 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. Surgically corrupts the root inode (injecting random data at
> offset 1152) while leaving the superblock intact so it mounts.
> 3. Mounts the image in the foreground to capture daemon stderr.
> 4. Runs 'stat' to trigger the inode read failure.
> 5. 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>
> ---
> tests/Makefile.am | 3 ++
> tests/erofs/099 | 76 +++++++++++++++++++++++++++++++++++++++++++++
> tests/erofs/099.out | 2 ++
> 3 files changed, 81 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..952bdbd
> --- /dev/null
> +++ b/tests/erofs/099
> @@ -0,0 +1,76 @@
> +#!/bin/sh
> +# SPDX-License-Identifier: GPL-2.0+
> +#
> +# Test FUSE daemon error handling on corrupted inodes (missing return fix)
> +#
> +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"
> +
> +[ -z "$EROFSFUSE_PROG" ] && _notrun "erofsfuse is not available"
> +
> +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 $localdir >> $seqres.full 2>&1 || _fail "failed to mkfs"
> +
> +# Corrupt the root inode to force erofs_read_inode_from_disk to fail.
> +dd if=/dev/urandom of=$SCRATCH_DEV bs=1 seek=1152 count=1024 conv=notrunc >> $seqres.full 2>&1
> +
> +# Bypass _scratch_mount to run erofsfuse in the foreground (-f)
> +# This lets us capture libfuse's internal stderr warnings.
> +$EROFSFUSE_PROG -f $SCRATCH_DEV $SCRATCH_MNT > $tmp/fuse_err.log 2>&1 &
If this testcase is erofsfuse-specific, we should
check FSTYP="erofsfuse" instead, otherwise this
case should be skiped.
Otherwise if this test case can test the kernel
as well, we should make the kernel tested too.
Thanks,
Gao Xiang
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v2 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
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 ` Nithurshen
2026-03-31 2:33 ` Gao Xiang
2026-04-01 7:10 ` [PATCH v3 " Nithurshen
1 sibling, 2 replies; 11+ messages in thread
From: Nithurshen @ 2026-03-30 10:30 UTC (permalink / raw)
To: nithurshen.dev; +Cc: hsiangkao, linux-erofs, newajay.11r, xiang
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. Surgically corrupts the root inode (injecting random data at
offset 1152) while leaving the superblock intact so it mounts.
3. Mounts the image in the foreground to capture daemon stderr.
4. Runs 'stat' to trigger the inode read failure.
5. 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 v2:
- Added $FSTYP check to ensure the test is skipped when testing the
kernel driver (Gao Xiang).
- Renamed cleanup() to _cleanup() to align with standard rc teardown.
---
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..11dab4d
--- /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 $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
+ umount $SCRATCH_MNT >> $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
\ No newline at end of file
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
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v2 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
2026-03-30 10:30 ` [PATCH v2 " Nithurshen
@ 2026-03-31 2:33 ` Gao Xiang
2026-04-01 7:10 ` [PATCH v3 " Nithurshen
1 sibling, 0 replies; 11+ messages in thread
From: Gao Xiang @ 2026-03-31 2:33 UTC (permalink / raw)
To: Nithurshen; +Cc: linux-erofs, newajay.11r, xiang
On 2026/3/30 18:30, Nithurshen wrote:
> 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. Surgically corrupts the root inode (injecting random data at
> offset 1152) while leaving the superblock intact so it mounts.
> 3. Mounts the image in the foreground to capture daemon stderr.
> 4. Runs 'stat' to trigger the inode read failure.
> 5. 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 v2:
> - Added $FSTYP check to ensure the test is skipped when testing the
> kernel driver (Gao Xiang).
> - Renamed cleanup() to _cleanup() to align with standard rc teardown.
> ---
> 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..11dab4d
> --- /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 $localdir >> $seqres.full 2>&1 || _fail "failed to mkfs"
You need to disable superblock checksum using:
_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).
I think a better way is to use `dump.erofs` to get the NID, and use
meta_blkaddr * block_size + NID * 32 to calculate the inode offset:
for example:
$EROFSDUMP_PROG --path=/testfile $SCRATCH_DEV
...
> +# 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
> + umount $SCRATCH_MNT >> $seqres.full 2>&1
Can you use `_scratch_unmount`? Does that work?
> + # 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
> \ No newline at end of file
Need a new line here.
Thanks,
Gao Xiang
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v3 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
2026-03-30 10:30 ` [PATCH v2 " Nithurshen
2026-03-31 2:33 ` Gao Xiang
@ 2026-04-01 7:10 ` Nithurshen
2026-04-01 7:19 ` Gao Xiang
2026-04-01 7:55 ` [PATCH v4 " Nithurshen
1 sibling, 2 replies; 11+ messages in thread
From: Nithurshen @ 2026-04-01 7:10 UTC (permalink / raw)
To: nithurshen.dev; +Cc: hsiangkao, linux-erofs, newajay.11r, xiang
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
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v3 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
2026-04-01 7:10 ` [PATCH v3 " Nithurshen
@ 2026-04-01 7:19 ` Gao Xiang
2026-04-01 7:55 ` [PATCH v4 " Nithurshen
1 sibling, 0 replies; 11+ messages in thread
From: Gao Xiang @ 2026-04-01 7:19 UTC (permalink / raw)
To: Nithurshen; +Cc: linux-erofs, newajay.11r, xiang
On 2026/4/1 15:10, Nithurshen wrote:
> 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.
I don't find this.
>
> 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.
I don't find this as well.
Thanks,
Gao Xiang
> ---
> 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
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v4 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
2026-04-01 7:10 ` [PATCH v3 " Nithurshen
2026-04-01 7:19 ` Gao Xiang
@ 2026-04-01 7:55 ` Nithurshen
2026-04-01 8:05 ` Gao Xiang
2026-04-03 0:34 ` [PATCH v5 " Nithurshen
1 sibling, 2 replies; 11+ messages in thread
From: Nithurshen @ 2026-04-01 7:55 UTC (permalink / raw)
To: nithurshen.dev; +Cc: hsiangkao, linux-erofs, newajay.11r, xiang
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. Surgically corrupts the root inode (injecting random data at
offset 1152) while leaving the superblock intact so it mounts.
3. Mounts the image in the foreground to capture daemon stderr.
4. Runs 'stat' to trigger the inode read failure.
5. 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 v4:
- Corrected the commit message and notes to accurately match the
code submitted (v3 accidentally included a draft message that
did not match the diff).
Changes in v3:
- Disabled superblock checksums using `-Enosbcrc` in _scratch_mkfs.
- Used `_scratch_unmount` instead of standard `umount`.
Note regarding the corruption method:
My apologies for the confusion in v3. The email described
using `dump.erofs` and `0xFF`, but the patch contained my code
using the hardcoded offset 1152 and `/dev/urandom`. I am resending
the patch as v4 so the commit message accurately reflects the code.
I originally kept the hardcoded root offset (1152) because targeting
`/testfile` dynamically with `/dev/urandom` was slightly flaky. If
the random bytes happened to form a valid-looking layout, the bug
was bypassed. Wiping 1024 bytes at offset 1152 reliably destroys the
root metadata and guarantees the bug triggers 100% of the time.
Is this hardcoded offset approach acceptable for this specific test?
If you strictly prefer the `dump.erofs` approach (using 0xFF instead
of urandom to guarantee the error), please let me know and I will
gladly send those updates in a v5 patch.
---
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
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v4 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
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
1 sibling, 1 reply; 11+ messages in thread
From: Gao Xiang @ 2026-04-01 8:05 UTC (permalink / raw)
To: Nithurshen; +Cc: linux-erofs, newajay.11r, xiang
On 2026/4/1 15:55, Nithurshen wrote:
> 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. Surgically corrupts the root inode (injecting random data at
> offset 1152) while leaving the superblock intact so it mounts.
> 3. Mounts the image in the foreground to capture daemon stderr.
> 4. Runs 'stat' to trigger the inode read failure.
> 5. 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 v4:
> - Corrected the commit message and notes to accurately match the
> code submitted (v3 accidentally included a draft message that
> did not match the diff).
>
> Changes in v3:
> - Disabled superblock checksums using `-Enosbcrc` in _scratch_mkfs.
> - Used `_scratch_unmount` instead of standard `umount`.
>
> Note regarding the corruption method:
> My apologies for the confusion in v3. The email described
> using `dump.erofs` and `0xFF`, but the patch contained my code
> using the hardcoded offset 1152 and `/dev/urandom`. I am resending
> the patch as v4 so the commit message accurately reflects the code.
>
> I originally kept the hardcoded root offset (1152) because targeting
> `/testfile` dynamically with `/dev/urandom` was slightly flaky. If
> the random bytes happened to form a valid-looking layout, the bug
> was bypassed. Wiping 1024 bytes at offset 1152 reliably destroys the
> root metadata and guarantees the bug triggers 100% of the time.
>
> Is this hardcoded offset approach acceptable for this specific test?
> If you strictly prefer the `dump.erofs` approach (using 0xFF instead
> of urandom to guarantee the error), please let me know and I will
> gladly send those updates in a v5 patch.
Are we still miscommunicating? I asked using `dump.erofs` for many
many times but you still send those useless patches?
Is it hard to understand? No hardcode offset please.
Thanks,
Gao Xiang
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v4 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
2026-04-01 8:05 ` Gao Xiang
@ 2026-04-01 8:09 ` Gao Xiang
0 siblings, 0 replies; 11+ messages in thread
From: Gao Xiang @ 2026-04-01 8:09 UTC (permalink / raw)
To: Nithurshen; +Cc: linux-erofs, newajay.11r, xiang
On 2026/4/1 16:05, Gao Xiang wrote:
>
>
> On 2026/4/1 15:55, Nithurshen wrote:
>> 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. Surgically corrupts the root inode (injecting random data at
>> offset 1152) while leaving the superblock intact so it mounts.
>> 3. Mounts the image in the foreground to capture daemon stderr.
>> 4. Runs 'stat' to trigger the inode read failure.
>> 5. 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 v4:
>> - Corrected the commit message and notes to accurately match the
>> code submitted (v3 accidentally included a draft message that
>> did not match the diff).
>>
>> Changes in v3:
>> - Disabled superblock checksums using `-Enosbcrc` in _scratch_mkfs.
>> - Used `_scratch_unmount` instead of standard `umount`.
>>
>> Note regarding the corruption method:
>> My apologies for the confusion in v3. The email described
>> using `dump.erofs` and `0xFF`, but the patch contained my code
>> using the hardcoded offset 1152 and `/dev/urandom`. I am resending
>> the patch as v4 so the commit message accurately reflects the code.
>>
>> I originally kept the hardcoded root offset (1152) because targeting
>> `/testfile` dynamically with `/dev/urandom` was slightly flaky. If
>> the random bytes happened to form a valid-looking layout, the bug
>> was bypassed. Wiping 1024 bytes at offset 1152 reliably destroys the
>> root metadata and guarantees the bug triggers 100% of the time.
>>
>> Is this hardcoded offset approach acceptable for this specific test?
>> If you strictly prefer the `dump.erofs` approach (using 0xFF instead
>> of urandom to guarantee the error), please let me know and I will
>> gladly send those updates in a v5 patch.
>
> Are we still miscommunicating? I asked using `dump.erofs` for many
> many times but you still send those useless patches?
>
> Is it hard to understand? No hardcode offset please.
And why do you think /dev/urandom is a good idea? A regression test
is needed, determination is needed, why bother with /dev/urandom?
>
> Thanks,
> Gao Xiang
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v5 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
2026-04-01 7:55 ` [PATCH v4 " Nithurshen
2026-04-01 8:05 ` Gao Xiang
@ 2026-04-03 0:34 ` Nithurshen
2026-04-07 3:01 ` [PATCH v6 " Gao Xiang
1 sibling, 1 reply; 11+ messages in thread
From: Nithurshen @ 2026-04-03 0:34 UTC (permalink / raw)
To: nithurshen.dev; +Cc: hsiangkao, linux-erofs, newajay.11r, xiang
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@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
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v6 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
2026-04-03 0:34 ` [PATCH v5 " Nithurshen
@ 2026-04-07 3:01 ` Gao Xiang
0 siblings, 0 replies; 11+ messages in thread
From: Gao Xiang @ 2026-04-07 3:01 UTC (permalink / raw)
To: linux-erofs; +Cc: Nithurshen, Gao Xiang
From: Nithurshen <nithurshen.dev@gmail.com>
This patch introduces a regression test 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@gmail.com>
Signed-off-by: Gao Xiang <xiang@kernel.org>
---
I will apply this refined version.
tests/Makefile.am | 4 ++
tests/erofs/029 | 93 +++++++++++++++++++++++++++++++++++++++++++++
tests/erofs/029.out | 2 +
3 files changed, 99 insertions(+)
create mode 100755 tests/erofs/029
create mode 100644 tests/erofs/029.out
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 226955cdccbd..d8ac067805e8 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -22,6 +22,7 @@ TESTS_ENVIRONMENT = \
fi; \
[ -z $$MKFS_EROFS_PROG ] && export MKFS_EROFS_PROG=../mkfs/mkfs.erofs; \
[ -z $$FSCK_EROFS_PROG ] && export FSCK_EROFS_PROG=../fsck/fsck.erofs; \
+ [ -z $$DUMP_EROFS_PROG ] && export DUMP_EROFS_PROG=../dump/dump.erofs; \
[ -z $$EROFSFUSE_PROG ] && export EROFSFUSE_PROG=../fuse/erofsfuse;
if ENABLE_LZ4
@@ -122,6 +123,9 @@ TESTS += erofs/027
# 028 - test inode page cache sharing functionality
TESTS += erofs/028
+# 029 - test FUSE daemon and kernel error handling on corrupted inodes
+TESTS += erofs/029
+
# NEW TEST CASE HERE
# TESTS += erofs/999
diff --git a/tests/erofs/029 b/tests/erofs/029
new file mode 100755
index 000000000000..82fdd5d01892
--- /dev/null
+++ b/tests/erofs/029
@@ -0,0 +1,93 @@
+#!/bin/sh
+# SPDX-License-Identifier: MIT
+#
+# 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
+}
+
+_require_erofs
+
+# remove previous $seqres.full before test
+rm -f $seqres.full
+
+# real QA test starts here
+echo "QA output created by $seq"
+
+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 -T0 --all-time -Enosbcrc $localdir >> $seqres.full 2>&1 || _fail "failed to mkfs"
+
+sbdump=$($DUMP_EROFS_PROG $SCRATCH_DEV)
+meta_blkaddr=$(printf "$sbdump" | grep -i "meta_blkaddr" | grep -oE '[0-9]+' | head -n 1)
+[ -n "$meta_blkaddr" ] && _fail "failed get get meta_blkaddr"
+blocksize=$(printf "$sbdump" | grep -i "block size" | grep -oE '[0-9]+' | head -n 1)
+[ -n "$blocksize" ] && _fail "failed to get block size"
+
+victim=$($DUMP_EROFS_PROG --path=/ $SCRATCH_DEV | grep -iE 'nid\s*[:=]?\s*[0-9]+' -o | grep -oE '[0-9]+' | head -n 1)
+[ -z "$victim" ] && _fail "failed to find root NID"
+
+seeko=$((meta_blkaddr*blocksize+victim*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=$seeko 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/erofsfuse.log 2>&1 &
+ fuse_pid=$!
+ # Wait for the mount to establish
+ sleep 1
+elif ! _try_scratch_mount >> $seqres.full 2>&1; then
+ echo Silence is golden
+ status=0
+ exit 0
+fi
+
+# Attempt to stat the root directory to directly trigger getattr without a lookup
+timeout 5 stat $SCRATCH_MNT >> $seqres.full 2>&1
+res=$?
+[ $res -eq 124 ] && _fail "stat command timed out (kernel hung?)"
+[ $res -eq 0 ] && _fail "stat unexpectedly succeeded on a corrupted image"
+
+if [ "$FSTYP" = "erofsfuse" ]; then
+ _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/erofsfuse.log >> $seqres.full
+
+ if grep -q -i "multiple replies" $tmp/erofsfuse.log; then
+ _fail "Bug detected: libfuse reported multiple replies to request"
+ elif grep -q -i "segmentation fault\|aborted" $tmp/erofsfuse.log; then
+ _fail "Bug detected: FUSE daemon crashed"
+ fi
+else
+ _scratch_unmount >> $seqres.full 2>&1
+fi
+
+echo Silence is golden
+status=0
+exit 0
diff --git a/tests/erofs/029.out b/tests/erofs/029.out
new file mode 100644
index 000000000000..8ee6db49fd4d
--- /dev/null
+++ b/tests/erofs/029.out
@@ -0,0 +1,2 @@
+QA output created by 029
+Silence is golden
--
2.43.5
^ permalink raw reply related [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-04-07 3:01 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH v3 " Nithurshen
2026-04-01 7:19 ` 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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox