Root Cause Analysis
1. Involved Components and Data Structures
- Source Files:
block/bdev.c: Contains nr_blockdev_pages(), which calculates the total page count across all block devices.
mm/filemap.c: Contains __filemap_add_folio(), which adds newly allocated folios to an address space's page cache.
include/linux/fs.h: Defines struct address_space.
- Structures and Fields:
struct address_space::nrpages: Tracks the total number of page entries present in the address space page cache. As documented in include/linux/fs.h, this field is protected by the i_pages lock (xa_lock).
struct super_block::s_inode_list_lock: Protects the list of inodes associated with blockdev_superblock.
2. Synchronization Mechanism and Invariant Violation
According to the synchronization rules of the page cache (struct address_space), modifications to mapping->nrpages must be performed while holding the i_pages.xa_lock:
/* mm/filemap.c: __filemap_add_folio() */
xas_lock_irq(&xas);
...
xas_store(&xas, folio);
if (xas_error(&xas))
goto unlock;
mapping->nrpages += nr; /* line 919 */
unlock:
xas_unlock_irq(&xas);
In nr_blockdev_pages(), the kernel calculates the total number of block device buffer pages (val->bufferram) for si_meminfo() by iterating through all block device inodes on blockdev_superblock->s_inodes:
/* block/bdev.c: nr_blockdev_pages() */
long nr_blockdev_pages(void)
{
struct inode *inode;
long ret = 0;
spin_lock(&blockdev_superblock->s_inode_list_lock);
list_for_each_entry(inode, &blockdev_superblock->s_inodes, i_sb_list)
ret += inode->i_mapping->nrpages; /* line 568 */
spin_unlock(&blockdev_superblock->s_inode_list_lock);
return ret;
}
While nr_blockdev_pages() holds blockdev_superblock->s_inode_list_lock to protect the list traversal of block device inodes, it intentionally does not acquire each individual block device's inode->i_mapping->i_pages.xa_lock when reading nrpages, avoiding global spinlock contention with block device I/O and readahead paths.
However, the read ret += inode->i_mapping->nrpages; is an unannotated plain read. When a concurrent thread (such as task 8211 executing __filemap_add_folio()) concurrently executes mapping->nrpages += nr;, KCSAN detects a data race between the concurrent read-write and the unannotated read on mapping->nrpages.
3. Resolution
To inform concurrency tools and the compiler that lockless, approximate reads of mapping->nrpages in statistical accounting functions like nr_blockdev_pages() are intentional, the read should be annotated using data_race(...) or READ_ONCE(...) (e.g., ret += data_race(inode->i_mapping->nrpages);).