* [PATCH 1/2] mm: add infrastructure for per-dirty-page accounting
[not found] <20261027112702.3747009-1-dipiets@amazon.it>
@ 2026-10-27 11:27 ` Salvatore Dipietro
2026-10-27 11:27 ` [PATCH 2/2] mm, iomap: account dirty at page granularity for per-block dirty folios Salvatore Dipietro
2026-10-27 23:10 ` [syzbot ci] " syzbot ci
2 siblings, 0 replies; 3+ messages in thread
From: Salvatore Dipietro @ 2026-10-27 11:27 UTC (permalink / raw)
To: akpm, linux-mm
Cc: ryan.roberts, abuehaze, alisaidi, blakgeof, dipietro.salvatore,
rvvandan, viro, brauner, jack, linkinjeon, sj1557.seo,
yuezhang.mo, miklos, agruenba, djwong, hyc.lee,
almaz.alexandrovich, cem, dlemoal, naohiro.aota, jth, willy,
david, ljs, liam, vbabka, rppt, surenb, mhocko, ziy,
matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
ying.huang, apopple, ritesh.list, linux-fsdevel, linux-kernel,
exfat, fuse-devel, gfs2, linux-xfs, ntfs, ntfs3, stable
Add the mm-side machinery that lets a filesystem charge the dirty
counters (NR_FILE_DIRTY, NR_ZONE_WRITE_PENDING, WB_RECLAIMABLE) per
dirty page instead of per whole folio, without yet enabling it for
any filesystem.
Add an AS_DIRTY_PER_BLOCK mapping flag with
mapping_set_dirty_per_block()/mapping_dirty_per_block() helpers, an
accounted_dirty_pages() address_space_operations callback, and an
@accounted argument to __folio_mark_dirty() (with filemap_dirty_folio()
split into __filemap_dirty_folio()) so the generic per-folio charge can
be suppressed for folios a filesystem has already accounted.
folio_accounted_dirty_pages() dispatches through the new a_ops callback,
falling back to folio_nr_pages() when no filesystem opts in, and
folio_account_cleaned() and folio migration use it in place of
folio_nr_pages(). folio_clear_dirty_for_io() leaves the charge of
per-block folios to the filesystem. No filesystem sets the flag or
registers the callback yet, so this commit is a no-op on its own.
This is a prerequisite for the following iomap patch, which fixes the
over-accounting; both are needed for stable.
Cc: stable@vger.kernel.org # v6.17+
Signed-off-by: Salvatore Dipietro <dipiets@amazon.it>
Reviewed-by: Hazem Mohamed Abuelfotoh <abuehaze@amazon.com>
---
fs/buffer.c | 4 +-
include/linux/fs.h | 7 ++++
include/linux/pagemap.h | 20 ++++++++-
mm/internal.h | 3 ++
mm/migrate.c | 17 ++++++--
mm/page-writeback.c | 91 ++++++++++++++++++++++++++++++++---------
6 files changed, 115 insertions(+), 27 deletions(-)
diff --git a/fs/buffer.c b/fs/buffer.c
index ed966fa73b1b..7ed5250dda9d 100644
--- a/fs/buffer.c
+++ b/fs/buffer.c
@@ -693,7 +693,7 @@ bool block_dirty_folio(struct address_space *mapping, struct folio *folio)
spin_unlock(&mapping->i_private_lock);
if (newly_dirty)
- __folio_mark_dirty(folio, mapping, 1);
+ __folio_mark_dirty(folio, mapping, 1, false);
if (newly_dirty)
__mark_inode_dirty(mapping->host, I_DIRTY_PAGES);
@@ -1011,7 +1011,7 @@ void mark_buffer_dirty(struct buffer_head *bh)
if (!folio_test_set_dirty(folio)) {
mapping = folio->mapping;
if (mapping)
- __folio_mark_dirty(folio, mapping, 0);
+ __folio_mark_dirty(folio, mapping, 0, false);
}
if (mapping)
__mark_inode_dirty(mapping->host, I_DIRTY_PAGES);
diff --git a/include/linux/fs.h b/include/linux/fs.h
index deb411e86661..5abd2af50b59 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -407,6 +407,13 @@ struct address_space_operations {
/* Mark a folio dirty. Return true if this dirtied it */
bool (*dirty_folio)(struct address_space *, struct folio *);
+ /*
+ * Pages of the folio currently charged to the dirty counters, for
+ * filesystems that account dirty state at sub-folio (per-block)
+ * granularity.
+ */
+ long (*accounted_dirty_pages)(struct folio *folio);
+
void (*readahead)(struct readahead_control *);
int (*write_begin)(const struct kiocb *, struct address_space *mapping,
diff --git a/include/linux/pagemap.h b/include/linux/pagemap.h
index 0adfa6605653..5bbcd9da1a6b 100644
--- a/include/linux/pagemap.h
+++ b/include/linux/pagemap.h
@@ -210,6 +210,7 @@ enum mapping_flags {
AS_WRITEBACK_MAY_DEADLOCK_ON_RECLAIM = 9,
AS_KERNEL_FILE = 10, /* mapping for a fake kernel file that shouldn't
account usage to user cgroups */
+ AS_DIRTY_PER_BLOCK = 11, /* fs accounts dirty pages, not whole folios */
/* Bits 16-25 are used for FOLIO_ORDER */
AS_FOLIO_ORDER_BITS = 5,
AS_FOLIO_ORDER_MIN = 16,
@@ -304,6 +305,20 @@ static inline void mapping_clear_release_always(struct address_space *mapping)
clear_bit(AS_RELEASE_ALWAYS, &mapping->flags);
}
+/*
+ * Set by filesystems that charge the mm dirty counters per dirty page rather
+ * than per folio, so the generic paths must not also account whole folios.
+ */
+static inline void mapping_set_dirty_per_block(struct address_space *mapping)
+{
+ set_bit(AS_DIRTY_PER_BLOCK, &mapping->flags);
+}
+
+static inline bool mapping_dirty_per_block(const struct address_space *mapping)
+{
+ return test_bit(AS_DIRTY_PER_BLOCK, &mapping->flags);
+}
+
static inline bool mapping_stable_writes(const struct address_space *mapping)
{
return test_bit(AS_STABLE_WRITES, &mapping->flags);
@@ -1311,7 +1326,10 @@ void folio_end_writeback(struct folio *folio);
void folio_end_writeback_no_dropbehind(struct folio *folio);
void folio_end_dropbehind(struct folio *folio);
void folio_wait_stable(struct folio *folio);
-void __folio_mark_dirty(struct folio *folio, struct address_space *, int warn);
+void __folio_mark_dirty(struct folio *folio, struct address_space *mapping,
+ int warn, bool accounted);
+bool __filemap_dirty_folio(struct address_space *mapping, struct folio *folio,
+ bool accounted);
void folio_account_cleaned(struct folio *folio, struct bdi_writeback *wb);
void __folio_cancel_dirty(struct folio *folio);
static inline void folio_cancel_dirty(struct folio *folio)
diff --git a/mm/internal.h b/mm/internal.h
index 38b1165212c9..4647aca5fe8d 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -40,6 +40,9 @@ void *workingset_eviction(struct folio *folio,
void workingset_refault(struct folio *folio, void *shadow);
void workingset_activation(struct folio *folio);
+/* mm/page-writeback.c */
+long folio_accounted_dirty_pages(struct folio *folio);
+
/* mm/folio.c */
void folio_add_lru_vma(struct folio *folio, struct vm_area_struct *vma);
diff --git a/mm/migrate.c b/mm/migrate.c
index 15b45832bcfa..0e7b7834a9fd 100644
--- a/mm/migrate.c
+++ b/mm/migrate.c
@@ -588,6 +588,7 @@ static int __folio_migrate_mapping(struct address_space *mapping,
struct zone *oldzone, *newzone;
int dirty;
long nr = folio_nr_pages(folio);
+ long nr_dirty = nr;
if (!mapping) {
/* Take off deferred split queue while frozen and memcg set */
@@ -614,6 +615,14 @@ static int __folio_migrate_mapping(struct address_space *mapping,
oldzone = folio_zone(folio);
newzone = folio_zone(newfolio);
+ /*
+ * Only what was charged moves between the zones below, which may be less
+ * than the whole folio. Sample it outside the i_pages lock, as it walks
+ * the per-block state, and while the folio is still dirty.
+ */
+ if (folio_test_dirty(folio))
+ nr_dirty = folio_accounted_dirty_pages(folio);
+
if (folio_test_swapcache(folio))
ci = swap_cluster_get_and_lock_irq(folio);
else
@@ -708,10 +717,10 @@ static int __folio_migrate_mapping(struct address_space *mapping,
}
#endif
if (dirty && mapping_can_writeback(mapping)) {
- mod_lruvec_state(old_lruvec, NR_FILE_DIRTY, -nr);
- __mod_zone_page_state(oldzone, NR_ZONE_WRITE_PENDING, -nr);
- mod_lruvec_state(new_lruvec, NR_FILE_DIRTY, nr);
- __mod_zone_page_state(newzone, NR_ZONE_WRITE_PENDING, nr);
+ mod_lruvec_state(old_lruvec, NR_FILE_DIRTY, -nr_dirty);
+ __mod_zone_page_state(oldzone, NR_ZONE_WRITE_PENDING, -nr_dirty);
+ mod_lruvec_state(new_lruvec, NR_FILE_DIRTY, nr_dirty);
+ __mod_zone_page_state(newzone, NR_ZONE_WRITE_PENDING, nr_dirty);
}
rcu_read_unlock();
}
diff --git a/mm/page-writeback.c b/mm/page-writeback.c
index eeab25d6ce36..6b4a8b6ad34c 100644
--- a/mm/page-writeback.c
+++ b/mm/page-writeback.c
@@ -2601,7 +2601,7 @@ EXPORT_SYMBOL(noop_dirty_folio);
* NOTE: This relies on being atomic wrt interrupts.
*/
static void folio_account_dirtied(struct folio *folio,
- struct address_space *mapping)
+ struct address_space *mapping, bool accounted)
{
struct inode *inode = mapping->host;
@@ -2614,14 +2614,21 @@ static void folio_account_dirtied(struct folio *folio,
inode_attach_wb(inode, folio);
wb = inode_to_wb(inode);
- lruvec_stat_mod_folio(folio, NR_FILE_DIRTY, nr);
- if (folio_test_dropbehind(folio))
- wb_stat_mod(wb, WB_DONTCACHE_DIRTY, nr);
- __zone_stat_mod_folio(folio, NR_ZONE_WRITE_PENDING, nr);
- __node_stat_mod_folio(folio, NR_DIRTIED, nr);
- wb_stat_mod(wb, WB_RECLAIMABLE, nr);
- wb_stat_mod(wb, WB_DIRTIED, nr);
- task_io_account_write(nr * PAGE_SIZE);
+ /*
+ * @accounted means the filesystem charged these per dirty page
+ * already; the rate limiters below stay per-folio. Keep in sync
+ * with folio_account_cleaned() and folio_clear_dirty_for_io().
+ */
+ if (!accounted) {
+ lruvec_stat_mod_folio(folio, NR_FILE_DIRTY, nr);
+ if (folio_test_dropbehind(folio))
+ wb_stat_mod(wb, WB_DONTCACHE_DIRTY, nr);
+ __zone_stat_mod_folio(folio, NR_ZONE_WRITE_PENDING, nr);
+ wb_stat_mod(wb, WB_RECLAIMABLE, nr);
+ __node_stat_mod_folio(folio, NR_DIRTIED, nr);
+ wb_stat_mod(wb, WB_DIRTIED, nr);
+ task_io_account_write(nr * PAGE_SIZE);
+ }
current->nr_dirtied += nr;
__this_cpu_add(bdp_ratelimits, nr);
@@ -2629,13 +2636,41 @@ static void folio_account_dirtied(struct folio *folio,
}
}
+/**
+ * folio_accounted_dirty_pages - pages of @folio charged to the dirty counters
+ * @folio: folio to query
+ *
+ * Use this rather than folio_nr_pages() when moving or discharging a folio's dirty
+ * accounting, as a folio is not necessarily charged for all of its pages. Walks
+ * per-block state that truncation frees, so the caller must hold the folio lock.
+ *
+ * Return: the whole folio for the usual per-folio accounting, otherwise only the
+ * pages holding a dirty block, and zero once those have been discharged.
+ */
+long folio_accounted_dirty_pages(struct folio *folio)
+{
+ struct address_space *mapping = folio->mapping;
+
+ if (mapping && mapping_dirty_per_block(mapping) && folio->private &&
+ mapping->a_ops->accounted_dirty_pages)
+ return mapping->a_ops->accounted_dirty_pages(folio);
+ return folio_nr_pages(folio);
+}
+
/*
* Helper function for deaccounting dirty page without writeback.
*
*/
void folio_account_cleaned(struct folio *folio, struct bdi_writeback *wb)
{
- long nr = folio_nr_pages(folio);
+ /*
+ * Discharge exactly what was charged; zero if the fs already did it.
+ * Counters below mirror folio_account_dirtied().
+ */
+ long nr = folio_accounted_dirty_pages(folio);
+
+ if (!nr)
+ return;
lruvec_stat_mod_folio(folio, NR_FILE_DIRTY, -nr);
if (folio_test_dropbehind(folio))
@@ -2661,7 +2696,7 @@ void folio_account_cleaned(struct folio *folio, struct bdi_writeback *wb)
* try_to_free_buffers() to fail.
*/
void __folio_mark_dirty(struct folio *folio, struct address_space *mapping,
- int warn)
+ int warn, bool accounted)
{
unsigned long flags;
@@ -2674,7 +2709,7 @@ void __folio_mark_dirty(struct folio *folio, struct address_space *mapping,
xa_lock_irqsave(&mapping->i_pages, flags);
if (folio->mapping) { /* Race with truncate? */
WARN_ON_ONCE(warn && !folio_test_uptodate(folio));
- folio_account_dirtied(folio, mapping);
+ folio_account_dirtied(folio, mapping, accounted);
__xa_set_mark(&mapping->i_pages, folio->index,
PAGECACHE_TAG_DIRTY);
}
@@ -2700,12 +2735,13 @@ void __folio_mark_dirty(struct folio *folio, struct address_space *mapping,
* simply hold the folio lock, but e.g. zap_pte_range() calls with the
* folio mapped and the pte lock held, which also locks out truncation.
*/
-bool filemap_dirty_folio(struct address_space *mapping, struct folio *folio)
+bool __filemap_dirty_folio(struct address_space *mapping, struct folio *folio,
+ bool accounted)
{
if (folio_test_set_dirty(folio))
return false;
- __folio_mark_dirty(folio, mapping, !folio_test_private(folio));
+ __folio_mark_dirty(folio, mapping, !folio_test_private(folio), accounted);
if (mapping->host) {
/* !PageAnon && !swapper_space */
@@ -2713,6 +2749,12 @@ bool filemap_dirty_folio(struct address_space *mapping, struct folio *folio)
}
return true;
}
+EXPORT_SYMBOL_GPL(__filemap_dirty_folio);
+
+bool filemap_dirty_folio(struct address_space *mapping, struct folio *folio)
+{
+ return __filemap_dirty_folio(mapping, folio, false);
+}
EXPORT_SYMBOL(filemap_dirty_folio);
/**
@@ -2912,12 +2954,21 @@ bool folio_clear_dirty_for_io(struct folio *folio)
*/
wb = unlocked_inode_to_wb_begin(inode, &cookie);
if (folio_test_clear_dirty(folio)) {
- long nr = folio_nr_pages(folio);
- lruvec_stat_mod_folio(folio, NR_FILE_DIRTY, -nr);
- if (folio_test_dropbehind(folio))
- wb_stat_mod(wb, WB_DONTCACHE_DIRTY, -nr);
- zone_stat_mod_folio(folio, NR_ZONE_WRITE_PENDING, -nr);
- wb_stat_mod(wb, WB_RECLAIMABLE, -nr);
+ /*
+ * Per-block folios keep their dirty bits, and the charge,
+ * until the fs writes them back. Folios of such a mapping
+ * that never got per-block state were charged whole, so
+ * they discharge here as usual.
+ */
+ if (!mapping_dirty_per_block(mapping) || !folio->private) {
+ long nr = folio_nr_pages(folio);
+
+ lruvec_stat_mod_folio(folio, NR_FILE_DIRTY, -nr);
+ if (folio_test_dropbehind(folio))
+ wb_stat_mod(wb, WB_DONTCACHE_DIRTY, -nr);
+ zone_stat_mod_folio(folio, NR_ZONE_WRITE_PENDING, -nr);
+ wb_stat_mod(wb, WB_RECLAIMABLE, -nr);
+ }
ret = true;
}
unlocked_inode_to_wb_end(inode, &cookie);
--
2.50.1
AMAZON DEVELOPMENT CENTER ITALY SRL, viale Monte Grappa 3/5, 20124 Milano, Italia, Registro delle Imprese di Milano Monza Brianza Lodi REA n. 2504859, Capitale Sociale: 10.000 EUR i.v., Cod. Fisc. e P.IVA 10100050961, Societa con Socio Unico
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH 2/2] mm, iomap: account dirty at page granularity for per-block dirty folios
[not found] <20261027112702.3747009-1-dipiets@amazon.it>
2026-10-27 11:27 ` [PATCH 1/2] mm: add infrastructure for per-dirty-page accounting Salvatore Dipietro
@ 2026-10-27 11:27 ` Salvatore Dipietro
2026-10-27 23:10 ` [syzbot ci] " syzbot ci
2 siblings, 0 replies; 3+ messages in thread
From: Salvatore Dipietro @ 2026-10-27 11:27 UTC (permalink / raw)
To: akpm, linux-mm
Cc: ryan.roberts, abuehaze, alisaidi, blakgeof, dipietro.salvatore,
rvvandan, viro, brauner, jack, linkinjeon, sj1557.seo,
yuezhang.mo, miklos, agruenba, djwong, hyc.lee,
almaz.alexandrovich, cem, dlemoal, naohiro.aota, jth, willy,
david, ljs, liam, vbabka, rppt, surenb, mhocko, ziy,
matthew.brost, joshua.hahnjy, rakie.kim, byungchul, gourry,
ying.huang, apopple, ritesh.list, linux-fsdevel, linux-kernel,
exfat, fuse-devel, gfs2, linux-xfs, ntfs, ntfs3, stable
The mm dirty counters (NR_FILE_DIRTY, NR_ZONE_WRITE_PENDING,
WB_RECLAIMABLE) are charged per folio, which charges up to 512
pages for a single dirty 4 kB block of a 2 MB folio. iomap knows
exactly which blocks are dirty and writeback only writes those
ranges, so balance_dirty_pages() ends up throttling writers
against a dirty count far larger than the data that actually
needs writeback.
Charge the counters per dirty page for folios carrying per-block
dirty state, using the infrastructure from the previous patch:
struct iomap_folio_state counts the pages holding a dirty block, and
each dirty bitmap change charges or discharges the difference. The
charge survives folio_clear_dirty_for_io() and is dropped when
writeback or invalidation clears the blocks. The monotonic
NR_DIRTIED / WB_DIRTIED / task_io totals are charged only for newly
dirtied data, and the dirty rate limiters stay per-folio.
Every iomap-based filesystem sets .accounted_dirty_pages alongside
.dirty_folio = iomap_dirty_folio.
On pgbench simple-update (1024 clients, XFS, 96-vCPU arm64,
10 iterations) on v7.3-rc7, the unpatched kernel charges 159 TB
of dirtied pages while writing 6 TB to the array, a factor of 27;
with this patch the dirtied pages charged match the data written.
Unpatched, nr_dirty is above dirty_threshold in ~17% of 5s samples
and its peak exceeds it in 9 of 10 iterations; patched, it never
reaches the threshold.
On v7.3-rc7, average throughput rises 46.8% (139.8k -> 205.2k tps)
and run-to-run variability drops from 23.60% to 3.98% coefficient
of variation. The best iteration improves by 8% (199.4k -> 215.9k
tps); most of the gain comes from the worst iteration, up from 97.9k
to 191.5k tps, i.e. from throttling episodes that no longer happen.
Fixes: 4ce02c679722 ("iomap: Add per-block dirty state tracking to improve performance")
Cc: stable@vger.kernel.org # v6.17+
Signed-off-by: Salvatore Dipietro <dipiets@amazon.it>
Reviewed-by: Hazem Mohamed Abuelfotoh <abuehaze@amazon.com>
---
fs/exfat/inode.c | 1 +
fs/fuse/file.c | 1 +
fs/gfs2/aops.c | 1 +
fs/iomap/buffered-io.c | 167 +++++++++++++++++++++++++++++++++++++++--
fs/ntfs/aops.c | 2 +
fs/ntfs3/inode.c | 2 +
fs/xfs/xfs_aops.c | 1 +
fs/zonefs/file.c | 1 +
include/linux/iomap.h | 1 +
9 files changed, 171 insertions(+), 6 deletions(-)
diff --git a/fs/exfat/inode.c b/fs/exfat/inode.c
index ccd13630187e..48704fe5d59b 100644
--- a/fs/exfat/inode.c
+++ b/fs/exfat/inode.c
@@ -302,6 +302,7 @@ static const struct address_space_operations exfat_aops = {
.readahead = exfat_readahead,
.writepages = exfat_writepages,
.dirty_folio = iomap_dirty_folio,
+ .accounted_dirty_pages = iomap_accounted_dirty_pages,
.bmap = exfat_aop_bmap,
.migrate_folio = filemap_migrate_folio,
.is_partially_uptodate = iomap_is_partially_uptodate,
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index 9a36d0329e22..eb7179f854ee 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -3101,6 +3101,7 @@ static const struct address_space_operations fuse_file_aops = {
.writepages = fuse_writepages,
.launder_folio = fuse_launder_folio,
.dirty_folio = iomap_dirty_folio,
+ .accounted_dirty_pages = iomap_accounted_dirty_pages,
.release_folio = iomap_release_folio,
.invalidate_folio = iomap_invalidate_folio,
.is_partially_uptodate = iomap_is_partially_uptodate,
diff --git a/fs/gfs2/aops.c b/fs/gfs2/aops.c
index 0a7b8076af3a..ea77af00b111 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,
.release_folio = iomap_release_folio,
.invalidate_folio = iomap_invalidate_folio,
.bmap = gfs2_bmap,
diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c
index 0a5ebfda90f1..75671b29f7cc 100644
--- a/fs/iomap/buffered-io.c
+++ b/fs/iomap/buffered-io.c
@@ -5,6 +5,8 @@
*/
#include <linux/iomap.h>
#include <linux/buffer_head.h>
+#include <linux/backing-dev.h>
+#include <linux/task_io_accounting_ops.h>
#include <linux/writeback.h>
#include <linux/swap.h>
#include <linux/migrate.h>
@@ -24,6 +26,13 @@ struct iomap_folio_state {
unsigned int read_bytes_pending;
atomic_t write_bytes_pending;
+ /*
+ * Pages holding at least one dirty block, the unit the mm dirty counters
+ * work in. Maintained under @state_lock alongside the dirty bits below,
+ * so it is what those counters were charged for.
+ */
+ unsigned int dirty_pages;
+
/*
* Each block has two bits in this bitmap:
* Bits [0..blocks_per_folio) has the uptodate status.
@@ -180,6 +189,89 @@ static unsigned iomap_find_dirty_range(struct folio *folio, u64 *range_start,
return range_end - *range_start;
}
+/*
+ * Count pages in [first_blk, last_blk] holding at least one dirty block. Callers
+ * take this before and after flipping the bitmap for an exact delta, so each page
+ * is scanned over its full block span to land in the same page in both counts.
+ * state_lock must be held.
+ */
+static unsigned int ifs_count_dirty_pages(struct folio *folio,
+ struct iomap_folio_state *ifs, unsigned int first_blk,
+ unsigned int last_blk)
+{
+ struct inode *inode = folio->mapping->host;
+ unsigned int blks_per_folio = i_blocks_per_folio(inode, folio);
+ unsigned int blkbits = inode->i_blkbits;
+ size_t fsize = folio_size(folio);
+ size_t start = (size_t)first_blk << blkbits;
+ size_t end = min(((size_t)last_blk << blkbits) + (1UL << blkbits) - 1,
+ fsize - 1);
+ unsigned long first_pg = start >> PAGE_SHIFT;
+ unsigned long last_pg = end >> PAGE_SHIFT;
+ unsigned int dirty_pages = 0;
+ unsigned long pg;
+
+ for (pg = first_pg; pg <= last_pg; pg++) {
+ size_t pg_start = pg << PAGE_SHIFT;
+ size_t pg_end = min(pg_start + PAGE_SIZE - 1, fsize - 1);
+ unsigned int b_lo = pg_start >> blkbits;
+ unsigned int b_hi = pg_end >> blkbits;
+ unsigned int b;
+
+ for (b = b_lo; b <= b_hi; b++) {
+ if (test_bit(b + blks_per_folio, ifs->state)) {
+ dirty_pages++;
+ break;
+ }
+ }
+ }
+ return dirty_pages;
+}
+
+/*
+ * Mirror folio_account_dirtied() in page-sized increments; @nr_pages is positive
+ * to charge and negative to discharge.
+ *
+ * The NR_DIRTIED/WB_DIRTIED/task_io totals are monotonic, so only @new_dirty data
+ * may charge them; writeback re-marks an already charged folio.
+ *
+ * Only the folio lock is held, not i_pages, hence the IRQ-safe stat helpers.
+ */
+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);
+
+ if (dirtied) {
+ task_io_account_write(nr_pages * PAGE_SIZE);
+ } else if (nr_pages < 0 && !folio_test_writeback(folio)) {
+ /* truncated or invalidated before reaching disk, so refund */
+ task_io_account_cancelled_write((-nr_pages) * PAGE_SIZE);
+ }
+}
+
/*
* Clear the per-block dirty bits for the range [@off, @off + @len) within a
* folio. The range is rounded inwards so that only blocks fully covered by
@@ -195,15 +287,22 @@ static void ifs_clear_range_dirty(struct folio *folio,
unsigned int first_blk = round_up(off, i_blocksize(inode)) >>
inode->i_blkbits;
unsigned int last_blk = (off + len) >> inode->i_blkbits;
+ unsigned int before, after;
unsigned long flags;
if (first_blk >= last_blk)
return;
spin_lock_irqsave(&ifs->state_lock, flags);
+ before = ifs_count_dirty_pages(folio, ifs, first_blk, last_blk - 1);
bitmap_clear(ifs->state, first_blk + blks_per_folio,
last_blk - first_blk);
+ after = ifs_count_dirty_pages(folio, ifs, first_blk, last_blk - 1);
+ ifs->dirty_pages -= before - after;
spin_unlock_irqrestore(&ifs->state_lock, flags);
+
+ /* Pages that lost their last dirty block, so before >= after. */
+ iomap_account_dirty_pages(folio, -(long)(before - after), false);
}
static void iomap_clear_range_dirty(struct folio *folio, size_t off, size_t len)
@@ -222,11 +321,13 @@ static void iomap_clear_range_dirty(struct folio *folio, size_t off, size_t len)
* data loss when partial blocks are written.
*/
static void ifs_set_range_dirty(struct folio *folio,
- struct iomap_folio_state *ifs, size_t off, size_t len)
+ struct iomap_folio_state *ifs, size_t off, size_t len,
+ bool new_dirty)
{
struct inode *inode = folio->mapping->host;
unsigned int blks_per_folio = i_blocks_per_folio(inode, folio);
unsigned int first_blk, last_blk;
+ unsigned int before, after;
unsigned long flags;
if (!len)
@@ -235,9 +336,15 @@ static void ifs_set_range_dirty(struct folio *folio,
first_blk = off >> inode->i_blkbits;
last_blk = (off + len - 1) >> inode->i_blkbits;
spin_lock_irqsave(&ifs->state_lock, flags);
+ before = ifs_count_dirty_pages(folio, ifs, first_blk, last_blk);
bitmap_set(ifs->state, first_blk + blks_per_folio,
last_blk - first_blk + 1);
+ after = ifs_count_dirty_pages(folio, ifs, first_blk, last_blk);
+ ifs->dirty_pages += after - before;
spin_unlock_irqrestore(&ifs->state_lock, flags);
+
+ /* Pages that gained their first dirty block, so after >= before. */
+ iomap_account_dirty_pages(folio, (long)(after - before), new_dirty);
}
static void iomap_set_range_dirty(struct folio *folio, size_t off, size_t len)
@@ -245,7 +352,20 @@ static void iomap_set_range_dirty(struct folio *folio, size_t off, size_t len)
struct iomap_folio_state *ifs = folio->private;
if (ifs)
- ifs_set_range_dirty(folio, ifs, off, len);
+ ifs_set_range_dirty(folio, ifs, off, len, true);
+}
+
+/*
+ * Set the dirty bits for blocks that were already dirty, and already counted,
+ * before this folio had per-block state: the reclaimable counters still move,
+ * but the dirtied totals must not be charged twice.
+ */
+static void iomap_redirty_range(struct folio *folio, size_t off, size_t len)
+{
+ struct iomap_folio_state *ifs = folio->private;
+
+ if (ifs)
+ ifs_set_range_dirty(folio, ifs, off, len, false);
}
static struct iomap_folio_state *ifs_alloc(struct inode *inode,
@@ -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);
return ifs;
}
@@ -839,6 +963,11 @@ void iomap_invalidate_folio(struct folio *folio, size_t offset, size_t len)
*/
if (offset == 0 && len == folio_size(folio)) {
WARN_ON_ONCE(folio_test_writeback(folio));
+ /*
+ * Discharge while the bitmap is still attached; ifs_free() below
+ * drops the state that records how many pages were charged.
+ */
+ iomap_clear_range_dirty(folio, 0, folio_size(folio));
folio_cancel_dirty(folio);
ifs_free(folio);
} else {
@@ -854,10 +983,31 @@ bool iomap_dirty_folio(struct address_space *mapping, struct folio *folio)
ifs_alloc(inode, folio, 0);
iomap_set_range_dirty(folio, 0, len);
- return filemap_dirty_folio(mapping, folio);
+ return __filemap_dirty_folio(mapping, folio, folio->private);
}
EXPORT_SYMBOL_GPL(iomap_dirty_folio);
+/**
+ * iomap_accounted_dirty_pages - pages of @folio charged to the dirty counters
+ * @folio: folio to query
+ *
+ * For callers that discharge what iomap charged. The caller must hold the folio
+ * lock, and @folio must have per-block state attached.
+ *
+ * Return: pages of @folio holding at least one dirty block, zero once they have
+ * been cleared, so a later discharge does not double count.
+ */
+long iomap_accounted_dirty_pages(struct folio *folio)
+{
+ struct iomap_folio_state *ifs = folio->private;
+
+ if (WARN_ON_ONCE(!ifs))
+ return 0;
+
+ return ifs->dirty_pages;
+}
+EXPORT_SYMBOL_GPL(iomap_accounted_dirty_pages);
+
static void
iomap_write_failed(struct inode *inode, loff_t pos, unsigned len)
{
@@ -1137,7 +1287,7 @@ static bool __iomap_write_end(struct inode *inode, loff_t pos, size_t len,
return false;
iomap_set_range_uptodate(folio, offset_in_folio(folio, pos), len);
iomap_set_range_dirty(folio, offset_in_folio(folio, pos), copied);
- filemap_dirty_folio(inode->i_mapping, folio);
+ __filemap_dirty_folio(inode->i_mapping, folio, folio->private);
return true;
}
@@ -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);
diff --git a/fs/ntfs/aops.c b/fs/ntfs/aops.c
index 517d9a1563e7..d26ddd5ae118 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 4ac26c80bd34..509570091e48 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,
.migrate_folio = filemap_migrate_folio,
.release_folio = iomap_release_folio,
.invalidate_folio = iomap_invalidate_folio,
@@ -2309,6 +2310,7 @@ const struct address_space_operations ntfs_aops = {
const struct address_space_operations ntfs_aops_cmpr = {
.read_folio = ntfs_read_folio,
.dirty_folio = iomap_dirty_folio,
+ .accounted_dirty_pages = iomap_accounted_dirty_pages,
.release_folio = iomap_release_folio,
.invalidate_folio = iomap_invalidate_folio,
};
diff --git a/fs/xfs/xfs_aops.c b/fs/xfs/xfs_aops.c
index 8b6119776fb3..869248241f3b 100644
--- a/fs/xfs/xfs_aops.c
+++ b/fs/xfs/xfs_aops.c
@@ -682,6 +682,7 @@ const struct address_space_operations xfs_address_space_operations = {
.readahead = xfs_vm_readahead,
.writepages = xfs_vm_writepages,
.dirty_folio = iomap_dirty_folio,
+ .accounted_dirty_pages = iomap_accounted_dirty_pages,
.release_folio = iomap_release_folio,
.invalidate_folio = iomap_invalidate_folio,
.bmap = xfs_vm_bmap,
diff --git a/fs/zonefs/file.c b/fs/zonefs/file.c
index 5b34849be7a2..9847ce9cd68b 100644
--- a/fs/zonefs/file.c
+++ b/fs/zonefs/file.c
@@ -192,6 +192,7 @@ const struct address_space_operations zonefs_file_aops = {
.readahead = zonefs_readahead,
.writepages = zonefs_writepages,
.dirty_folio = iomap_dirty_folio,
+ .accounted_dirty_pages = iomap_accounted_dirty_pages,
.release_folio = iomap_release_folio,
.invalidate_folio = iomap_invalidate_folio,
.migrate_folio = filemap_migrate_folio,
diff --git a/include/linux/iomap.h b/include/linux/iomap.h
index bc7ae6327dbf..fbf6480ac928 100644
--- a/include/linux/iomap.h
+++ b/include/linux/iomap.h
@@ -444,6 +444,7 @@ bool iomap_release_folio(struct folio *folio, gfp_t gfp_flags);
void iomap_invalidate_folio(struct folio *folio, size_t offset, size_t len);
bool iomap_dirty_folio(struct address_space *mapping, struct folio *folio);
void iomap_folio_mark_uptodate(struct folio *folio);
+long iomap_accounted_dirty_pages(struct folio *folio);
int iomap_file_unshare(struct inode *inode, loff_t pos, loff_t len,
const struct iomap_ops *ops,
const struct iomap_write_ops *write_ops);
--
2.50.1
AMAZON DEVELOPMENT CENTER ITALY SRL, viale Monte Grappa 3/5, 20124 Milano, Italia, Registro delle Imprese di Milano Monza Brianza Lodi REA n. 2504859, Capitale Sociale: 10.000 EUR i.v., Cod. Fisc. e P.IVA 10100050961, Societa con Socio Unico
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [syzbot ci] Re: mm, iomap: account dirty at page granularity for per-block dirty folios
[not found] <20261027112702.3747009-1-dipiets@amazon.it>
2026-10-27 11:27 ` [PATCH 1/2] mm: add infrastructure for per-dirty-page accounting Salvatore Dipietro
2026-10-27 11:27 ` [PATCH 2/2] mm, iomap: account dirty at page granularity for per-block dirty folios Salvatore Dipietro
@ 2026-10-27 23:10 ` syzbot ci
2 siblings, 0 replies; 3+ messages in thread
From: syzbot ci @ 2026-10-27 23:10 UTC (permalink / raw)
To: abuehaze, agruenba, akpm, alisaidi, almaz.alexandrovich, apopple,
blakgeof, brauner, byungchul, cem, david, dipietro.salvatore,
dipiets, djwong, dlemoal, exfat, fuse-devel, gfs2, gourry,
hyc.lee, jack, joshua.hahnjy, jth, liam, linkinjeon,
linux-fsdevel, linux-kernel, linux-mm, linux-xfs, ljs,
matthew.brost, mhocko, miklos, naohiro.aota, ntfs3, ntfs,
rakie.kim, ritesh.list, rppt, rvvandan, ryan.roberts, sj1557.seo,
stable, surenb, vbabka, viro, willy, ying.huang, yuezhang.mo, ziy
Cc: syzbot, syzkaller-bugs
syzbot ci has tested the following series
[v1] mm, iomap: account dirty at page granularity for per-block dirty folios
https://lore.kernel.org/all/20261027112702.3747009-1-dipiets@amazon.it
* [PATCH 1/2] mm: add infrastructure for per-dirty-page accounting
* [PATCH 2/2] mm, iomap: account dirty at page granularity for per-block dirty folios
and found the following issues:
* KASAN: slab-out-of-bounds Read in folio_accounted_dirty_pages
* KASAN: slab-use-after-free Read in folio_accounted_dirty_pages
Full report is available here:
https://ci.syzbot.org/series/05041e6c-d357-4fc5-9bbe-6aae1760d639
***
KASAN: slab-out-of-bounds Read in folio_accounted_dirty_pages
tree: axboe
URL: https://kernel.googlesource.com/pub/scm/linux/kernel/git/axboe/linux.git
base: 24a19b953e2a3d0ea5a8c4951a0ab8f32720c907
arch: amd64
compiler: Debian clang version 22.1.8 (1~deb13u4), Debian LLD 22.1.8
config: https://ci.syzbot.org/builds/9865cf75-69fb-4fbd-b5fe-d0be9cd03802/config
syz repro: https://ci.syzbot.org/findings/98cdf846-0b4c-4f94-8430-3b7db9efe057/syz_repro
==================================================================
BUG: KASAN: slab-out-of-bounds in instrument_atomic_read include/linux/instrumented.h:82 [inline]
BUG: KASAN: slab-out-of-bounds in _test_bit include/asm-generic/bitops/instrumented-non-atomic.h:141 [inline]
BUG: KASAN: slab-out-of-bounds in mapping_dirty_per_block include/linux/pagemap.h:319 [inline]
BUG: KASAN: slab-out-of-bounds in folio_accounted_dirty_pages+0x53/0x1f0 mm/page-writeback.c:2654
Read of size 8 at addr ffff888168669f89 by task syz.1.47/5860
CPU: 0 UID: 0 PID: 5860 Comm: syz.1.47 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
Call Trace:
<TASK>
dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
print_address_description+0x55/0x1e0 mm/kasan/report.c:378
print_report+0x58/0x70 mm/kasan/report.c:482
kasan_report+0x117/0x150 mm/kasan/report.c:595
check_region_inline mm/kasan/generic.c:-1 [inline]
kasan_check_range+0x264/0x2c0 mm/kasan/generic.c:200
instrument_atomic_read include/linux/instrumented.h:82 [inline]
_test_bit include/asm-generic/bitops/instrumented-non-atomic.h:141 [inline]
mapping_dirty_per_block include/linux/pagemap.h:319 [inline]
folio_accounted_dirty_pages+0x53/0x1f0 mm/page-writeback.c:2654
__folio_migrate_mapping+0x3d0/0x2830 mm/migrate.c:624
__migrate_folio mm/migrate.c:893 [inline]
migrate_folio+0x10a/0x1a0 mm/migrate.c:920
move_to_new_folio+0x1ed/0x460 mm/migrate.c:1135
migrate_folio_move mm/migrate.c:1401 [inline]
migrate_folios_move mm/migrate.c:1758 [inline]
migrate_pages_batch+0x30b3/0x4f50 mm/migrate.c:2014
migrate_pages_sync mm/migrate.c:2044 [inline]
migrate_pages+0x1b9f/0x2920 mm/migrate.c:2153
migrate_to_node mm/mempolicy.c:1322 [inline]
do_migrate_pages+0x656/0x8b0 mm/mempolicy.c:1421
kernel_migrate_pages mm/mempolicy.c:1935 [inline]
__do_sys_migrate_pages mm/mempolicy.c:1953 [inline]
__se_sys_migrate_pages+0xaa2/0xc90 mm/mempolicy.c:1949
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f04fcf9f559
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f04fc5fe028 EFLAGS: 00000246 ORIG_RAX: 0000000000000100
RAX: ffffffffffffffda RBX: 00007f04fd225fa0 RCX: 00007f04fcf9f559
RDX: 0000200000000040 RSI: 0000000000000009 RDI: 0000000000000000
RBP: 00007f04fd036b20 R08: 0000000000000000 R09: 0000000000000000
R10: 0000200000000380 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f04fd226038 R14: 00007f04fd225fa0 R15: 00007ffc667db8b8
</TASK>
Allocated by task 5729:
kasan_save_stack mm/kasan/common.c:57 [inline]
kasan_save_track+0x3e/0x80 mm/kasan/common.c:78
unpoison_slab_object mm/kasan/common.c:351 [inline]
__kasan_slab_alloc+0x6c/0x80 mm/kasan/common.c:377
kasan_slab_alloc include/linux/kasan.h:253 [inline]
slab_post_alloc_hook mm/slub.c:4683 [inline]
slab_alloc_node mm/slub.c:4996 [inline]
kmem_cache_alloc_noprof+0x2b9/0x600 mm/slub.c:5010
anon_vma_alloc mm/rmap.c:94 [inline]
anon_vma_fork+0x88/0x540 mm/rmap.c:395
dup_mmap+0x954/0x1dc0 mm/mmap.c:1781
dup_mm kernel/fork.c:1543 [inline]
copy_mm+0x115/0x480 kernel/fork.c:1595
copy_process+0x1e79/0x4400 kernel/fork.c:2311
kernel_clone+0x2d7/0x940 kernel/fork.c:2771
__do_sys_clone kernel/fork.c:2913 [inline]
__se_sys_clone kernel/fork.c:2897 [inline]
__x64_sys_clone+0x1b6/0x230 kernel/fork.c:2897
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84
entry_SYSCALL_64_after_hwframe+0x77/0x7f
The buggy address belongs to the object at ffff888168669e70
which belongs to the cache anon_vma of size 200
The buggy address is located 81 bytes to the right of
allocated 200-byte region [ffff888168669e70, ffff888168669f38)
The buggy address belongs to the physical page:
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0xffff888168669108 pfn:0x168669
memcg:ffff88816db0e201
flags: 0x57ff00000000200(workingset|node=1|zone=2|lastcpupid=0x7ff)
page_type: f5(slab)
raw: 057ff00000000200 ffff888101687140 ffff88816040c7c8 ffffea0005b62950
raw: ffff888168669108 00000002000f000b 00000000f5000000 ffff88816db0e201
page dumped because: kasan: bad access detected
page_owner tracks the page as allocated
page last allocated via order 0, migratetype Unmovable, gfp_mask 0xd2cc0(GFP_KERNEL|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 5535, tgid 5535 (rm), ts 43255388535
set_page_owner include/linux/page_owner.h:33 [inline]
post_alloc_hook+0x1f9/0x250 mm/page_alloc.c:1871
prep_new_page mm/page_alloc.c:1879 [inline]
get_page_from_freelist+0x2209/0x2280 mm/page_alloc.c:3943
__alloc_frozen_pages_noprof+0x217/0x5a0 mm/page_alloc.c:5436
alloc_slab_page mm/slub.c:3347 [inline]
allocate_slab+0x7d/0x620 mm/slub.c:3462
new_slab mm/slub.c:3513 [inline]
refill_objects+0x2d5/0x350 mm/slub.c:7418
refill_sheaf mm/slub.c:2885 [inline]
__pcs_replace_empty_main+0x2c8/0x6c0 mm/slub.c:4774
alloc_from_pcs mm/slub.c:4850 [inline]
slab_alloc_node mm/slub.c:4984 [inline]
kmem_cache_alloc_noprof+0x399/0x600 mm/slub.c:5010
anon_vma_alloc mm/rmap.c:94 [inline]
__anon_vma_prepare+0x10e/0x4d0 mm/rmap.c:201
__vmf_anon_prepare mm/memory.c:3961 [inline]
vmf_anon_prepare mm/internal.h:572 [inline]
do_anonymous_page mm/memory.c:5478 [inline]
do_pte_missing+0x2f18/0x34b0 mm/memory.c:4698
handle_pte_fault mm/memory.c:6558 [inline]
__handle_mm_fault mm/memory.c:6707 [inline]
handle_mm_fault+0x1a53/0x3750 mm/memory.c:6876
do_user_addr_fault+0xa4f/0x1340 arch/x86/mm/fault.c:1343
handle_page_fault arch/x86/mm/fault.c:1483 [inline]
exc_page_fault+0x6a/0xc0 arch/x86/mm/fault.c:1536
asm_exc_page_fault+0x26/0x30 arch/x86/include/asm/idtentry.h:595
page last free pid 5049 tgid 5049 ts 18476194117 stack trace:
reset_page_owner include/linux/page_owner.h:26 [inline]
__free_pages_prepare mm/page_alloc.c:1418 [inline]
__free_frozen_pages+0xc93/0xd90 mm/page_alloc.c:2962
__slab_free+0x274/0x2c0 mm/slub.c:5823
qlink_free mm/kasan/quarantine.c:163 [inline]
qlist_free_all+0x99/0x100 mm/kasan/quarantine.c:179
kasan_quarantine_reduce+0x148/0x160 mm/kasan/quarantine.c:286
__kasan_slab_alloc+0x22/0x80 mm/kasan/common.c:361
kasan_slab_alloc include/linux/kasan.h:253 [inline]
slab_post_alloc_hook mm/slub.c:4683 [inline]
slab_alloc_node mm/slub.c:4996 [inline]
kmem_cache_alloc_node_noprof+0x354/0x610 mm/slub.c:5068
__alloc_skb+0x1db/0x7a0 net/core/skbuff.c:704
alloc_skb include/linux/skbuff.h:1384 [inline]
alloc_skb_with_frags+0xc6/0x760 net/core/skbuff.c:6803
sock_alloc_send_pskb+0x878/0x990 net/core/sock.c:3022
unix_dgram_sendmsg+0x4da/0x17b0 net/unix/af_unix.c:2144
sock_sendmsg_nosec+0x13a/0x180 net/socket.c:800
__sock_sendmsg net/socket.c:815 [inline]
sock_write_iter+0x2de/0x3e0 net/socket.c:1266
new_sync_write fs/read_write.c:595 [inline]
vfs_write+0x612/0xba0 fs/read_write.c:687
ksys_write+0x150/0x270 fs/read_write.c:739
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84
entry_SYSCALL_64_after_hwframe+0x77/0x7f
Memory state around the buggy address:
ffff888168669e80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
ffff888168669f00: 00 00 00 00 00 00 00 fc fc fc fc fc fc fc fc fc
>ffff888168669f80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
^
ffff88816866a000: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
ffff88816866a080: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fc fc
==================================================================
***
KASAN: slab-use-after-free Read in folio_accounted_dirty_pages
tree: axboe
URL: https://kernel.googlesource.com/pub/scm/linux/kernel/git/axboe/linux.git
base: 24a19b953e2a3d0ea5a8c4951a0ab8f32720c907
arch: amd64
compiler: Debian clang version 22.1.8 (1~deb13u4), Debian LLD 22.1.8
config: https://ci.syzbot.org/builds/9865cf75-69fb-4fbd-b5fe-d0be9cd03802/config
syz repro: https://ci.syzbot.org/findings/f9e47a91-9c37-465b-b43c-7404e404cbfd/syz_repro
==================================================================
BUG: KASAN: slab-use-after-free in instrument_atomic_read include/linux/instrumented.h:82 [inline]
BUG: KASAN: slab-use-after-free in _test_bit include/asm-generic/bitops/instrumented-non-atomic.h:141 [inline]
BUG: KASAN: slab-use-after-free in mapping_dirty_per_block include/linux/pagemap.h:319 [inline]
BUG: KASAN: slab-use-after-free in folio_accounted_dirty_pages+0x53/0x1f0 mm/page-writeback.c:2654
Read of size 8 at addr ffff888111a3d539 by task syz.0.17/5870
CPU: 0 UID: 0 PID: 5870 Comm: syz.0.17 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
Call Trace:
<TAS
<TASK>
dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
print_address_description+0x55/0x1e0 mm/kasan/report.c:378
print_report+0x58/0x70 mm/kasan/report.c:482
kasan_report+0x117/0x150 mm/kasan/report.c:595
check_region_inline mm/kasan/generic.c:-1 [inline]
kasan_check_range+0x264/0x2c0 mm/kasan/generic.c:200
instrument_atomic_read include/linux/instrumented.h:82 [inline]
_test_bit include/asm-generic/bitops/instrumented-non-atomic.h:141 [inline]
mapping_dirty_per_block include/linux/pagemap.h:319 [inline]
folio_accounted_dirty_pages+0x53/0x1f0 mm/page-writeback.c:2654
__folio_migrate_mapping+0x3d0/0x2830 mm/migrate.c:624
__migrate_folio mm/migrate.c:893 [inline]
migrate_folio+0x10a/0x1a0 mm/migrate.c:920
move_to_new_folio+0x1ed/0x460 mm/migrate.c:1135
migrate_folio_move mm/migrate.c:1401 [inline]
migrate_folios_move mm/migrate.c:1758 [inline]
migrate_pages_batch+0x30b3/0x4f50 mm/migrate.c:2014
migrate_pages_sync mm/migrate.c:2044 [inline]
migrate_pages+0x1b9f/0x2920 mm/migrate.c:2153
migrate_to_node mm/mempolicy.c:1322 [inline]
do_migrate_pages+0x656/0x8b0 mm/mempolicy.c:1421
kernel_migrate_pages mm/mempolicy.c:1935 [inline]
__do_sys_migrate_pages mm/mempolicy.c:1953 [inline]
__se_sys_migrate_pages+0xaa2/0xc90 mm/mempolicy.c:1949
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f600f39f559
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f6010302028 EFLAGS: 00000246 ORIG_RAX: 0000000000000100
RAX: ffffffffffffffda RBX: 00007f600f625fa0 RCX: 00007f600f39f559
RDX: 0000200000000040 RSI: 0000000000000009 RDI: 0000000000000000
RBP: 00007f600f436b20 R08: 0000000000000000 R09: 0000000000000000
R10: 0000200000000380 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f600f626038 R14: 00007f600f625fa0 R15: 00007ffcbfa14028
</TASK>
Allocated by task 5645:
kasan_save_stack mm/kasan/common.c:57 [inline]
kasan_save_track+0x3e/0x80 mm/kasan/common.c:78
unpoison_slab_object mm/kasan/common.c:351 [inline]
__kasan_slab_alloc+0x6c/0x80 mm/kasan/common.c:377
kasan_slab_alloc include/linux/kasan.h:253 [inline]
slab_post_alloc_hook mm/slub.c:4683 [inline]
slab_alloc_node mm/slub.c:4996 [inline]
kmem_cache_alloc_noprof+0x2b9/0x600 mm/slub.c:5010
anon_vma_alloc mm/rmap.c:94 [inline]
__anon_vma_prepare+0x10e/0x4d0 mm/rmap.c:201
__vmf_anon_prepare mm/memory.c:3961 [inline]
vmf_anon_prepare mm/internal.h:572 [inline]
do_anonymous_page mm/memory.c:5478 [inline]
do_pte_missing+0x2f18/0x34b0 mm/memory.c:4698
handle_pte_fault mm/memory.c:6558 [inline]
__handle_mm_fault mm/memory.c:6707 [inline]
handle_mm_fault+0x1a53/0x3750 mm/memory.c:6876
do_user_addr_fault+0xa4f/0x1340 arch/x86/mm/fault.c:1343
handle_page_fault arch/x86/mm/fault.c:1483 [inline]
exc_page_fault+0x6a/0xc0 arch/x86/mm/fault.c:1536
asm_exc_page_fault+0x26/0x30 arch/x86/include/asm/idtentry.h:595
Freed by task 5646:
kasan_save_stack mm/kasan/common.c:57 [inline]
kasan_save_track+0x3e/0x80 mm/kasan/common.c:78
kasan_save_free_info+0x40/0x50 mm/kasan/generic.c:584
poison_slab_object mm/kasan/common.c:264 [inline]
__kasan_slab_free+0x5c/0x80 mm/kasan/common.c:296
kasan_slab_free include/linux/kasan.h:235 [inline]
slab_free_hook mm/slub.c:2748 [inline]
slab_free_after_rcu_debug+0x12a/0x220 mm/slub.c:6571
rcu_do_batch kernel/rcu/tree.c:2650 [inline]
rcu_core+0x926/0x1260 kernel/rcu/tree.c:2919
handle_softirqs+0x226/0x860 kernel/softirq.c:645
__do_softirq kernel/softirq.c:679 [inline]
invoke_softirq kernel/softirq.c:519 [inline]
__irq_exit_rcu+0xcb/0x220 kernel/softirq.c:758
irq_exit_rcu+0x9/0x30 kernel/softirq.c:775
instr_sysvec_apic_timer_interrupt arch/x86/kernel/apic/apic.c:1062 [inline]
sysvec_apic_timer_interrupt+0xa6/0xc0 arch/x86/kernel/apic/apic.c:1062
asm_sysvec_apic_timer_interrupt+0x1a/0x20 arch/x86/include/asm/idtentry.h:674
Last potentially related work creation:
kasan_save_stack+0x3e/0x60 mm/kasan/common.c:57
kasan_record_aux_stack+0xbd/0xd0 mm/kasan/generic.c:556
slab_free_hook mm/slub.c:2709 [inline]
slab_free mm/slub.c:6509 [inline]
kmem_cache_free+0x45e/0x650 mm/slub.c:6636
anon_vma_free mm/rmap.c:137 [inline]
__put_anon_vma+0x12b/0x2d0 mm/rmap.c:2974
put_anon_vma mm/internal.h:293 [inline]
unlink_anon_vmas+0x544/0x720 mm/rmap.c:540
free_pgtables+0x663/0xb70 mm/memory.c:401
unmap_region+0x29d/0x330 mm/vma.c:534
vms_clear_ptes mm/vma.c:1383 [inline]
vms_complete_munmap_vmas+0x427/0xbe0 mm/vma.c:1425
do_vmi_align_munmap+0x3b7/0x4b0 mm/vma.c:1684
do_vmi_munmap+0x252/0x2d0 mm/vma.c:1732
__vm_munmap+0x241/0x3e0 mm/vma.c:3415
__do_sys_munmap mm/mmap.c:1094 [inline]
__se_sys_munmap mm/mmap.c:1091 [inline]
__x64_sys_munmap+0x60/0x70 mm/mmap.c:1091
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84
entry_SYSCALL_64_after_hwframe+0x77/0x7f
The buggy address belongs to the object at ffff888111a3d528
which belongs to the cache anon_vma of size 200
The buggy address is located 17 bytes inside of
freed 200-byte region [ffff888111a3d528, ffff888111a3d5f0)
The buggy address belongs to the physical page:
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0xffff888111a3d528 pfn:0x111a3d
memcg:ffff888111a3df79
flags: 0x17ff00000000200(workingset|node=0|zone=2|lastcpupid=0x7ff)
page_type: f5(slab)
raw: 017ff00000000200 ffff888101688140 ffff8881006957c8 ffffea00047d6bd0
raw: ffff888111a3d528 00000002000f0009 00000000f5000000 ffff888111a3df79
page dumped because: kasan: bad access detected
page_owner tracks the page as allocated
page last allocated via order 0, migratetype Unmovable, gfp_mask 0xd2cc0(GFP_KERNEL|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 5634, tgid 5634 (syz-executor), ts 59634551582
set_page_owner include/linux/page_owner.h:33 [inline]
post_alloc_hook+0x1f9/0x250 mm/page_alloc.c:1871
prep_new_page mm/page_alloc.c:1879 [inline]
get_page_from_freelist+0x2209/0x2280 mm/page_alloc.c:3943
__alloc_frozen_pages_noprof+0x217/0x5a0 mm/page_alloc.c:5436
alloc_slab_page mm/slub.c:3347 [inline]
allocate_slab+0x7d/0x620 mm/slub.c:3462
new_slab mm/slub.c:3513 [inline]
refill_objects+0x2d5/0x350 mm/slub.c:7418
refill_sheaf mm/slub.c:2885 [inline]
__pcs_replace_empty_main+0x2c8/0x6c0 mm/slub.c:4774
alloc_from_pcs mm/slub.c:4850 [inline]
slab_alloc_node mm/slub.c:4984 [inline]
kmem_cache_alloc_noprof+0x399/0x600 mm/slub.c:5010
anon_vma_alloc mm/rmap.c:94 [inline]
anon_vma_fork+0x88/0x540 mm/rmap.c:395
dup_mmap+0x954/0x1dc0 mm/mmap.c:1781
dup_mm kernel/fork.c:1543 [inline]
copy_mm+0x115/0x480 kernel/fork.c:1595
copy_process+0x1e79/0x4400 kernel/fork.c:2311
kernel_clone+0x2d7/0x940 kernel/fork.c:2771
__do_sys_clone kernel/fork.c:2913 [inline]
__se_sys_clone kernel/fork.c:2897 [inline]
__x64_sys_clone+0x1b6/0x230 kernel/fork.c:2897
do_syscall_x64 arch/x86/entry/syscall_64.c:61 [inline]
do_syscall_64+0x166/0x520 arch/x86/entry/syscall_64.c:84
entry_SYSCALL_64_after_hwframe+0x77/0x7f
page last free pid 805 tgid 805 ts 42732995302 stack trace:
reset_page_owner include/linux/page_owner.h:26 [inline]
__free_pages_prepare mm/page_alloc.c:1418 [inline]
__free_frozen_pages+0xc93/0xd90 mm/page_alloc.c:2962
__slab_free+0x274/0x2c0 mm/slub.c:5823
qlink_free mm/kasan/quarantine.c:163 [inline]
qlist_free_all+0x99/0x100 mm/kasan/quarantine.c:179
kasan_quarantine_reduce+0x148/0x160 mm/kasan/quarantine.c:286
__kasan_slab_alloc+0x22/0x80 mm/kasan/common.c:361
kasan_slab_alloc include/linux/kasan.h:253 [inline]
slab_post_alloc_hook mm/slub.c:4683 [inline]
slab_alloc_node mm/slub.c:4996 [inline]
__kmalloc_cache_noprof+0x29f/0x600 mm/slub.c:5559
_kmalloc_noprof include/linux/slab.h:991 [inline]
_kzalloc_noprof include/linux/slab.h:1312 [inline]
drm_atomic_commit_alloc+0xa9/0x100 drivers/gpu/drm/drm_atomic.c:258
drm_atomic_helper_dirtyfb+0x129/0xfd0 drivers/gpu/drm/drm_damage_helper.c:128
drm_fbdev_shmem_helper_fb_dirty+0x160/0x310 drivers/gpu/drm/drm_fbdev_shmem.c:117
drm_fb_helper_fb_dirty drivers/gpu/drm/drm_fb_helper.c:339 [inline]
drm_fb_helper_damage_work+0x84c/0xf20 drivers/gpu/drm/drm_fb_helper.c:365
process_one_work kernel/workqueue.c:3396 [inline]
process_scheduled_works+0xc3d/0x1630 kernel/workqueue.c:3479
worker_thread+0xa47/0xfb0 kernel/workqueue.c:3560
kthread+0x38b/0x480 kernel/kthread.c:436
ret_from_fork+0x514/0xb70 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
Memory state around the buggy address:
ffff888111a3d400: fc fc fc fc 00 00 00 00 00 00 00 00 00 00 00 00
ffff888111a3d480: 00 00 00 00 00 00 00 00 00 00 00 00 00 fc fc fc
>ffff888111a3d500: fc fc fc fc fc fa fb fb fb fb fb fb fb fb fb fb
^
ffff888111a3d580: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fc fc
ffff888111a3d600: fc fc fc fc fc fc fa fb fb fb fb fb fb fb fb fb
==================================================================
***
If these findings have caused you to resend the series or submit a
separate fix, please add the following tag to your commit message:
Tested-by: syzbot@syzkaller.appspotmail.com
---
This report is generated by a bot. It may contain errors.
syzbot ci engineers can be reached at syzkaller@googlegroups.com.
To test a fix for this bug, please reply with `#syz test`
(on a separate line) and attach the patch to the email.
Notes:
- The patch will be applied on top of the tested series (as an
incremental fix).
- To test a new version of the whole series, please send it directly
to syzbot@lists.linux.dev.
- Arguments like custom git repos and branches are not supported.
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-10-27 23:10 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
[not found] <20261027112702.3747009-1-dipiets@amazon.it>
2026-10-27 11:27 ` [PATCH 1/2] mm: add infrastructure for per-dirty-page accounting Salvatore Dipietro
2026-10-27 11:27 ` [PATCH 2/2] mm, iomap: account dirty at page granularity for per-block dirty folios Salvatore Dipietro
2026-10-27 23:10 ` [syzbot ci] " syzbot ci
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox