[PATCH AUTOSEL 6.18] fat: stop reading directory entries past the end-of-directory marker

From: Sasha Levin

Date: Mon Aug 31 2026 - 11:13:40 EST


From: Matteo Croce <technoboy85@xxxxxxxxx>

[ Upstream commit 6a2875517c778ac1111b6920e94cbab91cda8724 ]

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@xxxxxxxxxx/
[3] https://lore.kernel.org/lkml/20181216231510.26854-1-mcroce@xxxxxxxxxx/
[4] https://lore.kernel.org/lkml/20190201001408.7453-1-mcroce@xxxxxxxxxx/

Reported-by: Timothy Redaelli <tredaelli@xxxxxxxxxx>
Suggested-by: OGAWA Hirofumi <hirofumi@xxxxxxxxxxxxxxxxxx>
Signed-off-by: Matteo Croce <teknoraver@xxxxxxxx>
Link: https://patch.msgid.link/20260616163346.32603-1-technoboy85@xxxxxxxxx
Acked-by: OGAWA Hirofumi <hirofumi@xxxxxxxxxxxxxxxxxx>
Signed-off-by: Christian Brauner (Amutable) <brauner@xxxxxxxxxx>
Signed-off-by: Sasha Levin <sashal@xxxxxxxxxx>
---

LLM Generated explanations, may be completely bogus:

