-
Root Directory Header Acquisition in ntfs_readdir():
In ntfs_readdir() (fs/ntfs3/dir.c), the function acquires the internal inode mutex via ni_lock(ni) and retrieves the root index header via:
root = indx_get_root(&ni->dir, ni, NULL, NULL);
indx_get_root() locates the resident ATTR_ROOT attribute using ni_find_attr(). Because ATTR_ROOT is resident, root is not an independently allocated object; it is an interior pointer directly referencing bytes inside an MFT record buffer (ni->mi.mrec for the primary record, or an attached subrecord mi->mrec).
-
Faulty Invariant Assumption and Premature Lock Release:
Before invoking ntfs_read_hdr(sbi, ni, &root->ihdr, 0, pos, name, ctx) to iterate through entries, ntfs_readdir() drops ni_lock(ni) at line 568:
/*
* Release ni_lock before dir_emit (which may fault into
* user pages). root->ihdr lives in the main MFT record
* whose lifetime is bound to the inode, and i_rwsem shared
* prevents concurrent directory modifications.
*/
ni_unlock(ni);
err = ntfs_read_hdr(sbi, ni, &root->ihdr, 0, pos, name, ctx);
The comment explicitly documents the developer's assumption: that root->ihdr's underlying memory is tied to the inode's lifetime and that VFS shared i_rwsem prevents concurrent modifications.
-
Concurrent Background Writeback (ni_write_inode):
VFS i_rwsem does not serialize filesystem writeback operations. Writeback workers (wb_workfn -> __writeback_single_inode -> write_inode -> ni_write_inode) run asynchronously without holding i_rwsem.
In ni_write_inode() (fs/ntfs3/frecord.c:3196), the driver checks synchronization using:
if (!ni_trylock(ni)) {
mark_inode_dirty_sync(inode);
return 0;
}
Because ntfs_readdir() dropped ni_lock(ni), ni_trylock(ni) succeeds in the writeback worker thread.
-
In-Place Attribute List Compaction and Primary Buffer Deallocation:
When ni->attr_list.dirty is set, ni_write_inode() invokes ni_try_remove_attr_list(ni).
In ni_try_remove_attr_list() (fs/ntfs3/frecord.c:679):
Instead of restoring data into the existing ni->mi.mrec via memcpy(), swap() replaces ni->mi.mrec with the temporary kmemdup buffer and calls kfree(mrec) on the original buffer allocated during mi_init().
(Additionally, even without triggering the rollback swap, ni_try_remove_attr_list() shifts attributes within ni->mi.mrec, and ni_write_inode() deallocates and frees emptied subrecords via mi_put(mi)).
-
Slab Use-After-Free in ntfs_read_hdr():
While the writeback thread frees the original ni->mi.mrec buffer via kfree(mrec), the reader task in ntfs_read_hdr() (fs/ntfs3/dir.c:422-427) is still looping over the entries pointed to by hdr = &root->ihdr:
e = Add2Ptr(hdr, off);
e_size = le16_to_cpu(e->size);
The dereference of e->size accesses the freed 1024-byte kmalloc buffer (kmalloc-1k), triggering the reported KASAN slab-use-after-free read.