From: Jingbo Xu <jefflexu@linux.alibaba.com>
To: xiang@kernel.org, linux-erofs@lists.ozlabs.org
Subject: [PATCH v2] erofs-utils: tests: check SEEK_HOLE/SEEK_DATA in inode_share mode
Date: Wed, 9 Sep 2026 17:34:33 +0800 [thread overview]
Message-ID: <20260909093433.95193-1-jefflexu@linux.alibaba.com> (raw)
In inode_share mode erofs_ishare_fops used to fall back to
generic_file_llseek(), which considers the whole file to be data, so
SEEK_HOLE always returned i_size and the real holes of sparse files
were hidden. This is fixed by kernel commit 8ebf95897885 ("erofs:
support SEEK_HOLE/SEEK_DATA in inode_share mode").
Add a regression test for it.
Signed-off-by: Jingbo Xu <jefflexu@linux.alibaba.com>
---
changes since v1:
- add a minimal util probing SEEK_HOLE/SEEK_DATA, rather than relying
on xfs_io
---
tests/Makefile.am | 3 +
tests/erofs/033 | 151 ++++++++++++++++++++++++++++++++++++++++++
tests/erofs/033.out | 2 +
tests/src/Makefile.am | 5 +-
tests/src/lseek.c | 65 ++++++++++++++++++
5 files changed, 225 insertions(+), 1 deletion(-)
create mode 100755 tests/erofs/033
create mode 100644 tests/erofs/033.out
create mode 100644 tests/src/lseek.c
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 363b93dc..b60a6cc9 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -135,6 +135,9 @@ TESTS += erofs/031
# 032 - check that the xattr layout does not depend on listxattr(2) order
TESTS += erofs/032
+# 033 - regression test for SEEK_HOLE/SEEK_DATA in inode_share mode
+TESTS += erofs/033
+
# NEW TEST CASE HERE
# TESTS += erofs/999
diff --git a/tests/erofs/033 b/tests/erofs/033
new file mode 100755
index 00000000..b3e58628
--- /dev/null
+++ b/tests/erofs/033
@@ -0,0 +1,151 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0+
+#
+# Check that SEEK_HOLE/SEEK_DATA see the real holes in inode_share mode
+#
+# In an inode_share mount regular files are served by erofs_ishare_fops,
+# whose .llseek used to be generic_file_llseek(). That one considers the
+# whole file to be data, so SEEK_HOLE always returned i_size and SEEK_DATA
+# always returned the offset it was handed.
+#
+# Regression test for Linux commit 8ebf95897885 ("erofs: support
+# SEEK_HOLE/SEEK_DATA in inode_share mode").
+#
+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.*
+}
+
+_require_erofs
+_require_erofs_inode_sharing
+_require_mkfs_blksize
+
+# 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
+
+# eight chunks, of which chunk 1, 3, 4, 6, 7 stay holes
+chunksize=4096
+truncate -s $((8 * chunksize)) $localdir/sparse ||
+ _fail "failed to truncate $localdir/sparse"
+for i in 0 2 5; do
+ dd if=/dev/urandom of=$localdir/sparse bs=$chunksize seek=$i count=1 \
+ conv=notrunc >> $seqres.full 2>&1 ||
+ _fail "failed to write chunk $i of $localdir/sparse"
+done
+
+# the same size without any hole at all
+dd if=/dev/urandom of=$localdir/dense bs=$chunksize count=8 \
+ >> $seqres.full 2>&1 || _fail "failed to write $localdir/dense"
+
+sparse_layout="data 0 4096
+hole 4096 4096
+data 8192 4096
+hole 12288 8192
+data 20480 4096
+hole 24576 8192"
+dense_layout="data 0 32768"
+
+# seek to the next data or hole, the result ends up in $seek_result
+_seek()
+{
+ local whence=$1
+ local offset=$2
+ local file=$3
+
+ seek_result=`${PWD}/src/lseek "$file" $whence $offset` ||
+ _fail "lseek failed to seek $whence at $offset in $file"
+}
+
+# write the data/hole layout of $1 to $tmp.layout as "<type> <off> <len>"
+_seek_layout()
+{
+ local file="$1"
+ local size=`_get_filesize "$file"`
+ local pos=0
+
+ : > $tmp.layout
+ while [ $pos -lt $size ]; do
+ _seek data $pos "$file"
+ if [ "$seek_result" = "EOF" ]; then
+ echo "hole $pos $((size - pos))" >> $tmp.layout
+ break
+ fi
+ if [ $seek_result -gt $pos ]; then
+ echo "hole $pos $((seek_result - pos))" >> $tmp.layout
+ fi
+ pos=$seek_result
+
+ _seek hole $pos "$file"
+ [ "$seek_result" = "EOF" ] && seek_result=$size
+ echo "data $pos $((seek_result - pos))" >> $tmp.layout
+ pos=$seek_result
+ done
+}
+
+_check_layout()
+{
+ local tag="$1"
+ local file="$2"
+ local expected="$3"
+
+ _seek_layout "$file"
+ if [ "`cat $tmp.layout`" != "$expected" ]; then
+ {
+ echo "--- $tag expected ---"
+ echo "$expected"
+ echo "--- $tag reported ---"
+ cat $tmp.layout
+ } >> $seqres.full
+ _fail "$tag: SEEK_DATA/SEEK_HOLE reported an unexpected layout"
+ fi
+}
+
+for format in force-chunk-indexes force-inode-blockmap; do
+ echo "=== -E$format ===" >> $seqres.full
+
+ _scratch_mkfs -b$chunksize --chunksize=$chunksize -E$format \
+ --xattr-inode-digest=system.erofs.fingerprint \
+ $localdir >> $seqres.full 2>&1 ||
+ _fail "failed to mkfs (-E$format)"
+
+ # make sure that mkfs wrote ishare_xattrs feature bit
+ $DUMP_EROFS_PROG -s $SCRATCH_DEV | grep -q ishare_xattrs ||
+ _fail "the image carries no inode fingerprints (-E$format)"
+
+ # the very same image without inode_share tells them apart
+ _scratch_mount 2>> $seqres.full
+ _check_layout "sparse (-E$format)" \
+ $SCRATCH_MNT/sparse "$sparse_layout"
+ _check_layout "dense (-E$format)" $SCRATCH_MNT/dense "$dense_layout"
+ _scratch_unmount
+
+ _scratch_mount -oinode_share,domain_id=erofs-$seq-$format 2>> $seqres.full
+ _check_layout "sparse (-E$format, inode_share)" \
+ $SCRATCH_MNT/sparse "$sparse_layout"
+ _check_layout "dense (-E$format, inode_share)" \
+ $SCRATCH_MNT/dense "$dense_layout"
+ _scratch_unmount
+done
+
+echo Silence is golden
+status=0
+exit 0
diff --git a/tests/erofs/033.out b/tests/erofs/033.out
new file mode 100644
index 00000000..c5d29149
--- /dev/null
+++ b/tests/erofs/033.out
@@ -0,0 +1,2 @@
+QA output created by 033
+Silence is golden
diff --git a/tests/src/Makefile.am b/tests/src/Makefile.am
index 16de41a1..4d9975e0 100644
--- a/tests/src/Makefile.am
+++ b/tests/src/Makefile.am
@@ -3,12 +3,15 @@
AUTOMAKE_OPTIONS = foreign
check_PROGRAMS =
-noinst_PROGRAMS = fssum
+noinst_PROGRAMS = fssum lseek
fssum_CFLAGS = -Wall -I$(top_srcdir)/include
fssum_LDADD = $(top_builddir)/lib/liberofs.la
fssum_SOURCES = fssum.c
+lseek_CFLAGS = -Wall
+lseek_SOURCES = lseek.c
+
if ENABLE_LZ4
check_PROGRAMS += badlz4
badlz4_SOURCES = badlz4.c
diff --git a/tests/src/lseek.c b/tests/src/lseek.c
new file mode 100644
index 00000000..c8bd2998
--- /dev/null
+++ b/tests/src/lseek.c
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * erofs-utils/tests/src/lseek.c
+ *
+ * Minimal SEEK_DATA/SEEK_HOLE probe: report the offset the kernel
+ * resolves a seek to, or EOF if there is nothing left to find.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+int main(int argc, char *argv[])
+{
+ const char *file, *whence_name;
+ char *end;
+ long long offset, ret;
+ int whence, fd;
+
+ if (argc != 4) {
+ fprintf(stderr, "Usage: %s FILE data|hole OFFSET\n", argv[0]);
+ return 2;
+ }
+ file = argv[1];
+ whence_name = argv[2];
+ if (!strcmp(whence_name, "data")) {
+ whence = SEEK_DATA;
+ } else if (!strcmp(whence_name, "hole")) {
+ whence = SEEK_HOLE;
+ } else {
+ fprintf(stderr, "bad whence: %s\n", whence_name);
+ return 2;
+ }
+ errno = 0;
+ offset = strtoll(argv[3], &end, 0);
+ if (*argv[3] == '\0' || *end != '\0' || errno) {
+ fprintf(stderr, "bad offset: %s\n", argv[3]);
+ return 2;
+ }
+
+ fd = open(file, O_RDONLY);
+ if (fd < 0) {
+ fprintf(stderr, "failed to open %s: %s\n", file, strerror(errno));
+ return 2;
+ }
+ /* the kernel reports -ENXIO when no data/hole is left past EOF */
+ ret = lseek(fd, offset, whence);
+ if (ret < 0) {
+ if (errno == ENXIO) {
+ printf("EOF\n");
+ close(fd);
+ return 0;
+ }
+ fprintf(stderr, "lseek %s %lld in %s: %s\n", whence_name,
+ offset, file, strerror(errno));
+ close(fd);
+ return 2;
+ }
+ printf("%lld\n", ret);
+ close(fd);
+ return 0;
+}
--
2.19.1.6.gb485710b
reply other threads:[~2026-09-09 9:34 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
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=20260909093433.95193-1-jefflexu@linux.alibaba.com \
--to=jefflexu@linux.alibaba.com \
--cc=linux-erofs@lists.ozlabs.org \
--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