Re: [PATCH] btrfs: write-protect folios during data writeback

Boris Burkov <[email protected]>
Newsgroups org.kernel.vger.linux-btrfs
Message-ID <[email protected]>
On Thu, Jul 09, 2026 at 03:08:49PM +0930, Qu Wenruo wrote:
> 
> 
> 在 2026/7/9 14:01, Boris Burkov 写道:
> > Commit 095be159f3eb ("btrfs: unify folio dirty flag clearing") replaced
> > the folio_clear_dirty_for_io() call in extent_write_cache_pages() with a
> > plain folio_test_dirty() check. Besides clearing the dirty flag,
> > folio_clear_dirty_for_io() also calls folio_mkclean(), which write-protects
> > the shared mmap PTEs mapping the folio.
> > 
> > Without the write-protection, a process with the file mmap'd can modify
> > a sector while it is being used by writeback in a way that expects a
> > stable folio (checksumming, compressing, copying, etc...) which
> > manifests as a handful of concrete bugs.
> > 
> > For large folios or subpage sectorsize, this will result in an invalid
> > checksum and later corruption reports on read (if sectorsize ==
> > folio_size() then we will always be in the case where we call
> > folio_mkclean() when we clear the dirty on the sector, which is the
> > whole folio).
> > 
> > For zoned submissions which are done in batch separate from the main
> > extent_writepage() loop, we also risk csum violations for those
> > submissions.
> > 
> > For inline extents this will subtly risk losing writes that happen
> > after/while we copy the inline extent but before we clear dirty on
> > the folio.
> > 
> > For folios spanning EOF, mmap could tamper with the zeroed bytes past
> > EOF and cause them to be persisted where future faults would impoperly
> > see them instead of zeros.
> > 
> > Finally, for compressed extents, we risk modifying the folios while we
> > work on compressing them which will result in corrupted compressed data.
> > This particular gap was introduced by a second patch in the same series:
> > Commit a4ef54dbb576 ("btrfs: make extent_range_clear_dirty_for_io() to handle sector size < page size cases")
> > 
> > We cannot simply restore the call to folio_clear_dirty_for_io() because
> > that also drops the dirty flag off the folio which violates invariants
> > introduced for large folios by
> > commit 334509ce9d07 ("btrfs: use dirty flag to check if an ordered extent needs to be truncated")
> > and results in failing to invalidate clean folios past i_size, resulting
> > in deadlocks.
> 
> Just to mention, we can workaround the problem, by extracting the real work
> of btrfs_invalidate_folio() into a helper, with a bool parameter to indicate
> if we're called from the writeback path.
> 
> Then we would have a better chance to rever to folio_clear_dirty_for_io(),
> but there could be other things involved.
> So I'm totally fine with the current hot fix, but eventually we will still
> revert back to folio_clear_dirty_for_io(), to align the code to iomap.
> 
> [...]
> > +#ifdef CONFIG_BTRFS_DEBUG
> > +/*
> > + * Writeback must write-protect a folio when locking it for IO, before
> > + * anything consumes its data (zeroing, inline copy, compression,
> > + * checksumming). If this fails, then an mmap writer would be able to
> > + * modify the data concurrently while we need it to be stable.
> > + */
> > +void btrfs_check_folio_write_protected(struct folio *folio)
> > +{
> > +	if (folio_mkclean(folio)) {
> > +		const struct btrfs_inode *inode = BTRFS_I(folio->mapping->host);
> > +
> > +		DEBUG_WARN("writable mmap PTEs, root %llu ino %llu pos %llu order %u",
> > +			   btrfs_root_id(inode->root), btrfs_ino(inode), folio_pos(folio),
> > +			   folio_order(folio));
> > +	}
> > +}
> > +#endif
> > +
> >   static noinline int lock_delalloc_folios(struct inode *inode,
> >   					 struct folio *locked_folio,
> >   					 u64 start, u64 end)
> > @@ -332,6 +352,8 @@ static noinline int lock_delalloc_folios(struct inode *inode,
> >   				folio_unlock(folio);
> >   				goto out;
> >   			}
> > +			/* Locked for writeback; revoke writable mmap PTEs before using the data. */
> > +			folio_mkclean(folio);
> 
> My question is, why we need the call here?
> 
> Isn't folio_mkclean() called during extent_write_cache_pages() when we're
> writing back the folio?
> Calling it here looks a little too early, as the folio may not even go
> through writeback later (e.g. being truncated).

The idea is:
1. call it early when you lock to not call it too late
2. it covers compression and zoned batch write in one unified spot with
   a describable invariant (locked for delalloc <-> cleaned) so its more
   future proof against future refactors or writing models.

This patch says v1 but on my git tree it's more like v6 :). Including
variants where I tried to do it last second at every relevant callsite
(basically where I put the asserts) if you want that instead, it's fine
with me, but I personally liked this the best.

> 
> Otherwise looks good to me.
> 
> Thanks,
> Qu
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.