mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v3] fat: stop reading directory entries past the end-of-directory marker
@ 2026-06-13 17:28 Matteo Croce
  2026-06-16 10:59 ` OGAWA Hirofumi
  0 siblings, 1 reply; 5+ messages in thread
From: Matteo Croce @ 2026-06-13 17:28 UTC (permalink / raw)
  To: OGAWA Hirofumi
  Cc: Timothy Redaelli, linux-fsdevel, linux-kernel, Matteo Croce,
	Matteo Croce

The FAT specification[1] (FAT Directory Structure -> "DIR_Name[0]") states:

    If DIR_Name[0] == 0x00, then the directory entry is free (same as for
    0xE5), and there are no allocated directory entries after this one
    (all of the DIR_Name[0] bytes in all of the entries after this one
    are also set to 0).

    The special 0 value, rather than the 0xE5 value, indicates to FAT
    file system driver code that the rest of the entries in this
    directory do not need to be examined because they are all free.

Linux did not honour this. fat_get_entry() kept advancing past the 0x00
terminator; if the trailing on-disk slots were not zero-filled (buggy
formatters, read-only media written by other operating systems, on-disk
corruption) the driver surfaced arbitrary bytes as real directory
entries. On a typical affected image, `ls /mnt` returns ~150 bogus
entries with random binary names, multi-gigabyte sizes, dates ranging
from 1980 to 2106, and a flood of -EIO from stat().

Earlier attempts (v1..v3, see [2][3][4]) added `de->name[0] == 0` guards
at each call site. As Hirofumi pointed out on v3, those guards reject
the entry but fat_get_entry() has already advanced *pos past it; the
next readdir() resumes after the marker and walks straight back into
the garbage. His suggestion was to centralise the check.

This patch:

  * Adds fat_get_entry_eod(), a small wrapper around fat_get_entry()
    that returns -1 when name[0] == 0 and seeks *pos to dir->i_size.
    Per spec every slot after the 0x00 marker is also zero, so jumping
    to the end of the directory is correct: subsequent reads return -1
    from fat_bmap() without re-fetching trailing zero slots, and
    callers persisting *pos across invocations (notably readdir's
    ctx->pos) keep reporting end-of-directory on re-entry.

  * Converts the read/search paths to use the new wrapper:
      fat_parse_long(), fat_search_long(), __fat_readdir(),
      and fat_get_short_entry() -- the last covers
      fat_get_dotdot_entry(), fat_dir_empty(), fat_subdirs(),
      fat_scan(), and fat_scan_logstart() transitively.

  * Leaves fat_add_entries() and __fat_remove_entries() on raw
    fat_get_entry(): the write paths legitimately need to operate on
    free/zero slots. fat_add_entries() additionally detects an
    allocated entry past a 0x00 marker (the spec violation that
    produces the garbage) and treats it as filesystem corruption:
    fat_fs_error_ratelimit() is called -- which honours the configured
    errors= mount option (panic / remount-ro / continue) -- and the
    operation returns -EIO so we don't write fresh entries into an
    already-corrupt directory.

[1] https://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/fatgen103.doc
[2] https://lore.kernel.org/lkml/20181207013410.7050-1-mcroce@redhat.com/
[3] https://lore.kernel.org/lkml/20181216231510.26854-1-mcroce@redhat.com/
[4] https://lore.kernel.org/lkml/20190201001408.7453-1-mcroce@redhat.com/

Reported-by: Timothy Redaelli <tredaelli@redhat.com>
Suggested-by: OGAWA Hirofumi <hirofumi@mail.parknet.co.jp>
Signed-off-by: Matteo Croce <teknoraver@meta.com>
---
 fs/fat/dir.c | 41 +++++++++++++++++++++++++++++++++++++----
 1 file changed, 37 insertions(+), 4 deletions(-)

diff --git a/fs/fat/dir.c b/fs/fat/dir.c
index 4f6f42f33613..c7d0bc5de697 100644
--- a/fs/fat/dir.c
+++ b/fs/fat/dir.c
@@ -130,6 +130,28 @@ static inline int fat_get_entry(struct inode *dir, loff_t *pos,
 	return fat__get_entry(dir, pos, bh, de);
 }
 
+/*
+ * Like fat_get_entry(), but honour the FAT end-of-directory marker:
+ * a dirent whose first name byte is NUL terminates iteration per the
+ * spec, which also guarantees that every following slot is zeroed.
+ * Skip straight to the end of the directory so the next call returns
+ * -1 from fat_bmap() without re-reading the trailing zero slots, and
+ * so callers that persist *pos across invocations (e.g. readdir's
+ * ctx->pos) keep reporting EOD.
+ */
+static int fat_get_entry_eod(struct inode *dir, loff_t *pos,
+			     struct buffer_head **bh,
+			     struct msdos_dir_entry **de)
+{
+	int err = fat_get_entry(dir, pos, bh, de);
+
+	if (err == 0 && (*de)->name[0] == 0) {
+		*pos = dir->i_size;
+		return -1;
+	}
+	return err;
+}
+
 /*
  * Convert Unicode 16 to UTF-8, translated Unicode, or ASCII.
  * If uni_xlate is enabled and we can't get a 1:1 conversion, use a
@@ -327,7 +349,7 @@ static int fat_parse_long(struct inode *dir, loff_t *pos,
 
 		if (ds->id & 0x40)
 			(*unicode)[offset + 13] = 0;
-		if (fat_get_entry(dir, pos, bh, de) < 0)
+		if (fat_get_entry_eod(dir, pos, bh, de) < 0)
 			return PARSE_EOF;
 		if (slot == 0)
 			break;
@@ -489,7 +511,7 @@ int fat_search_long(struct inode *inode, const unsigned char *name,
 
 	err = -ENOENT;
 	while (1) {
-		if (fat_get_entry(inode, &cpos, &bh, &de) == -1)
+		if (fat_get_entry_eod(inode, &cpos, &bh, &de) == -1)
 			goto end_of_dir;
 parse_record:
 		nr_slots = 0;
@@ -601,7 +623,7 @@ static int __fat_readdir(struct inode *inode, struct file *file,
 
 	bh = NULL;
 get_new:
-	if (fat_get_entry(inode, &cpos, &bh, &de) == -1)
+	if (fat_get_entry_eod(inode, &cpos, &bh, &de) == -1)
 		goto end_of_dir;
 parse_record:
 	nr_slots = 0;
@@ -885,7 +907,7 @@ 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) {
+	while (fat_get_entry_eod(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;
@@ -1302,6 +1324,7 @@ int fat_add_entries(struct inode *dir, void *slots, int nr_slots,
 	struct msdos_dir_entry *de;
 	int err, free_slots, i, nr_bhs;
 	loff_t pos;
+	bool saw_eod;
 
 	sinfo->nr_slots = nr_slots;
 
@@ -1310,12 +1333,15 @@ int fat_add_entries(struct inode *dir, void *slots, int nr_slots,
 	bh = prev = NULL;
 	pos = 0;
 	err = -ENOSPC;
+	saw_eod = false;
 	while (fat_get_entry(dir, &pos, &bh, &de) > -1) {
 		/* check the maximum size of directory */
 		if (pos >= FAT_MAX_DIR_SIZE)
 			goto error;
 
 		if (IS_FREE(de->name)) {
+			if (de->name[0] == 0)
+				saw_eod = true;
 			if (prev != bh) {
 				get_bh(bh);
 				bhs[nr_bhs] = prev = bh;
@@ -1325,6 +1351,13 @@ int fat_add_entries(struct inode *dir, void *slots, int nr_slots,
 			if (free_slots == nr_slots)
 				goto found;
 		} else {
+			if (saw_eod) {
+				fat_fs_error_ratelimit(sb,
+					"allocated dir entry found after end-of-directory marker (i_pos %lld)",
+					MSDOS_I(dir)->i_pos);
+				err = -EIO;
+				goto error;
+			}
 			for (i = 0; i < nr_bhs; i++)
 				brelse(bhs[i]);
 			prev = NULL;
-- 
2.50.1


^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH v3] fat: stop reading directory entries past the end-of-directory marker
  2026-06-13 17:28 [PATCH v3] fat: stop reading directory entries past the end-of-directory marker Matteo Croce
@ 2026-06-16 10:59 ` OGAWA Hirofumi
  2026-06-16 14:54   ` OGAWA Hirofumi
  0 siblings, 1 reply; 5+ messages in thread
From: OGAWA Hirofumi @ 2026-06-16 10:59 UTC (permalink / raw)
  To: Andrew Morton
  Cc: Matteo Croce, Timothy Redaelli, linux-fsdevel, linux-kernel,
	Matteo Croce

Matteo Croce <technoboy85@gmail.com> writes:

> The FAT specification[1] (FAT Directory Structure -> "DIR_Name[0]") states:
>
>     If DIR_Name[0] == 0x00, then the directory entry is free (same as for
>     0xE5), and there are no allocated directory entries after this one
>     (all of the DIR_Name[0] bytes in all of the entries after this one
>     are also set to 0).
>
>     The special 0 value, rather than the 0xE5 value, indicates to FAT
>     file system driver code that the rest of the entries in this
>     directory do not need to be examined because they are all free.
>
> Linux did not honour this. fat_get_entry() kept advancing past the 0x00
> terminator; if the trailing on-disk slots were not zero-filled (buggy
> formatters, read-only media written by other operating systems, on-disk
> corruption) the driver surfaced arbitrary bytes as real directory
> entries. On a typical affected image, `ls /mnt` returns ~150 bogus
> entries with random binary names, multi-gigabyte sizes, dates ranging
> from 1980 to 2106, and a flood of -EIO from stat().
>
> Earlier attempts (v1..v3, see [2][3][4]) added `de->name[0] == 0` guards
> at each call site. As Hirofumi pointed out on v3, those guards reject
> the entry but fat_get_entry() has already advanced *pos past it; the
> next readdir() resumes after the marker and walks straight back into
> the garbage. His suggestion was to centralise the check.
>
> This patch:
>
>   * Adds fat_get_entry_eod(), a small wrapper around fat_get_entry()
>     that returns -1 when name[0] == 0 and seeks *pos to dir->i_size.
>     Per spec every slot after the 0x00 marker is also zero, so jumping
>     to the end of the directory is correct: subsequent reads return -1
>     from fat_bmap() without re-fetching trailing zero slots, and
>     callers persisting *pos across invocations (notably readdir's
>     ctx->pos) keep reporting end-of-directory on re-entry.
>
>   * Converts the read/search paths to use the new wrapper:
>       fat_parse_long(), fat_search_long(), __fat_readdir(),
>       and fat_get_short_entry() -- the last covers
>       fat_get_dotdot_entry(), fat_dir_empty(), fat_subdirs(),
>       fat_scan(), and fat_scan_logstart() transitively.
>
>   * Leaves fat_add_entries() and __fat_remove_entries() on raw
>     fat_get_entry(): the write paths legitimately need to operate on
>     free/zero slots. fat_add_entries() additionally detects an
>     allocated entry past a 0x00 marker (the spec violation that
>     produces the garbage) and treats it as filesystem corruption:
>     fat_fs_error_ratelimit() is called -- which honours the configured
>     errors= mount option (panic / remount-ro / continue) -- and the
>     operation returns -EIO so we don't write fresh entries into an
>     already-corrupt directory.
>
> [1] https://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/fatgen103.doc
> [2] https://lore.kernel.org/lkml/20181207013410.7050-1-mcroce@redhat.com/
> [3] https://lore.kernel.org/lkml/20181216231510.26854-1-mcroce@redhat.com/
> [4] https://lore.kernel.org/lkml/20190201001408.7453-1-mcroce@redhat.com/
>
> Reported-by: Timothy Redaelli <tredaelli@redhat.com>
> Suggested-by: OGAWA Hirofumi <hirofumi@mail.parknet.co.jp>
> Signed-off-by: Matteo Croce <teknoraver@meta.com>

There are some strange behavior though (e.g. seek after name[0] == 0,
then readdir()), the strange should only be on already a corrupted image.

Well, if we found a serious issue with this, there is possibility to
revert later though. Looks good for me.

Acked-by: OGAWA Hirofumi <hirofumi@mail.parknet.co.jp>

> ---
>  fs/fat/dir.c | 41 +++++++++++++++++++++++++++++++++++++----
>  1 file changed, 37 insertions(+), 4 deletions(-)
>
> diff --git a/fs/fat/dir.c b/fs/fat/dir.c
> index 4f6f42f33613..c7d0bc5de697 100644
> --- a/fs/fat/dir.c
> +++ b/fs/fat/dir.c
> @@ -130,6 +130,28 @@ static inline int fat_get_entry(struct inode *dir, loff_t *pos,
>  	return fat__get_entry(dir, pos, bh, de);
>  }
>  
> +/*
> + * Like fat_get_entry(), but honour the FAT end-of-directory marker:
> + * a dirent whose first name byte is NUL terminates iteration per the
> + * spec, which also guarantees that every following slot is zeroed.
> + * Skip straight to the end of the directory so the next call returns
> + * -1 from fat_bmap() without re-reading the trailing zero slots, and
> + * so callers that persist *pos across invocations (e.g. readdir's
> + * ctx->pos) keep reporting EOD.
> + */
> +static int fat_get_entry_eod(struct inode *dir, loff_t *pos,
> +			     struct buffer_head **bh,
> +			     struct msdos_dir_entry **de)
> +{
> +	int err = fat_get_entry(dir, pos, bh, de);
> +
> +	if (err == 0 && (*de)->name[0] == 0) {
> +		*pos = dir->i_size;
> +		return -1;
> +	}
> +	return err;
> +}
> +
>  /*
>   * Convert Unicode 16 to UTF-8, translated Unicode, or ASCII.
>   * If uni_xlate is enabled and we can't get a 1:1 conversion, use a
> @@ -327,7 +349,7 @@ static int fat_parse_long(struct inode *dir, loff_t *pos,
>  
>  		if (ds->id & 0x40)
>  			(*unicode)[offset + 13] = 0;
> -		if (fat_get_entry(dir, pos, bh, de) < 0)
> +		if (fat_get_entry_eod(dir, pos, bh, de) < 0)
>  			return PARSE_EOF;
>  		if (slot == 0)
>  			break;
> @@ -489,7 +511,7 @@ int fat_search_long(struct inode *inode, const unsigned char *name,
>  
>  	err = -ENOENT;
>  	while (1) {
> -		if (fat_get_entry(inode, &cpos, &bh, &de) == -1)
> +		if (fat_get_entry_eod(inode, &cpos, &bh, &de) == -1)
>  			goto end_of_dir;
>  parse_record:
>  		nr_slots = 0;
> @@ -601,7 +623,7 @@ static int __fat_readdir(struct inode *inode, struct file *file,
>  
>  	bh = NULL;
>  get_new:
> -	if (fat_get_entry(inode, &cpos, &bh, &de) == -1)
> +	if (fat_get_entry_eod(inode, &cpos, &bh, &de) == -1)
>  		goto end_of_dir;
>  parse_record:
>  	nr_slots = 0;
> @@ -885,7 +907,7 @@ 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) {
> +	while (fat_get_entry_eod(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;
> @@ -1302,6 +1324,7 @@ int fat_add_entries(struct inode *dir, void *slots, int nr_slots,
>  	struct msdos_dir_entry *de;
>  	int err, free_slots, i, nr_bhs;
>  	loff_t pos;
> +	bool saw_eod;
>  
>  	sinfo->nr_slots = nr_slots;
>  
> @@ -1310,12 +1333,15 @@ int fat_add_entries(struct inode *dir, void *slots, int nr_slots,
>  	bh = prev = NULL;
>  	pos = 0;
>  	err = -ENOSPC;
> +	saw_eod = false;
>  	while (fat_get_entry(dir, &pos, &bh, &de) > -1) {
>  		/* check the maximum size of directory */
>  		if (pos >= FAT_MAX_DIR_SIZE)
>  			goto error;
>  
>  		if (IS_FREE(de->name)) {
> +			if (de->name[0] == 0)
> +				saw_eod = true;
>  			if (prev != bh) {
>  				get_bh(bh);
>  				bhs[nr_bhs] = prev = bh;
> @@ -1325,6 +1351,13 @@ int fat_add_entries(struct inode *dir, void *slots, int nr_slots,
>  			if (free_slots == nr_slots)
>  				goto found;
>  		} else {
> +			if (saw_eod) {
> +				fat_fs_error_ratelimit(sb,
> +					"allocated dir entry found after end-of-directory marker (i_pos %lld)",
> +					MSDOS_I(dir)->i_pos);
> +				err = -EIO;
> +				goto error;
> +			}
>  			for (i = 0; i < nr_bhs; i++)
>  				brelse(bhs[i]);
>  			prev = NULL;

-- 
OGAWA Hirofumi <hirofumi@mail.parknet.co.jp>

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH v3] fat: stop reading directory entries past the end-of-directory marker
  2026-06-16 10:59 ` OGAWA Hirofumi
