[<prev] [next>] [day] [month] [year] [list]
Message-ID: <CALf2hKtp5SQCAzjkY8UvKU6Qqq4Qt=ZSjN18WK_BU==v4JOLuA@mail.gmail.com>
Date: Tue, 30 Dec 2025 02:18:17 +0800
From: Zhiyu Zhang <zhiyuzhang999@...il.com>
To: viro@...iv.linux.org.uk, brauner@...nel.org, Jan Kara <jack@...e.cz>,
hirofumi@...l.parknet.co.jp, linux-fsdevel@...r.kernel.org,
syzkaller <syzkaller@...glegroups.com>, linux-kernel@...r.kernel.org
Subject: [Kernel Bug] WARNING in vfat_rmdir
Dear Linux kernel developers and maintainers,
We would like to report a filesystem corruption triggered bug that
causes a WARNING in drop_nlink() from the VFAT rmdir path, and leads
to a kernel panic when panic_on_warn is enabled. The bug titled
"WARNING in vfat_rmdir" was found on linux-6.17.1 and is also
reproducible on the latest 6.19-rc3.
The possible root cause is that the FAT directory iteration helpers
conflate real errors with "end of directory" in a way that hides
corruption from higher layers. Concretely, fat__get_entry() returns -1
both when it reaches EOF (!phys) and when fat_bmap() fails due to a
corrupted cluster chain (err != 0). Then fat_get_short_entry() treats
any < 0 from fat_get_entry() as "no more entries" and returns -ENOENT.
As a result, callers such as fat_dir_empty() and fat_subdirs() cannot
distinguish a genuinely empty directory from a directory walk that
terminates early due to corruption. In this situation, fat_dir_empty()
may incorrectly return success (empty), allowing vfat_rmdir() to
proceed with metadata updates, including drop_nlink(dir). Separately,
fat_subdirs() may silently "succeed" with an incorrect count (e.g., 0)
when the walk is cut short, which can further poison in-memory link
counts when inodes are built from corrupted on-disk state. Eventually,
the VFAT rmdir path can reach drop_nlink() with an already-zero
i_nlink, triggering WARN_ON(inode->i_nlink == 0) and panicking under
panic_on_warn.
This bug may lead to denial-of-service on systems that enable
panic_on_warn, and more broadly to inconsistent in-memory metadata
updates when operating on corrupted VFAT images.
We suggest the following potential patch:
(1) Propagate real errors from the directory iteration path instead of
folding them into -ENOENT and make fat_get_short_entry() translate
only true EOF into -ENOENT while propagating other negative errors.
(2) Update fat_dir_empty() / fat_subdirs() to treat propagated errors
as failures rather than "empty" / a weird count, and handle negative
returns at their call sites.
diff --git a/fs/fat/dir.c b/fs/fat/dir.c
index 92b091783966..f4c5a6f0cc84 100644
--- a/fs/fat/dir.c
+++ b/fs/fat/dir.c
@@ -92,8 +92,10 @@ static int fat__get_entry(struct inode *dir, loff_t *pos,
*bh = NULL;
iblock = *pos >> sb->s_blocksize_bits;
err = fat_bmap(dir, iblock, &phys, &mapped_blocks, 0, false);
- if (err || !phys)
- return -1; /* beyond EOF or error */
+ if (err)
+ return err; /* real error (e.g., -EIO, -EUCLEAN) */
+ if (!phys)
+ return -1; /* beyond EOF */
fat_dir_readahead(dir, iblock, phys);
@@ -882,12 +884,14 @@ static int fat_get_short_entry(struct inode
*dir, loff_t *pos,
struct buffer_head **bh,
struct msdos_dir_entry **de)
{
- while (fat_get_entry(dir, pos, bh, de) >= 0) {
+ int err;
+ while ((err = fat_get_entry(dir, pos, bh, de)) >= 0) {
/* free entry or long name entry or volume label */
if (!IS_FREE((*de)->name) && !((*de)->attr & ATTR_VOLUME))
return 0;
}
- return -ENOENT;
+ /* -1 is EOF sentinel; propagate other errors */
+ return (err == -1) ? -ENOENT : err;
}
/*
@@ -919,11 +923,11 @@ int fat_dir_empty(struct inode *dir)
struct buffer_head *bh;
struct msdos_dir_entry *de;
loff_t cpos;
- int result = 0;
+ int result = 0, err;
bh = NULL;
cpos = 0;
- while (fat_get_short_entry(dir, &cpos, &bh, &de) >= 0) {
+ while ((err = fat_get_short_entry(dir, &cpos, &bh, &de)) >= 0) {
if (strncmp(de->name, MSDOS_DOT , MSDOS_NAME) &&
strncmp(de->name, MSDOS_DOTDOT, MSDOS_NAME)) {
result = -ENOTEMPTY;
@@ -931,6 +935,8 @@ int fat_dir_empty(struct inode *dir)
}
}
brelse(bh);
+ if (err < 0 && err != -ENOENT)
+ return err;
return result;
}
EXPORT_SYMBOL_GPL(fat_dir_empty);
@@ -944,15 +950,17 @@ int fat_subdirs(struct inode *dir)
struct buffer_head *bh;
struct msdos_dir_entry *de;
loff_t cpos;
- int count = 0;
+ int count = 0, err;
bh = NULL;
cpos = 0;
- while (fat_get_short_entry(dir, &cpos, &bh, &de) >= 0) {
+ while ((err = fat_get_short_entry(dir, &cpos, &bh, &de)) >= 0) {
if (de->attr & ATTR_DIR)
count++;
}
brelse(bh);
+ if (err < 0 && err != -ENOENT)
+ return err;
return count;
}
diff --git a/fs/fat/inode.c b/fs/fat/inode.c
index 0b6009cd1844..36ec8901253e 100644
--- a/fs/fat/inode.c
+++ b/fs/fat/inode.c
@@ -535,7 +535,10 @@ int fat_fill_inode(struct inode *inode, struct
msdos_dir_entry *de)
return error;
MSDOS_I(inode)->mmu_private = inode->i_size;
- set_nlink(inode, fat_subdirs(inode));
+ int nsubs = fat_subdirs(inode);
+ if (nsubs < 0)
+ return nsubs;
+ set_nlink(inode, nsubs);
error = fat_validate_dir(inode);
if (error < 0)
@@ -1345,7 +1348,10 @@ static int fat_read_root(struct inode *inode)
fat_save_attrs(inode, ATTR_DIR);
inode_set_mtime_to_ts(inode,
inode_set_atime_to_ts(inode,
inode_set_ctime(inode, 0, 0)));
- set_nlink(inode, fat_subdirs(inode)+2);
+ int nsubs = fat_subdirs(inode);
+ if (nsubs < 0)
+ return nsubs;
+ set_nlink(inode, nsubs+2);
return 0;
}
If the approach above is acceptable, we are willing to submit a proper
patch immediately. Please let us know if any further information is
required.
Best regards,
Zhiyu Zhang
View attachment "repro.c" of type "text/plain" (49535 bytes)
Download attachment "report0" of type "application/octet-stream" (6031 bytes)
Download attachment "repro.log" of type "application/octet-stream" (109422 bytes)
Download attachment "repro.syz" of type "application/octet-stream" (7276 bytes)
Download attachment "6.19-rc1.config" of type "application/xml" (278471 bytes)
Download attachment "6.17.0.config" of type "application/xml" (275784 bytes)
Powered by blists - more mailing lists