In exfat, an I/O error during sync_blockdev() triggers the error handler
(__exfat_fs_error()), which directly sets SB_RDONLY without holding the
s_umount semaphore. If thaw_super() is called after this, it sees the
filesystem as read-only and skips releasing the freeze semaphores. When the
filesystem is later unmounted and destroyed, the s_writers.rw_sem per-CPU
rw-semaphores are freed while still held for write, triggering a warning in
rcu_sync_dtor():
WARNING: kernel/rcu/sync.c:177 at rcu_sync_dtor+0xcd/0x180
Call Trace:
percpu_free_rwsem+0x43/0x80 kernel/locking/percpu-rwsem.c:42
destroy_super_work+0x217/0x310 fs/super.c:284
process_one_work kernel/workqueue.c:3322 [inline]
process_scheduled_works+0xa8e/0x14e0 kernel/workqueue.c:3405
worker_thread+0xa47/0xfb0 kernel/workqueue.c:3486
kthread+0x388/0x470 kernel/kthread.c:436
ret_from_fork+0x514/0xb70 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
Fix this by introducing an internal EXFAT_FLAGS_ERROR_RO flag instead of
directly setting SB_RDONLY on error. This prevents confusing the VFS freeze
logic and allows thaw_super() to correctly release the semaphores. We also
add the exfat_check_writable() helper to check both EXFAT_FLAGS_SHUTDOWN
and EXFAT_FLAGS_ERROR_RO, and use it to reject write operations when the
filesystem is in an error-induced read-only state.
Fixes: 772b29cca528 ("exfat: add misc operations")
Assisted-by: Gemini:gemini-3.5-flash Gemini:gemini-3.1-pro-preview syzbot
Reported-by: syzbot+823cd0d24881f21ab9f1@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=823cd0d24881f21ab9f1
Link: https://syzkaller.appspot.com/ai_job?id=7b3f57df-f2f0-4b0d-a492-627c4efe465b
To: "Namjae Jeon"
To:
To: "Sungjong Seo"
To: "Namjae Jeon"
Cc:
Cc: "Yuezhang Mo"
---
v2:
- Dropped the fs/super.c changes to handle rollback of freeze on sync_blockdev failure.
- Fixed the issue entirely inside exfat by introducing an internal EXFAT_FLAGS_ERROR_RO flag instead of directly setting SB_RDONLY.
- Added exfat_check_writable() helper to check both EXFAT_FLAGS_SHUTDOWN and EXFAT_FLAGS_ERROR_RO.
- Updated write paths in exfat to use exfat_check_writable().
- Cleared EXFAT_FLAGS_ERROR_RO in exfat_reconfigure() when remounting read-write.
v1:
https://lore.kernel.org/all/fcc77fd9-e38f-49f9-8268-e86683444f29@mail.kernel.org/T/
---
diff --git a/fs/exfat/exfat_fs.h b/fs/exfat/exfat_fs.h
index 9be50949c..50846630e 100644
--- a/fs/exfat/exfat_fs.h
+++ b/fs/exfat/exfat_fs.h
@@ -122,6 +122,7 @@ enum {
/* Superblock flags */
#define EXFAT_FLAGS_SHUTDOWN 1
+#define EXFAT_FLAGS_ERROR_RO 2
struct exfat_dentry_namebuf {
char *lfn;
@@ -320,6 +321,15 @@ static inline int exfat_forced_shutdown(struct super_block *sb)
return test_bit(EXFAT_FLAGS_SHUTDOWN, &EXFAT_SB(sb)->s_exfat_flags);
}
+static inline int exfat_check_writable(struct super_block *sb)
+{
+ if (unlikely(exfat_forced_shutdown(sb)))
+ return -EIO;
+ if (unlikely(test_bit(EXFAT_FLAGS_ERROR_RO, &EXFAT_SB(sb)->s_exfat_flags)))
+ return -EROFS;
+ return 0;
+}
+
/*
* If ->i_mode can't hold 0222 (i.e. ATTR_RO), we use ->i_attrs to
* save ATTR_RO instead of ->i_mode.
diff --git a/fs/exfat/file.c b/fs/exfat/file.c
index 5fc13378d..b6afeede2 100644
--- a/fs/exfat/file.c
+++ b/fs/exfat/file.c
@@ -125,8 +125,9 @@ static long exfat_fallocate(struct file *file, int mode,
if (!S_ISREG(inode->i_mode))
return -EOPNOTSUPP;
- if (unlikely(exfat_forced_shutdown(inode->i_sb)))
- return -EIO;
+ err = exfat_check_writable(inode->i_sb);
+ if (err)
+ return err;
inode_lock(inode);
@@ -354,8 +355,9 @@ int exfat_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
unsigned int ia_valid;
int error;
- if (unlikely(exfat_forced_shutdown(inode->i_sb)))
- return -EIO;
+ error = exfat_check_writable(inode->i_sb);
+ if (error)
+ return error;
if ((attr->ia_valid & ATTR_SIZE) &&
attr->ia_size > i_size_read(inode)) {
@@ -738,8 +740,9 @@ static ssize_t exfat_file_write_iter(struct kiocb *iocb, struct iov_iter *iter)
loff_t valid_size;
int err;
- if (unlikely(exfat_forced_shutdown(inode->i_sb)))
- return -EIO;
+ ret = exfat_check_writable(inode->i_sb);
+ if (ret < 0)
+ return ret;
inode_lock(inode);
@@ -826,6 +829,11 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
struct exfat_inode_info *ei = EXFAT_I(inode);
vm_fault_t ret;
loff_t new_valid_size, mmap_valid_size;
+ int err;
+
+ err = exfat_check_writable(inode->i_sb);
+ if (err)
+ return vmf_fs_error(err);
if (!inode_trylock(inode))
return VM_FAULT_RETRY;
@@ -835,8 +843,6 @@ static vm_fault_t exfat_page_mkwrite(struct vm_fault *vmf)
if (ei->valid_size < new_valid_size) {
if (ei->zeroed_size < mmap_valid_size) {
- int err;
-
/*
* Only zero the range that hasn't been zeroed yet for
* this mmap write path. zeroed_size tracks the largest
diff --git a/fs/exfat/misc.c b/fs/exfat/misc.c
index 6f11a96a4..ab875473f 100644
--- a/fs/exfat/misc.c
+++ b/fs/exfat/misc.c
@@ -41,8 +41,8 @@ void __exfat_fs_error(struct super_block *sb, int report, const char *fmt, ...)
panic("exFAT-fs (%s): fs panic from previous error\n",
sb->s_id);
} else if (opts->errors == EXFAT_ERRORS_RO && !sb_rdonly(sb)) {
- sb->s_flags |= SB_RDONLY;
- exfat_err(sb, "Filesystem has been set read-only");
+ if (!test_and_set_bit(EXFAT_FLAGS_ERROR_RO, &EXFAT_SB(sb)->s_exfat_flags))
+ exfat_err(sb, "Filesystem has been set read-only");
}
}
diff --git a/fs/exfat/namei.c b/fs/exfat/namei.c
index b7d5e44ad..f08170de6 100644
--- a/fs/exfat/namei.c
+++ b/fs/exfat/namei.c
@@ -547,8 +547,9 @@ static int exfat_create(struct mnt_idmap *idmap, struct inode *dir,
int err;
loff_t size = i_size_read(dir);
- if (unlikely(exfat_forced_shutdown(sb)))
- return -EIO;
+ err = exfat_check_writable(sb);
+ if (err)
+ return err;
mutex_lock(&EXFAT_SB(sb)->s_lock);
exfat_set_volume_dirty(sb);
@@ -765,10 +766,11 @@ static int exfat_unlink(struct inode *dir, struct dentry *dentry)
struct inode *inode = dentry->d_inode;
struct exfat_inode_info *ei = EXFAT_I(inode);
struct exfat_entry_set_cache es;
- int err = 0;
+ int err;
- if (unlikely(exfat_forced_shutdown(sb)))
- return -EIO;
+ err = exfat_check_writable(sb);
+ if (err)
+ return err;
mutex_lock(&EXFAT_SB(sb)->s_lock);
if (ei->dir.dir == DIR_DELETED) {
@@ -820,8 +822,9 @@ static struct dentry *exfat_mkdir(struct mnt_idmap *idmap, struct inode *dir,
int err;
loff_t size = i_size_read(dir);
- if (unlikely(exfat_forced_shutdown(sb)))
- return ERR_PTR(-EIO);
+ err = exfat_check_writable(sb);
+ if (err)
+ return ERR_PTR(err);
mutex_lock(&EXFAT_SB(sb)->s_lock);
exfat_set_volume_dirty(sb);
@@ -911,8 +914,9 @@ static int exfat_rmdir(struct inode *dir, struct dentry *dentry)
struct exfat_entry_set_cache es;
int err;
- if (unlikely(exfat_forced_shutdown(sb)))
- return -EIO;
+ err = exfat_check_writable(sb);
+ if (err)
+ return err;
mutex_lock(&EXFAT_SB(inode->i_sb)->s_lock);
@@ -995,9 +999,6 @@ static int exfat_rename_file(struct inode *parent_inode,
int sync = IS_DIRSYNC(parent_inode);
unsigned int num_extra_entries, num_total_entries;
- if (unlikely(exfat_forced_shutdown(sb)))
- return -EIO;
-
num_new_entries = exfat_calc_num_entries(p_uniname);
if (num_new_entries < 0)
return num_new_entries;
@@ -1267,6 +1268,10 @@ static int exfat_rename(struct mnt_idmap *idmap,
if (flags & ~RENAME_NOREPLACE)
return -EINVAL;
+ err = exfat_check_writable(sb);
+ if (err)
+ return err;
+
mutex_lock(&EXFAT_SB(sb)->s_lock);
old_inode = old_dentry->d_inode;
new_inode = new_dentry->d_inode;
diff --git a/fs/exfat/super.c b/fs/exfat/super.c
index 388db271c..dcb6e15e4 100644
--- a/fs/exfat/super.c
+++ b/fs/exfat/super.c
@@ -807,6 +807,9 @@ static int exfat_reconfigure(struct fs_context *fc)
swap(*cur_opts, *new_opts);
+ if (!(fc->sb_flags & SB_RDONLY))
+ clear_bit(EXFAT_FLAGS_ERROR_RO, &sbi->s_exfat_flags);
+
return 0;
}
base-commit: 1590cf0329716306e948a8fc29f1d3ee87d3989f
--
This is an AI-generated patch subject to moderation.
Reply with '#syz upstream' to Sign-off the patch as a human author
and send it to the upstream kernel mailing lists.
Reply with '#syz reject' to reject it ('#syz unreject' to undo).
See https://goo.gle/syzbot-ai-patches for information about AI-generated patches.
The person who has signed off on the patch is responsible for
addressing comments.
syzbot engineers can be reached at syzkaller@googlegroups.com.