@ 2026-06-16 14:54   ` OGAWA Hirofumi
  2026-06-16 15:27     ` Matteo Croce
  0 siblings, 1 reply; 5+ messages in thread
From: OGAWA Hirofumi @ 2026-06-16 14:54 UTC (permalink / raw)
  To: Matteo Croce
  Cc: Andrew Morton, Timothy Redaelli, linux-fsdevel, linux-kernel,
	Matteo Croce

OGAWA Hirofumi <hirofumi@mail.parknet.co.jp> writes:

>
> There are some strange behavior though (e.g. seek after name[0] == 0,
> then readdir()), the strange should only be on already a corrupted image.
>
> Well, if we found a serious issue with this, there is possibility to
> revert later though. Looks good for me.
>
> Acked-by: OGAWA Hirofumi <hirofumi@mail.parknet.co.jp>
>

[...]

>> +static int fat_get_entry_eod(struct inode *dir, loff_t *pos,
>> +			     struct buffer_head **bh,
>> +			     struct msdos_dir_entry **de)
>> +{
>> +	int err = fat_get_entry(dir, pos, bh, de);
>> +
>> +	if (err == 0 && (*de)->name[0] == 0) {
>> +		*pos = dir->i_size;

I re-reviewed this part. Since here is failure case, so we have to
release *bh, isn't it?

		brelse(*bh);
                *bh = NULL;

>> +		return -1;
>> +	}
>> +	return err;
-- 
OGAWA Hirofumi <hirofumi@mail.parknet.co.jp>

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH v3] fat: stop reading directory entries past the end-of-directory marker
  2026-06-16 14:54   ` OGAWA Hirofumi
@ 2026-06-16 15:27     ` Matteo Croce
  2026-06-16 15:36       ` OGAWA Hirofumi
  0 siblings, 1 reply; 5+ messages in thread
From: Matteo Croce @ 2026-06-16 15:27 UTC (permalink / raw)
  To: OGAWA Hirofumi
  Cc: Andrew Morton, Timothy Redaelli, linux-fsdevel, linux-kernel,
	Matteo Croce

Il giorno mar 16 giu 2026 alle ore 16:54 OGAWA Hirofumi
<hirofumi@mail.parknet.co.jp> ha scritto:
> I re-reviewed this part. Since here is failure case, so we have to
> release *bh, isn't it?
>
>                 brelse(*bh);
>                 *bh = NULL;
>
> >> +            return -1;
> >> +    }
> >> +    return err;
> --

Oops, right.
Can I send a v4 or it was already merged somewhere?

Thanks,
-- 
Matteo Croce

perl -e 'for($t=0;;$t++){print chr($t*($t>>8|$t>>13)&255)}' |aplay

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH v3] fat: stop reading directory entries past the end-of-directory marker
  2026-06-16 15:27     ` Matteo Croce
