From: sashiko-bot@kernel.org
To: "Salvatore Dipietro" <dipiets@amazon.it>,
akpm@linux-foundation.org, linux-mm@kvack.org
Cc: ryan.roberts@arm.com, abuehaze@amazon.com, alisaidi@amazon.com,
blakgeof@amazon.com, dipietro.salvatore@gmail.com,
rvvandan@amazon.com, viro@zeniv.linux.org.uk, brauner@kernel.org,
jack@suse.cz, linkinjeon@kernel.org, sj1557.seo@samsung.com,
yuezhang.mo@sony.com, miklos@szeredi.hu, agruenba@redhat.com,
djwong@kernel.org, hyc.lee@gmail.com,
almaz.alexandrovich@paragon-software.com, cem@kernel.org,
dlemoal@kernel.org, naohiro.aota@wdc.com, jth@kernel.org,
willy@infradead.org, david@kernel.org, ljs@kernel.org,
liam@infradead.org, vbabka@kernel.org, rppt@kernel.org,
surenb@google.com, mhocko@suse.com, ziy@nvidia.com,
matthew.brost@intel.com, joshua.hahnjy@gmail.com,
rakie.kim@sk.com, byungchul@sk.com, gourry@gourry.net,
ying.huang@linux.alibaba.com, apopple@nvidia.com,
ritesh.list@gmail.com
Subject: Re: [PATCH 2/2] mm, iomap: account dirty at page granularity for per-block dirty folios
Date: Wed, 28 Oct 2026 11:27:54 +0000 [thread overview]
Message-ID: <sashiko-outbox-187814@kernel.org> (raw)
In-Reply-To: <20261027112702.3747009-3-dipiets@amazon.it>
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] iomap: dirty counter leak on early EOF return in iomap_writeback_folio()
- [High] ntfs, ntfs3: dirty counter leak in ntfs_mft_writepages() and ntfs_writepages()
- [High] block: missing iomap per-block dirty callbacks in def_blk_aops
- [Medium] mm: wrong wb stats in inode_do_switch_wbs() and __filemap_get_folio_mpol()
- [Medium] gfs2: stale AS_DIRTY_PER_BLOCK flag after switching to gfs2_jdata_aops
--
commit ec03cb36d468351e028d982f93868e296ff14cfc
Author: Salvatore Dipietro <dipiets@amazon.it>
mm, iomap: account dirty at page granularity for per-block dirty folios
This commit updates iomap to account mm and writeback dirty page counters at
page granularity rather than full folio granularity when folios carry
per-block dirty state in struct iomap_folio_state, and wires up
.accounted_dirty_pages = iomap_accounted_dirty_pages across iomap-based
filesystems.
> diff --git a/fs/gfs2/aops.c b/fs/gfs2/aops.c
> index 0a7b8076af3a5..ea77af00b111e 100644
> --- a/fs/gfs2/aops.c
> +++ b/fs/gfs2/aops.c
> @@ -713,6 +713,7 @@ static const struct address_space_operations gfs2_aops = {
> .read_folio = gfs2_read_folio,
> .readahead = gfs2_readahead,
> .dirty_folio = iomap_dirty_folio,
> + .accounted_dirty_pages = iomap_accounted_dirty_pages,
[Severity: Medium]
What happens if a GFS2 file using gfs2_aops with blocksize < folio_size()
allocates struct iomap_folio_state via ifs_alloc() (which sets
AS_DIRTY_PER_BLOCK on inode->i_mapping), and later has GFS2_DIF_JDATA
enabled via gfs2_fileattr_set() -> do_gfs2_set_flags() -> gfs2_set_aops()?
fs/gfs2/aops.c:gfs2_set_aops() {
if (gfs2_is_jdata(GFS2_I(inode)))
inode->i_mapping->a_ops = &gfs2_jdata_aops;
else
inode->i_mapping->a_ops = &gfs2_aops;
}
Switching inode->i_mapping->a_ops to gfs2_jdata_aops leaves
AS_DIRTY_PER_BLOCK set on inode->i_mapping. Subsequent writes in jdata mode
attach struct buffer_head rings to folio->private and charge
folio_nr_pages(folio) via gfs2_jdata_dirty_folio() -> block_dirty_folio().
When gfs2_jdata_writepages() later runs folio_clear_dirty_for_io():
mm/page-writeback.c:folio_clear_dirty_for_io() {
...
if (folio_test_clear_dirty(folio)) {
if (!mapping_dirty_per_block(mapping) || !folio->private) {
long nr = folio_nr_pages(folio);
...
}
Because mapping_dirty_per_block(mapping) is still true and folio->private is
non-NULL (pointing to buffer_heads), won't folio_clear_dirty_for_io() skip
discharging NR_FILE_DIRTY, WB_DONTCACHE_DIRTY, NR_ZONE_WRITE_PENDING, and
WB_RECLAIMABLE on every writeback?
> diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c
> index 0a5ebfda90f12..75671b29f7cce 100644
> --- a/fs/iomap/buffered-io.c
> +++ b/fs/iomap/buffered-io.c
[ ... ]
> @@ -180,6 +189,89 @@ static unsigned iomap_find_dirty_range(struct folio *folio, u64 *range_start,
[ ... ]
> +static void iomap_account_dirty_pages(struct folio *folio, long nr_pages,
> + bool new_dirty)
> +{
> + struct address_space *mapping = folio->mapping;
> + struct inode *inode = mapping->host;
> + struct wb_lock_cookie cookie = {};
> + struct bdi_writeback *wb;
> + bool dirtied = nr_pages > 0 && new_dirty;
> +
> + if (!nr_pages || !mapping_can_writeback(mapping))
> + return;
> +
> + lruvec_stat_mod_folio(folio, NR_FILE_DIRTY, nr_pages);
> + zone_stat_mod_folio(folio, NR_ZONE_WRITE_PENDING, nr_pages);
> + if (dirtied)
> + node_stat_mod_folio(folio, NR_DIRTIED, nr_pages);
> +
> + /* Pin the wb against a concurrent inode_do_switch_wbs(). */
> + inode_attach_wb(inode, folio);
> + wb = unlocked_inode_to_wb_begin(inode, &cookie);
> + wb_stat_mod(wb, WB_RECLAIMABLE, nr_pages);
> + if (folio_test_dropbehind(folio))
> + wb_stat_mod(wb, WB_DONTCACHE_DIRTY, nr_pages);
> + if (dirtied)
> + wb_stat_mod(wb, WB_DIRTIED, nr_pages);
> + unlocked_inode_to_wb_end(inode, &cookie);
[Severity: Medium]
Can charging WB_RECLAIMABLE and WB_DONTCACHE_DIRTY by nr_pages here corrupt
per-wb counters when inode_do_switch_wbs() or __filemap_get_folio_mpol()
runs on a partially dirty large folio?
In fs/fs-writeback.c, inode_do_switch_wbs() still transfers
folio_nr_pages(folio) instead of folio_accounted_dirty_pages(folio):
fs/fs-writeback.c:inode_do_switch_wbs() {
...
xas_for_each_marked(&xas, folio, ULONG_MAX, PAGECACHE_TAG_DIRTY) {
if (folio_test_dirty(folio)) {
long nr = folio_nr_pages(folio);
wb_stat_mod(old_wb, WB_RECLAIMABLE, -nr);
wb_stat_mod(new_wb, WB_RECLAIMABLE, nr);
if (folio_test_dropbehind(folio)) {
wb_stat_mod(old_wb, WB_DONTCACHE_DIRTY, -nr);
wb_stat_mod(new_wb, WB_DONTCACHE_DIRTY, nr);
}
}
}
...
}
When new_wb later writes back or invalidates the folio,
iomap_clear_range_dirty() only subtracts ifs->dirty_pages from new_wb,
leaving old_wb underflowed and new_wb inflated by
folio_nr_pages(folio) - ifs->dirty_pages.
Also, because iomap_account_dirty_pages() updates WB_RECLAIMABLE and
WB_DONTCACHE_DIRTY outside mapping->i_pages.xa_lock before
__filemap_dirty_folio() sets PG_dirty, and because
folio_clear_dirty_for_io() clears PG_dirty while keeping WB_RECLAIMABLE and
WB_DONTCACHE_DIRTY charged until iomap_clear_range_dirty(), can a concurrent
inode_do_switch_wbs() miss or misaccount a folio whose PG_dirty state does
not match its active wb charge?
Similarly, in mm/filemap.c, __filemap_get_folio_mpol() decrements
WB_DONTCACHE_DIRTY by folio_nr_pages(folio) when clearing PG_dropbehind:
mm/filemap.c:__filemap_get_folio_mpol() {
...
if (!(fgp_flags & FGP_DONTCACHE) && folio_test_clear_dropbehind(folio)) {
if (folio_test_dirty(folio) &&
mapping_can_writeback(mapping)) {
struct inode *inode = mapping->host;
struct bdi_writeback *wb;
struct wb_lock_cookie cookie = {};
long nr = folio_nr_pages(folio);
wb = unlocked_inode_to_wb_begin(inode, &cookie);
wb_stat_mod(wb, WB_DONTCACHE_DIRTY, -nr);
unlocked_inode_to_wb_end(inode, &cookie);
}
}
...
}
Won't that underflow WB_DONTCACHE_DIRTY when a partially dirty folio was
only charged ifs->dirty_pages, or leak WB_DONTCACHE_DIRTY if PG_dropbehind
is cleared after folio_clear_dirty_for_io() cleared PG_dirty but before
iomap_clear_range_dirty() runs?
[ ... ]
> @@ -276,9 +396,13 @@ static struct iomap_folio_state *ifs_alloc(struct inode *inode,
> spin_lock_init(&ifs->state_lock);
> if (folio_test_uptodate(folio))
> bitmap_set(ifs->state, 0, nr_blocks);
> - if (folio_test_dirty(folio))
> + if (folio_test_dirty(folio)) {
> bitmap_set(ifs->state, nr_blocks, nr_blocks);
> + ifs->dirty_pages = folio_nr_pages(folio);
> + }
> folio_attach_private(folio, ifs);
> + /* The mm dirty counters are now charged per dirty page for this file. */
> + mapping_set_dirty_per_block(inode->i_mapping);
[Severity: High]
What happens on block devices when !CONFIG_BUFFER_HEAD is enabled?
In block/fops.c, def_blk_aops uses iomap for buffered I/O, writepages,
release_folio, and invalidate_folio when !CONFIG_BUFFER_HEAD:
block/fops.c:
const struct address_space_operations def_blk_aops = {
.dirty_folio = filemap_dirty_folio,
.release_folio = iomap_release_folio,
.invalidate_folio = iomap_invalidate_folio,
.read_folio = blkdev_read_folio,
.readahead = blkdev_readahead,
.writepages = blkdev_writepages,
.is_partially_uptodate = iomap_is_partially_uptodate,
.error_remove_folio = generic_error_remove_folio,
.migrate_folio = filemap_migrate_folio,
};
When ifs_alloc() attaches ifs to a block device folio with
i_blocks_per_folio(inode, folio) > 1, it sets AS_DIRTY_PER_BLOCK on the
block device mapping, but def_blk_aops does not set
.accounted_dirty_pages = iomap_accounted_dirty_pages or
.dirty_folio = iomap_dirty_folio.
When iomap_invalidate_folio(folio, 0, folio_size(folio)) later runs on a
dirty block device folio, it first calls iomap_clear_range_dirty() (which
discharges ifs->dirty_pages and sets ifs->dirty_pages = 0) and then calls
folio_cancel_dirty() -> folio_account_cleaned() ->
folio_accounted_dirty_pages(). Because mapping->a_ops->accounted_dirty_pages
is NULL, folio_accounted_dirty_pages() returns folio_nr_pages(folio) instead
of 0, discharging the counters a second time.
Also, if a block device folio that already has ifs attached from a read is
dirtied via mmap (filemap_dirty_folio()), the dirty bits in ifs->state are
not set, so writeback via folio_clear_dirty_for_io() and
iomap_writeback_folio() neither writes the blocks nor discharges the dirty
counters. Should def_blk_aops also use iomap_dirty_folio and
iomap_accounted_dirty_pages when !CONFIG_BUFFER_HEAD?
[ ... ]
> @@ -2001,8 +2151,13 @@ int iomap_writeback_folio(struct iomap_writepage_ctx *wpc, struct folio *folio)
>
> if (i_blocks_per_folio(inode, folio) > 1) {
> if (!ifs) {
> + /*
> + * Dirtied before it had per-block state, so charged
> + * per-folio. Re-mark the blocks for the walk below,
> + * paired with the clear at the end of this function.
> + */
> ifs = ifs_alloc(inode, folio, 0);
> - iomap_set_range_dirty(folio, 0, end_pos - pos);
> + iomap_redirty_range(folio, 0, end_pos - pos);
> }
>
> iomap_writeback_init(inode, folio);
[Severity: High]
Can iomap_writeback_folio() leak the dirty page counters when a folio with
per-block state is at or beyond EOF?
Before calling iomap_writeback_folio() (via writeback_iter() ->
folio_prepare_writeback() or fuse_launder_folio()), the caller invokes
folio_clear_dirty_for_io(folio), which clears PG_dirty without discharging
NR_FILE_DIRTY, WB_DONTCACHE_DIRTY, NR_ZONE_WRITE_PENDING, or WB_RECLAIMABLE
when mapping_dirty_per_block(mapping) && folio->private is true.
However, at the start of iomap_writeback_folio():
fs/iomap/buffered-io.c:iomap_writeback_folio() {
...
if (!iomap_writeback_handle_eof(folio, wpc, &end_pos))
return 0;
...
}
If iomap_writeback_handle_eof() returns false (for instance, when writeback
races with truncate_setsize() after i_size_write() lowers i_size but before
truncate_pagecache() locks the folio, or in folio_unmap_invalidate() ->
fuse_launder_folio()), iomap_writeback_folio() returns 0 without calling
iomap_clear_range_dirty(folio, 0, folio_size(folio)).
That leaves the folio clean (PG_dirty == false, PG_writeback == false) while
ifs->dirty_pages remains charged. If filemap_release_folio() ->
iomap_release_folio() then runs (immediately in folio_unmap_invalidate(), or
via reclaim, splitting, or eviction before truncate_cleanup_folio() locks
the folio):
fs/iomap/buffered-io.c:iomap_release_folio() {
...
if (folio_test_dirty(folio))
return false;
ifs_free(folio);
return true;
}
Won't iomap_release_folio() see folio_test_dirty(folio) == false and free
ifs without ever discharging ifs->dirty_pages?
> diff --git a/fs/ntfs/aops.c b/fs/ntfs/aops.c
> index 517d9a1563e7d..d26ddd5ae1185 100644
> --- a/fs/ntfs/aops.c
> +++ b/fs/ntfs/aops.c
> @@ -308,6 +308,7 @@ const struct address_space_operations ntfs_aops = {
> .readahead = ntfs_readahead,
> .writepages = ntfs_writepages,
> .dirty_folio = iomap_dirty_folio,
> + .accounted_dirty_pages = iomap_accounted_dirty_pages,
> .bmap = ntfs_bmap,
> .migrate_folio = filemap_migrate_folio,
> .is_partially_uptodate = iomap_is_partially_uptodate,
> @@ -322,6 +323,7 @@ const struct address_space_operations ntfs_mft_aops = {
> .readahead = ntfs_readahead,
> .writepages = ntfs_mft_writepages,
> .dirty_folio = iomap_dirty_folio,
> + .accounted_dirty_pages = iomap_accounted_dirty_pages,
> .bmap = ntfs_bmap,
> .migrate_folio = filemap_migrate_folio,
> .is_partially_uptodate = iomap_is_partially_uptodate,
> diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c
> index 4ac26c80bd34f..509570091e483 100644
> --- a/fs/ntfs3/inode.c
> +++ b/fs/ntfs3/inode.c
> @@ -2301,6 +2301,7 @@ const struct address_space_operations ntfs_aops = {
> .writepages = ntfs_writepages,
> .bmap = ntfs_bmap,
> .dirty_folio = iomap_dirty_folio,
> + .accounted_dirty_pages = iomap_accounted_dirty_pages,
[Severity: High]
Does enabling per-block dirty accounting on ntfs_mft_aops (in fs/ntfs) and
resident files using ntfs_aops (in fs/ntfs3) leak dirty page counters during
writeback?
In fs/ntfs, ntfs_mft_aops uses ntfs_mft_writepages(), which iterates with
writeback_iter() and calls ntfs_write_mft_block(folio, wbc) rather than
iomap_writeback_folio():
fs/ntfs/mft.c:ntfs_mft_writepages() {
...
while ((folio = writeback_iter(mapping, wbc, folio, &error)))
error = ntfs_write_mft_block(folio, wbc);
...
}
Similarly, in fs/ntfs3, ntfs_writepages() handles resident files via
writeback_iter() and attr_data_write_resident(ni, folio) without calling
iomap_writepages():
fs/ntfs3/inode.c:ntfs_writepages() {
...
if (is_resident(ni)) {
struct folio *folio = NULL;
err = 0;
while ((folio = writeback_iter(mapping, wbc, folio, &err))) {
int err2;
ni_lock(ni);
err2 = attr_data_write_resident(ni, folio);
ni_unlock(ni);
...
}
When these folios are dirtied via iomap_dirty_folio() (such as
ntfs_mft_mark_dirty() on $MFT or mmap writes on a resident file),
ifs_alloc() attaches ifs and sets AS_DIRTY_PER_BLOCK, and
iomap_set_range_dirty() charges ifs->dirty_pages. During writeback,
writeback_iter() calls folio_clear_dirty_for_io(), which skips discharging
the dirty counters because mapping_dirty_per_block(mapping) &&
folio->private is true, and neither ntfs_write_mft_block() nor
attr_data_write_resident() clears ifs dirty state or discharges
ifs->dirty_pages.
Won't this permanently leak NR_FILE_DIRTY, NR_ZONE_WRITE_PENDING,
WB_RECLAIMABLE, and WB_DONTCACHE_DIRTY on writeback, and prevent subsequent
calls to iomap_dirty_folio() on the same folio from charging the counters
because all dirty bits in ifs->state remain set?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261027112702.3747009-1-dipiets@amazon.it?part=2
next prev parent reply other threads:[~2026-10-28 11:28 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-27 11:27 [PATCH 0/2] mm, iomap: account dirty at page granularity for per-block dirty folios Salvatore Dipietro
2026-10-27 11:27 ` [PATCH 1/2] mm: add infrastructure for per-dirty-page accounting Salvatore Dipietro
2026-10-28 8:10 ` kernel test robot
2026-10-28 11:27 ` sashiko-bot
2026-10-27 11:27 ` [PATCH 2/2] mm, iomap: account dirty at page granularity for per-block dirty folios Salvatore Dipietro
2026-10-28 11:27 ` sashiko-bot [this message]
2026-10-27 12:22 ` [PATCH 0/2] " David Hildenbrand (Arm)
2026-10-27 13:45 ` Kiryl Shutsemau
2026-10-27 13:59 ` Pedro Falcato
2026-10-27 23:10 ` [syzbot ci] " syzbot ci
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-187814@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=abuehaze@amazon.com \
--cc=agruenba@redhat.com \
--cc=akpm@linux-foundation.org \
--cc=alisaidi@amazon.com \
--cc=almaz.alexandrovich@paragon-software.com \
--cc=apopple@nvidia.com \
--cc=blakgeof@amazon.com \
--cc=brauner@kernel.org \
--cc=byungchul@sk.com \
--cc=cem@kernel.org \
--cc=david@kernel.org \
--cc=dipietro.salvatore@gmail.com \
--cc=dipiets@amazon.it \
--cc=djwong@kernel.org \
--cc=dlemoal@kernel.org \
--cc=gourry@gourry.net \
--cc=hyc.lee@gmail.com \
--cc=jack@suse.cz \
--cc=joshua.hahnjy@gmail.com \
--cc=jth@kernel.org \
--cc=liam@infradead.org \
--cc=linkinjeon@kernel.org \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=matthew.brost@intel.com \
--cc=mhocko@suse.com \
--cc=miklos@szeredi.hu \
--cc=naohiro.aota@wdc.com \
--cc=rakie.kim@sk.com \
--cc=ritesh.list@gmail.com \
--cc=rppt@kernel.org \
--cc=rvvandan@amazon.com \
--cc=ryan.roberts@arm.com \
--cc=sashiko-reviews@lists.linux.dev \
--cc=sj1557.seo@samsung.com \
--cc=surenb@google.com \
--cc=vbabka@kernel.org \
--cc=viro@zeniv.linux.org.uk \
--cc=willy@infradead.org \
--cc=ying.huang@linux.alibaba.com \
--cc=yuezhang.mo@sony.com \
--cc=ziy@nvidia.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox