[PATCH] generic: test fsync against a removed directory

[email protected]
Newsgroups org.kernel.vger.fstests,org.kernel.vger.linux-btrfs
Message-ID <d4b466fd960d1dd8e25f7db75fd323f48053cef1.1775747936.git.fdmanana@suse.com>
From: Filipe Manana <[email protected]>

Test that if we fsync a directory that became empty after removing it
and we have a power failure, we are able to mount the filesystem and the
directory does not exists anymore.

This exercises a bug found in btrfs which is fixed by the following patch:

  btrfs: fix missing last_unlink_trans update when removing a directory

Signed-off-by: Filipe Manana <[email protected]>
---
 src/unlink-fsync.c    | 39 ++++++++++++++++++------
 tests/generic/792     | 69 +++++++++++++++++++++++++++++++++++++++++++
 tests/generic/792.out | 10 +++++++
 3 files changed, 109 insertions(+), 9 deletions(-)
 create mode 100755 tests/generic/792
 create mode 100644 tests/generic/792.out

diff --git a/src/unlink-fsync.c b/src/unlink-fsync.c
index ce008c6b..364f9226 100644
--- a/src/unlink-fsync.c
+++ b/src/unlink-fsync.c
@@ -4,10 +4,12 @@
  */
 
 /*
- * Utility to open an existing file, unlink it while holding an open fd on it
- * and then fsync the file before closing the fd.
+ * Utility to open an existing file or directory, unlink or rmdir it while
+ * holding an open fd on it and then fsync it before closing the fd.
  */
 
+#include <sys/types.h>
+#include <sys/stat.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
@@ -15,24 +17,43 @@
 
 int main(int argc, char *argv[])
 {
+	struct stat path_stat;
 	int fd;
 	int ret;
 
 	if (argc != 2) {
-		fprintf(stderr, "Use: %s <file path>\n", argv[0]);
+		fprintf(stderr, "Use: %s <file or dir path>\n", argv[0]);
 		return 1;
 	}
 
-	fd = open(argv[1], O_WRONLY, 0666);
+	ret = stat(argv[1], &path_stat);
+	if (ret == -1) {
+		perror("stat failed");
+		exit(1);
+	}
+
+	if (S_ISDIR(path_stat.st_mode))
+		fd = open(argv[1], O_RDONLY | O_DIRECTORY);
+	else
+		fd = open(argv[1], O_WRONLY, 0666);
+
 	if (fd == -1) {
-		perror("failed to open file");
+		perror("failed to open file/dir");
 		exit(1);
 	}
 
-	ret = unlink(argv[1]);
-	if (ret == -1) {
-		perror("unlink failed");
-		exit(2);
+	if (S_ISDIR(path_stat.st_mode)) {
+		ret = rmdir(argv[1]);
+		if (ret == -1) {
+			perror("rmdir failed");
+			exit(2);
+		}
+	} else {
+		ret = unlink(argv[1]);
+		if (ret == -1) {
+			perror("unlink failed");
+			exit(2);
+		}
 	}
 
 	ret = fsync(fd);
diff --git a/tests/generic/792 b/tests/generic/792
new file mode 100755
index 00000000..892ca2d8
--- /dev/null
+++ b/tests/generic/792
@@ -0,0 +1,69 @@
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 SUSE S.A.  All Rights Reserved.
+#
+# FS QA Test 792
+#
+# Test that if we fsync a directory that became empty after removing it and we
+# have a power failure, we are able to mount the filesystem and the directory
+# does not exists anymore.
+#
+. ./common/preamble
+_begin_fstest auto quick log
+
+_cleanup()
+{
+	_cleanup_flakey
+	cd /
+	rm -r -f $tmp.*
+}
+
+. ./common/filter
+. ./common/dmflakey
+
+_require_scratch
+_require_test_program unlink-fsync
+_require_dm_target flakey
+
+_fixed_by_fs_commit btrfs xxxxxxxxxxxx \
+	"btrfs: fix missing last_unlink_trans update when removing a directory"
+
+_scratch_mkfs >> $seqres.full 2>&1 || _fail "mkfs failed"
+_require_metadata_journaling $SCRATCH_DEV
+_init_flakey
+_scratch_mount
+
+# Create our first directory and define its permissions (we will later change
+# them).
+mkdir $SCRATCH_MNT/dir1
+chmod a+rwx $SCRATCH_MNT/dir1
+
+# Created another directory inside that first directoy and then another one at
+# the top level.
+mkdir $SCRATCH_MNT/dir1/dir2
+mkdir $SCRATCH_MNT/dir3
+
+# Persist everything done so far.
+_scratch_sync
+
+# Do some change to the first directory, like change its permissions, fsync it.
+chmod o-rwx $SCRATCH_MNT/dir1
+$XFS_IO_PROG -c "fsync" $SCRATCH_MNT/dir1
+
+# Now move the second directory from dir1 to dir3, so that dir1 becomes empty.
+mv $SCRATCH_MNT/dir1/dir2 $SCRATCH_MNT/dir3/
+
+# Now open an fd for dir1, rmdir it and fsync it before closing the fd.
+$here/src/unlink-fsync $SCRATCH_MNT/dir1
+
+# Simulate a power failure and replay the log/journal.
+# The mount should succeed, dir1 must not exist anymore and dir2 must now be
+# inside dir3.
+_flakey_drop_and_remount
+
+echo -e "Filesystem content after power failure:\n"
+# Exclude 'lost+found' dir from ext4 and last line if it's blank (due to removal
+# of 'lost+found').
+ls -R $SCRATCH_MNT/ | grep -v 'lost+found' | sed -e '${/^$/d;}' | _filter_scratch
+
+_exit 0
diff --git a/tests/generic/792.out b/tests/generic/792.out
new file mode 100644
index 00000000..f806258c
--- /dev/null
+++ b/tests/generic/792.out
@@ -0,0 +1,10 @@
+QA output created by 792
+Filesystem content after power failure:
+
+SCRATCH_MNT/:
+dir3
+
+SCRATCH_MNT/dir3:
+dir2
+
+SCRATCH_MNT/dir3/dir2:
-- 
2.47.2
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.