@ 2026-06-16 15:36       ` OGAWA Hirofumi
  0 siblings, 0 replies; 5+ messages in thread
From: OGAWA Hirofumi @ 2026-06-16 15:36 UTC (permalink / raw)
  To: Matteo Croce
  Cc: Andrew Morton, Timothy Redaelli, linux-fsdevel, linux-kernel,
	Matteo Croce

Matteo Croce <technoboy85@gmail.com> writes:

> Il giorno mar 16 giu 2026 alle ore 16:54 OGAWA Hirofumi
> <hirofumi@mail.parknet.co.jp> ha scritto:
>> I re-reviewed this part. Since here is failure case, so we have to
>> release *bh, isn't it?
>>
>>                 brelse(*bh);
>>                 *bh = NULL;
>>
>> >> +            return -1;
>> >> +    }
>> >> +    return err;
>> --
>
> Oops, right.
> Can I send a v4 or it was already merged somewhere?
>
> Thanks,

Maybe it is not merged yet. Please send a v4.

Thanks.
-- 
OGAWA Hirofumi <hirofumi@mail.parknet.co.jp>

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-06-16 15:36 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-13 17:28 [PATCH v3] fat: stop reading directory entries past the end-of-directory marker Matteo Croce
2026-06-16 10:59 ` OGAWA Hirofumi
2026-06-16 14:54   ` OGAWA Hirofumi
2026-06-16 15:27     ` Matteo Croce
2026-06-16 15:36       ` OGAWA Hirofumi

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

Powered by JetHome