diff --git a/common/btrfs b/common/btrfs
index 42777df2..bd4dc31f 100644
--- a/common/btrfs
+++ b/common/btrfs
@@ -647,3 +647,32 @@ _btrfs_get_file_extent_item_bytenr()
 		grep -A4 "$file_extent_key" | grep "disk byte" | \
 		$AWK_PROG '{ print $5 }'
 }
+
+# Check that btrfs-progs has support for the logical-resolve command, with the
+# -o option, and that the kernel supports the logical to ino ioctl v2 (which
+# adds the ignore offset parameter).
+_require_btrfs_scratch_logical_resolve_v2()
+{
+	local bytenr
+
+	# First check if we have support for calling the v2 logical resolve
+	# ioctl in btrfs-progs. Check if the -o options exists, which makes
+	# btrfs-progs call v2 of the ioctl (because the flag
+	# BTRFS_LOGICAL_INO_ARGS_IGNORE_OFFSET is only supported in v2).
+	_require_btrfs_command inspect-internal logical-resolve -o
+	_require_scratch
+
+	_scratch_mkfs > /dev/null || \
+		_fail "_require_btrfs_scratch_logical_resolve_v2: mkfs failed"
+	_scratch_mount
+
+	$XFS_IO_PROG -f -c "pwrite -q 0 128K" "$SCRATCH_MNT/file1"
+	bytenr=$(_btrfs_get_file_extent_item_bytenr "$SCRATCH_MNT/file1" 0)
+	$BTRFS_UTIL_PROG inspect-internal logical-resolve -o $bytenr \
+			 $SCRATCH_MNT > /dev/null
+	if [ $? -ne 0 ]; then
+		_scratch_unmount
+		_notrun "Logical resolve ioctl v2 not supported in the kernel"
+	fi
+	_scratch_unmount
+}
diff --git a/tests/btrfs/287 b/tests/btrfs/287
new file mode 100755
index 00000000..a7e29e2b
--- /dev/null
+++ b/tests/btrfs/287
@@ -0,0 +1,154 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (C) 2023 SUSE Linux Products GmbH. All Rights Reserved.
+#
+# FS QA Test 287
+#
+# Test btrfs' logical to inode ioctls (v1 and v2).
+#
+. ./common/preamble
+_begin_fstest auto quick snapshot clone punch
+
+. ./common/filter
+. ./common/reflink
+
+_supported_fs btrfs
+_require_btrfs_scratch_logical_resolve_v2
+_require_scratch_reflink
+_require_xfs_io_command "fpunch"
+
+# This is a test case to test the logical to ino ioctl in general but it also
+# serves as a regression a test for an issue fixed by the following commit.
+_fixed_by_kernel_commit XXXXXXXXXXXX \
+	"btrfs: fix backref walking not returning all inode refs"
+
+query_logical_ino()
+{
+	$BTRFS_UTIL_PROG inspect-internal logical-resolve -P $* $SCRATCH_MNT
+}
+
+_scratch_mkfs >> $seqres.full || _fail "mkfs failed"
+_scratch_mount
+
+# Create a file with two extents:
+#
+# 1) One 4M extent covering the file range [0, 4M)
+# 2) Another 4M extent covering the file range [4M, 8M)
+$XFS_IO_PROG -f -c "pwrite -S 0xab -b 4M 0 4M" \
+	     -c "fsync" \
+	     -c "pwrite -S 0xcd -b 4M 4M 8M" \
+	     -c "fsync" $SCRATCH_MNT/foo | _filter_xfs_io
+
+echo "resolve first extent:"
+first_extent_bytenr=$(_btrfs_get_file_extent_item_bytenr "$SCRATCH_MNT/foo" 0)
+query_logical_ino $first_extent_bytenr
+
+echo "resolve second extent:"
+sz_4m=$((4 * 1024 * 1024))
+second_extent_bytenr=$(_btrfs_get_file_extent_item_bytenr "$SCRATCH_MNT/foo" $sz_4m)
+query_logical_ino $second_extent_bytenr
+
+# Now clone both extents twice to the end of the file.
+sz_8m=$((8 * 1024 * 1024))
+$XFS_IO_PROG -c "reflink $SCRATCH_MNT/foo 0 $sz_8m $sz_8m" $SCRATCH_MNT/foo \
+	| _filter_xfs_io
+sz_16m=$((16 * 1024 * 1024))
+$XFS_IO_PROG -c "reflink $SCRATCH_MNT/foo 0 $sz_16m $sz_8m" $SCRATCH_MNT/foo \
+	| _filter_xfs_io
+
+# Now lets resolve the extents again. They should now be listed 3 times each, at
+# the right file offsets.
+echo "resolve first extent:"
+query_logical_ino $first_extent_bytenr
+
+echo "resolve second extent:"
+query_logical_ino $second_extent_bytenr
+
+# Now lets punch a 2M hole at file offset 0. This changes the first file extent
+# item to point to the first extent with an offset of 2M and a length of 2M, so
+# doing a logical resolve with the bytenr of the first extent should not return
+# file offset 0.
+$XFS_IO_PROG -c "fpunch 0 2M" $SCRATCH_MNT/foo
+echo "resolve first extent after punching hole at file range [0, 2M):"
+query_logical_ino $first_extent_bytenr
+
+# Doing a logical resolve call with the BTRFS_LOGICAL_INO_ARGS_IGNORE_OFFSET
+# flag (passing -o to logical-resolve command) should ignore file extent offsets
+# and return file offsets for all file extent items that point to any section of
+# the extent (3 of them, file offsets 2M, 8M and 16M).
+echo "resolve first extent with ignore offset option:"
+query_logical_ino -o $first_extent_bytenr
+
+# Now query for file extent items containing the first extent at offset +1M.
+# Should only return the file offsets 9M and 17M.
+bytenr=$(( $first_extent_bytenr + 1024 * 1024))
+echo "resolve first extent +1M offset:"
+query_logical_ino $bytenr
+
+# Now do the same query again but with the ignore offset ioctl argument. This
+# should returns 3 results, for file offsets 2M, 8M and 16M.
+echo "resolve first extent +1M offset with ignore offset option:"
+query_logical_ino -o $bytenr
+
+# Now query for file extent items containing the first extent at offset +3M.
+# Should return the file offsets 3M and 11M and 19M.
+bytenr=$(( $first_extent_bytenr + 3 * 1024 * 1024))
+echo "resolve first extent +3M offset:"
+query_logical_ino $bytenr
+
+# Now do the same query again but with the ignore offset ioctl argument. This
+# should returns 3 results, for file offsets 2M, 8M and 16M.
+echo "resolve first extent +3M offset with ignore offset option:"
+query_logical_ino -o $bytenr
+
+# Now create two snapshots and then do some queries.
+$BTRFS_UTIL_PROG subvolume snapshot -r $SCRATCH_MNT $SCRATCH_MNT/snap1 \
+	| _filter_scratch
+$BTRFS_UTIL_PROG subvolume snapshot -r $SCRATCH_MNT $SCRATCH_MNT/snap2 \
+	| _filter_scratch
+
+# Query for the first extent (at offset 0). Should give two entries for each
+# root - default subvolume and the 2 snapshots, for file offsets 8M and 16M.
+echo "resolve first extent:"
+query_logical_ino $first_extent_bytenr
+
+# Query for the first extent (at offset 0) with the ignore offset option.
+# Should give 3 entries for each root - default subvolume and the 2 snapshots,
+# for file offsets 2M, 8M and 16M.
+echo "resolve first extent with ignore offset option:"
+query_logical_ino -o $first_extent_bytenr
+
+# Now lets punch a 1M hole at file offset 4M. This changes the second file
+# extent item to point to the second extent with an offset of 1M and a length
+# of 3M, so doing a logical resolve with the bytenr of the second extent should
+# not return file offset 4M for root 5 (default subvolume), bit it should return
+# file offset 4M for the files in the snapshots. For all the roots, it should
+# return file offsets 12M and 20M.
+$XFS_IO_PROG -c "fpunch 4M 1M" $SCRATCH_MNT/foo
+echo "resolve second extent after punching hole at file range [4M, 5M):"
+query_logical_ino $second_extent_bytenr
+
+# Repeat the query but with the ignore offset option. We should get 3 entries
+# for each root. For the snapshot roots, we should get entries for file offsets
+# 4M, 12M and 20M, while for the default subvolume (root 5) we should get for
+# file offsets 5M, 12M and 20M.
+echo "resolve second extent with ignore offset option:"
+query_logical_ino -o $second_extent_bytenr
+
+# Now delete the first snapshot and repeat the last 2 queries.
+$BTRFS_UTIL_PROG subvolume delete -C $SCRATCH_MNT/snap1 | _filter_scratch
+
+# Query the second extent with an offset of 0, should return file offsets 12M
+# and 20M for the default subvolume (root 5) and file offsets 4M, 12M and 20M
+# for the second snapshot root.
+echo "resolve second extent:"
+query_logical_ino $second_extent_bytenr
+
+# Query the second extent with the ignore offset option, should return file
+# offsets 5M, 12M and 20M for the default subvolume (root 5) and file offsets
+# 4M, 12M and 20M for the second snapshot root.
+echo "resolve second extent with ignore offset option:"
+query_logical_ino -o $second_extent_bytenr
+
+status=0
+exit
diff --git a/tests/btrfs/287.out b/tests/btrfs/287.out
new file mode 100644
index 00000000..683f9875
--- /dev/null
+++ b/tests/btrfs/287.out
@@ -0,0 +1,95 @@
+QA output created by 287
+wrote 4194304/4194304 bytes at offset 0
+XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+wrote 8388608/8388608 bytes at offset 4194304
+XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+resolve first extent:
+inode 257 offset 0 root 5
+resolve second extent:
+inode 257 offset 4194304 root 5
+linked 8388608/8388608 bytes at offset 8388608
+XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+linked 8388608/8388608 bytes at offset 16777216
+XXX Bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+resolve first extent:
+inode 257 offset 16777216 root 5
+inode 257 offset 8388608 root 5
+inode 257 offset 0 root 5
+resolve second extent:
+inode 257 offset 20971520 root 5
+inode 257 offset 12582912 root 5
+inode 257 offset 4194304 root 5
+resolve first extent after punching hole at file range [0, 2M):
+inode 257 offset 16777216 root 5
+inode 257 offset 8388608 root 5
+resolve first extent with ignore offset option:
+inode 257 offset 16777216 root 5
+inode 257 offset 8388608 root 5
+inode 257 offset 2097152 root 5
+resolve first extent +1M offset:
+inode 257 offset 17825792 root 5
+inode 257 offset 9437184 root 5
+resolve first extent +1M offset with ignore offset option:
+inode 257 offset 16777216 root 5
+inode 257 offset 8388608 root 5
+inode 257 offset 2097152 root 5
+resolve first extent +3M offset:
+inode 257 offset 19922944 root 5
+inode 257 offset 11534336 root 5
+inode 257 offset 3145728 root 5
+resolve first extent +3M offset with ignore offset option:
+inode 257 offset 16777216 root 5
+inode 257 offset 8388608 root 5
+inode 257 offset 2097152 root 5
+Create a readonly snapshot of 'SCRATCH_MNT' in 'SCRATCH_MNT/snap1'
+Create a readonly snapshot of 'SCRATCH_MNT' in 'SCRATCH_MNT/snap2'
+resolve first extent:
+inode 257 offset 16777216 root 257
+inode 257 offset 8388608 root 257
+inode 257 offset 16777216 root 256
+inode 257 offset 8388608 root 256
+inode 257 offset 16777216 root 5
+inode 257 offset 8388608 root 5
+resolve first extent with ignore offset option:
+inode 257 offset 16777216 root 257
+inode 257 offset 8388608 root 257
+inode 257 offset 2097152 root 257
+inode 257 offset 16777216 root 256
+inode 257 offset 8388608 root 256
+inode 257 offset 2097152 root 256
+inode 257 offset 16777216 root 5
+inode 257 offset 8388608 root 5
+inode 257 offset 2097152 root 5
+resolve second extent after punching hole at file range [4M, 5M):
+inode 257 offset 20971520 root 257
+inode 257 offset 12582912 root 257
+inode 257 offset 4194304 root 257
+inode 257 offset 20971520 root 256
+inode 257 offset 12582912 root 256
+inode 257 offset 4194304 root 256
+inode 257 offset 20971520 root 5
+inode 257 offset 12582912 root 5
+resolve second extent with ignore offset option:
+inode 257 offset 20971520 root 257
+inode 257 offset 12582912 root 257
+inode 257 offset 4194304 root 257
+inode 257 offset 20971520 root 256
+inode 257 offset 12582912 root 256
+inode 257 offset 4194304 root 256
+inode 257 offset 20971520 root 5
+inode 257 offset 12582912 root 5
+inode 257 offset 5242880 root 5
+Delete subvolume (commit): 'SCRATCH_MNT/snap1'
+resolve second extent:
+inode 257 offset 20971520 root 257
+inode 257 offset 12582912 root 257
+inode 257 offset 4194304 root 257
+inode 257 offset 20971520 root 5
+inode 257 offset 12582912 root 5
+resolve second extent with ignore offset option:
+inode 257 offset 20971520 root 257
+inode 257 offset 12582912 root 257
+inode 257 offset 4194304 root 257
+inode 257 offset 20971520 root 5
+inode 257 offset 12582912 root 5
+inode 257 offset 5242880 root 5
