6.12-stable review patch. If anyone has any objections, please let me know. ------------------ From: Johannes Thumshirn [ Upstream commit db4b9eefc8ee0bcaeee4d5e6a7313905f6a2fe7c ] On a zoned filesystem a freed-but-still-dirty tree block is written out as zeros (EXTENT_BUFFER_ZONED_ZEROOUT) only to keep the zone write pointer advancing. btree_csum_one_bio() implemented this by memzeroing the extent buffer's own folios before submission. That destroys the in-memory buffer while it may still be referenced. In particular btrfs_free_tree_block() can run on it afterwards and reads the header to add a delayed reference; once the header has been zeroed it frees bytenr 0 and corrupts the extent tree (the btrfs_header_bytenr(buf) != 0 ASSERT in btrfs_free_tree_block(), or an "unable to find ref" abort). It is flaky and reproduces under fsstress, e.g. generic/461 and generic/013. Write the zeros to disk from the shared zero page instead and leave the extent buffer content untouched, so any later reference - including the delayed reference from btrfs_free_tree_block() - still sees a valid header. end_bbio_meta_write() now clears writeback on the buffer's own folios, as the bio no longer carries them. Fixes: aa6313e6ff2b ("btrfs: zoned: don't clear dirty flag of extent buffer") Assisted-by: LLM (debugging, commit message) Reviewed-by: Boris Burkov Signed-off-by: Johannes Thumshirn Signed-off-by: David Sterba Signed-off-by: Sasha Levin --- fs/btrfs/disk-io.c | 13 +++++----- fs/btrfs/extent_io.c | 58 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 74af70897bcb0..d1f245ef1911f 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -272,14 +272,15 @@ blk_status_t btree_csum_one_bio(struct btrfs_bio *bbio) return BLK_STS_IOERR; /* - * If an extent_buffer is marked as EXTENT_BUFFER_ZONED_ZEROOUT, don't - * checksum it but zero-out its content. This is done to preserve - * ordering of I/O without unnecessarily writing out data. + * An extent_buffer marked EXTENT_BUFFER_ZONED_ZEROOUT is written out as + * zeros to preserve ordering of I/O without persisting the now + * unnecessary block. The bio is fed from the shared zero page (see + * write_one_eb()), so there is nothing to checksum here. Crucially, the + * buffer's own content is left intact: it may still be referenced, e.g. + * btrfs_free_tree_block() reads its header to add a delayed reference. */ - if (test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags)) { - memzero_extent_buffer(eb, 0, eb->len); + if (test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags)) return BLK_STS_OK; - } if (WARN_ON_ONCE(found_start != eb->start)) return BLK_STS_IOERR; diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index 8753ce416c5c6..dbb81c58a2e3c 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -1839,18 +1839,21 @@ static void end_bbio_meta_write(struct btrfs_bio *bbio) struct extent_buffer *eb = bbio->private; struct btrfs_fs_info *fs_info = eb->fs_info; bool uptodate = !bbio->bio.bi_status; - struct folio_iter fi; u32 bio_offset = 0; if (!uptodate) set_btree_ioerr(eb); - bio_for_each_folio_all(fi, &bbio->bio) { - u64 start = eb->start + bio_offset; - struct folio *folio = fi.folio; - u32 len = fi.length; + /* + * Clear writeback on the buffer's own folios. The bio may carry the + * shared zero page instead (EXTENT_BUFFER_ZONED_ZEROOUT), so iterate + * the extent buffer folios rather than the bio folios. + */ + for (int i = 0; i < num_extent_folios(eb); i++) { + u32 len = min(eb->len - bio_offset, (u32)eb->folio_size); - btrfs_folio_clear_writeback(fs_info, folio, start, len); + btrfs_folio_clear_writeback(fs_info, eb->folios[i], + eb->start + bio_offset, len); bio_offset += len; } @@ -1895,7 +1898,8 @@ static noinline_for_stack void write_one_eb(struct extent_buffer *eb, struct btrfs_fs_info *fs_info = eb->fs_info; struct btrfs_bio *bbio; - prepare_eb_write(eb); + if (!test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags)) + prepare_eb_write(eb); bbio = btrfs_bio_alloc(INLINE_EXTENT_BUFFER_PAGES, REQ_OP_WRITE | REQ_META | wbc_to_write_flags(wbc), @@ -1916,9 +1920,23 @@ static noinline_for_stack void write_one_eb(struct extent_buffer *eb, folio_clear_dirty_for_io(folio); wbc->nr_to_write--; } - ret = bio_add_folio(&bbio->bio, folio, eb->len, - eb->start - folio_pos(folio)); - ASSERT(ret); + if (test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags)) { + u32 off = 0; + + while (off < eb->len) { + u32 add = min_t(u32, PAGE_SIZE, eb->len - off); + + ret = bio_add_folio(&bbio->bio, + page_folio(ZERO_PAGE(0)), + add, 0); + ASSERT(ret); + off += add; + } + } else { + ret = bio_add_folio(&bbio->bio, folio, eb->len, + eb->start - folio_pos(folio)); + ASSERT(ret); + } wbc_account_cgroup_owner(wbc, folio, eb->len); folio_unlock(folio); } else { @@ -1931,8 +1949,24 @@ static noinline_for_stack void write_one_eb(struct extent_buffer *eb, folio_lock(folio); folio_clear_dirty_for_io(folio); folio_start_writeback(folio); - ret = bio_add_folio(&bbio->bio, folio, eb->folio_size, 0); - ASSERT(ret); + if (test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags)) { + u32 off = 0; + + while (off < eb->folio_size) { + u32 add = min_t(u32, PAGE_SIZE, + eb->folio_size - off); + + ret = bio_add_folio(&bbio->bio, + page_folio(ZERO_PAGE(0)), + add, 0); + ASSERT(ret); + off += add; + } + } else { + ret = bio_add_folio(&bbio->bio, folio, + eb->folio_size, 0); + ASSERT(ret); + } wbc_account_cgroup_owner(wbc, folio, eb->folio_size); wbc->nr_to_write -= folio_nr_pages(folio); folio_unlock(folio); -- 2.53.0