From: Christian Brauner <brauner@kernel.org>
To: linux-fsdevel@vger.kernel.org, Jan Kara <jack@suse.com>
Cc: Al Viro <viro@zeniv.linux.org.uk>,
ecryptfs@vger.kernel.org, linux-ext4@vger.kernel.org,
jfs-discussion@lists.sourceforge.net,
linux-nilfs@vger.kernel.org, exfat@lists.linux.dev,
linux-mm@kvack.org, ntfs@lists.linux.dev,
ocfs2-devel@lists.linux.dev, linux-btrfs@vger.kernel.org,
gfs2@lists.linux.dev, linux-efi@vger.kernel.org,
linux-mtd@lists.infradead.org, linux-xfs@vger.kernel.org,
"Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH v2 29/34] selftests/filesystems: test filesystem errors while frozen
Date: Thu, 29 Oct 2026 17:07:20 +0100 [thread overview]
Message-ID: <20261029-work-fs-sb_rdonly-v2-29-2f44f64f0b66@kernel.org> (raw)
In-Reply-To: <20261029-work-fs-sb_rdonly-v2-0-2f44f64f0b66@kernel.org>
Add tests for filesystems that hit an error while they are frozen:
- ext2, jfs and nilfs2: look up a file in a corrupted directory while
frozen and thaw
- the superblock shows "ro", statfs() reports ST_RDONLY and statmount()
SB_RDONLY
- create, write, chmod, mkdir, rename, unlink, truncate and write opens
fail with EROFS instead of blocking, also after another freeze
- remounting read-only works, read-write fails with EROFS
- remount read-only and umount after a write, fsync fails with EROFS
- gfs2: withdraw while frozen and thaw, writes fail instead of blocking
and freezing a withdrawn gfs2 fails
Everything skips without root, loop devices, the filesystem or its mkfs
tool.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
tools/testing/selftests/Makefile | 1 +
.../selftests/filesystems/sb_rdonly/.gitignore | 3 +
.../selftests/filesystems/sb_rdonly/Makefile | 6 +
.../testing/selftests/filesystems/sb_rdonly/config | 5 +
.../filesystems/sb_rdonly/emergency_ro_test.c | 560 +++++++++++++++++++++
.../filesystems/sb_rdonly/gfs2_withdraw_test.c | 318 ++++++++++++
.../selftests/filesystems/sb_rdonly/settings | 1 +
7 files changed, 894 insertions(+)
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 6c77beba0956..507fecdccda6 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -55,6 +55,7 @@ TARGETS += filesystems/fscontext_ns
TARGETS += filesystems/xattr
TARGETS += filesystems/mntns_unbindable
TARGETS += filesystems/umount_propagation
+TARGETS += filesystems/sb_rdonly
TARGETS += firmware
TARGETS += fpu
TARGETS += ftrace
diff --git a/tools/testing/selftests/filesystems/sb_rdonly/.gitignore b/tools/testing/selftests/filesystems/sb_rdonly/.gitignore
new file mode 100644
index 000000000000..96daf47a494f
--- /dev/null
+++ b/tools/testing/selftests/filesystems/sb_rdonly/.gitignore
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+emergency_ro_test
+gfs2_withdraw_test
diff --git a/tools/testing/selftests/filesystems/sb_rdonly/Makefile b/tools/testing/selftests/filesystems/sb_rdonly/Makefile
new file mode 100644
index 000000000000..89d72fcf8600
--- /dev/null
+++ b/tools/testing/selftests/filesystems/sb_rdonly/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+TEST_GEN_PROGS := emergency_ro_test gfs2_withdraw_test
+
+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/filesystems/sb_rdonly/config b/tools/testing/selftests/filesystems/sb_rdonly/config
new file mode 100644
index 000000000000..ab660b588e6d
--- /dev/null
+++ b/tools/testing/selftests/filesystems/sb_rdonly/config
@@ -0,0 +1,5 @@
+CONFIG_BLK_DEV_LOOP=y
+CONFIG_EXT2_FS=y
+CONFIG_GFS2_FS=y
+CONFIG_JFS_FS=y
+CONFIG_NILFS2_FS=y
diff --git a/tools/testing/selftests/filesystems/sb_rdonly/emergency_ro_test.c b/tools/testing/selftests/filesystems/sb_rdonly/emergency_ro_test.c
new file mode 100644
index 000000000000..de442b8ee709
--- /dev/null
+++ b/tools/testing/selftests/filesystems/sb_rdonly/emergency_ro_test.c
@@ -0,0 +1,560 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Filesystems that hit an error while frozen used to set SB_RDONLY and left
+ * thaw_super() with the s_writers rwsems locked. Freeze, read a corrupted
+ * directory, thaw and check that nothing blocks, that modifications fail with
+ * EROFS and that a remount can go read-only but not back to read-write.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/fs.h>
+#include <linux/loop.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/statvfs.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../../kselftest_harness.h"
+#include "../statmount/statmount.h"
+
+/* ext2 and nilfs2 directory entry names, jfs dtree names must fit 11 chars */
+#define MARKER "sbrdonlyQ7Zk2Lp9"
+#define MARKER_JFS "sbrdonlyjfs"
+
+enum layout { LAYOUT_EXT2, LAYOUT_NILFS2, LAYOUT_JFS };
+
+FIXTURE(emergency_ro) {
+ char dir[64];
+ char img[96];
+ char mnt[96];
+ char dev[64];
+ int loop_fd;
+ bool mounted;
+ bool wedged; /* writers are blocked for good, leave it mounted */
+};
+
+FIXTURE_VARIANT(emergency_ro) {
+ const char *fstype;
+ const char *mkfs;
+ const char *opts;
+ off_t size;
+ enum layout layout;
+};
+
+FIXTURE_VARIANT_ADD(emergency_ro, ext2) {
+ .fstype = "ext2",
+ .mkfs = "mkfs.ext2 -q -F -b 4096",
+ .opts = "errors=remount-ro",
+ .size = 64 << 20,
+ .layout = LAYOUT_EXT2,
+};
+
+FIXTURE_VARIANT_ADD(emergency_ro, jfs) {
+ .fstype = "jfs",
+ .mkfs = "mkfs.jfs -q",
+ .opts = "errors=remount-ro",
+ .size = 256 << 20,
+ .layout = LAYOUT_JFS,
+};
+
+FIXTURE_VARIANT_ADD(emergency_ro, nilfs2) {
+ .fstype = "nilfs2",
+ .mkfs = "mkfs.nilfs2 -q -f",
+ .opts = "errors=remount-ro",
+ .size = 512 << 20,
+ .layout = LAYOUT_NILFS2,
+};
+
+static const char *at(const char *mnt, const char *rel)
+{
+ static char buf[4][PATH_MAX];
+ static unsigned int next;
+ char *p = buf[next++ % 4];
+
+ snprintf(p, PATH_MAX, "%s/%s", mnt, rel);
+ return p;
+}
+
+static bool have_fs(const char *fstype)
+{
+ char line[128], cmd[128];
+ bool found = false;
+ FILE *f;
+
+ for (int tries = 0; tries < 2 && !found; tries++) {
+ f = fopen("/proc/filesystems", "r");
+ if (!f)
+ return false;
+ while (fgets(line, sizeof(line), f)) {
+ char *name = strrchr(line, '\t');
+
+ name = name ? name + 1 : line;
+ name[strcspn(name, "\n")] = '\0';
+ if (!strcmp(name, fstype))
+ found = true;
+ }
+ fclose(f);
+ if (!found) {
+ snprintf(cmd, sizeof(cmd), "modprobe -q %s", fstype);
+ if (system(cmd))
+ break;
+ }
+ }
+ return found;
+}
+
+/*
+ * With CONFIG_EXT4_USE_FOR_EXT2 ext4 serves "ext2" with its own emergency
+ * state. Real ext2 doesn't register /sys/fs/ext4 or /proc/fs/ext4.
+ */
+static bool served_by_ext4(const char *dev)
+{
+ const char *name = strrchr(dev, '/') + 1;
+ char path[128];
+
+ snprintf(path, sizeof(path), "/sys/fs/ext4/%s", name);
+ if (!access(path, F_OK))
+ return true;
+ snprintf(path, sizeof(path), "/proc/fs/ext4/%s", name);
+ return !access(path, F_OK);
+}
+
+static bool have_tool(const char *mkfs)
+{
+ char cmd[128];
+ int len = strcspn(mkfs, " ");
+
+ snprintf(cmd, sizeof(cmd), "command -v %.*s >/dev/null 2>&1", len, mkfs);
+ return system(cmd) == 0;
+}
+
+static int write_file(const char *path, const char *data)
+{
+ int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644);
+ ssize_t len = strlen(data);
+
+ if (fd < 0)
+ return -1;
+ if (write(fd, data, len) != len || fsync(fd)) {
+ close(fd);
+ return -1;
+ }
+ return close(fd);
+}
+
+static uint16_t le16(const unsigned char *p) { return p[0] | p[1] << 8; }
+static uint32_t le32(const unsigned char *p) { return le16(p) | (uint32_t)le16(p + 2) << 16; }
+static uint64_t le64(const unsigned char *p) { return le32(p) | (uint64_t)le32(p + 4) << 32; }
+
+/*
+ * Corrupt the entry of the marker file in directory d, found by searching the
+ * device for its name. ext2 and nilfs2: rec_len := 0 (2 bytes before the
+ * name_len byte). jfs: the entry sits in the dtree root inside d's dinode at
+ * offset 224 (32 byte slots, the name at +6), freecnt := -128. Returns the
+ * number of corrupted entries.
+ */
+static int corrupt(int fd, enum layout layout, ino_t dir_ino, ino_t mark_ino)
+{
+ const size_t chunk = 1 << 22, overlap = 4096;
+ unsigned char *buf, pat[2 * sizeof(MARKER)];
+ size_t patlen = 0;
+ off_t size, off;
+ int hits = 0;
+
+ if (layout == LAYOUT_JFS) {
+ for (const char *m = MARKER_JFS; *m; m++) {
+ pat[patlen++] = *m;
+ pat[patlen++] = 0; /* UCS-2 little endian */
+ }
+ } else {
+ patlen = strlen(MARKER);
+ memcpy(pat, MARKER, patlen);
+ }
+
+ /* drop cached blocks, nilfs2 writes its logs around the page cache */
+ if (ioctl(fd, BLKFLSBUF, 0))
+ return -1;
+ buf = malloc(chunk + overlap);
+ if (!buf)
+ return -1;
+ size = lseek(fd, 0, SEEK_END);
+ for (off = 0; off < size; off += chunk) {
+ off_t base = off >= (off_t)overlap ? off - overlap : 0;
+ ssize_t n = pread(fd, buf, chunk + (off - base), base);
+
+ if (n <= 0)
+ break;
+ for (size_t i = off - base; i + patlen <= (size_t)n; i++) {
+ unsigned char *p = buf + i, zero[2] = { 0, 0 };
+ off_t pos = base + i;
+
+ if (memcmp(p, pat, patlen))
+ continue;
+
+ if (layout == LAYOUT_EXT2) {
+ if (i < 8 || le32(p - 8) != mark_ino || p[-2] != patlen)
+ continue;
+ if (pwrite(fd, zero, 2, pos - 4) != 2)
+ goto out;
+ hits++;
+ } else if (layout == LAYOUT_NILFS2) {
+ if (i < 12 || le64(p - 12) != mark_ino || p[-2] != patlen)
+ continue;
+ if (pwrite(fd, zero, 2, pos - 4) != 2)
+ goto out;
+ hits++;
+ } else {
+ off_t entry = pos - 6, dino, slot;
+ unsigned char d[512], bad = 0x80;
+
+ if (i < 6 || le32(p - 6) != mark_ino ||
+ p[-1] != strlen(MARKER_JFS))
+ continue;
+ slot = ((entry - 224) % 512) / 32;
+ if ((entry - 224) % 32 || slot < 1 || slot > 8)
+ continue;
+ dino = entry - 224 - 32 * slot;
+ if (pread(fd, d, sizeof(d), dino) != sizeof(d))
+ goto out;
+ /* di_fileset is FILESYSTEM_I, di_number is d */
+ if (le32(d + 4) != 16 || le32(d + 8) != dir_ino)
+ continue;
+ if (pwrite(fd, &bad, 1, dino + 224 + 18) != 1)
+ goto out;
+ hits++;
+ }
+ }
+ }
+out:
+ free(buf);
+ if (fsync(fd))
+ return -1;
+ return hits;
+}
+
+/* The superblock options start with its own "ro" or "rw". */
+static bool super_rdonly(const char *mnt)
+{
+ char line[4096], needle[128], fstype[64], source[256], opts[2048];
+ bool ro = false;
+ FILE *f;
+
+ f = fopen("/proc/self/mountinfo", "r");
+ if (!f)
+ return false;
+ snprintf(needle, sizeof(needle), " %s ", mnt);
+ while (fgets(line, sizeof(line), f)) {
+ char *super;
+
+ if (!strstr(line, needle))
+ continue;
+ super = strstr(line, " - ");
+ if (super && sscanf(super, " - %63s %255s %2047s",
+ fstype, source, opts) == 3)
+ ro = !strncmp(opts, "ro", 2) && (opts[2] == ',' || !opts[2]);
+ }
+ fclose(f);
+ return ro;
+}
+
+/* statmount() has to agree with the superblock options in mountinfo. */
+static bool statmount_rdonly(int fd)
+{
+ struct statmount sm;
+
+ if (statmount(0, 0, fd, STATMOUNT_SB_BASIC, &sm, sizeof(sm),
+ STATMOUNT_BY_FD))
+ return false;
+ return sm.sb_flags & MS_RDONLY;
+}
+
+/* Run fn() in a child, it must neither block on s_writers nor fail. */
+static int run_child(int (*fn)(const char *, int), const char *mnt, int prefd)
+{
+ int status, fd;
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ return -1;
+ if (!pid) {
+ /* a child stuck in sb_start_write() mustn't hold the runner's pipe */
+ fd = open("/dev/null", O_RDWR);
+ if (fd >= 0) {
+ dup2(fd, 0);
+ dup2(fd, 1);
+ dup2(fd, 2);
+ }
+ _exit(fn(mnt, prefd));
+ }
+
+ for (int i = 0; i < 100; i++) {
+ if (waitpid(pid, &status, WNOHANG) == pid)
+ return WIFEXITED(status) ? WEXITSTATUS(status) : 255;
+ usleep(100000);
+ }
+ /* stuck in sb_start_write(), cannot be killed */
+ return -ETIMEDOUT;
+}
+
+/* Every modification fails with EROFS, returns the number of mismatches. */
+static int modify_all(const char *mnt, int prefd)
+{
+ int bad = 0, fd;
+
+ fd = open(at(mnt, "w/new"), O_CREAT | O_WRONLY, 0644);
+ bad += !(fd < 0 && errno == EROFS);
+ bad += !(pwrite(prefd, "x", 1, 0) < 0 && errno == EROFS);
+ bad += !(chmod(at(mnt, "f"), 0600) && errno == EROFS);
+ bad += !(mkdir(at(mnt, "w/dir"), 0755) && errno == EROFS);
+ bad += !(rename(at(mnt, "f"), at(mnt, "w/f")) && errno == EROFS);
+ bad += !(unlink(at(mnt, "f")) && errno == EROFS);
+ bad += !(truncate(at(mnt, "f"), 0) && errno == EROFS);
+ fd = open(at(mnt, "f"), O_WRONLY);
+ bad += !(fd < 0 && errno == EROFS);
+ return bad;
+}
+
+FIXTURE_SETUP(emergency_ro)
+{
+ struct loop_config cfg = { .info.lo_flags = LO_FLAGS_AUTOCLEAR };
+ int ctl, nr, img_fd;
+ char cmd[256];
+
+ self->loop_fd = -1;
+ self->mounted = false;
+ self->wedged = false;
+ self->dir[0] = '\0';
+
+ if (geteuid())
+ SKIP(return, "needs root");
+ if (!have_fs(variant->fstype))
+ SKIP(return, "no %s support", variant->fstype);
+ if (!have_tool(variant->mkfs))
+ SKIP(return, "no %.*s", (int)strcspn(variant->mkfs, " "), variant->mkfs);
+ ctl = open("/dev/loop-control", O_RDWR | O_CLOEXEC);
+ if (ctl < 0)
+ SKIP(return, "no loop devices");
+
+ strcpy(self->dir, "/tmp/sb_rdonly_XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ snprintf(self->img, sizeof(self->img), "%s/img", self->dir);
+ snprintf(self->mnt, sizeof(self->mnt), "%s/mnt", self->dir);
+ ASSERT_EQ(mkdir(self->mnt, 0755), 0);
+
+ img_fd = open(self->img, O_CREAT | O_RDWR, 0600);
+ ASSERT_GE(img_fd, 0);
+ ASSERT_EQ(ftruncate(img_fd, variant->size), 0);
+ cfg.fd = img_fd;
+ /* another user can grab the free device first */
+ for (int tries = 0;; tries++) {
+ nr = ioctl(ctl, LOOP_CTL_GET_FREE);
+ ASSERT_GE(nr, 0);
+ snprintf(self->dev, sizeof(self->dev), "/dev/loop%d", nr);
+ self->loop_fd = open(self->dev, O_RDWR | O_CLOEXEC);
+ ASSERT_GE(self->loop_fd, 0);
+ if (!ioctl(self->loop_fd, LOOP_CONFIGURE, &cfg))
+ break;
+ ASSERT_EQ(errno, EBUSY);
+ ASSERT_LT(tries, 10);
+ close(self->loop_fd);
+ self->loop_fd = -1;
+ }
+ close(ctl);
+ close(img_fd);
+
+ snprintf(cmd, sizeof(cmd), "%s %s >/dev/null 2>&1", variant->mkfs, self->dev);
+ ASSERT_EQ(system(cmd), 0);
+}
+
+FIXTURE_TEARDOWN(emergency_ro)
+{
+ if (self->mounted) {
+ if (self->wedged)
+ TH_LOG("leaving %s mounted, its writers are blocked", self->mnt);
+ else if (umount2(self->mnt, MNT_DETACH))
+ TH_LOG("umount %s: %s", self->mnt, strerror(errno));
+ else
+ self->mounted = false;
+ }
+ if (self->loop_fd >= 0)
+ close(self->loop_fd); /* LO_FLAGS_AUTOCLEAR detaches it */
+ if (!self->dir[0])
+ return;
+ unlink(self->img);
+ if (!self->mounted) {
+ rmdir(self->mnt);
+ rmdir(self->dir);
+ }
+}
+
+static const char *marker(const FIXTURE_VARIANT(emergency_ro) *variant)
+{
+ return variant->layout == LAYOUT_JFS ? "d/" MARKER_JFS : "d/" MARKER;
+}
+
+/* Populate, corrupt the marker entry offline and mount again. */
+static bool prepare(struct __test_metadata *_metadata,
+ FIXTURE_DATA(emergency_ro) *self,
+ const FIXTURE_VARIANT(emergency_ro) *variant)
+{
+ const char *opts = variant->opts;
+ const char *mark = marker(variant);
+ struct stat st;
+ ino_t dir_ino, mark_ino;
+ int ret;
+
+ ASSERT_EQ(mount(self->dev, self->mnt, variant->fstype, 0, opts), 0);
+ self->mounted = true;
+ if (variant->layout == LAYOUT_EXT2 && served_by_ext4(self->dev))
+ return false;
+ ASSERT_EQ(mkdir(at(self->mnt, "d"), 0755), 0);
+ ASSERT_EQ(mkdir(at(self->mnt, "w"), 0755), 0);
+ ASSERT_EQ(write_file(at(self->mnt, mark), "marker\n"), 0);
+ ASSERT_EQ(write_file(at(self->mnt, "f"), "data\n"), 0);
+ sync();
+ /* nilfs2 only checksums the last log at mount, keep d's block out of it */
+ ASSERT_EQ(write_file(at(self->mnt, "z"), "later\n"), 0);
+ sync();
+ ASSERT_EQ(stat(at(self->mnt, "d"), &st), 0);
+ dir_ino = st.st_ino;
+ ASSERT_EQ(stat(at(self->mnt, mark), &st), 0);
+ mark_ino = st.st_ino;
+ ASSERT_EQ(umount(self->mnt), 0);
+ self->mounted = false;
+
+ ret = corrupt(self->loop_fd, variant->layout, dir_ino, mark_ino);
+ ASSERT_GE(ret, 1) {
+ TH_LOG("marker entry not found on %s", self->dev);
+ }
+
+ ASSERT_EQ(mount(self->dev, self->mnt, variant->fstype, 0, opts), 0);
+ self->mounted = true;
+ return true;
+}
+
+TEST_F_TIMEOUT(emergency_ro, freeze_error_thaw, 120)
+{
+ const char *opts = variant->opts;
+ const char *mark = marker(variant);
+ struct statvfs sv;
+ struct stat st;
+ int prefd, rootfd, ret;
+
+ if (!prepare(_metadata, self, variant))
+ SKIP(return, "ext2 is served by ext4 (CONFIG_EXT4_USE_FOR_EXT2)");
+
+ prefd = open(at(self->mnt, "f"), O_RDWR);
+ ASSERT_GE(prefd, 0);
+ rootfd = open(self->mnt, O_RDONLY | O_DIRECTORY);
+ ASSERT_GE(rootfd, 0);
+ ASSERT_FALSE(statmount_rdonly(rootfd));
+
+ ASSERT_EQ(ioctl(rootfd, FIFREEZE, 0), 0);
+ /* reads still go to the device and find the corruption */
+ ret = stat(at(self->mnt, mark), &st);
+ EXPECT_EQ(ret, -1);
+ EXPECT_EQ(errno, EIO);
+ ASSERT_EQ(ioctl(rootfd, FITHAW, 0), 0);
+
+ /* the error made it read-only for the VFS, SB_RDONLY stays clear */
+ ASSERT_EQ(statvfs(self->mnt, &sv), 0);
+ ASSERT_TRUE(!!(sv.f_flag & ST_RDONLY));
+ ASSERT_TRUE(super_rdonly(self->mnt));
+ ASSERT_TRUE(statmount_rdonly(rootfd));
+
+ ret = run_child(modify_all, self->mnt, prefd);
+ if (ret == -ETIMEDOUT)
+ self->wedged = true;
+ ASSERT_EQ(ret, 0) {
+ TH_LOG("%s", ret == -ETIMEDOUT ? "a writer is blocked after thaw" :
+ "a modification did not fail with EROFS");
+ }
+
+ /* freezing still works and doesn't leave writers blocked */
+ ASSERT_EQ(ioctl(rootfd, FIFREEZE, 0), 0);
+ ASSERT_EQ(ioctl(rootfd, FITHAW, 0), 0);
+ ret = run_child(modify_all, self->mnt, prefd);
+ if (ret == -ETIMEDOUT)
+ self->wedged = true;
+ ASSERT_EQ(ret, 0);
+
+ /* remount keeps refusing, read-only works, read-write doesn't */
+ close(prefd);
+ ASSERT_EQ(mount(NULL, self->mnt, NULL, MS_REMOUNT, opts), 0);
+ EXPECT_TRUE(super_rdonly(self->mnt));
+ ret = open(at(self->mnt, "w/new2"), O_CREAT | O_WRONLY, 0644);
+ EXPECT_EQ(ret, -1);
+ EXPECT_EQ(errno, EROFS);
+ ASSERT_EQ(mount(NULL, self->mnt, NULL, MS_REMOUNT | MS_RDONLY, opts), 0);
+ EXPECT_TRUE(super_rdonly(self->mnt));
+ ret = mount(NULL, self->mnt, NULL, MS_REMOUNT, opts);
+ EXPECT_EQ(ret, -1);
+ EXPECT_EQ(errno, EROFS);
+
+ close(rootfd);
+ ASSERT_EQ(umount(self->mnt), 0);
+ self->mounted = false;
+}
+
+/* Allocate blocks without syncing them, then hit the error. */
+static int write_then_error(struct __test_metadata *_metadata,
+ FIXTURE_DATA(emergency_ro) *self,
+ const FIXTURE_VARIANT(emergency_ro) *variant)
+{
+ static char buf[64 << 10];
+ struct stat st;
+ int fd;
+
+ memset(buf, 'x', sizeof(buf));
+ fd = open(at(self->mnt, "g"), O_CREAT | O_RDWR, 0644);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(write(fd, buf, sizeof(buf)), (ssize_t)sizeof(buf));
+ EXPECT_EQ(stat(at(self->mnt, marker(variant)), &st), -1);
+ ASSERT_TRUE(super_rdonly(self->mnt));
+ return fd;
+}
+
+/* jfs BUG()ed on uncommitted tlocks when umount followed a remount,ro */
+TEST_F_TIMEOUT(emergency_ro, error_remount_ro, 120)
+{
+ if (!prepare(_metadata, self, variant))
+ SKIP(return, "ext2 is served by ext4 (CONFIG_EXT4_USE_FOR_EXT2)");
+
+ close(write_then_error(_metadata, self, variant));
+ ASSERT_EQ(mount(NULL, self->mnt, NULL, MS_REMOUNT | MS_RDONLY,
+ variant->opts), 0);
+ EXPECT_TRUE(super_rdonly(self->mnt));
+ ASSERT_EQ(umount(self->mnt), 0);
+ self->mounted = false;
+}
+
+/* fsync() mustn't claim an inode is on disk that writeback refused */
+TEST_F_TIMEOUT(emergency_ro, error_fsync, 120)
+{
+ int fd, ret;
+
+ if (!prepare(_metadata, self, variant))
+ SKIP(return, "ext2 is served by ext4 (CONFIG_EXT4_USE_FOR_EXT2)");
+
+ fd = write_then_error(_metadata, self, variant);
+ sync();
+ ret = fsync(fd);
+ EXPECT_EQ(ret, -1);
+ EXPECT_EQ(errno, EROFS);
+ close(fd);
+ ASSERT_EQ(umount(self->mnt), 0);
+ self->mounted = false;
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/sb_rdonly/gfs2_withdraw_test.c b/tools/testing/selftests/filesystems/sb_rdonly/gfs2_withdraw_test.c
new file mode 100644
index 000000000000..1b946cac37e8
--- /dev/null
+++ b/tools/testing/selftests/filesystems/sb_rdonly/gfs2_withdraw_test.c
@@ -0,0 +1,318 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * gfs2 used to set SB_RDONLY on withdraw. Withdrawn while frozen it couldn't
+ * be thawed and once it was the s_writers rwsems stayed locked. Freeze a
+ * lock_nolock gfs2, withdraw it through sysfs, thaw it and check that writers
+ * fail instead of blocking. Paths stop working after the withdraw so all of
+ * it goes through file descriptors opened before.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/fs.h>
+#include <linux/loop.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/statvfs.h>
+#include <sys/sysmacros.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <dirent.h>
+#include <unistd.h>
+
+#include "../../kselftest_harness.h"
+
+FIXTURE(gfs2) {
+ char dir[64];
+ char img[96];
+ char mnt[96];
+ char dev[64];
+ char table[32];
+ char sysfs[PATH_MAX];
+ int loop_fd;
+ bool mounted;
+ bool wedged; /* frozen or writers blocked for good, leave it mounted */
+};
+
+static bool have_fs(const char *fstype)
+{
+ char line[128], cmd[128];
+ bool found = false;
+ FILE *f;
+
+ for (int tries = 0; tries < 2 && !found; tries++) {
+ f = fopen("/proc/filesystems", "r");
+ if (!f)
+ return false;
+ while (fgets(line, sizeof(line), f)) {
+ char *name = strrchr(line, '\t');
+
+ name = name ? name + 1 : line;
+ name[strcspn(name, "\n")] = '\0';
+ if (!strcmp(name, fstype))
+ found = true;
+ }
+ fclose(f);
+ if (!found) {
+ snprintf(cmd, sizeof(cmd), "modprobe -q %s", fstype);
+ if (system(cmd))
+ break;
+ }
+ }
+ return found;
+}
+
+/* gfs2 shows the withdraw in sysfs, not in the mount options. */
+static bool withdrawn(FIXTURE_DATA(gfs2) *self)
+{
+ char path[PATH_MAX + 16], buf[8] = "";
+ FILE *f;
+
+ snprintf(path, sizeof(path), "%s/withdraw", self->sysfs);
+ f = fopen(path, "r");
+ if (!f)
+ return false;
+ if (!fgets(buf, sizeof(buf), f))
+ buf[0] = '\0';
+ fclose(f);
+ return buf[0] == '1';
+}
+
+/* Find the gfs2 sysfs directory of the mounted filesystem by its device. */
+static int find_sysfs(FIXTURE_DATA(gfs2) *self, char *dir, size_t len)
+{
+ char path[PATH_MAX], want[32], have[32];
+ struct dirent *d;
+ struct stat st;
+ int ret = -1;
+ DIR *sys;
+ FILE *f;
+
+ if (stat(self->mnt, &st))
+ return -1;
+ snprintf(want, sizeof(want), "%u:%u\n", major(st.st_dev), minor(st.st_dev));
+ sys = opendir("/sys/fs/gfs2");
+ if (!sys)
+ return -1;
+ while ((d = readdir(sys))) {
+ if (d->d_name[0] == '.')
+ continue;
+ snprintf(path, sizeof(path), "/sys/fs/gfs2/%s/id", d->d_name);
+ f = fopen(path, "r");
+ if (!f)
+ continue;
+ if (fgets(have, sizeof(have), f) && !strcmp(have, want)) {
+ snprintf(dir, len, "/sys/fs/gfs2/%s", d->d_name);
+ ret = 0;
+ }
+ fclose(f);
+ if (!ret)
+ break;
+ }
+ closedir(sys);
+ return ret;
+}
+
+static int withdraw(FIXTURE_DATA(gfs2) *self)
+{
+ char path[PATH_MAX + 16];
+ int fd, ret;
+
+ /* paths on the filesystem stop working after the withdraw */
+ if (find_sysfs(self, self->sysfs, sizeof(self->sysfs)))
+ return -1;
+ snprintf(path, sizeof(path), "%s/withdraw", self->sysfs);
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -1;
+ ret = write(fd, "1", 1) == 1 ? 0 : -1;
+ close(fd);
+ /* do_withdraw() runs from a work item and waits up to 5s for the log */
+ sleep(8);
+ return ret;
+}
+
+/* Writes through fd must fail, not block on s_writers. */
+static int modify_fd(int fd)
+{
+ int bad = 0;
+
+ bad += !(pwrite(fd, "x", 1, 0) < 0);
+ bad += !(fchmod(fd, 0600) < 0);
+ bad += !(ftruncate(fd, 0) < 0);
+ return bad;
+}
+
+/* Run modify_fd() in a child: 0, the number of mismatches or -ETIMEDOUT. */
+static int run_child(int fd)
+{
+ int status, null;
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ return -1;
+ if (!pid) {
+ /* a child stuck in sb_start_write() mustn't hold the runner's pipe */
+ null = open("/dev/null", O_RDWR);
+ if (null >= 0) {
+ dup2(null, 0);
+ dup2(null, 1);
+ dup2(null, 2);
+ }
+ _exit(modify_fd(fd));
+ }
+
+ for (int i = 0; i < 100; i++) {
+ if (waitpid(pid, &status, WNOHANG) == pid)
+ return WIFEXITED(status) ? WEXITSTATUS(status) : 255;
+ usleep(100000);
+ }
+ /* stuck in sb_start_write(), cannot be killed */
+ return -ETIMEDOUT;
+}
+
+FIXTURE_SETUP(gfs2)
+{
+ struct loop_config cfg = { .info.lo_flags = LO_FLAGS_AUTOCLEAR };
+ int ctl, nr, img_fd;
+ char cmd[384];
+
+ self->loop_fd = -1;
+ self->mounted = false;
+ self->wedged = false;
+ self->dir[0] = '\0';
+ self->sysfs[0] = '\0';
+
+ if (geteuid())
+ SKIP(return, "needs root");
+ if (!have_fs("gfs2"))
+ SKIP(return, "no gfs2 support");
+ if (system("command -v mkfs.gfs2 >/dev/null 2>&1"))
+ SKIP(return, "no mkfs.gfs2");
+ ctl = open("/dev/loop-control", O_RDWR | O_CLOEXEC);
+ if (ctl < 0)
+ SKIP(return, "no loop devices");
+
+ strcpy(self->dir, "/tmp/sb_rdonly_XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ snprintf(self->img, sizeof(self->img), "%s/img", self->dir);
+ snprintf(self->mnt, sizeof(self->mnt), "%s/mnt", self->dir);
+ /* the lock table names the sysfs directory, keep it unique */
+ snprintf(self->table, sizeof(self->table), "sbrdonly:t%d", getpid() % 100000);
+ ASSERT_EQ(mkdir(self->mnt, 0755), 0);
+
+ img_fd = open(self->img, O_CREAT | O_RDWR, 0600);
+ ASSERT_GE(img_fd, 0);
+ ASSERT_EQ(ftruncate(img_fd, 256 << 20), 0);
+ cfg.fd = img_fd;
+ /* another user can grab the free device first */
+ for (int tries = 0;; tries++) {
+ nr = ioctl(ctl, LOOP_CTL_GET_FREE);
+ ASSERT_GE(nr, 0);
+ snprintf(self->dev, sizeof(self->dev), "/dev/loop%d", nr);
+ self->loop_fd = open(self->dev, O_RDWR | O_CLOEXEC);
+ ASSERT_GE(self->loop_fd, 0);
+ if (!ioctl(self->loop_fd, LOOP_CONFIGURE, &cfg))
+ break;
+ ASSERT_EQ(errno, EBUSY);
+ ASSERT_LT(tries, 10);
+ close(self->loop_fd);
+ self->loop_fd = -1;
+ }
+ close(ctl);
+ close(img_fd);
+
+ snprintf(cmd, sizeof(cmd),
+ "mkfs.gfs2 -O -p lock_nolock -j 1 -t %s %s >/dev/null 2>&1",
+ self->table, self->dev);
+ ASSERT_EQ(system(cmd), 0);
+}
+
+FIXTURE_TEARDOWN(gfs2)
+{
+ if (self->mounted) {
+ if (self->wedged)
+ TH_LOG("leaving %s mounted, it is frozen or its writers are blocked",
+ self->mnt);
+ else if (umount2(self->mnt, MNT_DETACH))
+ TH_LOG("umount %s: %s", self->mnt, strerror(errno));
+ else
+ self->mounted = false;
+ }
+ if (self->loop_fd >= 0)
+ close(self->loop_fd); /* LO_FLAGS_AUTOCLEAR detaches it */
+ if (!self->dir[0])
+ return;
+ unlink(self->img);
+ if (!self->mounted) {
+ rmdir(self->mnt);
+ rmdir(self->dir);
+ }
+}
+
+TEST_F_TIMEOUT(gfs2, withdraw_while_frozen, 120)
+{
+ struct statvfs sv;
+ char path[PATH_MAX];
+ int fd, prefd, rootfd, ret;
+
+ ASSERT_EQ(mount(self->dev, self->mnt, "gfs2", 0, NULL), 0);
+ self->mounted = true;
+ snprintf(path, sizeof(path), "%s/f", self->mnt);
+ fd = open(path, O_CREAT | O_WRONLY, 0644);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(write(fd, "data\n", 5), 5);
+ ASSERT_EQ(fsync(fd), 0);
+ close(fd);
+ prefd = open(path, O_RDWR);
+ ASSERT_GE(prefd, 0);
+ rootfd = open(self->mnt, O_RDONLY | O_DIRECTORY);
+ ASSERT_GE(rootfd, 0);
+
+ ASSERT_EQ(ioctl(rootfd, FIFREEZE, 0), 0);
+ ret = withdraw(self);
+ if (ret)
+ ioctl(rootfd, FITHAW, 0);
+ ASSERT_EQ(ret, 0);
+ EXPECT_TRUE(withdrawn(self));
+
+ ret = ioctl(rootfd, FITHAW, 0);
+ if (ret)
+ self->wedged = true;
+ ASSERT_EQ(ret, 0) {
+ TH_LOG("FITHAW: %s, a withdrawn gfs2 stays frozen", strerror(errno));
+ }
+ ASSERT_EQ(fstatvfs(rootfd, &sv), 0);
+ ASSERT_FALSE(!!(sv.f_flag & ST_RDONLY)) {
+ TH_LOG("the withdraw set SB_RDONLY on the frozen filesystem");
+ }
+
+ ret = run_child(prefd);
+ if (ret == -ETIMEDOUT)
+ self->wedged = true;
+ ASSERT_EQ(ret, 0) {
+ TH_LOG("%s", ret == -ETIMEDOUT ? "a writer is blocked after thaw" :
+ "a modification did not fail");
+ }
+
+ /* freezing a withdrawn filesystem fails and leaves it thawed */
+ EXPECT_NE(ioctl(rootfd, FIFREEZE, 0), 0);
+ ret = run_child(prefd);
+ if (ret == -ETIMEDOUT)
+ self->wedged = true;
+ ASSERT_EQ(ret, 0);
+
+ close(prefd);
+ close(rootfd);
+ ASSERT_EQ(umount(self->mnt), 0);
+ self->mounted = false;
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/sb_rdonly/settings b/tools/testing/selftests/filesystems/sb_rdonly/settings
new file mode 100644
index 000000000000..694d70710ff0
--- /dev/null
+++ b/tools/testing/selftests/filesystems/sb_rdonly/settings
@@ -0,0 +1 @@
+timeout=300
--
2.53.0
______________________________________________________
Linux MTD discussion mailing list
http://lists.infradead.org/mailman/listinfo/linux-mtd/
next prev parent reply other threads:[~2026-10-29 16:09 UTC|newest]
Thread overview: 49+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-29 16:06 [PATCH v2 00/34] fs: don't set SB_RDONLY after filesystem errors Christian Brauner
2026-10-29 16:06 ` [PATCH v2 01/34] ecryptfs: don't leak the inode when a lookup can't open the lower file Christian Brauner
2026-10-29 16:06 ` [PATCH v2 02/34] ecryptfs: fall back to a read-only " Christian Brauner
2026-10-30 16:09 ` sashiko-bot
2026-10-29 16:06 ` [PATCH v2 03/34] fs: add a generic emergency read-only state Christian Brauner
2026-10-29 16:06 ` [PATCH v2 04/34] ext2: don't set SB_RDONLY after filesystem errors Christian Brauner
2026-10-29 16:06 ` [PATCH v2 05/34] jfs: " Christian Brauner
2026-10-30 16:09 ` sashiko-bot
2026-10-29 16:06 ` [PATCH v2 06/34] nilfs2: " Christian Brauner
2026-10-30 16:09 ` sashiko-bot
2026-10-29 16:06 ` [PATCH v2 07/34] fat: " Christian Brauner
2026-10-30 16:09 ` sashiko-bot
2026-10-29 16:06 ` [PATCH v2 08/34] exfat: keep the current mount options on remount Christian Brauner
2026-10-29 16:07 ` [PATCH v2 09/34] exfat: don't set SB_RDONLY after filesystem errors Christian Brauner
2026-10-30 16:09 ` sashiko-bot
2026-10-29 16:07 ` [PATCH v2 10/34] exfat: use the generic emergency read-only state Christian Brauner
2026-10-29 16:07 ` [PATCH v2 11/34] hfs: don't set SB_RDONLY after MDB write errors Christian Brauner
2026-10-30 16:09 ` sashiko-bot
2026-10-29 16:07 ` [PATCH v2 12/34] hfs: use the generic emergency read-only state Christian Brauner
2026-10-29 16:07 ` [PATCH v2 13/34] hfsplus: don't set SB_RDONLY when creating the attributes tree Christian Brauner
2026-10-29 16:07 ` [PATCH v2 14/34] affs: don't set SB_RDONLY after filesystem errors Christian Brauner
2026-10-29 16:07 ` [PATCH v2 15/34] ufs: " Christian Brauner
2026-10-29 16:07 ` [PATCH v2 16/34] hpfs: " Christian Brauner
2026-10-30 16:10 ` sashiko-bot
2026-10-29 16:07 ` [PATCH v2 17/34] ntfs: " Christian Brauner
2026-10-30 16:10 ` sashiko-bot
2026-10-29 16:07 ` [PATCH v2 18/34] ntfs: use the generic emergency read-only state Christian Brauner
2026-10-29 16:07 ` [PATCH v2 19/34] ocfs2: keep the current mount options on remount Christian Brauner
2026-10-30 2:58 ` Joseph Qi
2026-10-29 16:07 ` [PATCH v2 20/34] ocfs2: don't set SB_RDONLY after filesystem errors Christian Brauner
2026-10-30 3:01 ` Joseph Qi
2026-10-29 16:07 ` [PATCH v2 21/34] zonefs: don't set SB_RDONLY after I/O errors Christian Brauner
2026-10-29 16:07 ` [PATCH v2 22/34] zonefs: use the generic emergency read-only state Christian Brauner
2026-10-29 16:07 ` [PATCH v2 23/34] btrfs: check BTRFS_FS_STATE_RO instead of SB_RDONLY at runtime Christian Brauner
2026-10-29 16:07 ` [PATCH v2 24/34] btrfs: don't set SB_RDONLY after filesystem errors Christian Brauner
2026-10-29 16:07 ` [PATCH v2 25/34] gfs2: wait for a pending withdraw before remounting read-only Christian Brauner
2026-10-30 16:10 ` sashiko-bot
2026-10-29 16:07 ` [PATCH v2 26/34] gfs2: don't set SB_RDONLY on withdraw Christian Brauner
2026-10-30 16:10 ` sashiko-bot
2026-10-29 16:07 ` [PATCH v2 27/34] efivarfs: stop using SB_RDONLY for read-only efivar ops Christian Brauner
2026-10-29 16:07 ` [PATCH v2 28/34] ubifs: don't set SB_RDONLY after errors Christian Brauner
2026-10-30 3:42 ` Zhihao Cheng
2026-10-30 16:10 ` sashiko-bot
2026-10-29 16:07 ` Christian Brauner [this message]
2026-10-29 16:07 ` [PATCH v2 30/34] ext4: allow remounting read-only in the emergency state Christian Brauner
2026-10-29 16:07 ` [PATCH v2 31/34] ext4: use the generic emergency read-only state Christian Brauner
2026-10-29 16:07 ` [PATCH v2 32/34] selftests/filesystems: test ext4 remounts in the emergency state Christian Brauner
2026-10-29 16:07 ` [PATCH v2 33/34] xfs: allow remounting read-only after a shutdown Christian Brauner
2026-10-29 16:07 ` [PATCH v2 34/34] selftests/filesystems: test xfs remounts " Christian Brauner
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=20261029-work-fs-sb_rdonly-v2-29-2f44f64f0b66@kernel.org \
--to=brauner@kernel.org \
--cc=ecryptfs@vger.kernel.org \
--cc=exfat@lists.linux.dev \
--cc=gfs2@lists.linux.dev \
--cc=jack@suse.com \
--cc=jfs-discussion@lists.sourceforge.net \
--cc=linux-btrfs@vger.kernel.org \
--cc=linux-efi@vger.kernel.org \
--cc=linux-ext4@vger.kernel.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=linux-mtd@lists.infradead.org \
--cc=linux-nilfs@vger.kernel.org \
--cc=linux-xfs@vger.kernel.org \
--cc=ntfs@lists.linux.dev \
--cc=ocfs2-devel@lists.linux.dev \
--cc=viro@zeniv.linux.org.uk \
/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