All of lore.kernel.org
 help / color / mirror / Atom feed
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


  reply	other threads:[~2026-10-28 11:28 UTC|newest]

Thread overview: 11+ 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
2026-10-28 21:59 ` [PATCH 0/2] " Dave Chinner

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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.