mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Jan Prusakowski <jprusakowski@google.com>
To: Jaegeuk Kim <jaegeuk@kernel.org>, Chao Yu <chao@kernel.org>
Cc: linux-f2fs-devel@lists.sourceforge.net,
	linux-kernel@vger.kernel.org,
	 Jan Prusakowski <jprusakowski@google.com>,
	Eric Biggers <ebiggers@google.com>,
	 Yonggil Song <yonggil.song@samsung.com>,
	Hans Holmberg <hans.holmberg@wdc.com>
Subject: [PATCH RESEND 9/9] f2fs: compress: roll back failed cluster writes of O_DIRECT buffered fallback
Date: Thu, 12 Nov 2026 14:33:55 +0000	[thread overview]
Message-ID: <20261112143355.2923939-10-jprusakowski@google.com> (raw)
In-Reply-To: <20261112143355.2923939-1-jprusakowski@google.com>

O_DIRECT writes to compressed files always fall back to buffered I/O.
The out-of-place updates done by writeback of compressed files don't go
through f2fs_do_write_data_page(), so they are not tracked for rollback
by the buffered fallback, and a failed write (e.g. with dm-error in
generic/250 and generic/252) leaves the dnode pointing to blocks that
were never written:

- f2fs_write_compressed_pages() points the slots of the cluster to new
  blocks, sets the header to COMPRESS_ADDR and invalidates the old
  blocks before the bios complete.

- f2fs_write_raw_pages(), when overwriting a compressed cluster, writes
  the pages with compr_blocks set, which f2fs_do_write_data_page() does
  not track, and also changes i_compr_blocks.

Track both as one entry per cluster:

- f2fs_write_compressed_pages() records the old and new block addresses
  of all slots, and the number of compressed blocks of the old and new
  cluster, so that f2fs_rollback_dio_opu_entry() can restore
  i_compr_blocks as well. f2fs_compress_write_end_io() marks the entry
  failed on I/O error; compressed pages carry the index of a page of the
  cluster.

- f2fs_write_raw_pages() records the block addresses of the cluster
  before and after writing it. An error other than a failed I/O can
  leave the cluster partially overwritten, so the entry is marked failed
  then too, to restore the old cluster.

Quota files are not tracked, they are written by the kernel, not with
O_DIRECT.

Fixes: 4c8ff7095bef ("f2fs: support data compression")
Assisted-by: LLM
Signed-off-by: Jan Prusakowski <jprusakowski@google.com>
---
 fs/f2fs/compress.c | 97 +++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 92 insertions(+), 5 deletions(-)