# Stable Backport Analysis: `fat: stop reading directory entries past
the end-of-directory marker`

**Local tree:** Linux **6.18.44** (`v6.18.44-1-g2736c32da98b9`)

---

## PHASE 1: COMMIT MESSAGE FORENSICS

### Step 1.1: PARSE THE SUBJECT LINE
**Record:** `[fat]` `[stop]` — FAT filesystem driver; stop directory
iteration past the FAT end-of-directory (EOD) marker (`DIR_Name[0] ==
0x00`).

### Step 1.2: PARSE ALL COMMIT MESSAGE TAGS
**Record:**
| Tag | Value |
|-----|-------|
| Reported-by | Timothy Redaelli \<tredaelli@xxxxxxxxxx\> |
| Suggested-by | OGAWA Hirofumi \<hirofumi@xxxxxxxxxxxxxxxxxx\> (FAT
maintainer) |
| Signed-off-by | Matteo Croce \<teknoraver@xxxxxxxx\> (author) |
| Link |
https://patch.msgid.link/20260616163346.32603-1-technoboy85@xxxxxxxxx |
| Acked-by | OGAWA Hirofumi \<hirofumi@xxxxxxxxxxxxxxxxxx\> |
| Signed-off-by | Christian Brauner (Amutable) \<brauner@xxxxxxxxxx\>
(VFS maintainer merge) |

**Notable patterns:** Real user report from Red Hat; subsystem
maintainer Acked-by and Suggested-by; references to earlier v1–v3
attempts (2018–2019). No syzbot, no Fixes: tag (expected for manual
review).

### Step 1.3: ANALYZE THE COMMIT BODY TEXT
**Record:**
- **Bug:** Linux `fat_get_entry()` does not honor the FAT spec EOD
marker. After `name[0] == 0`, iteration continues into trailing
directory slots.
- **Symptom:** On non-spec-compliant images (buggy formatters, other
OSes, corruption), `ls` shows ~150 bogus entries with random binary
names, multi-GB sizes, invalid dates; `stat()` floods `-EIO`.
- **Root cause:** Per FAT spec, `name[0] == 0` means no allocated
entries follow; Linux kept scanning. Prior per-call-site guards were
insufficient because `fat_get_entry()` had already advanced `*pos`
past the marker.
- **Fix approach:** Centralize EOD handling in `fat_get_entry_eod()`;
use it on read/search paths; keep write paths on raw
`fat_get_entry()`; detect spec violations on write in
`fat_add_entries()`.

### Step 1.4: DETECT HIDDEN BUG FIXES
**Record:** Not disguised — this is an explicit filesystem correctness
bug fix, though described in terms of spec compliance rather than "fix
crash."

---

## PHASE 2: DIFF ANALYSIS

### Step 2.1: INVENTORY THE CHANGES
**Record:**
- **File:** `fs/fat/dir.c` only (~65 lines added/changed)
- **Functions modified/added:**
- **Added:** `fat_get_entry_eod()`
- **Modified call sites:** `fat_parse_long()`, `fat_search_long()`,
`__fat_readdir()`, `fat_get_short_entry()`, `fat_add_entries()`
- **Scope:** Single-file surgical fix

### Step 2.2: CODE FLOW CHANGE (per hunk)
**Record:**

| Hunk | Before | After |
|------|--------|-------|
| `fat_get_entry_eod()` (new) | N/A | Wraps `fat_get_entry()`; on
`name[0]==0`, releases `*bh`, sets `*pos = dir->i_size`, returns `-1` |
| `fat_parse_long()` | Advances past EOD during LFN parse | Stops at EOD
via wrapper |
| `fat_search_long()` | Scans past EOD | Stops at EOD |
| `__fat_readdir()` | `IS_FREE()` skips EOD slot but loop continues into
garbage | Stops directory iteration at EOD |
| `fat_get_short_entry()` | Skips free slots including EOD, keeps
scanning | Stops at EOD |
| `fat_add_entries()` | No EOD awareness on write path | Tracks
`saw_eod`; errors if allocated entry found after EOD marker |

### Step 2.3: BUG MECHANISM
**Record:** **Category:** Logic / filesystem correctness (spec
violation).

Verified in current tree `__fat_readdir()`:

```615:616:fs/fat/dir.c
if (de->attr != ATTR_EXT && IS_FREE(de->name))
goto record_end;
```

`IS_FREE()` is true for `name[0]==0` (EOD):

```52:53:include/uapi/linux/msdos_fs.h
#define DELETED_FLAG 0xe5 /* marks file as deleted when in name[0]
*/
#define IS_FREE(n) (!*(n) || *(n) == DELETED_FLAG)
```

At `record_end`, `ctx->pos = cpos` and the loop returns to `get_new`,
which calls `fat_get_entry()` again — advancing **past** the EOD marker
into potentially non-zero garbage slots that are then emitted as real
directory entries.

### Step 2.4: FIX QUALITY
**Record:** Fix is obviously correct per FAT spec; minimal wrapper
centralizes behavior; write paths correctly remain on raw
`fat_get_entry()`. **Regression risk: LOW** — only affects behavior
after EOD marker; well-formed filesystems unchanged.

---

## PHASE 3: GIT HISTORY INVESTIGATION

### Step 3.1: BLAME THE CHANGED LINES
**Record:** `fat_get_entry()` at lines 116–129 last touched by merge
commit `5d324e5159d9e` (2025-11-28); the EOD-ignoring behavior is
longstanding (not a recent regression). Bug predates this stable branch.

### Step 3.2: FOLLOW Fixes: TAG
**Record:** N/A — no Fixes: tag in commit message.

### Step 3.3: FILE HISTORY FOR RELATED CHANGES
**Record:** Recent FAT change in this tree: `17866f8a0822d` "fat: avoid
parent link count underflow in rmdir" (touches `fs/fat/` but not this
EOD logic). No prior EOD fix in this tree. Standalone fix (v4 of a long-
standing effort per commit message references to v1–v3 from 2018–2019).

### Step 3.4: AUTHOR'S OTHER COMMITS
**Record:** No Matteo Croce commits found in this tree's `fs/fat/`
history. Author is external contributor; fix endorsed by maintainer
OGAWA Hirofumi.

### Step 3.5: DEPENDENT/PREREQUISITE COMMITS
**Record:** **None.** Self-contained; uses existing
`fat_fs_error_ratelimit()` (present in `fs/fat/fat.h:447`). No patch-
series dependency.

---

## PHASE 4: MAILING LIST AND EXTERNAL RESEARCH

### Step 4.1: ORIGINAL PATCH DISCUSSION
**Record:** Commit not present in local tree — `b4 dig -c <hash>` could
not be run. `b4 dig` requires a commit-ish object. Link and
lore.kernel.org fetches returned 403/bot protection. Commit message
references v1–v3 at lore.kernel.org (2018–2019) — unverified directly
but cited by author.

### Step 4.2: REVIEWERS
**Record:** Acked-by and Suggested-by from OGAWA Hirofumi (FAT
maintainer). Merged by VFS maintainer Christian Brauner. Strong
maintainer endorsement (from commit message tags).

### Step 4.3: BUG REPORT
**Record:** Reported-by Timothy Redaelli (Red Hat). Concrete user-
visible symptoms described in commit message (~150 bogus `ls` entries,
`-EIO` from `stat()`). No syzbot/KASAN report.

### Step 4.4: RELATED PATCHES/SERIES
**Record:** v4 of a fix series; earlier v1–v3 added per-call-site guards
that maintainer identified as insufficient. This version centralizes the
check per maintainer suggestion.

### Step 4.5: STABLE MAILING LIST HISTORY
**Record:** UNVERIFIED — lore.kernel.org inaccessible from this
environment.

---

## PHASE 5: CODE SEMANTIC ANALYSIS

### Step 5.1: KEY FUNCTIONS
**Record:** `fat_get_entry_eod()` (new), `fat_parse_long()`,
`fat_search_long()`, `__fat_readdir()`, `fat_get_short_entry()`,
`fat_add_entries()`.

### Step 5.2: CALLERS
**Record:**
- `__fat_readdir()` → `fat_readdir()` →
`fat_dir_operations.iterate_shared` — **every `ls`/`getdents()` on
FAT/vfat**
- `fat_search_long()` → vfat name lookup (`namei_vfat.c`)
- `fat_get_short_entry()` → `fat_get_dotdot_entry()`, `fat_dir_empty()`,
`fat_subdirs()`, `fat_scan()`, `fat_scan_logstart()` — rmdir, lookup,
NFS export
- `fat_add_entries()` → file/directory creation (`namei_vfat.c`,
`namei_msdos.c`)

### Step 5.3: CALLEES
**Record:** `fat_get_entry()`, `fat__get_entry()`, `brelse()`,
`fat_fs_error_ratelimit()`, `fat_bmap()` (indirectly via position
advance to `dir->i_size`).

### Step 5.4: CALL CHAIN / REACHABILITY
**Record:** Reachable from **userspace syscalls**: `open()` +
`getdents64()`/`readdir()`, `stat()`, `mkdir()`, `unlink()`, `rename()`
on FAT/vfat mounts. **High reachability** for anyone mounting FAT media.

### Step 5.5: SIMILAR PATTERNS
**Record:** Sibling filesystem `exfat` had related directory-entry
bounds fixes backported to stable (`33c0b96d7e167`, `adfacfbaeae2c` in
this tree's history), indicating stable maintainers value directory-
entry correctness fixes.

---

## PHASE 6: CROSS-REFERENCING AGAINST LOCAL TREE (6.18.44)

### Step 6.1: DOES THE BUGGY CODE EXIST?
**Record:** **YES.** `fat_get_entry_eod` does **not** exist (grep: 0
matches). All vulnerable `fat_get_entry()` call sites present at lines
328, 490, 602, 885, 1309. Bug is longstanding, not post-branch.

### Step 6.2: BACKPORT COMPLICATIONS
**Record:** **Clean apply** for main hunks verified via `git apply
--check`. `fat_add_entries()` hunk may need minor line-number adjustment
due to `17866f8a0822d` (parent link count fix) — content matches,
trivial backport adjustment.

### Step 6.3: RELATED FIXES ALREADY PRESENT?
**Record:** **No.** No EOD handling, no `fat_get_entry_eod`, no
`saw_eod` logic in current tree.

---

## PHASE 7: SUBSYSTEM AND MAINTAINER CONTEXT

### Step 7.1: SUBSYSTEM CRITICALITY
**Record:** **Filesystem (FAT/vfat)** — IMPORTANT. Widely used for USB
sticks, SD cards, EFI partitions, embedded devices.

### Step 7.2: SUBSYSTEM ACTIVITY
**Record:** Active — recent fix `17866f8a0822d` in same subsystem. FAT
driver is mature but still receives correctness fixes.

---

## PHASE 8: IMPACT AND RISK ASSESSMENT

### Step 8.1: WHO IS AFFECTED
**Record:** All users mounting FAT12/FAT16/FAT32/vfat filesystems where
directory trailing slots are not zero-filled after the EOD marker.
Common with cross-platform media and buggy/corrupt images.

### Step 8.2: TRIGGER CONDITIONS
**Record:** Mount FAT filesystem and list/access directory on media with
non-zero data after EOD marker. **Not** every boot, but **common** for
removable media. Unprivileged users can trigger via normal file
operations on mounted filesystem.

### Step 8.3: FAILURE MODE SEVERITY
**Record:** Bogus directory entries surfaced to userspace; `stat()`
returns `-EIO`; potential application confusion/failures; write path
could add entries to corrupt directories without the `fat_add_entries()`
guard. **Severity: MEDIUM-HIGH** (filesystem correctness, not kernel
oops/UAF, but serious user-visible malfunction).

### Step 8.4: RISK-BENEFIT
**Record:**
- **Benefit: HIGH** for FAT users — fixes real, reproducible directory
listing corruption
- **Risk: LOW** — ~65 lines, one file, spec-aligned, maintainer-acked,
write paths unchanged except corruption detection
- **Ratio: Strongly favors backport**

---

## PHASE 9: FINAL SYNTHESIS

### Step 9.1: EVIDENCE COMPILED

**FOR backport:**
- Real, user-reported filesystem correctness bug
- FAT spec violation with concrete symptoms (~150 bogus entries)
- Centralized fix addresses root cause (prior per-site guards were
broken)
- Maintainer Acked-by + Suggested-by
- Small, single-file, no new userspace API
- Buggy code confirmed present in 6.18.44; fix not yet applied
- Patch applies cleanly to this tree
- Affects common syscall paths (readdir, lookup, create)
- Write-path corruption detection prevents worsening damaged directories

**AGAINST backport:**
- Not a kernel crash/UAF/security issue
- Requires non-spec-compliant on-disk layout to trigger (but commit
documents real-world occurrence)
- Mailing list discussion unverified due to lore access restrictions

**UNRESOLVED:**
- Full lore review thread content
- Exact mainline commit hash (not in this tree)

### Step 9.2: STABLE RULES CHECKLIST
| Criterion | Result |
|-----------|--------|
| 1. Obviously correct and tested? | **PASS** — spec-aligned,
maintainer-acked; Tested-by not present but logic is verifiable |
| 2. Fixes a real bug affecting users? | **PASS** — Reported-by with
concrete symptoms |
| 3. Important issue? | **PASS** — filesystem correctness, user-visible
directory corruption |
| 4. Small and contained? | **PASS** — one file, ~65 lines |
| 5. No new features/APIs? | **PASS** — internal static wrapper only |
| 6. Can apply to local tree? | **PASS** — verified clean apply for core
hunks |

### Step 9.3: EXCEPTION CATEGORIES
**Record:** N/A — standard bug fix, not device ID/quirk/DT/build/doc
exception.

### Step 9.4: DECISION RATIONALE
This is a longstanding FAT driver spec-compliance bug that causes real
user-visible directory corruption on commonly encountered removable
media. The fix is small, maintainer-reviewed, self-contained, and the
vulnerable code is present in Linux 6.18.44 without an existing fix.
Filesystem correctness on widely used FAT/vfat mounts meets stable
criteria.

---

## Verification

- **[Phase 1]** Parsed commit message tags: Reported-by, Suggested-by,
Acked-by (OGAWA Hirofumi), Link, Signed-off-by chain
- **[Phase 1]** Identified subsystem: `fat`, action: stop EOD iteration
- **[Phase 2]** Diff inventory: 1 file (`fs/fat/dir.c`), ~65 lines, 1
new function + 5 modified sites
- **[Phase 2]** Verified `IS_FREE()` treats `name[0]==0` as free:
`include/uapi/linux/msdos_fs.h:53`
- **[Phase 2]** Traced `__fat_readdir()` EOD→`record_end`→`get_new` loop
at `fs/fat/dir.c:615-616,694-697,601-602`
- **[Phase 2]** Confirmed `fat_fs_error_ratelimit` exists:
`fs/fat/fat.h:447`
- **[Phase 3]** `git describe HEAD`: `v6.18.44-1-g2736c32da98b9`;
Makefile: 6.18.44
- **[Phase 3]** `git blame` on `fat_get_entry()`: lines 116-129,
longstanding code
- **[Phase 3]** No Fixes: tag to follow
- **[Phase 3]** Recent related commit: `17866f8a0822d` in `fs/fat/`
- **[Phase 4]** `b4 dig -c`: FAILED — commit not in local tree
- **[Phase 4]** lore.kernel.org / patch.msgid.link fetch: FAILED
(403/bot protection) — discussion unverified
- **[Phase 5]** `grep fat_get_entry` call sites: 8 uses in `dir.c`; read
paths at 328, 490, 602, 885; write paths at 1015, 1309
- **[Phase 5]** Caller trace: `fat_readdir` → `iterate_shared`;
`fat_search_long`/`fat_scan` → namei paths
- **[Phase 6]** `grep fat_get_entry_eod`: 0 matches — fix NOT in tree
- **[Phase 6]** Buggy `fat_get_entry()` without EOD handling: CONFIRMED
present
- **[Phase 6]** `git apply --check`: PASS for `fat_get_entry_eod`
addition and 4 call-site hunks
- **[Phase 6]** `git apply --check` for `fat_add_entries` hunk: needs
minor line adjustment (due to `17866f8a0822d`), content matches
- **[Phase 8]** Failure mode: bogus directory entries + `-EIO` on stat —
MEDIUM-HIGH severity, user-visible

**YES**

fs/fat/dir.c | 44 ++++++++++++++++++++++++++++++++++++++++----
1 file changed, 40 insertions(+), 4 deletions(-)

diff --git a/fs/fat/dir.c b/fs/fat/dir.c
index 92b091783966a..415c22a9e6361 100644
--- a/fs/fat/dir.c
+++ b/fs/fat/dir.c
@@ -128,6 +128,31 @@ 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. Release *bh and set it to NULL to
+ * match fat_get_entry()'s contract that *bh is NULL on the -1 return.
+ */
+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) {
+ brelse(*bh);
+ *bh = NULL;
+ *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
@@ -325,7 +350,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;
@@ -487,7 +512,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;
@@ -599,7 +624,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;
@@ -882,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;
@@ -1298,6 +1323,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;

@@ -1306,12 +1332,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;
@@ -1321,6 +1350,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.53.0