#! /bin/bash
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2022 Oracle.  All Rights Reserved.
#
# FS QA Test No. 191
#
# Make sure that XFS can handle empty leaf xattr blocks correctly.  These
# blocks can appear in files as a result of system crashes in the middle of
# xattr operations, which means that we /must/ handle them gracefully.
# Check that read and write verifiers won't trip, that the get/list/setxattr
# operations don't stumble over them, and that xfs_repair will offer to remove
# the entire xattr fork if the root xattr leaf block is empty.
#
# Regression test for
# kernel commit:
# 7be3bd8856fb ("xfs: empty xattr leaf header blocks are not corruption")
# xfsprogs commit:
# f50d3462c654 ("xfs_repair: ignore empty xattr leaf blocks")
#
. ./common/preamble
_begin_fstest auto quick attr

# Import common functions.
. ./common/filter
. ./common/attr


_require_scratch
_require_scratch_xfs_crc # V4 is deprecated
_fixed_by_kernel_commit 7be3bd8856fb "xfs: empty xattr leaf header blocks are not corruption"
_fixed_by_kernel_commit e87021a2bc10 "xfs: use larger in-core attr firstused field and detect overflow"
_fixed_by_git_commit xfsprogs f50d3462c654 "xfs_repair: ignore empty xattr leaf blocks"

# Parent pointers change the xattr formats sufficiently to break this test.
# Disable parent pointers if mkfs supports it.
_xfs_force_no_pptrs
_scratch_mkfs_xfs | _filter_mkfs >$seqres.full 2>$tmp.mkfs
cat $tmp.mkfs >> $seqres.full
source $tmp.mkfs
_scratch_mount

$XFS_IO_PROG -f -c 'pwrite -S 0x58 0 64k' $SCRATCH_MNT/largefile >> $seqres.full
$XFS_IO_PROG -f -c "pwrite -S 0x58 0 $isize" $SCRATCH_MNT/smallfile >> $seqres.full

smallfile_md5=$(_md5_checksum $SCRATCH_MNT/smallfile)
largefile_md5=$(_md5_checksum $SCRATCH_MNT/largefile)

# Try to force the creation of a single leaf block in each of three files.
# The first one gets a local attr, the second a remote attr, and the third
# is left for scrub and repair to find.
touch $SCRATCH_MNT/e0
touch $SCRATCH_MNT/e1
touch $SCRATCH_MNT/e2

$ATTR_PROG -s x $SCRATCH_MNT/e0 < $SCRATCH_MNT/smallfile >> $seqres.full
$ATTR_PROG -s x $SCRATCH_MNT/e1 < $SCRATCH_MNT/smallfile >> $seqres.full
$ATTR_PROG -s x $SCRATCH_MNT/e2 < $SCRATCH_MNT/smallfile >> $seqres.full

e0_ino=$(stat -c '%i' $SCRATCH_MNT/e0)
e1_ino=$(stat -c '%i' $SCRATCH_MNT/e1)
e2_ino=$(stat -c '%i' $SCRATCH_MNT/e2)

_scratch_unmount

# We used to think that it wasn't possible for empty xattr leaf blocks to
# exist, but it turns out that setting a large xattr on a file that has no
# xattrs can race with a log flush and crash, which results in an empty
# leaf block being logged and recovered.  This is rather hard to trip, so we
# use xfs_db to turn a regular leaf block into an empty one.
make_empty_leaf() {
	local inum="$1"

	echo "editing inode $inum" >> $seqres.full

	magic=$(_scratch_xfs_get_metadata_field hdr.info.hdr.magic "inode $inum" "ablock 0")
	if [ "$magic" != "0x3bee" ]; then
		_scratch_xfs_db -x -c "inode $inum" -c "ablock 0" -c print >> $seqres.full
		_fail "inode $inum ablock 0 is not a leaf block?"
	fi

	base=$(_scratch_xfs_get_metadata_field "hdr.freemap[0].base" "inode $inum" "ablock 0")

	# 64k dbsize is a special case since it overflows the 16 bit firstused
	# field and it needs to be set to the special XFS_ATTR3_LEAF_NULLOFF (0)
	# value to indicate a null leaf.
	if [ $dbsize -eq 65536 ]; then
		firstused=0
	else
		firstused=$dbsize
	fi

	_scratch_xfs_db -x -c "inode $inum" -c "ablock 0" \
		-c "write -d hdr.count 0" \
		-c "write -d hdr.usedbytes 0" \
		-c "write -d hdr.firstused $firstused" \
		-c "write -d hdr.freemap[0].size $((dbsize - base))" \
		-c print >> $seqres.full
}

make_empty_leaf $e0_ino
make_empty_leaf $e1_ino
make_empty_leaf $e2_ino

_scratch_mount

# Check that listxattr/getxattr/removexattr do nothing.
$ATTR_PROG -l $SCRATCH_MNT/e0 2>&1 | _filter_scratch
$ATTR_PROG -g x $SCRATCH_MNT/e0 2>&1 | _filter_scratch
$ATTR_PROG -r x $SCRATCH_MNT/e0 2>&1 | _filter_scratch

# Add a small attr to e0
$ATTR_PROG -s x $SCRATCH_MNT/e0 < $SCRATCH_MNT/smallfile > /dev/null
$ATTR_PROG -l $SCRATCH_MNT/e0 2>&1 | sed -e 's/\([0-9]*\) byte/XXX byte/g' | _filter_scratch
small_md5="$($GETFATTR_PROG -n user.x --absolute-names --only-values $SCRATCH_MNT/e0 | _md5_checksum)"
test "$small_md5" = "$smallfile_md5" || \
	echo "smallfile $smallfile_md5 does not match small attr $small_md5"

# Add a large attr to e1
$ATTR_PROG -s x $SCRATCH_MNT/e1 < $SCRATCH_MNT/largefile > /dev/null
$ATTR_PROG -l $SCRATCH_MNT/e1 2>&1 | _filter_scratch
large_md5="$($GETFATTR_PROG -n user.x --absolute-names --only-values $SCRATCH_MNT/e1 | _md5_checksum)"
test "$large_md5" = "$largefile_md5" || \
	echo "largefile $largefile_md5 does not match large attr $large_md5"


# Leave e2 to try to trip the repair tools, since xfs_repair used to flag
# empty leaf blocks incorrectly too.

# success, all done
status=0
exit