diff --git a/fs/f2fs/compress.c b/fs/f2fs/compress.c
index 0147a7e05022..8aed34dcc89e 100644
--- a/fs/f2fs/compress.c
+++ b/fs/f2fs/compress.c
@@ -1345,8 +1345,10 @@ static int f2fs_write_compressed_pages(struct compress_ctx *cc,
 	struct node_info ni;
 	struct compress_io_ctx *cic;
 	struct f2fs_lock_context lc;
+	struct f2fs_dio_opu_entry *opu_entry = NULL;
 	block_t old_blkaddr_onstack[F2FS_ONSTACK_CLUSTER_SIZE];
 	block_t *old_blkaddr = old_blkaddr_onstack;
+	block_t *new_blkaddr = NULL;
 	pgoff_t start_idx = start_idx_of_cluster(cc);
 	unsigned int last_index = cc->cluster_size - 1;
 	loff_t psize;
@@ -1404,7 +1406,15 @@ static int f2fs_write_compressed_pages(struct compress_ctx *cc,
 
 	cic->nr_rpages = cc->cluster_size;
 
-	if (cc->cluster_size > ARRAY_SIZE(old_blkaddr_onstack)) {
+	if (!quota_inode &&
+	    unlikely(f2fs_should_track_dio_opu(inode, start_idx,
+					       cc->cluster_size)))
+		opu_entry = f2fs_start_dio_opu_track(inode, start_idx,
+						     cc->cluster_size);
+	if (opu_entry) {
+		old_blkaddr = opu_entry->old_blkaddr;
+		new_blkaddr = opu_entry->new_blkaddr;
+	} else if (cc->cluster_size > ARRAY_SIZE(old_blkaddr_onstack)) {
 		/* At most 1 KiB, don't fail the write for it. */
 		old_blkaddr = kmalloc_array(cc->cluster_size, sizeof(block_t),
 					    GFP_NOFS | __GFP_NOFAIL);
@@ -1472,6 +1482,8 @@ static int f2fs_write_compressed_pages(struct compress_ctx *cc,
 		cc->cpages[i - 1] = NULL;
 		(*submitted)++;
 unlock_continue:
+		if (new_blkaddr)
+			new_blkaddr[i] = f2fs_data_blkaddr(&dn);
 		inode_dec_dirty_block(cc->inode);
 		folio_unlock(fio.folio);
 	}
@@ -1481,8 +1493,13 @@ static int f2fs_write_compressed_pages(struct compress_ctx *cc,
 	f2fs_i_compr_blocks_update(inode, cc->valid_nr_cpages, true);
 	add_compr_block_stat(inode, cc->valid_nr_cpages);
 
-	if (old_blkaddr != old_blkaddr_onstack)
+	if (unlikely(opu_entry)) {
+		opu_entry->old_compr_blocks = fio.compr_blocks;
+		opu_entry->new_compr_blocks = 1 + cc->valid_nr_cpages;
+		f2fs_end_dio_opu_track(inode, opu_entry, true, false);
+	} else if (old_blkaddr != old_blkaddr_onstack) {
 		kfree(old_blkaddr);
+	}
 
 	set_inode_flag(cc->inode, FI_APPEND_WRITE);
 
@@ -1504,7 +1521,9 @@ static int f2fs_write_compressed_pages(struct compress_ctx *cc,
 	return 0;
 
 out_submit_err:
-	if (old_blkaddr != old_blkaddr_onstack)
+	if (unlikely(opu_entry))
+		f2fs_end_dio_opu_track(inode, opu_entry, false, false);
+	else if (old_blkaddr != old_blkaddr_onstack)
 		kfree(old_blkaddr);
 	page_array_free(sbi, cic->rpages, cc->cluster_size);
 out_put_cic:
@@ -1534,8 +1553,10 @@ void f2fs_compress_write_end_io(struct bio *bio, struct folio *folio)
 	enum count_type type = WB_DATA_TYPE(folio, true);
 	int i;
 
-	if (unlikely(bio->bi_status != BLK_STS_OK))
+	if (unlikely(bio->bi_status != BLK_STS_OK)) {
 		mapping_set_error(cic->inode->i_mapping, -EIO);
+		f2fs_mark_dio_opu_failed(cic->inode, folio->index);
+	}
 
 	f2fs_compress_free_page(page);
 
@@ -1565,6 +1586,58 @@ void f2fs_compress_write_end_io(struct bio *bio, struct folio *folio)
 	dec_cache_count(sbi, type);
 }
 
+/*
+ * Track the overwrite of a compressed cluster with raw pages during the
+ * buffered fallback of an O_DIRECT write. If the dnode can't be read, write
+ * without tracking rather than failing the write for it.
+ */
+static struct f2fs_dio_opu_entry *f2fs_track_raw_cluster(struct compress_ctx *cc,
+							 pgoff_t start_idx)
+{
+	struct f2fs_dio_opu_entry *entry;
+	struct dnode_of_data dn;
+	unsigned int i;
+
+	set_new_dnode(&dn, cc->inode, NULL, NULL, 0);
+	if (f2fs_get_dnode_of_data(&dn, start_idx, LOOKUP_NODE))
+		return NULL;
+
+	entry = f2fs_start_dio_opu_track(cc->inode, start_idx,
+					 cc->cluster_size);
+	for (i = 0; entry && i < cc->cluster_size; i++) {
+		entry->old_blkaddr[i] = data_blkaddr(dn.inode, dn.node_entry,
+						     dn.ofs_in_node + i);
+		entry->new_blkaddr[i] = entry->old_blkaddr[i];
+	}
+	f2fs_put_dnode(&dn);
+
+	return entry;
+}
+
+static void f2fs_end_raw_cluster_track(struct compress_ctx *cc,
+				       struct f2fs_dio_opu_entry *entry,
+				       int compr_blocks, bool failed)
+{
+	struct dnode_of_data dn;
+	unsigned int i;
+
+	set_new_dnode(&dn, cc->inode, NULL, NULL, 0);
+	if (!f2fs_get_dnode_of_data(&dn, entry->start_idx, LOOKUP_NODE)) {
+		for (i = 0; i < cc->cluster_size; i++, dn.ofs_in_node++)
+			entry->new_blkaddr[i] = f2fs_data_blkaddr(&dn);
+		f2fs_put_dnode(&dn);
+	} else {
+		failed = true;
+	}
+
+	/* The cluster is no longer compressed if its header was rewritten. */
+	if (entry->old_blkaddr[0] == COMPRESS_ADDR &&
+	    entry->new_blkaddr[0] != COMPRESS_ADDR)
+		entry->old_compr_blocks = compr_blocks;
+
+	f2fs_end_dio_opu_track(cc->inode, entry, true, failed);
+}
+
 static int f2fs_write_raw_pages(struct compress_ctx *cc,
 					int *submitted_p,
 					struct writeback_control *wbc,
@@ -1572,6 +1645,8 @@ static int f2fs_write_raw_pages(struct compress_ctx *cc,
 {
 	struct address_space *mapping = cc->inode->i_mapping;
 	struct f2fs_sb_info *sbi = F2FS_M_SB(mapping);
+	struct f2fs_dio_opu_entry *opu_entry = NULL;
+	pgoff_t start_idx = start_idx_of_cluster(cc);
 	struct f2fs_lock_context lc;
 	int submitted, compr_blocks, i;
 	int ret = 0;
@@ -1590,8 +1665,13 @@ static int f2fs_write_raw_pages(struct compress_ctx *cc,
 		return compr_blocks;
 
 	/* overwrite compressed cluster w/ normal cluster */
-	if (compr_blocks > 0)
+	if (compr_blocks > 0) {
 		f2fs_lock_op(sbi, &lc);
+		if (!IS_NOQUOTA(cc->inode) &&
+		    unlikely(f2fs_should_track_dio_opu(cc->inode, start_idx,
+						       cc->cluster_size)))
+			opu_entry = f2fs_track_raw_cluster(cc, start_idx);
+	}
 
 	for (i = 0; i < cc->cluster_size; i++) {
 		struct folio *folio;
@@ -1646,6 +1726,13 @@ static int f2fs_write_raw_pages(struct compress_ctx *cc,
 	}
 
 out:
+	/*
+	 * An error other than a failed I/O can leave the cluster partially
+	 * overwritten, so mark it failed too, to restore the old cluster.
+	 */
+	if (unlikely(opu_entry))
+		f2fs_end_raw_cluster_track(cc, opu_entry, compr_blocks,
+					   ret || f2fs_cp_error(sbi));
 	if (compr_blocks > 0)
 		f2fs_unlock_op(sbi, &lc);
 
-- 
2.56.0.567.gdb80ae8cd5-goog


      parent reply	other threads:[~2026-11-12 14:38 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-12 14:33 [PATCH RESEND 0/9] f2fs: fix stale data exposure on failed O_DIRECT writes Jan Prusakowski
2026-11-12 14:33 ` [PATCH RESEND 1/9] f2fs: compress: avoid sleeping in atomic context in f2fs_decompress_cluster() Jan Prusakowski
2026-11-12 14:33 ` [PATCH RESEND 2/9] f2fs: compress: abort on read error in prepare_compress_overwrite() Jan Prusakowski
2026-11-12 14:33 ` [PATCH RESEND 3/9] f2fs: keep old block address when out-of-place write fails to allocate Jan Prusakowski
2026-11-12 14:33 ` [PATCH RESEND 4/9] f2fs: propagate errors from buffered fallback of O_DIRECT writes Jan Prusakowski
2026-11-12 14:33 ` [PATCH RESEND 5/9] f2fs: truncate blocks allocated past EOF by a failed direct write Jan Prusakowski
2026-11-12 14:33 ` [PATCH RESEND 6/9] f2fs: roll back failed block updates of O_DIRECT buffered fallback Jan Prusakowski
2026-11-12 14:33 ` [PATCH RESEND 7/9] f2fs: roll back failed block updates of direct writes Jan Prusakowski
2026-11-12 14:33 ` [PATCH RESEND 8/9] f2fs: compress: fix cleanup when a cluster write fails to be submitted Jan Prusakowski
2026-11-12 14:33 ` Jan Prusakowski [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261112143355.2923939-10-jprusakowski@google.com \
    --to=jprusakowski@google.com \
    --cc=chao@kernel.org \
    --cc=ebiggers@google.com \
    --cc=hans.holmberg@wdc.com \
    --cc=jaegeuk@kernel.org \
    --cc=linux-f2fs-devel@lists.sourceforge.net \
    --cc=linux-kernel@vger.kernel.org \
    --cc=yonggil.song@samsung.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®