Root Cause Analysis
1. Overview and Invariant Violation
The reported KASAN slab-use-after-free occurs in fs/ntfs3 due to a race condition between directory enumeration (ntfs_readdir()) and asynchronous writeback (ni_write_inode()).
In ntfs_readdir(), the directory index root header pointer root is obtained via:
root = indx_get_root(&ni->dir, ni, NULL, NULL);
root->ihdr is an in-memory pointer residing directly inside the directory inode's primary MFT record buffer (ni->mi.mrec), which was allocated during inode initialization via mi_init() (kmalloc(sbi->record_size, GFP_NOFS)).
In commit 7a5aa6c45072 ("ntfs3: release ni_lock before dir_emit in ntfs_readdir to fix deadlocks"), ntfs_readdir() releases ni_lock(ni) right before traversing the index root entries:
/* fs/ntfs3/dir.c:568 */
ni_unlock(ni);
/* Add each name from root in 'ctx'. */
err = ntfs_read_hdr(sbi, ni, &root->ihdr, 0, pos, name, ctx);
The assumption stated in the code comments was that root->ihdr lives in the primary MFT record whose buffer lifetime is bound to the inode itself and that VFS i_rwsem held shared would protect against directory modifications.
However, background writeback does not take i_rwsem. Instead, ni_write_inode() (fs/ntfs3/frecord.c) relies entirely on ni_trylock(ni):
/* fs/ntfs3/frecord.c:3196 */
if (!ni_trylock(ni)) {
mark_inode_dirty_sync(inode);
return 0;
}
Because ni_unlock(ni) was called during ntfs_readdir(), a concurrent writeback worker successfully acquires ni_lock(ni).
2. Buffer Replacement and Freeing in ni_try_remove_attr_list
During writeback, if the inode has a dirty attribute list, ni_write_inode() invokes ni_try_remove_attr_list(ni). This function attempts to collapse attribute list entries back into the primary record (ni->mi.mrec). To facilitate rollback on failure, it duplicates the primary record:
/* fs/ntfs3/frecord.c:679 */
mrec = kmemdup(ni->mi.mrec, sbi->record_size, GFP_NOFS);
if (!mrec)
return 0;
...
If an error occurs while moving attributes into the primary record (such as mi_insert_attr() running out of space or failing to find a subrecord), the function jumps to label out::
/* fs/ntfs3/frecord.c:757-760 */
out:
/* Restore primary record. */
swap(mrec, ni->mi.mrec);
kfree(mrec);
return 0;
Instead of restoring the original content by copying data into ni->mi.mrec (e.g., via memcpy), it swaps the pointer ni->mi.mrec with the kmemdup buffer mrec and frees the original ni->mi.mrec buffer via kfree(mrec).
3. Triggering the Use-After-Free
- Task A (
ntfs_readdir) acquires ni_lock(ni), finds root = indx_get_root(...) pointing inside ni->mi.mrec, and drops ni_lock(ni).
- Task B (writeback worker) calls
ni_write_inode(), successfully acquires ni_trylock(ni), calls ni_try_remove_attr_list(), hits the out: rollback path, and executes swap(mrec, ni->mi.mrec); kfree(mrec);. The memory region originally backing ni->mi.mrec is freed to the kmalloc-1k slab cache.
- Task A continues executing
ntfs_read_hdr(sbi, ni, &root->ihdr, 0, pos, name, ctx), iterating across directory entries inside the now-freed memory.
- When
ntfs_read_hdr() evaluates entry e = Add2Ptr(hdr, off) and calls ntfs_dir_emit() -> ntfs_iget5(sbi->sb, &e->ref, NULL) -> ntfs_iget5_flags() -> ntfs_read_mft(inode, name, ref), it attempts to read ref->seq at fs/ntfs3/inode.c:86:
} else if (ref->seq != rec->seq) {
ref points to &e->ref inside the freed ni->mi.mrec buffer at byte offset 0x23e (574 bytes inside the 1024-byte slab allocation). Reading the 2-byte sequence number triggers the reported BUG: KASAN: slab-use-after-free.