Technical Root Cause Analysis
1. Functions, Structs, and Files Involved
- Files:
- Functions:
- Structs and Fields:
struct msdos_sb_info (fs/fat/fat.h): field fat_lock (mutex protecting FAT entry manipulations)
struct fat_entry: fields u.ent32_p and bhs[] (holding pointers to the FAT table buffer heads)
struct buffer_head: field b_data (holding cached FAT block data)
2. Trigger Mechanism and Violated Invariants
Even though file write paths serialize operations per-inode using inode_lock(inode) (e.g., in generic_file_write_iter()), two concurrent tasks operating on different files belonging to the same FAT filesystem operate under separate inode locks. Both tasks, however, share the filesystem-wide File Allocation Table (FAT) and its underlying buffer heads (bhs[n]->b_data).
In FAT filesystem internals, access to the FAT entries and the mirroring of dirty FAT blocks to the backup FAT copies are supposed to be serialized via sbi->fat_lock (accessed via lock_fat(sbi) and unlock_fat(sbi)):
-
Premature unlock in cluster allocation (__fat_alloc_clusters):
In fs/fat/fatent.c, __fat_alloc_clusters() acquires lock_fat(sbi) while modifying FAT entries. However, at line 585 it calls unlock_fat(sbi) before synchronizing and mirroring the dirty FAT blocks to the backup FAT table (fat_mirror_bhs(sb, bhs, nr_bhs) at line 592). This is in contrast to fat_free_clusters(), which keeps lock_fat(sbi) held throughout fat_mirror_bhs().
-
Missing lock_fat() synchronization in fat_ent_write:
fat_ent_write() is called when writing an EOF or appending clusters (such as from fat_free() during truncate or fat_chain_add()). fat_ent_write() directly calls ops->ent_put(fatent, new) and fat_mirror_bhs() without acquiring lock_fat(sbi) at all.
-
The Race:
- Task 6394 in
__fat_alloc_clusters() drops lock_fat(sbi) and calls fat_mirror_bhs(), which iterates over bhs and runs memcpy(c_bh->b_data, bhs[n]->b_data, sb->s_blocksize) on CPU 1 (reading 2048 bytes at 0xffff88811d9e5000).
- Simultaneously, Task 6401 on CPU 0 is handling a truncate on another file (
fat_truncate_blocks() -> fat_free() -> fat_ent_write()), calling fat32_ent_put(), which writes 4 bytes to *fatent->u.ent32_p at address 0xffff88811d9e53f0 without holding lock_fat().
- Because both tasks access the exact same primary FAT buffer concurrently without mutual exclusion, KCSAN detects the read-write data race between
memcpy() and *fatent->u.ent32_p = ....
3. Production Runtime Consequences and Classification
- Memory Corruption (
MemoryCorruption = false):
The race is an unsynchronized read/write to filesystem block buffers (b_data). No kernel objects, linked list pointers, allocation sizes, refcounts, or kernel control structures are overwritten or corrupted. The values manipulated are cluster index integers on buffer-head pages. Thus, no kernel memory corruption, use-after-free, or out-of-bounds write occurs.
- Information Leak (
InfoLeak = "none"):
The race does not expose kernel memory contents or kernel pointer addresses to userspace.
- Denial of Service (
RawDenialOfService = false):
In a production kernel without sanitizers and with panic_on_warn disabled:
- On architectures like x86_64, aligned 4-byte writes are atomic;
memcpy() in fat_mirror_bhs() reads either the pre-update or post-update value into the backup FAT copy.
- Furthermore,
fat_ent_write() immediately proceeds to call fat_mirror_bhs() for its own update.
- The primary in-memory FAT buffer remains consistent, and FAT cluster chain traversals in the kernel validate cluster limits and have explicit cycle detection (
fat_fs_error_ratelimit), preventing infinite loops or kernel deadlocks/panics.
- System execution proceeds normally without kernel panics, crashes, hangs, or resource leaks.
- Candidates (
IsNetCandidate = false, IsKVMCandidate = false):
The bug is entirely within the local FAT filesystem implementation and does not involve networking or virtualization/KVM subsystem code.