When the block size is smaller than the folio size, a folio can contain several extents that need mapping. Mapping an earlier extent can succeed before mapping a later extent fails with a fatal error such as -EIO. mpage_map_and_submit_extent() submits the mapped part of the folio only for retryable errors. On a fatal error it discards the whole folio, including dirty buffers whose mapping has already succeeded. This loses data that could still be submitted. With dioread_nolock, the mapped ranges can also remain in io_end's unwritten conversion list. Conversion can then mark those extents written without their data ever being submitted, exposing stale data. Also submit the mapped buffers after a fatal mapping error when the filesystem is not in an emergency state. Discard buffers still marked BH_Delay or BH_Unwritten so that writeback does not keep retrying them. Clearing their dirty bits alone would leave stale buffer mappings and delayed allocation reservations. Remove their extent status entries and invalidate the buffers. Do this after folio_clear_dirty_for_io(), since cleaning mmap writes can mark all buffers dirty again. Update i_disksize using the mapped boundary before moving start_pos past the folio. Advancing it earlier could include discarded data in the size update. Advancing it afterwards makes mpage_release_unused_pages() skip the handled folio and its extent status entries, including any entries created by new writes after the folio was unlocked. Count the handled folio against the writeback budget on fatal errors. Keep the existing retry behavior for transient errors. Record fatal mapping errors before discarding dirty buffers so that fsync() can report the lost writes. Fixes: c8cc88163f40 ("ext4: Add support for blocksize < pagesize in dioread_nolock") Cc: stable@vger.kernel.org Signed-off-by: Julian Sun --- fs/ext4/inode.c | 109 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 84 insertions(+), 25 deletions(-) diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c index 74437fe6aa24..81aac8744f3d 100644 --- a/fs/ext4/inode.c +++ b/fs/ext4/inode.c @@ -2192,13 +2192,46 @@ static void mpage_folio_done(struct mpage_da_data *mpd, struct folio *folio) folio_unlock(folio); } -static void mpage_submit_folio(struct mpage_da_data *mpd, struct folio *folio) +static void mpage_submit_folio(struct mpage_da_data *mpd, struct folio *folio, + bool fatal) { size_t len; loff_t size; WARN_ON_ONCE(folio_pos(folio) != mpd->start_pos); folio_clear_dirty_for_io(folio); + + /* + * If a later mapping fails with a fatal error, write the buffers + * ALREADY mapped and discard the data in buffers still needing + * mapping. Do this after folio_clear_dirty_for_io(), which may + * mark all buffers dirty again for mmap writes. + */ + if (fatal) { + struct buffer_head *bh, *head; + ext4_lblk_t start, length; + struct inode *inode = mpd->inode; + + /* + * Remove the ES entries, free the reserved space and clear the + * buffer mappings before new writes can use this folio. Hold + * i_data_sem to prevent races with delayed allocation lookups. + */ + down_write(&EXT4_I(inode)->i_data_sem); + + bh = head = folio_buffers(folio); + do { + if (buffer_delay(bh) || buffer_unwritten(bh)) { + start = EXT4_B_TO_LBLK(inode, folio_pos(folio) + bh_offset(bh)); + length = bh->b_size >> inode->i_blkbits; + ext4_es_remove_extent(inode, start, length); + block_invalidate_folio(folio, bh_offset(bh), bh->b_size); + } + } while ((bh = bh->b_this_page) != head); + + up_write(&EXT4_I(inode)->i_data_sem); + } + /* * We have to be very careful here! Nothing protects writeback path * against i_size changes and the page can be writeably mapped into @@ -2324,7 +2357,7 @@ static int mpage_process_page_bufs(struct mpage_da_data *mpd, } while (lblk++, (bh = bh->b_this_page) != head); /* So far everything mapped? Submit the page for IO. */ if (mpd->map.m_len == 0) { - mpage_submit_folio(mpd, head->b_folio); + mpage_submit_folio(mpd, head->b_folio, false); mpage_folio_done(mpd, head->b_folio); } if (lblk >= blocks) { @@ -2455,7 +2488,7 @@ static int mpage_map_and_submit_buffers(struct mpage_da_data *mpd, int *progress if (err < 0 || map_bh) goto out; /* Page fully mapped - let IO run! */ - mpage_submit_folio(mpd, folio); + mpage_submit_folio(mpd, folio, false); mpage_folio_done(mpd, folio); } folio_batch_release(&fbatch); @@ -2526,14 +2559,15 @@ static int mpage_map_one_extent(handle_t *handle, struct mpage_da_data *mpd) } /* - * This is used to submit mapped buffers in a single folio that is not fully - * mapped for various reasons, such as insufficient space or journal credits. + * Write the buffers that are ready in a partly mapped folio. On a fatal + * error, drop the buffers that still need mapping instead of retrying them. + * Return the byte offset just past the folio on success, or a negative error. */ -static int mpage_submit_partial_folio(struct mpage_da_data *mpd) +static loff_t mpage_submit_partial_folio(struct mpage_da_data *mpd, bool fatal) { struct inode *inode = mpd->inode; struct folio *folio; - loff_t pos; + loff_t pos, end; folio = filemap_get_folio(inode->i_mapping, mpd->start_pos >> PAGE_SHIFT); @@ -2548,18 +2582,21 @@ static int mpage_submit_partial_folio(struct mpage_da_data *mpd) !folio_contains(folio, pos >> PAGE_SHIFT))) return -EINVAL; - mpage_submit_folio(mpd, folio); + end = folio_next_pos(folio); + mpage_submit_folio(mpd, folio, fatal); /* * Update start_pos to prevent this folio from being released in - * mpage_release_unused_pages(), it will be reset to the aligned folio - * pos when this folio is written again in the next round. Additionally, - * do not update wbc->nr_to_write here, as it will be updated once the - * entire folio has finished processing. + * mpage_release_unused_pages(). For retryable errors, it will be reset + * to the aligned folio pos when this folio is written again in the next + * round. Count the folio now only for fatal errors; otherwise the + * retry will count it once the entire folio has finished processing. */ mpd->start_pos = pos; + if (fatal) + mpd->wbc->nr_to_write -= folio_nr_pages(folio); folio_unlock(folio); folio_put(folio); - return 0; + return end; } /* @@ -2593,6 +2630,8 @@ static int mpage_map_and_submit_extent(handle_t *handle, int progress = 0; ext4_io_end_t *io_end = mpd->io_submit.io_end; struct ext4_io_end_vec *io_end_vec; + bool fatal = false; + loff_t next_folio_pos = 0; io_end_vec = ext4_alloc_io_end_vec(io_end); if (IS_ERR(io_end_vec)) @@ -2626,18 +2665,29 @@ static int mpage_map_and_submit_extent(handle_t *handle, */ if ((err == -ENOMEM) || (err == -EAGAIN) || (err == -ENOSPC && ext4_count_free_clusters(sb))) { - /* - * We may have already allocated extents for - * some bhs inside the folio, issue the - * corresponding data to prevent stale data. - */ - if (progress) { - if (mpage_submit_partial_folio(mpd)) - goto invalidate_dirty_pages; - goto update_disksize; - } - return err; + /* Already false, but set it here to make the choice clear. */ + fatal = false; + } else { + fatal = true; + mapping_set_error(inode->i_mapping, err); + } + + /* + * We may have already allocated extents for + * some bhs inside the folio, issue the + * corresponding data to prevent stale data. + */ + if (progress) { + next_folio_pos = mpage_submit_partial_folio(mpd, fatal); + if (next_folio_pos < 0) + goto invalidate_dirty_pages; + + goto update_disksize; } + + if (!fatal) + return err; + ext4_msg(sb, KERN_CRIT, "Delayed block allocation failed for " "inode %llu at logical offset %llu with" @@ -2681,6 +2731,15 @@ static int mpage_map_and_submit_extent(handle_t *handle, if (!err) err = err2; } + if (fatal) { + /* + * We have submitted the mapped buffers and dropped the rest. + * After updating i_disksize, move start_pos past this folio so + * mpage_release_unused_pages() skips its buffers and ES entries. + */ + mpd->start_pos = next_folio_pos; + *give_up_on_write = true; + } return err; } @@ -2863,7 +2922,7 @@ static int mpage_prepare_extent_to_map(struct mpage_da_data *mpd) * through a pin. */ if (!mpd->can_map) { - mpage_submit_folio(mpd, folio); + mpage_submit_folio(mpd, folio, false); err = 0; /* Pending dirtying of journalled data? */ if (folio_test_checked(folio)) { -- 2.39.5