Re: [PATCH v3 experimental-tests] erofs-utils: tests: test FUSE error handling on corrupted inodes
Gao Xiang <[email protected]>
| Newsgroups | org.ozlabs.lists.linux-erofs |
|---|---|
| Message-ID | <[email protected]> |
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 <[email protected]> > --- > 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