From 1aeacc2cbf86eb522c3add7626477a4439048f8c Mon Sep 17 00:00:00 2001 From: "Matthew Wilcox (Oracle)" Date: Thu, 6 Aug 2026 12:58:24 -0400 Subject: [PATCH 01/21] buffer_head: Remove b_page All users except bh_offset() have been converted to use b_folio instead. Convert bh_offset() and remove b_page. Signed-off-by: Matthew Wilcox (Oracle) Signed-off-by: Chao Shi Acked-by: Weidong Zhu Reviewed-by: Jan Kara Link: https://patch.msgid.link/e9d168901578902bffe78ac8b5dfaf1210ee7fb3.1785951556.git.coshi036@gmail.com Signed-off-by: Christian Brauner (Amutable) --- include/linux/buffer_head.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/include/linux/buffer_head.h b/include/linux/buffer_head.h index fd2c7115c054..699970b4bbf2 100644 --- a/include/linux/buffer_head.h +++ b/include/linux/buffer_head.h @@ -59,10 +59,7 @@ struct address_space; struct buffer_head { unsigned long b_state; /* buffer state bitmap (see above) */ struct buffer_head *b_this_page;/* circular list of page's buffers */ - union { - struct page *b_page; /* the page this bh is mapped to */ - struct folio *b_folio; /* the folio this bh is mapped to */ - }; + struct folio *b_folio; /* the folio this bh is mapped to */ sector_t b_blocknr; /* start block number */ size_t b_size; /* size of mapping */ @@ -172,7 +169,7 @@ static __always_inline int buffer_uptodate(const struct buffer_head *bh) static inline unsigned long bh_offset(const struct buffer_head *bh) { - return (unsigned long)(bh)->b_data & (page_size(bh->b_page) - 1); + return (unsigned long)(bh)->b_data & (folio_size(bh->b_folio) - 1); } /* If we *know* page->private refers to buffer_heads */ From 781bf9c7e307910a843d019251f174d22796a005 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:25 -0400 Subject: [PATCH 02/21] buffer: allow a buffer_head to point at memory outside the page cache jbd2 builds a temporary buffer_head to write out the frozen copy of a metadata block, and that copy lives in slab memory. Today jbd2 points the temporary buffer at the slab folio backing it. A slab folio's ->mapping is not an address_space, so anything that follows bh->b_folio->mapping there gets garbage rather than NULL; mark_buffer_write_io_error() does exactly that, and we are about to start calling it on this buffer. Rather than teach every such helper about slab folios, allow bh->b_folio to be NULL and let b_data point straight at the memory. Code that needs the folio has to check. There are two places in this file: - __bh_submit() adds the data by virtual address using bio_add_virt_nofail(), and skips the cgroup accounting: a buffer that is not in the page cache has no owning folio to attribute writeback to. - buffer_set_crypto_ctx() returns early. fscrypt has no interest in a buffer that is not part of a file mapping, which is why it already returns when the folio has no mapping. Nothing sets b_folio to NULL yet, so this patch is a no-op on its own. A buffer_head without a folio is a narrow thing, not a new general capability. Most of the buffer_head API assumes a folio and will fault or corrupt state without one - touch_buffer(), bh_offset(), the async read completion path, and plenty more - so it is up to whoever builds such a buffer to keep it away from all of that. What NULL buys us is that getting it wrong fails loudly instead of quietly following a slab folio's overloaded ->mapping. It is also only valid over memory that is always mapped: buffers over highmem have no permanent kernel virtual address, which is why folio_set_bh() records a folio and an offset instead. Suggested-by: Matthew Wilcox (Oracle) Acked-by: Weidong Zhu Signed-off-by: Chao Shi Reviewed-by: Jan Kara Link: https://patch.msgid.link/bb6fab2111a48d7ba61887fc1372362576fce6e6.1785951556.git.coshi036@gmail.com Signed-off-by: Christian Brauner (Amutable) --- fs/buffer.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/fs/buffer.c b/fs/buffer.c index ed966fa73b1b..fa55a507835a 100644 --- a/fs/buffer.c +++ b/fs/buffer.c @@ -1070,12 +1070,16 @@ EXPORT_SYMBOL(__bforget); static void buffer_set_crypto_ctx(struct bio *bio, const struct buffer_head *bh, gfp_t gfp_mask) { - const struct address_space *mapping = folio_mapping(bh->b_folio); + const struct address_space *mapping; /* * The ext4 journal (jbd2) can submit a buffer_head it directly created - * for a non-pagecache page. fscrypt doesn't care about these. + * for memory that is not in the page cache at all. fscrypt doesn't + * care about these. */ + if (!bh->b_folio) + return; + mapping = folio_mapping(bh->b_folio); if (!mapping) return; fscrypt_set_bio_crypt_ctx(bio, mapping->host, @@ -1107,7 +1111,8 @@ static void __bh_submit(struct buffer_head *bh, blk_opf_t opf, bio = bio_alloc(bh->b_bdev, 1, opf, GFP_NOIO); - if (folio_test_dropbehind(bh->b_folio) && op_is_write(opf)) + if (bh->b_folio && folio_test_dropbehind(bh->b_folio) && + op_is_write(opf)) bio_set_flag(bio, BIO_COMPLETE_IN_TASK); if (IS_ENABLED(CONFIG_FS_ENCRYPTION)) @@ -1116,7 +1121,11 @@ static void __bh_submit(struct buffer_head *bh, blk_opf_t opf, bio->bi_iter.bi_sector = bh->b_blocknr * (bh->b_size >> 9); bio->bi_write_hint = write_hint; - bio_add_folio_nofail(bio, bh->b_folio, bh->b_size, bh_offset(bh)); + if (bh->b_folio) + bio_add_folio_nofail(bio, bh->b_folio, bh->b_size, + bh_offset(bh)); + else + bio_add_virt_nofail(bio, bh->b_data, bh->b_size); bio->bi_end_io = end_bio; bio->bi_private = bh; @@ -1126,7 +1135,8 @@ static void __bh_submit(struct buffer_head *bh, blk_opf_t opf, if (wbc) { wbc_init_bio(wbc, bio); - wbc_account_cgroup_owner(wbc, bh->b_folio, bh->b_size); + if (bh->b_folio) + wbc_account_cgroup_owner(wbc, bh->b_folio, bh->b_size); } blk_crypto_submit_bio(bio); From 3c9ab45852f6368a3c9d5c18451de7d1f893cc74 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:26 -0400 Subject: [PATCH 03/21] jbd2: point the shadow buffer at the frozen data directly When a metadata buffer has to be copied out before it can be journalled, jbd2_journal_write_metadata_buffer() writes jh->b_frozen_data rather than the page cache copy. b_frozen_data is kmalloc()ed, so folio_set_bh() makes the shadow buffer point at a slab folio. That is not something the buffer_head layer can reason about. A slab folio overloads ->mapping, so a shadow buffer looks like it belongs to an address_space when it does not. buffer_set_crypto_ctx() already has to work around this, and it is the reason mark_buffer_write_io_error() cannot be called on a shadow buffer today. Point the shadow buffer at the frozen data itself instead: leave b_folio NULL, which it already is out of alloc_buffer_head(), and set b_data. The previous patch taught fs/buffer.c to submit such a buffer. folio_set_bh() is now needed on only one path - the one that journals the page cache copy directly - so it moves there, and new_folio, new_offset and the flag that used to pick between them all go away. The two commit-path checksum helpers reach the shadow buffer's contents through a new kmap_local_bh()/kunmap_local_bh() pair, which handle a buffer with or without a folio. Memory outside the page cache is always mapped, so for those there is nothing to map or unmap. Mapping it anyway would be worse than pointless: with CONFIG_DEBUG_KMAP_LOCAL_FORCE_MAP, kmap_local_page() hands back a one page mapping even for such memory, which is not enough for a buffer bigger than a page. Tested with ext4 mounted data=journal,journal_checksum on a metadata_csum filesystem, writing files whose every block begins with the JBD2 magic so that escaping forces the copy-out, then crashing with sysrq-b without unmounting and replaying the journal on the next mount. Recovery completed, the file contents matched, e2fsck -fn was clean, and an instrumented build confirmed the b_folio == NULL path was taken. Suggested-by: Matthew Wilcox (Oracle) Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/6140cd23beb88e99f40eaeff4044a16213f6caab.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/jbd2/commit.c | 8 ++++---- fs/jbd2/journal.c | 29 +++++++++++++++++------------ include/linux/buffer_head.h | 29 +++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/fs/jbd2/commit.c b/fs/jbd2/commit.c index 3029cb6f6d64..0c85af91f9b2 100644 --- a/fs/jbd2/commit.c +++ b/fs/jbd2/commit.c @@ -330,9 +330,9 @@ static __u32 jbd2_checksum_data(__u32 crc32_sum, struct buffer_head *bh) char *addr; __u32 checksum; - addr = kmap_local_folio(bh->b_folio, bh_offset(bh)); + addr = kmap_local_bh(bh); checksum = crc32_be(crc32_sum, addr, bh->b_size); - kunmap_local(addr); + kunmap_local_bh(bh, addr); return checksum; } @@ -357,10 +357,10 @@ static void jbd2_block_tag_csum_set(journal_t *j, journal_block_tag_t *tag, return; seq = cpu_to_be32(sequence); - addr = kmap_local_folio(bh->b_folio, bh_offset(bh)); + addr = kmap_local_bh(bh); csum32 = jbd2_chksum(j->j_csum_seed, (__u8 *)&seq, sizeof(seq)); csum32 = jbd2_chksum(csum32, addr, bh->b_size); - kunmap_local(addr); + kunmap_local_bh(bh, addr); if (jbd2_has_feature_csum3(j)) tag3->t_checksum = cpu_to_be32(csum32); diff --git a/fs/jbd2/journal.c b/fs/jbd2/journal.c index 00f5a98f3d4f..a4f63d738337 100644 --- a/fs/jbd2/journal.c +++ b/fs/jbd2/journal.c @@ -328,8 +328,6 @@ int jbd2_journal_write_metadata_buffer(transaction_t *transaction, { int do_escape = 0; struct buffer_head *new_bh; - struct folio *new_folio; - unsigned int new_offset; struct buffer_head *bh_in = jh2bh(jh_in); journal_t *journal = transaction->t_journal; @@ -349,24 +347,31 @@ int jbd2_journal_write_metadata_buffer(transaction_t *transaction, /* keep subsequent assertions sane */ atomic_set(&new_bh->b_count, 1); + /* + * b_frozen_data is slab memory, not page cache, so when we use it the + * shadow buffer gets no folio at all: b_folio stays NULL from the + * allocation and b_data points straight at the copy. Pointing it at + * the slab folio instead would hand its overloaded ->mapping to + * anything that goes looking for an address_space. + */ + spin_lock(&jh_in->b_state_lock); /* * If a new transaction has already done a buffer copy-out, then * we use that version of the data for the commit. */ if (jh_in->b_frozen_data) { - new_folio = virt_to_folio(jh_in->b_frozen_data); - new_offset = offset_in_folio(new_folio, jh_in->b_frozen_data); do_escape = jbd2_data_needs_escaping(jh_in->b_frozen_data); if (do_escape) jbd2_data_do_escape(jh_in->b_frozen_data); + new_bh->b_data = jh_in->b_frozen_data; } else { + struct folio *folio = bh_in->b_folio; + unsigned int offset = offset_in_folio(folio, bh_in->b_data); char *tmp; char *mapped_data; - new_folio = bh_in->b_folio; - new_offset = offset_in_folio(new_folio, bh_in->b_data); - mapped_data = kmap_local_folio(new_folio, new_offset); + mapped_data = kmap_local_folio(folio, offset); /* * Fire data frozen trigger if data already wasn't frozen. Do * this before checking for escaping, as the trigger may modify @@ -380,8 +385,10 @@ int jbd2_journal_write_metadata_buffer(transaction_t *transaction, /* * Do we need to do a data copy? */ - if (!do_escape) + if (!do_escape) { + folio_set_bh(new_bh, folio, offset); goto escape_done; + } spin_unlock(&jh_in->b_state_lock); tmp = kmalloc(bh_in->b_size, GFP_NOFS | __GFP_NOFAIL); @@ -392,7 +399,7 @@ int jbd2_journal_write_metadata_buffer(transaction_t *transaction, } jh_in->b_frozen_data = tmp; - memcpy_from_folio(tmp, new_folio, new_offset, bh_in->b_size); + memcpy_from_folio(tmp, folio, offset, bh_in->b_size); /* * This isn't strictly necessary, as we're using frozen * data for the escaping, but it keeps consistency with @@ -401,13 +408,11 @@ int jbd2_journal_write_metadata_buffer(transaction_t *transaction, jh_in->b_frozen_triggers = jh_in->b_triggers; copy_done: - new_folio = virt_to_folio(jh_in->b_frozen_data); - new_offset = offset_in_folio(new_folio, jh_in->b_frozen_data); jbd2_data_do_escape(jh_in->b_frozen_data); + new_bh->b_data = jh_in->b_frozen_data; } escape_done: - folio_set_bh(new_bh, new_folio, new_offset); new_bh->b_size = bh_in->b_size; new_bh->b_bdev = journal->j_dev; new_bh->b_blocknr = blocknr; diff --git a/include/linux/buffer_head.h b/include/linux/buffer_head.h index 699970b4bbf2..20b8fca1abfa 100644 --- a/include/linux/buffer_head.h +++ b/include/linux/buffer_head.h @@ -172,6 +172,35 @@ static inline unsigned long bh_offset(const struct buffer_head *bh) return (unsigned long)(bh)->b_data & (folio_size(bh->b_folio) - 1); } +/** + * kmap_local_bh - Map the data of a buffer. + * @bh: The buffer. + * + * Buffers usually live in the page cache, but a few are built over memory + * which is not. Those carry no folio and b_data is already a kernel address + * which is always mapped, so there is nothing to do for them. Pair with + * kunmap_local_bh(). + * + * Return: A pointer to the buffer's data. + */ +static inline void *kmap_local_bh(const struct buffer_head *bh) +{ + if (!bh->b_folio) + return bh->b_data; + return kmap_local_folio(bh->b_folio, bh_offset(bh)); +} + +/** + * kunmap_local_bh - Unmap the data of a buffer. + * @bh: The buffer. + * @addr: The address returned by kmap_local_bh(). + */ +static inline void kunmap_local_bh(const struct buffer_head *bh, void *addr) +{ + if (bh->b_folio) + kunmap_local(addr); +} + /* If we *know* page->private refers to buffer_heads */ #define page_buffers(page) \ ({ \ From 8775d14a90e29fafa8b556e07d8142042fd58689 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:27 -0400 Subject: [PATCH 04/21] buffer: read the folio's mapping directly in buffer_set_crypto_ctx() folio_mapping() was doing two jobs here. One was to turn a slab folio into NULL, which is what made this safe for jbd2's shadow buffers; the previous patch removed the need for that by giving those buffers no folio at all. The other is a hazard. folio_mapping() maps a folio in the swap cache to its swap_address_space, so if a buffer_head were ever attached to such a folio this would hand fscrypt a swap mapping and dereference ->host on it. There is no reason to want that here: this path wants the file's mapping or nothing. Read ->mapping directly. Buffers with no folio are already handled above. Suggested-by: Matthew Wilcox (Oracle) Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/3fe72ec37bf8491a69031db5f3ba1319da935b97.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/buffer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/buffer.c b/fs/buffer.c index fa55a507835a..887151bbc5ae 100644 --- a/fs/buffer.c +++ b/fs/buffer.c @@ -1079,7 +1079,7 @@ static void buffer_set_crypto_ctx(struct bio *bio, const struct buffer_head *bh, */ if (!bh->b_folio) return; - mapping = folio_mapping(bh->b_folio); + mapping = bh->b_folio->mapping; if (!mapping) return; fscrypt_set_bio_crypt_ctx(bio, mapping->host, From d9a8f9ffce29dc8409c885996dc210a8f394e478 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:28 -0400 Subject: [PATCH 05/21] buffer: clear BH_Write_EIO when a buffer is forgotten BH_Write_EIO records that the last write of this buffer failed. It is cleared when the buffer is written again, but a filesystem freeing a metadata block never writes it again. It calls bforget() and hands the block back to the allocator, so the flag outlives the block it refers to. That does not matter much today, because the write error is also recorded by clearing BH_Uptodate and the buffer is discarded soon after. It starts to matter in the rest of this series, which stops clearing BH_Uptodate on write error and makes BH_Write_EIO the way a failed metadata write is reported. bforget() is where a filesystem says it no longer cares about this buffer's contents, so clear the error there alongside the dirty flag. Suggested-by: Jan Kara Acked-by: Weidong Zhu Signed-off-by: Chao Shi Reviewed-by: Jan Kara Link: https://patch.msgid.link/ebe0b4f179ccdac7a9400fe2611623ce218c8d87.1785951556.git.coshi036@gmail.com Signed-off-by: Christian Brauner (Amutable) --- fs/buffer.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/buffer.c b/fs/buffer.c index 887151bbc5ae..f3bf87ccb536 100644 --- a/fs/buffer.c +++ b/fs/buffer.c @@ -1062,6 +1062,7 @@ EXPORT_SYMBOL(__brelse); void __bforget(struct buffer_head *bh) { clear_buffer_dirty(bh); + clear_buffer_write_io_error(bh); remove_assoc_queue(bh); __brelse(bh); } From 7d439c46c9230b9ef9da8d164dcd8795b528e631 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:29 -0400 Subject: [PATCH 06/21] buffer: discard BH_Write_EIO along with the rest of the buffer state discard_buffer() strips the state that describes where a buffer lives and what has happened to it, because after an invalidate none of it applies any more. BH_Write_EIO belongs in that set for the same reason: it describes a write of the data that is being thrown away. Leaving it set means a buffer_head reused for a different block starts life carrying somebody else's write error. Like the bforget() change, this is mostly theoretical today and becomes load bearing once the rest of the series makes BH_Write_EIO the report of a failed metadata write. Suggested-by: Jan Kara Acked-by: Weidong Zhu Signed-off-by: Chao Shi Reviewed-by: Jan Kara Link: https://patch.msgid.link/c6e9db48d8d0feb83d4ca29306f4bc1e58f1ee0f.1785951556.git.coshi036@gmail.com Signed-off-by: Christian Brauner (Amutable) --- fs/buffer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/buffer.c b/fs/buffer.c index f3bf87ccb536..9ff946a2487a 100644 --- a/fs/buffer.c +++ b/fs/buffer.c @@ -1493,7 +1493,7 @@ EXPORT_SYMBOL(folio_set_bh); /* Bits that are cleared during an invalidate */ #define BUFFER_FLAGS_DISCARD \ (1 << BH_Mapped | 1 << BH_New | 1 << BH_Req | \ - 1 << BH_Delay | 1 << BH_Unwritten) + 1 << BH_Delay | 1 << BH_Unwritten | 1 << BH_Write_EIO) static void discard_buffer(struct buffer_head * bh) { From bc411b171a89abc9b3a267599919a4dbc826e626 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:30 -0400 Subject: [PATCH 07/21] buffer: detect metadata write errors with buffer_write_io_error() Both places in this file that report a metadata write error to a caller do it by testing !buffer_uptodate() after waiting for the write. That works only because the write completion handlers clear BH_Uptodate when the write fails, which is what this series is removing: a buffer whose write failed still holds the correct data, and saying otherwise makes callers rewrite, re-read or WARN over a buffer that was never wrong. BH_Write_EIO is the flag that actually means "the last write of this buffer failed", and both handlers already set it via mark_buffer_write_io_error(). Test that instead. No behaviour change: today a failed write through bh_end_write() or bh_end_async_write() sets BH_Write_EIO and clears BH_Uptodate together, so the two tests agree. They stop agreeing at the end of the series, and this one stays right. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Reviewed-by: Jan Kara Link: https://patch.msgid.link/2b309196b883cc8979800911a47668e225401b2c.1785951556.git.coshi036@gmail.com Signed-off-by: Christian Brauner (Amutable) --- fs/buffer.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/buffer.c b/fs/buffer.c index 9ff946a2487a..6e8aa77e0f38 100644 --- a/fs/buffer.c +++ b/fs/buffer.c @@ -589,7 +589,7 @@ int mmb_sync(struct mapping_metadata_bhs *mmb) } spin_unlock(&mmb->lock); wait_on_buffer(bh); - if (!buffer_uptodate(bh)) + if (buffer_write_io_error(bh)) err = -EIO; brelse(bh); spin_lock(&mmb->lock); @@ -2721,7 +2721,7 @@ int __sync_dirty_buffer(struct buffer_head *bh, blk_opf_t op_flags) bh_submit(bh, REQ_OP_WRITE | op_flags, bh_end_write); wait_on_buffer(bh); - if (!buffer_uptodate(bh)) + if (buffer_write_io_error(bh)) return -EIO; } else { unlock_buffer(bh); From 0b1576d602083aa4697df93c1d0ae126684ba2f6 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:31 -0400 Subject: [PATCH 08/21] adfs: check for a directory write error with buffer_write_io_error() adfs_dir_sync() spots a failed write by testing BH_Req together with !BH_Uptodate. That relies on the write completion handler clearing BH_Uptodate on error, which this series removes: a buffer whose write failed still holds the data the filesystem asked to be written, so declaring it not up to date is wrong and makes callers re-read it. BH_Write_EIO says exactly what this code wants to know, and it implies BH_Req, so the pair collapses into one test. No behaviour change today - a failed write sets BH_Write_EIO and clears BH_Uptodate together. It stops being a no-op at the end of the series, where the new test is the one that still works. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/05cf8ad911ad7a6eb68f42e38f1eea39c38e246b.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/adfs/dir.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/adfs/dir.c b/fs/adfs/dir.c index 11afa9e157aa..b8cc6a697a05 100644 --- a/fs/adfs/dir.c +++ b/fs/adfs/dir.c @@ -191,7 +191,7 @@ static int adfs_dir_sync(struct adfs_dir *dir) for (i = dir->nr_buffers - 1; i >= 0; i--) { struct buffer_head *bh = dir->bhs[i]; sync_dirty_buffer(bh); - if (buffer_req(bh) && !buffer_uptodate(bh)) + if (buffer_write_io_error(bh)) err = -EIO; } From 5c49ba63bead84b0584a57095a703147cca6c811 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:32 -0400 Subject: [PATCH 09/21] ext2: check for an xattr block write error with buffer_write_io_error() ext2_xattr_set2() spots a failed synchronous write by testing BH_Req together with !BH_Uptodate. That relies on the write completion handler clearing BH_Uptodate on error, which this series removes: a buffer whose write failed still holds the data the filesystem asked to be written, so declaring it not up to date is wrong and makes callers re-read it. BH_Write_EIO says exactly what this code wants to know, and it implies BH_Req, so the pair collapses into one test. No behaviour change today - a failed write sets BH_Write_EIO and clears BH_Uptodate together. It stops being a no-op at the end of the series, where the new test is the one that still works. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Reviewed-by: Jan Kara Link: https://patch.msgid.link/fcc530fffd0f1e18e8cd1a3974bc0ccc5b4c4b66.1785951556.git.coshi036@gmail.com Signed-off-by: Christian Brauner (Amutable) --- fs/ext2/xattr.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ext2/xattr.c b/fs/ext2/xattr.c index 9b68c490ab26..8f608930a48c 100644 --- a/fs/ext2/xattr.c +++ b/fs/ext2/xattr.c @@ -769,7 +769,7 @@ ext2_xattr_set2(struct inode *inode, struct buffer_head *old_bh, if (IS_SYNC(inode)) { sync_dirty_buffer(new_bh); error = -EIO; - if (buffer_req(new_bh) && !buffer_uptodate(new_bh)) + if (buffer_write_io_error(new_bh)) goto cleanup; } } From 5089db0f2dd38a64921eb44e55eb78dfd0b242fc Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:33 -0400 Subject: [PATCH 10/21] omfs: check for an inode write error with buffer_write_io_error() __omfs_write_inode() spots a failed synchronous write, on both the primary block and each mirror, by testing BH_Req together with !BH_Uptodate. That relies on the write completion handler clearing BH_Uptodate on error, which this series removes: a buffer whose write failed still holds the data the filesystem asked to be written, so declaring it not up to date is wrong and makes callers re-read it. BH_Write_EIO says exactly what this code wants to know, and it implies BH_Req, so each pair collapses into one test. No behaviour change today - a failed write sets BH_Write_EIO and clears BH_Uptodate together. It stops being a no-op at the end of the series, where the new test is the one that still works. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/b33ccb12c29ae743ccfa48cdf1e9140fbf2dabcc.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/omfs/inode.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/omfs/inode.c b/fs/omfs/inode.c index 1d915ef72119..bc37029a4afb 100644 --- a/fs/omfs/inode.c +++ b/fs/omfs/inode.c @@ -145,7 +145,7 @@ static int __omfs_write_inode(struct inode *inode, int wait) mark_buffer_dirty(bh); if (wait) { sync_dirty_buffer(bh); - if (buffer_req(bh) && !buffer_uptodate(bh)) + if (buffer_write_io_error(bh)) sync_failed = 1; } @@ -159,7 +159,7 @@ static int __omfs_write_inode(struct inode *inode, int wait) mark_buffer_dirty(bh2); if (wait) { sync_dirty_buffer(bh2); - if (buffer_req(bh2) && !buffer_uptodate(bh2)) + if (buffer_write_io_error(bh2)) sync_failed = 1; } brelse(bh2); From 0015b215eb6270d3e6bb5437b5198f5c38f4d2cd Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:34 -0400 Subject: [PATCH 11/21] exfat: check for a directory write error with buffer_write_io_error() exfat_update_bhs() waits for the writes it issued and then tests !buffer_uptodate() to find the ones that failed. That relies on the write completion handler clearing BH_Uptodate on error, which this series removes: a buffer whose write failed still holds the data the filesystem asked to be written, so declaring it not up to date is wrong and makes callers re-read it. Test BH_Write_EIO, which is what the completion handler sets and what this code actually wants to know. No behaviour change today - a failed write sets BH_Write_EIO and clears BH_Uptodate together. It stops being a no-op at the end of the series, where the new test is the one that still works. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/0784ef63a525434e7c0aff730eca7b43e043095d.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/exfat/misc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/exfat/misc.c b/fs/exfat/misc.c index 6f11a96a4ffa..dfd0bbf31c94 100644 --- a/fs/exfat/misc.c +++ b/fs/exfat/misc.c @@ -187,7 +187,7 @@ int exfat_update_bhs(struct buffer_head **bhs, int nr_bhs, int sync) for (i = 0; i < nr_bhs && sync; i++) { wait_on_buffer(bhs[i]); - if (!err && !buffer_uptodate(bhs[i])) + if (!err && buffer_write_io_error(bhs[i])) err = -EIO; } return err; From 8417ab622ffc140960818420cf72d42ecd3a12fe Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:35 -0400 Subject: [PATCH 12/21] fat: check for a metadata write error with buffer_write_io_error() fat_sync_bhs() waits for the writes it issued and then tests !buffer_uptodate() to find the ones that failed. That relies on the write completion handler clearing BH_Uptodate on error, which this series removes: a buffer whose write failed still holds the data the filesystem asked to be written, so declaring it not up to date is wrong and makes callers re-read it. Test BH_Write_EIO, which is what the completion handler sets and what this code actually wants to know. No behaviour change today - a failed write sets BH_Write_EIO and clears BH_Uptodate together. It stops being a no-op at the end of the series, where the new test is the one that still works. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/4b6019b5a48b83c8235918b084248983a57692e1.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/fat/misc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/fat/misc.c b/fs/fat/misc.c index e79762cf1975..0d04228f916e 100644 --- a/fs/fat/misc.c +++ b/fs/fat/misc.c @@ -360,7 +360,7 @@ int fat_sync_bhs(struct buffer_head **bhs, int nr_bhs) for (i = 0; i < nr_bhs; i++) { wait_on_buffer(bhs[i]); - if (!err && !buffer_uptodate(bhs[i])) + if (!err && buffer_write_io_error(bhs[i])) err = -EIO; } return err; From 4ed32fcd59ceb3dd9eeecf608ee612107030b5e3 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:36 -0400 Subject: [PATCH 13/21] ext4: check for a metadata write error with buffer_write_io_error() Two places detect a failed metadata write by testing !buffer_uptodate() after waiting for it. That relies on the write completion handler clearing BH_Uptodate on error, which this series removes: a buffer whose write failed still holds the data the filesystem asked to be written, so declaring it not up to date is wrong and makes callers re-read it. ext4 already does this correctly for the superblock - see ext4_commit_super(), which tests buffer_write_io_error() - so this brings the other two into line. In __ext4_handle_dirty_metadata() the old test also required BH_Req. BH_Write_EIO implies it, so the pair collapses into one test. The new test is also strictly stronger than consuming sync_dirty_buffer()'s return value, because it still fires when the buffer was written by background writeback and that write hit an error, which sync_dirty_buffer() does not report. Note that the failing write does not clear BH_Write_EIO, so an unrepaired itable block now reports on every subsequent sync of that inode rather than only on the write that failed. That is the intended behaviour, and matches what ocfs2 has always done with this flag. No behaviour change today - a failed write sets BH_Write_EIO and clears BH_Uptodate together. It stops being a no-op at the end of the series, where the new test is the one that still works. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Reviewed-by: Jan Kara Link: https://patch.msgid.link/568e57d184da17041872fd4b498f31dd6259a4c1.1785951556.git.coshi036@gmail.com Signed-off-by: Christian Brauner (Amutable) --- fs/ext4/ext4_jbd2.c | 2 +- fs/ext4/mmp.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/ext4/ext4_jbd2.c b/fs/ext4/ext4_jbd2.c index 53ddedb52a6f..c241f50b97bc 100644 --- a/fs/ext4/ext4_jbd2.c +++ b/fs/ext4/ext4_jbd2.c @@ -421,7 +421,7 @@ int __ext4_handle_dirty_metadata(const char *where, unsigned int line, } if (inode && inode_needs_sync(inode)) { sync_dirty_buffer(bh); - if (buffer_req(bh) && !buffer_uptodate(bh)) { + if (buffer_write_io_error(bh)) { ext4_error_inode_err(inode, where, line, bh->b_blocknr, EIO, "IO error syncing itable block"); diff --git a/fs/ext4/mmp.c b/fs/ext4/mmp.c index 7ce361484b38..4b18ddef468d 100644 --- a/fs/ext4/mmp.c +++ b/fs/ext4/mmp.c @@ -49,7 +49,7 @@ static int write_mmp_block_thawed(struct super_block *sb, bh_submit(bh, REQ_OP_WRITE | REQ_SYNC | REQ_META | REQ_PRIO, bh_end_write); wait_on_buffer(bh); - if (unlikely(!buffer_uptodate(bh))) + if (unlikely(buffer_write_io_error(bh))) return -EIO; return 0; } From a186ae5716fab861104e746930c34812530ac8bd Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:37 -0400 Subject: [PATCH 14/21] ocfs2: check for a metadata write error with buffer_write_io_error() ocfs2_write_block() and ocfs2_write_super_or_backup() detect a failed write by looking at BH_Uptodate afterwards. That relies on the write completion handler clearing BH_Uptodate on error, which this series removes: a buffer whose write failed still holds the data the filesystem asked to be written, so declaring it not up to date is wrong and makes callers re-read it. Test BH_Write_EIO instead. Note that ocfs2_write_block()'s test is the positive one, so the sense has to be inverted rather than the flag simply swapped. The comment in ocfs2_write_block()'s error arm needs updating for the same reason. It said the clustered uptodate information did not have to be removed because the buffer was not marked locally uptodate; after this series it is, so the reason no longer holds. Not advertising the block to the cluster is still the right thing to do - the data is in memory but not on disk - so only the justification changes, not the behaviour. No behaviour change today - a failed write sets BH_Write_EIO and clears BH_Uptodate together. It stops being a no-op at the end of the series, where the new test is the one that still works. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Reviewed-by: Jan Kara Link: https://patch.msgid.link/05cd3c34f2dc69b542db7baaef058cd75005d4cb.1785951556.git.coshi036@gmail.com Signed-off-by: Christian Brauner (Amutable) --- fs/ocfs2/buffer_head_io.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/fs/ocfs2/buffer_head_io.c b/fs/ocfs2/buffer_head_io.c index 7bfe377af2df..733ceda79ca1 100644 --- a/fs/ocfs2/buffer_head_io.c +++ b/fs/ocfs2/buffer_head_io.c @@ -66,12 +66,14 @@ int ocfs2_write_block(struct ocfs2_super *osb, struct buffer_head *bh, wait_on_buffer(bh); - if (buffer_uptodate(bh)) { + if (!buffer_write_io_error(bh)) { ocfs2_set_buffer_uptodate(ci, bh); } else { - /* We don't need to remove the clustered uptodate - * information for this bh as it's not marked locally - * uptodate. */ + /* + * The buffer still holds what we tried to write, but it did + * not reach the disk, so don't advertise it to the cluster + * as up to date. + */ ret = -EIO; mlog_errno(ret); } @@ -446,7 +448,7 @@ int ocfs2_write_super_or_backup(struct ocfs2_super *osb, wait_on_buffer(bh); - if (!buffer_uptodate(bh)) { + if (buffer_write_io_error(bh)) { ret = -EIO; mlog_errno(ret); } From 8bf5c5a94a8707f879706fd42a8bf5604955d12c Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:38 -0400 Subject: [PATCH 15/21] ocfs2: check for a stale write error before reusing a metadata buffer __ocfs2_journal_access() refuses to journal a buffer whose previous write failed, and turns the filesystem read-only rather than risk metadata inconsistency. That check sits inside an if (!buffer_uptodate(bh)) block, because until now a failed write also cleared BH_Uptodate. This series stops clearing BH_Uptodate on write error, so that outer test would never fire again and ocfs2 would silently start reusing buffers whose last write failed. Hoist the check out of the debug block, where it does not depend on BH_Uptodate any more, and drop the now dead second half of its condition. The mlog() pair keeps its own !buffer_uptodate() guard: it is a separate "we can safely remove this assertion after testing" debug aid about being handed a buffer with no valid contents, which is a different question from whether the last write of that buffer failed. The unlocked test followed by a locked retest is deliberate. BH_Write_EIO is cleared under the buffer lock, so taking the lock and looking a second time avoids turning the filesystem read-only over an error that a concurrent rewrite has already cleared, while keeping the common case lock-free. The code in this patch is Jan's, from the review discussion linked in the cover letter. Suggested-by: Jan Kara Acked-by: Weidong Zhu Signed-off-by: Chao Shi Reviewed-by: Jan Kara Link: https://patch.msgid.link/ad75c40927060cafe5a8ac45ae71c02ae4dfba04.1785951556.git.coshi036@gmail.com Signed-off-by: Christian Brauner (Amutable) --- fs/ocfs2/journal.c | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/fs/ocfs2/journal.c b/fs/ocfs2/journal.c index d8afbc1a76bb..ea6802d894c2 100644 --- a/fs/ocfs2/journal.c +++ b/fs/ocfs2/journal.c @@ -676,19 +676,20 @@ static int __ocfs2_journal_access(handle_t *handle, mlog(ML_ERROR, "giving me a buffer that's not uptodate!\n"); mlog(ML_ERROR, "b_blocknr=%llu, b_state=0x%lx\n", (unsigned long long)bh->b_blocknr, bh->b_state); - + } + /* + * A previous transaction with a couple of buffer heads fail + * to checkpoint, so all the bhs are marked as BH_Write_EIO. + * For current transaction, the bh is just among those error + * bhs which previous transaction handle. We can't just clear + * its BH_Write_EIO and reuse directly, since other bhs are + * not written to disk yet and that will cause metadata + * inconsistency. So we should set fs read-only to avoid + * further damage. + */ + if (buffer_write_io_error(bh)) { lock_buffer(bh); - /* - * A previous transaction with a couple of buffer heads fail - * to checkpoint, so all the bhs are marked as BH_Write_EIO. - * For current transaction, the bh is just among those error - * bhs which previous transaction handle. We can't just clear - * its BH_Write_EIO and reuse directly, since other bhs are - * not written to disk yet and that will cause metadata - * inconsistency. So we should set fs read-only to avoid - * further damage. - */ - if (buffer_write_io_error(bh) && !buffer_uptodate(bh)) { + if (buffer_write_io_error(bh)) { unlock_buffer(bh); return ocfs2_error(osb->sb, "A previous attempt to " "write this buffer head failed\n"); From 9ba9d0441569749391bea9938c91870cd1e68601 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:39 -0400 Subject: [PATCH 16/21] gfs2: check for a metadata write error with buffer_write_io_error() gfs2_ail1_start_one() and gfs2_ail1_empty_one() decide whether a buffer on the ail reached the disk by looking at BH_Uptodate once it is no longer busy. That relies on the write completion handler clearing BH_Uptodate on error, which this series removes: a buffer whose write failed still holds the data the filesystem asked to be written, so declaring it not up to date is wrong and makes callers re-read it. Test BH_Write_EIO instead. In gfs2_ail1_start_one() the test is the positive one, so the sense has to be inverted rather than the flag simply swapped. This is not a pure conversion for gfs2, because gfs2 already has a private write completion handler that behaves the way this series is heading: gfs2_end_log_write_bh() calls mark_buffer_write_io_error() and leaves BH_Uptodate alone. Buffers completed through it are therefore invisible to both tests today, and start being caught once they look at BH_Write_EIO. That is a real behaviour change, and it is the one gfs2 wanted: a failed log write now withdraws the filesystem instead of passing silently. gfs2_pin() is a different case and gets a different treatment. Its !buffer_uptodate() test is not only a proxy for a failed write - a buffer with no valid contents at all is equally a reason to withdraw before pinning it into a transaction - so the write error test is added to it rather than replacing it. Left alone deliberately: the BUG_ON(!buffer_uptodate(bh)) in gfs2_unpin() and the two WARN_ON()s in fs/gfs2/rgrp.c. After this series they simply stop firing for write errors, which is correct; turning them into BUG_ON(buffer_write_io_error(bh)) would newly panic on an I/O error. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/a09b6d08d31d24f69b833e4d855ed88875e87780.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/gfs2/log.c | 4 ++-- fs/gfs2/lops.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/gfs2/log.c b/fs/gfs2/log.c index 78bba8cc10b8..e3e0dcb1f567 100644 --- a/fs/gfs2/log.c +++ b/fs/gfs2/log.c @@ -107,7 +107,7 @@ __acquires(&sdp->sd_ail_lock) gfs2_assert(sdp, bd->bd_tr == tr); if (!buffer_busy(bh)) { - if (buffer_uptodate(bh)) { + if (!buffer_write_io_error(bh)) { list_move(&bd->bd_ail_st_list, &tr->tr_ail2_list); continue; @@ -321,7 +321,7 @@ static int gfs2_ail1_empty_one(struct gfs2_sbd *sdp, struct gfs2_trans *tr, active_count++; continue; } - if (!buffer_uptodate(bh) && + if (buffer_write_io_error(bh) && !cmpxchg(&sdp->sd_log_error, 0, -EIO)) gfs2_io_error_bh(sdp, bh); /* diff --git a/fs/gfs2/lops.c b/fs/gfs2/lops.c index 6dabe73ad790..3df6e4b7e8b9 100644 --- a/fs/gfs2/lops.c +++ b/fs/gfs2/lops.c @@ -48,7 +48,7 @@ void gfs2_pin(struct gfs2_sbd *sdp, struct buffer_head *bh) clear_buffer_dirty(bh); if (test_set_buffer_pinned(bh)) gfs2_assert_withdraw(sdp, 0); - if (!buffer_uptodate(bh)) + if (!buffer_uptodate(bh) || buffer_write_io_error(bh)) gfs2_io_error_bh(sdp, bh); bd = bh->b_private; /* If this buffer is in the AIL and it has already been written From a05d5c40af1bbf32a3fe78a1e25defcf0503f7bf Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:40 -0400 Subject: [PATCH 17/21] jbd2: report journal write errors with BH_Write_EIO The journal's own write completion handler, journal_end_buffer_io_sync(), reports a failed write by clearing BH_Uptodate, and the three places that wait for journal writes look for that. This series is removing that convention: a buffer whose write failed still holds the data that was supposed to reach the disk, and saying it is not up to date makes callers rewrite, re-read or WARN over data that was never wrong. Set BH_Write_EIO instead, with mark_buffer_write_io_error(), and test it in journal_wait_on_commit_record() and in the two commit-phase waits. The handler stops touching BH_Uptodate in either direction. Setting it on success was never needed: every caller marks the buffer up to date before submitting the write, because a buffer with no valid data is not something you can write out. The local flag is renamed to match what it now means. The two changes have to go together, because commit phase 4 waits on a mixed list: descriptor blocks are submitted with journal_end_buffer_io_sync(), while revoke blocks go through write_dirty_buffer() and land in bh_end_write(). bh_end_write() already sets BH_Write_EIO, so converting the consumer alone would keep working for revoke blocks and silently stop detecting failed descriptor writes. With the handler converted, both halves of the list report the same way. mark_buffer_write_io_error() is safe on all of these buffers. The shadow buffers from jbd2_journal_write_metadata_buffer() have no folio and no associated mapping, so it does nothing beyond setting the flag. Descriptor and commit blocks are ordinary buffers on the journal device, and marking the journal's mapping with the error is what write_dirty_buffer() already does for revoke blocks on the same device. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/c700983eea955429d927791fe5fb9ffe6cf11056.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/jbd2/commit.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/fs/jbd2/commit.c b/fs/jbd2/commit.c index 0c85af91f9b2..cd7ef783bd36 100644 --- a/fs/jbd2/commit.c +++ b/fs/jbd2/commit.c @@ -32,14 +32,12 @@ static void journal_end_buffer_io_sync(struct bio *bio) { struct buffer_head *bh; - bool uptodate = bio_endio_bh(bio, &bh); + bool success = bio_endio_bh(bio, &bh); struct buffer_head *orig_bh = bh->b_private; BUFFER_TRACE(bh, ""); - if (uptodate) - set_buffer_uptodate(bh); - else - clear_buffer_uptodate(bh); + if (!success) + mark_buffer_write_io_error(bh); if (orig_bh) { clear_and_wake_up_bit(BH_Shadow, &orig_bh->b_state); } @@ -169,7 +167,7 @@ static int journal_wait_on_commit_record(journal_t *journal, clear_buffer_dirty(bh); wait_on_buffer(bh); - if (unlikely(!buffer_uptodate(bh))) + if (unlikely(buffer_write_io_error(bh))) ret = -EIO; put_bh(bh); /* One for getblk() */ @@ -834,7 +832,7 @@ void jbd2_journal_commit_transaction(journal_t *journal) wait_on_buffer(bh); cond_resched(); - if (unlikely(!buffer_uptodate(bh))) + if (unlikely(buffer_write_io_error(bh))) err = -EIO; jbd2_unfile_log_bh(bh); stats.run.rs_blocks_logged++; @@ -877,7 +875,7 @@ void jbd2_journal_commit_transaction(journal_t *journal) wait_on_buffer(bh); cond_resched(); - if (unlikely(!buffer_uptodate(bh))) + if (unlikely(buffer_write_io_error(bh))) err = -EIO; BUFFER_TRACE(bh, "ph5: control buffer writeout done: unfile"); From b1c88ae0b554322fa29b4b490ce0b09c2c84ec52 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:41 -0400 Subject: [PATCH 18/21] jbd2: say what jbd2_freeze_jh_data()'s assertion is actually checking The assertion that the buffer about to be copied out is up to date is correct and stays, but its message - "Possible IO failure" - describes what a buffer that is not up to date used to mean rather than what is being checked. Once this series stops clearing BH_Uptodate on write error, that reading is wrong twice over. A failed write no longer makes a buffer not up to date, and a buffer that does carry BH_Write_EIO is fine here: it still holds valid data and the journal will write it again. What the assertion is really guarding is that there is something valid to copy at all. Say that instead. Suggested-by: Jan Kara Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/713ffb9c0cdfe55499b3cdd2c06a59a2c152c46c.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/jbd2/transaction.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/jbd2/transaction.c b/fs/jbd2/transaction.c index 5cc7d097b2ac..85d84d909f78 100644 --- a/fs/jbd2/transaction.c +++ b/fs/jbd2/transaction.c @@ -920,7 +920,7 @@ static void jbd2_freeze_jh_data(struct journal_head *jh) char *source; struct buffer_head *bh = jh2bh(jh); - J_EXPECT_JH(jh, buffer_uptodate(bh), "Possible IO failure.\n"); + J_EXPECT_JH(jh, buffer_uptodate(bh), "Buffer not uptodate!\n"); source = kmap_local_folio(bh->b_folio, bh_offset(bh)); /* Fire data frozen trigger just before we copy the data */ jbd2_buffer_frozen_trigger(jh, source, jh->b_triggers); From 8ed37e18b915a36bf382873833656c181d1fb942 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:42 -0400 Subject: [PATCH 19/21] jbd2: check for a fast commit write error with buffer_write_io_error() jbd2_fc_wait_bufs() decides whether ext4's fast commit blocks reached the disk by looking at BH_Uptodate once the write has completed. That relies on the write completion handler clearing BH_Uptodate on error, which this series removes: a buffer whose write failed still holds the data the filesystem asked to be written, so declaring it not up to date is wrong and makes callers rewrite or re-read it. Test BH_Write_EIO instead. Nothing is needed on the producer side. These buffers used to be submitted by ext4_fc_submit_bh() with a private completion handler that cleared BH_Uptodate, but commit 7f0485dd3017 ("ext4: remove ext4_end_buffer_io_sync()") dropped it in favour of bh_end_write(), which already reports a failed write with mark_buffer_write_io_error(). The wait in jbd2 is the only half left to convert. A stale flag from an earlier write cannot fool the new test. jbd2_fc_get_buf() looks the buffer up with __getblk() at a fixed block on the journal device, so the same buffer comes back commit after commit, but __bh_submit() clears BH_Write_EIO when it resubmits a buffer for writing. The flag jbd2_fc_wait_bufs() sees always belongs to the write it just waited for. No behaviour change today - bh_end_write() sets BH_Write_EIO and clears BH_Uptodate together. It stops being a no-op at the end of the series, where the new test is the one that still works. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/970d8b9603d4620ab73038f49bc36831e37e00a0.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/jbd2/journal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/jbd2/journal.c b/fs/jbd2/journal.c index a4f63d738337..cda1ff8851dc 100644 --- a/fs/jbd2/journal.c +++ b/fs/jbd2/journal.c @@ -887,7 +887,7 @@ int jbd2_fc_wait_bufs(journal_t *journal, int num_blks) * Update j_fc_off so jbd2_fc_release_bufs can release remain * buffer head. */ - if (unlikely(!buffer_uptodate(bh))) { + if (unlikely(buffer_write_io_error(bh))) { journal->j_fc_off = i + 1; return -EIO; } From 9f08e58149dca8453bf99e0af35cbb227ded17b1 Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:43 -0400 Subject: [PATCH 20/21] buffer: stop touching BH_Uptodate on write completion A buffer whose write failed still holds exactly the data the filesystem asked to be written. It is the disk that is out of date, not the buffer. Clearing BH_Uptodate says the opposite, and callers act on it: - mark_buffer_dirty() has a WARN_ON_ONCE(!buffer_uptodate(bh)). A filesystem that dirties the buffer again after a failed write - which is the normal way to retry - trips it. That is the warning this series started from. - a buffer that is not up to date gets re-read from disk, which replaces the data the filesystem was trying to write with the stale on-disk copy, silently. - the window between the write completing and the buffer being marked not up to date is visible to anyone holding the folio lock, so the state is not even self consistent while it lasts. BH_Write_EIO already records the failure, and by now every place in the tree that needs to know about it tests that flag instead: the two core helpers in this file, adfs, exfat, ext2, ext4, fat, gfs2, jbd2, ocfs2 and omfs, converted one filesystem at a time in the preceding patches. The private completion handler in jbd2 was converted along with its waiters, and ext4 fast commit needed only its waiter, because commit 7f0485dd3017 ("ext4: remove ext4_end_buffer_io_sync()") had already dropped its handler in favour of bh_end_write(). Nothing is left that reads BH_Uptodate to find out whether a write failed. Setting BH_Uptodate on success goes too. A buffer has to be up to date before it can be written - you cannot write out data you do not have - so the only thing that assignment could do is paper over a caller that got that wrong. Write completion now leaves BH_Uptodate alone in both directions. What this changes for readers. A buffer whose write failed stays up to date, so the read paths stop replacing it with the on-disk copy: __bread_gfp() no longer sends it to __bread_slow(), and bh_uptodate_or_lock() reports it as usable. That is the intent. ocfs2 changes the most, because ocfs2_read_blocks() decides whether to go to disk on its own cluster uptodate cache and only tests BH_Uptodate after the wait, so a block whose write failed makes that read return -EIO today and from here it succeeds and hands back the in-memory data. A caller that needs to know the write failed asks BH_Write_EIO. Found by FuzzNvme. Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/61d7d5737f5773f53ee543f375fcde81aa8d28c2.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/buffer.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/fs/buffer.c b/fs/buffer.c index 6e8aa77e0f38..db17841d62ba 100644 --- a/fs/buffer.c +++ b/fs/buffer.c @@ -202,12 +202,9 @@ void bh_end_write(struct bio *bio) struct buffer_head *bh; bool success = bio_endio_bh(bio, &bh); - if (success) { - set_buffer_uptodate(bh); - } else { + if (!success) { buffer_io_error(bh, ", lost sync page write"); mark_buffer_write_io_error(bh); - clear_buffer_uptodate(bh); } unlock_buffer(bh); } @@ -407,12 +404,9 @@ void bh_end_async_write(struct bio *bio) BUG_ON(!buffer_async_write(bh)); folio = bh->b_folio; - if (success) { - set_buffer_uptodate(bh); - } else { + if (!success) { buffer_io_error(bh, ", lost async page write"); mark_buffer_write_io_error(bh); - clear_buffer_uptodate(bh); } first = folio_buffers(folio); From 336e794c59173407d22cfc1f254c517c264ef9ed Mon Sep 17 00:00:00 2001 From: Chao Shi Date: Thu, 6 Aug 2026 12:58:44 -0400 Subject: [PATCH 21/21] buffer: clear BH_Write_EIO when a write succeeds, not when one starts BH_Write_EIO is cleared in __bh_submit(), when a buffer that has been written before is submitted for write again. That is early: it says the error is gone at the moment we start trying to fix it, rather than when we have. It also loses errors. A task whose write fails sets the flag and then goes to look at it; if another task redirties the buffer and resubmits it in between, the submission clears the flag and the first task sees no error at all. Neither of them is doing anything wrong. Clear it on successful write completion instead, in the end io handlers - the same three the rest of this series has been converting, plus gfs2's, which already marked errors this way. Then the flag means what it says: the last write of this buffer that finished, failed. A resubmission no longer hides an error that has not been fixed yet, and one that has been fixed clears the flag when the data reaches the disk. __bh_submit() keeps setting BH_Req, which is what the rest of the tree reads it for. Suggested-by: Jan Kara Acked-by: Weidong Zhu Signed-off-by: Chao Shi Link: https://patch.msgid.link/1c976fd191aa6e99dbe65d6a1ec63f8706cc0dfa.1785951556.git.coshi036@gmail.com Reviewed-by: Jan Kara Signed-off-by: Christian Brauner (Amutable) --- fs/buffer.c | 15 +++++++-------- fs/gfs2/lops.c | 2 ++ fs/jbd2/commit.c | 4 +++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/fs/buffer.c b/fs/buffer.c index db17841d62ba..a43f3c9ecaa0 100644 --- a/fs/buffer.c +++ b/fs/buffer.c @@ -202,7 +202,9 @@ void bh_end_write(struct bio *bio) struct buffer_head *bh; bool success = bio_endio_bh(bio, &bh); - if (!success) { + if (success) { + clear_buffer_write_io_error(bh); + } else { buffer_io_error(bh, ", lost sync page write"); mark_buffer_write_io_error(bh); } @@ -404,7 +406,9 @@ void bh_end_async_write(struct bio *bio) BUG_ON(!buffer_async_write(bh)); folio = bh->b_folio; - if (!success) { + if (success) { + clear_buffer_write_io_error(bh); + } else { buffer_io_error(bh, ", lost async page write"); mark_buffer_write_io_error(bh); } @@ -1085,7 +1089,6 @@ static void __bh_submit(struct buffer_head *bh, blk_opf_t opf, enum rw_hint write_hint, struct writeback_control *wbc, bio_end_io_t end_bio) { - const enum req_op op = opf & REQ_OP_MASK; struct bio *bio; BUG_ON(!buffer_locked(bh)); @@ -1093,11 +1096,7 @@ static void __bh_submit(struct buffer_head *bh, blk_opf_t opf, BUG_ON(buffer_delay(bh)); BUG_ON(buffer_unwritten(bh)); - /* - * Only clear out a write error when rewriting - */ - if (test_set_buffer_req(bh) && (op == REQ_OP_WRITE)) - clear_buffer_write_io_error(bh); + set_buffer_req(bh); if (buffer_meta(bh)) opf |= REQ_META; diff --git a/fs/gfs2/lops.c b/fs/gfs2/lops.c index 3df6e4b7e8b9..7440e5b72f8a 100644 --- a/fs/gfs2/lops.c +++ b/fs/gfs2/lops.c @@ -179,6 +179,8 @@ static void gfs2_end_log_write_bh(struct gfs2_sbd *sdp, struct folio *folio, do { if (error) mark_buffer_write_io_error(bh); + else + clear_buffer_write_io_error(bh); unlock_buffer(bh); next = bh->b_this_page; size -= bh->b_size; diff --git a/fs/jbd2/commit.c b/fs/jbd2/commit.c index cd7ef783bd36..ebf6ba58ff4d 100644 --- a/fs/jbd2/commit.c +++ b/fs/jbd2/commit.c @@ -36,7 +36,9 @@ static void journal_end_buffer_io_sync(struct bio *bio) struct buffer_head *orig_bh = bh->b_private; BUFFER_TRACE(bh, ""); - if (!success) + if (success) + clear_buffer_write_io_error(bh); + else mark_buffer_write_io_error(bh); if (orig_bh) { clear_and_wake_up_bit(BH_Shadow, &orig_bh->b_state);