All of lore.kernel.org
 help / color / mirror / Atom feed
* [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files
@ 2026-07-27 10:54 Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 1/9] ext4: add deprecation warning for inline_data feature Yun Zhou
                   ` (9 more replies)
  0 siblings, 10 replies; 11+ messages in thread
From: Yun Zhou @ 2026-07-27 10:54 UTC (permalink / raw)
  To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
	yi.zhang
  Cc: linux-ext4, linux-kernel, yun.zhou

This series phases out the inline data write paths for regular files
while preserving directory inline data support.

Motivation
==========

The inline_data feature has been a persistent source of bugs in
ext4 over the past three years:

- 15 kernel bug fixes since mid-2023, including fixes for BUG_ON,
  use-after-free, out-of-bounds reads, race conditions, and data
  corruption
- 4 e2fsprogs bug fixes in the same period
- 6-8 syzbot reports currently open or continuously reproducing,
  including "kernel BUG in ext4_write_inline_data" reported 5 times
- 8+ CVEs: CVE-2023-52786, CVE-2024-42304, CVE-2025-38222,
  CVE-2025-38701, CVE-2025-40167, CVE-2025-68264, CVE-2026-31451,
  CVE-2026-31452

The root cause is architectural: inline data requires special handling
in write_begin/write_end, writepages, page_mkwrite, truncate, and
directory operations, with complex locking interactions between
xattr_sem, i_rwsem, i_data_sem, and page locks.

The benefit is minimal: inline data saves at most one 4K block per
small file.  In practice, very few regular files are small enough to
benefit from inline storage -- mostly only small directories gain from
it.  Major distributions do not enable inline_data by default in
mkfs.ext4.

Approach
========

Rather than removing inline data entirely (which would break existing
filesystems), this series takes a phased approach:

1. Emit a mount-time deprecation warning
2. Stop creating inline data for new regular files (directories retain
   inline capability as they benefit with minimal complexity)
3-5. Remove inline write paths and dead code for regular files
     (DA convert path addressed separately in patch 7)
6-7. Rework the conversion path to be atomic (no restore needed) and
   fix the syzbot-reported BUG_ON race
8. Update documentation
9. Close a pre-existing theoretical i_data_sem race window

After this series, existing inline data files are transparently
converted to block format on first write or mmap.  Reads continue to
work normally.  No data loss, no user-visible behavior change beyond
the conversion.

Testing
=======

- Full ext4 build passes with zero warnings
- xfstests smoketest (ext4/4k): 7 tests passed
- xfstests smoketest (ext4/inline, -O inline_data): 7 tests passed
  (fsstress + dm-error with inline_data enabled filesystem)

Future work (separate series, after community consensus):
- e2fsprogs: deprecate mkfs.ext4 -O inline_data (warn or refuse)
- e2fsprogs: add offline conversion in e2fsck/tune2fs to convert all
  inline data inodes to block format and clear the feature flag

Yun Zhou (9):
  ext4: add deprecation warning for inline_data feature
  ext4: stop creating inline data for new regular files
  ext4: use safe convert path for inline data write overflow
  ext4: remove inline data write paths for regular files
  ext4: remove dead inline data write code
  ext4: allocate block before destroying inline data in conversion
  ext4: remove DA convert path for regular file inline data
  ext4: document inline_data deprecation
  ext4: populate extent entry atomically during inline data destroy

 Documentation/filesystems/ext4/inlinedata.rst |   8 +
 fs/ext4/ext4.h                                |   7 -
 fs/ext4/ialloc.c                              |   4 +-
 fs/ext4/inline.c                              | 518 ++----------------
 fs/ext4/inode.c                               |  25 +-
 fs/ext4/super.c                               |   6 +
 6 files changed, 70 insertions(+), 498 deletions(-)

-- 
2.43.0


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [RFC PATCH 1/9] ext4: add deprecation warning for inline_data feature
  2026-07-27 10:54 [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Yun Zhou
@ 2026-07-27 10:54 ` Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 2/9] ext4: stop creating inline data for new regular files Yun Zhou
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Yun Zhou @ 2026-07-27 10:54 UTC (permalink / raw)
  To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
	yi.zhang
  Cc: linux-ext4, linux-kernel, yun.zhou

Emit a mount-time warning when the inline_data feature is enabled,
directing users to convert their filesystems with tune2fs.

Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
 fs/ext4/super.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/fs/ext4/super.c b/fs/ext4/super.c
index 6c18b5adffca..9913e30b7040 100644
--- a/fs/ext4/super.c
+++ b/fs/ext4/super.c
@@ -3145,6 +3145,12 @@ static int ext4_setup_super(struct super_block *sb, struct ext4_super_block *es,
 		ext4_msg(sb, KERN_WARNING,
 			 "warning: checktime reached, "
 			 "running e2fsck is recommended");
+	if (ext4_has_feature_inline_data(sb))
+		ext4_msg(sb, KERN_WARNING,
+			 "warning: inline_data feature is deprecated and "
+			 "will be removed in a future kernel release. "
+			 "Use 'tune2fs -O ^inline_data' to convert.");
+
 	if (!sbi->s_journal)
 		es->s_state &= cpu_to_le16(~EXT4_VALID_FS);
 	if (!(__s16) le16_to_cpu(es->s_max_mnt_count))
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [RFC PATCH 2/9] ext4: stop creating inline data for new regular files
  2026-07-27 10:54 [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 1/9] ext4: add deprecation warning for inline_data feature Yun Zhou
@ 2026-07-27 10:54 ` Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 3/9] ext4: use safe convert path for inline data write overflow Yun Zhou
                   ` (7 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Yun Zhou @ 2026-07-27 10:54 UTC (permalink / raw)
  To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
	yi.zhang
  Cc: linux-ext4, linux-kernel, yun.zhou

Stop setting EXT4_STATE_MAY_INLINE_DATA for newly allocated regular
file inodes.  New files always use block-based storage.  Directories
retain inline data capability as it still provides value for small
directories without the write path complexity.

Existing file inodes with inline data are not affected -- MAY_INLINE_DATA
is still set for them during iget via ext4_find_inline_data_nolock().

Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
 fs/ext4/ialloc.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c
index a40cb27f8116..278f0ab8c996 100644
--- a/fs/ext4/ialloc.c
+++ b/fs/ext4/ialloc.c
@@ -1305,8 +1305,8 @@ struct inode *__ext4_new_inode(struct mnt_idmap *idmap,
 
 	ei->i_extra_isize = sbi->s_want_extra_isize;
 	ei->i_inline_off = 0;
-	if (ext4_has_feature_inline_data(sb) &&
-	    (!(ei->i_flags & (EXT4_DAX_FL|EXT4_EA_INODE_FL)) || S_ISDIR(mode)))
+	if (ext4_has_feature_inline_data(sb) && S_ISDIR(mode) &&
+	    !(ei->i_flags & EXT4_DAX_FL))
 		ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
 	ret = inode;
 	err = dquot_alloc_inode(inode);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [RFC PATCH 3/9] ext4: use safe convert path for inline data write overflow
  2026-07-27 10:54 [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 1/9] ext4: add deprecation warning for inline_data feature Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 2/9] ext4: stop creating inline data for new regular files Yun Zhou
@ 2026-07-27 10:54 ` Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 4/9] ext4: remove inline data write paths for regular files Yun Zhou
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Yun Zhou @ 2026-07-27 10:54 UTC (permalink / raw)
  To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
	yi.zhang
  Cc: linux-ext4, linux-kernel, yun.zhou

Replace ext4_convert_inline_data_to_extent() with
ext4_convert_inline_data() in the write paths that handle inline data
overflow.

ext4_convert_inline_data_to_extent() destroys inline data before
allocating the target block.  If block allocation fails (e.g. -ENOSPC),
the inline data is already gone and cannot be recovered -- a potential
data loss on crash.

ext4_convert_inline_data() keeps
a copy of the inline data in a buffer and restores it on failure, making
the conversion safe against allocation failures.

Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
 fs/ext4/inline.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index ceee69a66482..8f1efe4297a0 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -724,7 +724,7 @@ int ext4_generic_write_inline_data(struct address_space *mapping,
 		if (!da) {
 			brelse(iloc.bh);
 			/* Retry inside */
-			return ext4_convert_inline_data_to_extent(mapping, inode);
+			return ext4_convert_inline_data(inode);
 		}
 
 		ret = ext4_da_convert_inline_data_to_extent(mapping, inode);
@@ -785,7 +785,7 @@ int ext4_try_to_write_inline_data(struct address_space *mapping,
 				  struct folio **foliop)
 {
 	if (pos + len > ext4_get_max_inline_size(inode))
-		return ext4_convert_inline_data_to_extent(mapping, inode);
+		return ext4_convert_inline_data(inode);
 	return ext4_generic_write_inline_data(mapping, inode, pos, len,
 					      foliop, false);
 }
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [RFC PATCH 4/9] ext4: remove inline data write paths for regular files
  2026-07-27 10:54 [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Yun Zhou
                   ` (2 preceding siblings ...)
  2026-07-27 10:54 ` [RFC PATCH 3/9] ext4: use safe convert path for inline data write overflow Yun Zhou
@ 2026-07-27 10:54 ` Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 5/9] ext4: remove dead inline data write code Yun Zhou
                   ` (5 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Yun Zhou @ 2026-07-27 10:54 UTC (permalink / raw)
  To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
	yi.zhang
  Cc: linux-ext4, linux-kernel, yun.zhou

Any write operation on an existing inline data regular file now triggers
immediate conversion to block-based storage instead of writing inline.

Remove ext4_try_to_write_inline_data() and have its caller directly
call ext4_convert_inline_data().  ext4_generic_write_inline_data()
converts immediately for the non-DA path.

Directory inline data operations (ext4_try_add_inline_entry) are
preserved as directories benefit most from inline storage without the
write path complexity.

Read, truncate, and convert paths remain intact.

Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
 fs/ext4/ext4.h   |  4 ---
 fs/ext4/inline.c | 94 +++++-------------------------------------------
 fs/ext4/inode.c  |  3 +-
 3 files changed, 9 insertions(+), 92 deletions(-)

diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 21a951f10636..5f0448b960fe 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3759,10 +3759,6 @@ extern int ext4_destroy_inline_data(handle_t *handle, struct inode *inode);
 extern void ext4_update_final_de(void *de_buf, int old_size, int new_size);
 
 int ext4_readpage_inline(struct inode *inode, struct folio *folio);
-extern int ext4_try_to_write_inline_data(struct address_space *mapping,
-					 struct inode *inode,
-					 loff_t pos, unsigned len,
-					 struct folio **foliop);
 int ext4_write_inline_data_end(struct inode *inode, loff_t pos, unsigned len,
 			       unsigned copied, struct folio *folio);
 extern int ext4_generic_write_inline_data(struct address_space *mapping,
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 8f1efe4297a0..0803fa8eade7 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -699,97 +699,19 @@ int ext4_generic_write_inline_data(struct address_space *mapping,
 					  bool da)
 {
 	int ret;
-	handle_t *handle;
-	struct folio *folio;
-	struct ext4_iloc iloc;
 	int retries = 0;
 
-	ret = ext4_get_inode_loc(inode, &iloc);
-	if (ret)
-		return ret;
-
-retry_journal:
-	handle = ext4_journal_start(inode, EXT4_HT_INODE, 1);
-	if (IS_ERR(handle)) {
-		ret = PTR_ERR(handle);
-		goto out_release_bh;
-	}
-
-	ret = ext4_prepare_inline_data(handle, inode, pos + len);
-	if (ret && ret != -ENOSPC)
-		goto out_stop_journal;
-
-	if (ret == -ENOSPC) {
-		ext4_journal_stop(handle);
-		if (!da) {
-			brelse(iloc.bh);
-			/* Retry inside */
-			return ext4_convert_inline_data(inode);
-		}
-
-		ret = ext4_da_convert_inline_data_to_extent(mapping, inode);
-		if (ret == -ENOSPC &&
-		    ext4_should_retry_alloc(inode->i_sb, &retries))
-			goto retry_journal;
-		goto out_release_bh;
-	}
-
-	folio = __filemap_get_folio(mapping, 0, FGP_WRITEBEGIN | FGP_NOFS,
-					mapping_gfp_mask(mapping));
-	if (IS_ERR(folio)) {
-		ret = PTR_ERR(folio);
-		goto out_stop_journal;
-	}
-
-	down_read(&EXT4_I(inode)->xattr_sem);
-	/* Someone else had converted it to extent */
-	if (!ext4_has_inline_data(inode)) {
-		ret = 0;
-		goto out_release_folio;
-	}
-
-	if (!folio_test_uptodate(folio)) {
-		ret = ext4_read_inline_folio(inode, folio);
-		if (ret < 0)
-			goto out_release_folio;
-	}
-
-	ret = ext4_journal_get_write_access(handle, inode->i_sb, iloc.bh, EXT4_JTR_NONE);
-	if (ret)
-		goto out_release_folio;
-	*foliop = folio;
-	up_read(&EXT4_I(inode)->xattr_sem);
-	brelse(iloc.bh);
-	return 1;
+	/* Inline data is deprecated: always convert to block format */
+	if (!da)
+		return ext4_convert_inline_data(inode);
 
-out_release_folio:
-	up_read(&EXT4_I(inode)->xattr_sem);
-	folio_unlock(folio);
-	folio_put(folio);
-out_stop_journal:
-	ext4_journal_stop(handle);
-out_release_bh:
-	brelse(iloc.bh);
+retry:
+	ret = ext4_da_convert_inline_data_to_extent(mapping, inode);
+	if (ret == -ENOSPC && ext4_should_retry_alloc(inode->i_sb, &retries))
+		goto retry;
 	return ret;
 }
 
-/*
- * Try to write data in the inode.
- * If the inode has inline data, check whether the new write can be
- * in the inode also. If not, create the page the handle, move the data
- * to the page make it update and let the later codes create extent for it.
- */
-int ext4_try_to_write_inline_data(struct address_space *mapping,
-				  struct inode *inode,
-				  loff_t pos, unsigned len,
-				  struct folio **foliop)
-{
-	if (pos + len > ext4_get_max_inline_size(inode))
-		return ext4_convert_inline_data(inode);
-	return ext4_generic_write_inline_data(mapping, inode, pos, len,
-					      foliop, false);
-}
-
 int ext4_write_inline_data_end(struct inode *inode, loff_t pos, unsigned len,
 			       unsigned copied, struct folio *folio)
 {
@@ -828,7 +750,7 @@ int ext4_write_inline_data_end(struct inode *inode, loff_t pos, unsigned len,
 		/*
 		 * ei->i_inline_off may have changed since
 		 * ext4_write_begin() called
-		 * ext4_try_to_write_inline_data()
+		 * ext4_convert_inline_data()
 		 */
 		(void) ext4_find_inline_data_nolock(inode);
 
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index ad14dd58a003..f9e1f0a6024d 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -1312,8 +1312,7 @@ static int ext4_write_begin(const struct kiocb *iocb,
 	index = pos >> PAGE_SHIFT;
 
 	if (ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA)) {
-		ret = ext4_try_to_write_inline_data(mapping, inode, pos, len,
-						    foliop);
+		ret = ext4_convert_inline_data(inode);
 		if (ret < 0)
 			return ret;
 		if (ret == 1) {
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [RFC PATCH 5/9] ext4: remove dead inline data write code
  2026-07-27 10:54 [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Yun Zhou
                   ` (3 preceding siblings ...)
  2026-07-27 10:54 ` [RFC PATCH 4/9] ext4: remove inline data write paths for regular files Yun Zhou
@ 2026-07-27 10:54 ` Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 6/9] ext4: allocate block before destroying inline data in conversion Yun Zhou
                   ` (4 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Yun Zhou @ 2026-07-27 10:54 UTC (permalink / raw)
  To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
	yi.zhang
  Cc: linux-ext4, linux-kernel, yun.zhou

Now that inline data write paths for regular files always convert to
block format, the following unreachable dead code is removed:

- ext4_write_inline_data_end(): was called from write_end when
  EXT4_WRITE_DATA_INLINE was set in fsdata, which no longer happens
- ext4_convert_inline_data_to_extent(): was the unsafe convert path
  (no restore on failure), replaced by ext4_convert_inline_data()
- EXT4_WRITE_DATA_INLINE flag and all associated checks in inode.c

Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
 fs/ext4/ext4.h   |   3 -
 fs/ext4/inline.c | 210 -----------------------------------------------
 fs/ext4/inode.c  |  22 -----
 3 files changed, 235 deletions(-)

diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 5f0448b960fe..832bc1f3b600 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3148,7 +3148,6 @@ int do_journal_get_write_access(handle_t *handle, struct inode *inode,
 				struct buffer_head *bh);
 void ext4_set_inode_mapping_order(struct inode *inode);
 #define FALL_BACK_TO_NONDELALLOC 1
-#define EXT4_WRITE_DATA_INLINE	 2
 
 typedef enum {
 	EXT4_IGET_NORMAL =	0,
@@ -3759,8 +3758,6 @@ extern int ext4_destroy_inline_data(handle_t *handle, struct inode *inode);
 extern void ext4_update_final_de(void *de_buf, int old_size, int new_size);
 
 int ext4_readpage_inline(struct inode *inode, struct folio *folio);
-int ext4_write_inline_data_end(struct inode *inode, loff_t pos, unsigned len,
-			       unsigned copied, struct folio *folio);
 extern int ext4_generic_write_inline_data(struct address_space *mapping,
 					  struct inode *inode,
 					  loff_t pos, unsigned len,
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 0803fa8eade7..5c6202add3d8 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -568,122 +568,6 @@ int ext4_readpage_inline(struct inode *inode, struct folio *folio)
 	return ret >= 0 ? 0 : ret;
 }
 
-static int ext4_convert_inline_data_to_extent(struct address_space *mapping,
-					      struct inode *inode)
-{
-	int ret, needed_blocks, no_expand;
-	handle_t *handle = NULL;
-	int retries = 0, sem_held = 0;
-	struct folio *folio = NULL;
-	unsigned from, to;
-	struct ext4_iloc iloc;
-
-	if (!ext4_has_inline_data(inode)) {
-		/*
-		 * clear the flag so that no new write
-		 * will trap here again.
-		 */
-		ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
-		return 0;
-	}
-
-	needed_blocks = ext4_chunk_trans_extent(inode, 1);
-
-	ret = ext4_get_inode_loc(inode, &iloc);
-	if (ret)
-		return ret;
-
-retry:
-	handle = ext4_journal_start(inode, EXT4_HT_WRITE_PAGE, needed_blocks);
-	if (IS_ERR(handle)) {
-		ret = PTR_ERR(handle);
-		handle = NULL;
-		goto out;
-	}
-
-	/* We cannot recurse into the filesystem as the transaction is already
-	 * started */
-	folio = __filemap_get_folio(mapping, 0, FGP_WRITEBEGIN | FGP_NOFS,
-			mapping_gfp_mask(mapping));
-	if (IS_ERR(folio)) {
-		ret = PTR_ERR(folio);
-		goto out_nofolio;
-	}
-
-	ext4_write_lock_xattr(inode, &no_expand);
-	sem_held = 1;
-	/* If some one has already done this for us, just exit. */
-	if (!ext4_has_inline_data(inode)) {
-		ret = 0;
-		goto out;
-	}
-
-	from = 0;
-	to = ext4_get_inline_size(inode);
-	if (!folio_test_uptodate(folio)) {
-		ret = ext4_read_inline_folio(inode, folio);
-		if (ret < 0)
-			goto out;
-	}
-
-	ext4_fc_track_inode(handle, inode);
-	ret = ext4_destroy_inline_data_nolock(handle, inode);
-	if (ret)
-		goto out;
-
-	if (ext4_should_dioread_nolock(inode)) {
-		ret = ext4_block_write_begin(handle, folio, from, to,
-					     ext4_get_block_unwritten);
-	} else
-		ret = ext4_block_write_begin(handle, folio, from, to,
-					     ext4_get_block);
-	clear_buffer_new(folio_buffers(folio));
-
-	if (!ret && ext4_should_journal_data(inode)) {
-		ret = ext4_walk_page_buffers(handle, inode,
-					     folio_buffers(folio), from, to,
-					     NULL, do_journal_get_write_access);
-	}
-
-	if (ret) {
-		folio_unlock(folio);
-		folio_put(folio);
-		folio = NULL;
-		ext4_orphan_add(handle, inode);
-		ext4_write_unlock_xattr(inode, &no_expand);
-		sem_held = 0;
-		ext4_journal_stop(handle);
-		handle = NULL;
-		ext4_truncate_failed_write(inode);
-		/*
-		 * If truncate failed early the inode might
-		 * still be on the orphan list; we need to
-		 * make sure the inode is removed from the
-		 * orphan list in that case.
-		 */
-		if (inode->i_nlink)
-			ext4_orphan_del(NULL, inode);
-	}
-
-	if (ret == -ENOSPC && ext4_should_retry_alloc(inode->i_sb, &retries))
-		goto retry;
-
-	if (folio)
-		block_commit_write(folio, from, to);
-out:
-	if (folio) {
-		folio_unlock(folio);
-		folio_put(folio);
-	}
-out_nofolio:
-	if (sem_held)
-		ext4_write_unlock_xattr(inode, &no_expand);
-	if (handle)
-		ext4_journal_stop(handle);
-	brelse(iloc.bh);
-	return ret;
-}
-
 /*
  * Prepare the write for the inline data.
  * If the data can be written into the inode, we just read
@@ -712,100 +596,6 @@ int ext4_generic_write_inline_data(struct address_space *mapping,
 	return ret;
 }
 
-int ext4_write_inline_data_end(struct inode *inode, loff_t pos, unsigned len,
-			       unsigned copied, struct folio *folio)
-{
-	handle_t *handle = ext4_journal_current_handle();
-	int no_expand;
-	void *kaddr;
-	struct ext4_iloc iloc;
-	int ret = 0, ret2;
-
-	if (unlikely(copied < len) && !folio_test_uptodate(folio))
-		copied = 0;
-
-	if (likely(copied)) {
-		ret = ext4_get_inode_loc(inode, &iloc);
-		if (ret) {
-			folio_unlock(folio);
-			folio_put(folio);
-			ext4_std_error(inode->i_sb, ret);
-			goto out;
-		}
-		ext4_write_lock_xattr(inode, &no_expand);
-		/*
-		 * We could have raced with ext4_page_mkwrite() converting
-		 * the inode and clearing the inline data flag, so we just
-		 * release resources and retry the whole write.
-		 */
-		if (unlikely(!ext4_has_inline_data(inode))) {
-			ext4_write_unlock_xattr(inode, &no_expand);
-			brelse(iloc.bh);
-			folio_unlock(folio);
-			folio_put(folio);
-			ext4_journal_stop(handle);
-			return 0;
-		}
-
-		/*
-		 * ei->i_inline_off may have changed since
-		 * ext4_write_begin() called
-		 * ext4_convert_inline_data()
-		 */
-		(void) ext4_find_inline_data_nolock(inode);
-
-		kaddr = kmap_local_folio(folio, 0);
-		ext4_write_inline_data(inode, &iloc, kaddr, pos, copied);
-		kunmap_local(kaddr);
-		folio_mark_uptodate(folio);
-		/* clear dirty flag so that writepages wouldn't work for us. */
-		folio_clear_dirty(folio);
-
-		ext4_write_unlock_xattr(inode, &no_expand);
-		brelse(iloc.bh);
-
-		/*
-		 * It's important to update i_size while still holding folio
-		 * lock: page writeout could otherwise come in and zero
-		 * beyond i_size.
-		 */
-		ext4_update_inode_size(inode, pos + copied);
-	}
-	folio_unlock(folio);
-	folio_put(folio);
-
-	/*
-	 * Don't mark the inode dirty under folio lock. First, it unnecessarily
-	 * makes the holding time of folio lock longer. Second, it forces lock
-	 * ordering of folio lock and transaction start for journaling
-	 * filesystems.
-	 */
-	if (likely(copied))
-		mark_inode_dirty(inode);
-out:
-	/*
-	 * If we didn't copy as much data as expected, we need to trim back
-	 * size of xattr containing inline data.
-	 */
-	if (pos + len > inode->i_size && ext4_can_truncate(inode))
-		ext4_orphan_add(handle, inode);
-
-	ret2 = ext4_journal_stop(handle);
-	if (!ret)
-		ret = ret2;
-	if (pos + len > inode->i_size) {
-		ext4_truncate_failed_write(inode);
-		/*
-		 * If truncate failed early the inode might still be
-		 * on the orphan list; we need to make sure the inode
-		 * is removed from the orphan list in that case.
-		 */
-		if (inode->i_nlink)
-			ext4_orphan_del(NULL, inode);
-	}
-	return ret ? ret : copied;
-}
-
 /*
  * Try to make the page cache and handle ready for the inline data case.
  * We can call this function in 2 cases:
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index f9e1f0a6024d..2256fb3826a9 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -1300,8 +1300,6 @@ static int ext4_write_begin(const struct kiocb *iocb,
 	if (unlikely(ret))
 		return ret;
 
-	*fsdata = (void *)((unsigned long)*fsdata & ~EXT4_WRITE_DATA_INLINE);
-
 	trace_ext4_write_begin(inode, pos, len);
 	/*
 	 * Reserve one block more for addition to orphan list in case
@@ -1315,10 +1313,6 @@ static int ext4_write_begin(const struct kiocb *iocb,
 		ret = ext4_convert_inline_data(inode);
 		if (ret < 0)
 			return ret;
-		if (ret == 1) {
-			*fsdata = (void *)((unsigned long)*fsdata | EXT4_WRITE_DATA_INLINE);
-			return 0;
-		}
 	}
 
 	/*
@@ -1451,10 +1445,6 @@ static int ext4_write_end(const struct kiocb *iocb,
 
 	trace_ext4_write_end(inode, pos, len, copied);
 
-	if ((unsigned long)fsdata & EXT4_WRITE_DATA_INLINE)
-		return ext4_write_inline_data_end(inode, pos, len, copied,
-						  folio);
-
 	copied = block_write_end(pos, len, copied, folio);
 	/*
 	 * it's important to update i_size while still holding folio lock:
@@ -1560,10 +1550,6 @@ static int ext4_journalled_write_end(const struct kiocb *iocb,
 
 	BUG_ON(!ext4_handle_valid(handle));
 
-	if ((unsigned long)fsdata & EXT4_WRITE_DATA_INLINE)
-		return ext4_write_inline_data_end(inode, pos, len, copied,
-						  folio);
-
 	if (unlikely(copied < len) && !folio_test_uptodate(folio)) {
 		copied = 0;
 		ext4_journalled_zero_new_buffers(handle, inode, folio,
@@ -3172,10 +3158,6 @@ static int ext4_da_write_begin(const struct kiocb *iocb,
 						     foliop, true);
 		if (ret < 0)
 			return ret;
-		if (ret == 1) {
-			*fsdata = (void *)((unsigned long)*fsdata | EXT4_WRITE_DATA_INLINE);
-			return 0;
-		}
 	}
 
 retry:
@@ -3312,10 +3294,6 @@ static int ext4_da_write_end(const struct kiocb *iocb,
 
 	trace_ext4_da_write_end(inode, pos, len, copied);
 
-	if (write_mode & EXT4_WRITE_DATA_INLINE)
-		return ext4_write_inline_data_end(inode, pos, len, copied,
-						  folio);
-
 	if (unlikely(copied < len) && !folio_test_uptodate(folio))
 		copied = 0;
 
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [RFC PATCH 6/9] ext4: allocate block before destroying inline data in conversion
  2026-07-27 10:54 [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Yun Zhou
                   ` (4 preceding siblings ...)
  2026-07-27 10:54 ` [RFC PATCH 5/9] ext4: remove dead inline data write code Yun Zhou
@ 2026-07-27 10:54 ` Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 7/9] ext4: remove DA convert path for regular file inline data Yun Zhou
                   ` (3 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Yun Zhou @ 2026-07-27 10:54 UTC (permalink / raw)
  To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
	yi.zhang
  Cc: linux-ext4, linux-kernel, yun.zhou

Rework ext4_convert_inline_data_nolock() to allocate and write the
data block before destroying inline data.  Previously, the function
destroyed inline data first, then tried to allocate a block.  If
allocation failed, it attempted to restore the inline data -- which
could itself fail, leading to data loss or BUG_ON from inconsistent
inode state.

The new approach:
1. Read inline data into a buffer
2. Allocate a physical block (ext4_new_meta_blocks)
3. Write the data to the allocated block
4. Only after success: destroy inline data
5. Insert the pre-allocated block into the extent tree

On failure before destroy, the inline data is untouched and we simply
free the allocated block.  No restore needed.

Remove now-dead code: ext4_restore_inline_data(), ext4_write_inline_data().

Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
 fs/ext4/inline.c | 136 ++++++++++++++++-------------------------------
 1 file changed, 47 insertions(+), 89 deletions(-)

diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 5c6202add3d8..ebbb9b29e3e1 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -12,6 +12,7 @@
 
 #include "ext4_jbd2.h"
 #include "ext4.h"
+#include "ext4_extents.h"
 #include "xattr.h"
 #include "truncate.h"
 
@@ -218,51 +219,6 @@ static int ext4_read_inline_data(struct inode *inode, void *buffer,
 	return cp_len;
 }
 
-/*
- * write the buffer to the inline inode.
- * If 'create' is set, we don't need to do the extra copy in the xattr
- * value since it is already handled by ext4_xattr_ibody_set.
- * That saves us one memcpy.
- */
-static void ext4_write_inline_data(struct inode *inode, struct ext4_iloc *iloc,
-				   void *buffer, loff_t pos, unsigned int len)
-{
-	struct ext4_xattr_entry *entry;
-	struct ext4_xattr_ibody_header *header;
-	struct ext4_inode *raw_inode;
-	int cp_len = 0;
-
-	if (unlikely(ext4_emergency_state(inode->i_sb)))
-		return;
-
-	BUG_ON(!EXT4_I(inode)->i_inline_off);
-	BUG_ON(pos + len > EXT4_I(inode)->i_inline_size);
-
-	raw_inode = ext4_raw_inode(iloc);
-	buffer += pos;
-
-	if (pos < EXT4_MIN_INLINE_DATA_SIZE) {
-		cp_len = pos + len > EXT4_MIN_INLINE_DATA_SIZE ?
-			 EXT4_MIN_INLINE_DATA_SIZE - pos : len;
-		memcpy((void *)raw_inode->i_block + pos, buffer, cp_len);
-
-		len -= cp_len;
-		buffer += cp_len;
-		pos += cp_len;
-	}
-
-	if (!len)
-		return;
-
-	pos -= EXT4_MIN_INLINE_DATA_SIZE;
-	header = IHDR(inode, raw_inode);
-	entry = (struct ext4_xattr_entry *)((void *)raw_inode +
-					    EXT4_I(inode)->i_inline_off);
-
-	memcpy((void *)IFIRST(header) + le16_to_cpu(entry->e_value_offs) + pos,
-	       buffer, len);
-}
-
 static int ext4_create_inline_data(handle_t *handle,
 				   struct inode *inode, unsigned len)
 {
@@ -791,23 +747,6 @@ static int ext4_update_inline_dir(handle_t *handle, struct inode *dir,
 	return 0;
 }
 
-static void ext4_restore_inline_data(handle_t *handle, struct inode *inode,
-				     struct ext4_iloc *iloc,
-				     void *buf, int inline_size)
-{
-	int ret;
-
-	ret = ext4_create_inline_data(handle, inode, inline_size);
-	if (ret) {
-		ext4_msg(inode->i_sb, KERN_EMERG,
-			"error restoring inline_data for inode -- potential data loss! (inode %llu, error %d)",
-			inode->i_ino, ret);
-		return;
-	}
-	ext4_write_inline_data(inode, iloc, buf, 0, inline_size);
-	ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
-}
-
 static int ext4_convert_inline_data_nolock(handle_t *handle,
 					   struct inode *inode,
 					   struct ext4_iloc *iloc)
@@ -815,7 +754,7 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
 	int error;
 	void *buf = NULL;
 	struct buffer_head *data_bh = NULL;
-	struct ext4_map_blocks map;
+	ext4_fsblk_t pblk;
 	int inline_size;
 
 	inline_size = ext4_get_inline_size(inode);
@@ -841,25 +780,20 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
 			goto out;
 	}
 
-	error = ext4_destroy_inline_data_nolock(handle, inode);
+	/*
+	 * Allocate a data block and write the inline data to it before
+	 * destroying the inline data.  This ensures that on failure we
+	 * can simply free the allocated block without needing to restore
+	 * the inline data.
+	 */
+	pblk = ext4_new_meta_blocks(handle, inode, 0, 0, NULL, &error);
 	if (error)
 		goto out;
 
-	map.m_lblk = 0;
-	map.m_len = 1;
-	map.m_flags = 0;
-	error = ext4_map_blocks(handle, inode, &map, EXT4_GET_BLOCKS_CREATE);
-	if (error < 0)
-		goto out_restore;
-	if (!(map.m_flags & EXT4_MAP_MAPPED)) {
-		error = -EIO;
-		goto out_restore;
-	}
-
-	data_bh = sb_getblk(inode->i_sb, map.m_pblk);
+	data_bh = sb_getblk(inode->i_sb, pblk);
 	if (!data_bh) {
 		error = -ENOMEM;
-		goto out_restore;
+		goto out_free_block;
 	}
 
 	lock_buffer(data_bh);
@@ -867,8 +801,7 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
 					       EXT4_JTR_NONE);
 	if (error) {
 		unlock_buffer(data_bh);
-		error = -EIO;
-		goto out_restore;
+		goto out_free_block;
 	}
 	memset(data_bh->b_data, 0, inode->i_sb->s_blocksize);
 
@@ -876,26 +809,51 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
 		memcpy(data_bh->b_data, buf, inline_size);
 		set_buffer_uptodate(data_bh);
 		unlock_buffer(data_bh);
-		error = ext4_handle_dirty_metadata(handle,
-						   inode, data_bh);
+		error = ext4_handle_dirty_metadata(handle, inode, data_bh);
 	} else {
 		unlock_buffer(data_bh);
-		inode->i_size = inode->i_sb->s_blocksize;
-		i_size_write(inode, inode->i_sb->s_blocksize);
-		EXT4_I(inode)->i_disksize = inode->i_sb->s_blocksize;
-
 		error = ext4_init_dirblock(handle, inode, data_bh,
 			  le32_to_cpu(((struct ext4_dir_entry_2 *)buf)->inode),
 			  buf + EXT4_INLINE_DOTDOT_SIZE,
 			  inline_size - EXT4_INLINE_DOTDOT_SIZE);
-		if (!error)
-			error = ext4_mark_inode_dirty(handle, inode);
 	}
+	if (error)
+		goto out_free_block;
 
-out_restore:
+	/*
+	 * Data is safely in the allocated block.  Now destroy the inline
+	 * data (which also initializes the extent tree via
+	 * ext4_ext_tree_init) and then insert the pre-allocated block.
+	 */
+	error = ext4_destroy_inline_data_nolock(handle, inode);
 	if (error)
-		ext4_restore_inline_data(handle, inode, iloc, buf, inline_size);
+		goto out_free_block;
+
+	if (S_ISDIR(inode->i_mode)) {
+		inode->i_size = inode->i_sb->s_blocksize;
+		i_size_write(inode, inode->i_sb->s_blocksize);
+		EXT4_I(inode)->i_disksize = inode->i_sb->s_blocksize;
+	}
+
+	/* Insert the pre-allocated block into the extent tree */
+	if (ext4_has_feature_extents(inode->i_sb)) {
+		struct ext4_extent_header *eh = ext_inode_hdr(inode);
+		struct ext4_extent *ex = EXT_FIRST_EXTENT(eh);
+
+		ex->ee_block = cpu_to_le32(0);
+		ex->ee_len = cpu_to_le16(1);
+		ext4_ext_store_pblock(ex, pblk);
+		eh->eh_entries = cpu_to_le16(1);
+	} else {
+		/* indirect mapping: set i_data[0] directly */
+		EXT4_I(inode)->i_data[0] = cpu_to_le32(pblk);
+	}
+
+	error = ext4_mark_inode_dirty(handle, inode);
+	goto out;
 
+out_free_block:
+	ext4_free_blocks(handle, inode, NULL, pblk, 1, 0);
 out:
 	brelse(data_bh);
 	kfree(buf);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [RFC PATCH 7/9] ext4: remove DA convert path for regular file inline data
  2026-07-27 10:54 [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Yun Zhou
                   ` (5 preceding siblings ...)
  2026-07-27 10:54 ` [RFC PATCH 6/9] ext4: allocate block before destroying inline data in conversion Yun Zhou
@ 2026-07-27 10:54 ` Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 8/9] ext4: document inline_data deprecation Yun Zhou
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Yun Zhou @ 2026-07-27 10:54 UTC (permalink / raw)
  To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
	yi.zhang
  Cc: linux-ext4, linux-kernel, yun.zhou

Replace ext4_da_convert_inline_data_to_extent() with
ext4_convert_inline_data() in the DA write path.

The DA convert function left a problematic intermediate state
(has_inline_data=true, MAY_INLINE_DATA=false) that caused races with
concurrent page_mkwrite: the racing thread would call filemap_flush()
which triggers writepages, but writepages rejects inodes with
has_inline_data set, leading to BUG_ON or WARN_ON.

Since the synchronous convert (ext4_convert_inline_data_nolock) only
allocates one block, the delayed allocation benefit (block contiguity)
is negligible for this single-block case.

With DA convert eliminated for regular files, the !MAY_INLINE_DATA
branch in ext4_convert_inline_data() is dead code and removed.

Remove ext4_da_convert_inline_data_to_extent().

Fixes: 7b4cc9787fe3 ("ext4: evict inline data when writing to memory map")
Reported-by: syzbot+d1da16f03614058fdc48@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=d1da16f03614058fdc48
Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
 fs/ext4/inline.c | 86 +-----------------------------------------------
 1 file changed, 1 insertion(+), 85 deletions(-)

diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index ebbb9b29e3e1..009a4e058793 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -21,10 +21,6 @@
 #define EXT4_INLINE_DOTDOT_OFFSET	2
 #define EXT4_INLINE_DOTDOT_SIZE		4
 
-
-static int ext4_da_convert_inline_data_to_extent(struct address_space *mapping,
-						 struct inode *inode);
-
 static int ext4_get_inline_size(struct inode *inode)
 {
 	if (EXT4_I(inode)->i_inline_off)
@@ -538,76 +534,8 @@ int ext4_generic_write_inline_data(struct address_space *mapping,
 					  struct folio **foliop,
 					  bool da)
 {
-	int ret;
-	int retries = 0;
-
 	/* Inline data is deprecated: always convert to block format */
-	if (!da)
-		return ext4_convert_inline_data(inode);
-
-retry:
-	ret = ext4_da_convert_inline_data_to_extent(mapping, inode);
-	if (ret == -ENOSPC && ext4_should_retry_alloc(inode->i_sb, &retries))
-		goto retry;
-	return ret;
-}
-
-/*
- * Try to make the page cache and handle ready for the inline data case.
- * We can call this function in 2 cases:
- * 1. The inode is created and the first write exceeds inline size. We can
- *    clear the inode state safely.
- * 2. The inode has inline data, then we need to read the data, make it
- *    update and dirty so that ext4_da_writepages can handle it. We don't
- *    need to start the journal since the file's metadata isn't changed now.
- */
-static int ext4_da_convert_inline_data_to_extent(struct address_space *mapping,
-						 struct inode *inode)
-{
-	int ret = 0, inline_size;
-	struct folio *folio;
-
-	folio = __filemap_get_folio(mapping, 0, FGP_WRITEBEGIN,
-					mapping_gfp_mask(mapping));
-	if (IS_ERR(folio))
-		return PTR_ERR(folio);
-
-	down_read(&EXT4_I(inode)->xattr_sem);
-	if (!ext4_has_inline_data(inode)) {
-		ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
-		goto out;
-	}
-
-	inline_size = ext4_get_inline_size(inode);
-
-	if (!folio_test_uptodate(folio)) {
-		ret = ext4_read_inline_folio(inode, folio);
-		if (ret < 0)
-			goto out;
-	}
-
-	ret = ext4_block_write_begin(NULL, folio, 0, inline_size,
-				     ext4_da_get_block_prep);
-	if (ret) {
-		up_read(&EXT4_I(inode)->xattr_sem);
-		folio_unlock(folio);
-		folio_put(folio);
-		ext4_truncate_failed_write(inode);
-		return ret;
-	}
-
-	clear_buffer_new(folio_buffers(folio));
-	folio_mark_dirty(folio);
-	folio_mark_uptodate(folio);
-	ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
-
-out:
-	up_read(&EXT4_I(inode)->xattr_sem);
-	if (folio) {
-		folio_unlock(folio);
-		folio_put(folio);
-	}
-	return ret;
+	return ext4_convert_inline_data(inode);
 }
 
 #ifdef INLINE_DIR_DEBUG
@@ -1650,18 +1578,6 @@ int ext4_convert_inline_data(struct inode *inode)
 	if (!ext4_has_inline_data(inode)) {
 		ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
 		return 0;
-	} else if (!ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA)) {
-		/*
-		 * Inode has inline data but EXT4_STATE_MAY_INLINE_DATA is
-		 * cleared. This means we are in the middle of moving of
-		 * inline data to delay allocated block. Just force writeout
-		 * here to finish conversion.
-		 */
-		error = filemap_flush(inode->i_mapping);
-		if (error)
-			return error;
-		if (!ext4_has_inline_data(inode))
-			return 0;
 	}
 
 	needed_blocks = ext4_chunk_trans_extent(inode, 1);
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [RFC PATCH 8/9] ext4: document inline_data deprecation
  2026-07-27 10:54 [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Yun Zhou
                   ` (6 preceding siblings ...)
  2026-07-27 10:54 ` [RFC PATCH 7/9] ext4: remove DA convert path for regular file inline data Yun Zhou
@ 2026-07-27 10:54 ` Yun Zhou
  2026-07-27 10:54 ` [RFC PATCH 9/9] ext4: populate extent entry atomically during inline data destroy Yun Zhou
  2026-07-27 15:03 ` [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Theodore Tso
  9 siblings, 0 replies; 11+ messages in thread
From: Yun Zhou @ 2026-07-27 10:54 UTC (permalink / raw)
  To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
	yi.zhang
  Cc: linux-ext4, linux-kernel, yun.zhou

Add a deprecation warning to the inline data documentation, informing
users that the feature is deprecated and will be removed in a future
release.

Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
 Documentation/filesystems/ext4/inlinedata.rst | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/Documentation/filesystems/ext4/inlinedata.rst b/Documentation/filesystems/ext4/inlinedata.rst
index a728af0d2fd0..8dc1b6d75388 100644
--- a/Documentation/filesystems/ext4/inlinedata.rst
+++ b/Documentation/filesystems/ext4/inlinedata.rst
@@ -3,6 +3,14 @@
 Inline Data
 -----------
 
+.. warning::
+
+   The inline data feature is deprecated and will be removed in a future
+   kernel release.  New regular file inodes no longer store data inline,
+   and writes to existing inline data inodes trigger automatic conversion
+   to block-based storage.  Use ``tune2fs -O ^inline_data`` to convert
+   existing filesystems.
+
 The inline data feature was designed to handle the case that a file's
 data is so tiny that it readily fits inside the inode, which
 (theoretically) reduces disk block consumption and reduces seeks. If the
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [RFC PATCH 9/9] ext4: populate extent entry atomically during inline data destroy
  2026-07-27 10:54 [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Yun Zhou
                   ` (7 preceding siblings ...)
  2026-07-27 10:54 ` [RFC PATCH 8/9] ext4: document inline_data deprecation Yun Zhou
@ 2026-07-27 10:54 ` Yun Zhou
  2026-07-27 15:03 ` [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Theodore Tso
  9 siblings, 0 replies; 11+ messages in thread
From: Yun Zhou @ 2026-07-27 10:54 UTC (permalink / raw)
  To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
	yi.zhang
  Cc: linux-ext4, linux-kernel, yun.zhou

Pass the pre-allocated block number to ext4_destroy_inline_data_nolock()
so that the extent entry can be populated under i_data_sem, in the same
critical section where the extent tree is initialized.

Previously, ext4_convert_inline_data_nolock() wrote the extent entry
after ext4_destroy_inline_data_nolock() returned (and released
i_data_sem).  This left a window where concurrent readers could see
an empty extent tree via i_data_sem read lock.

Now the entire sequence -- clear i_block, init extent tree, populate
first extent entry -- happens atomically under i_data_sem write lock.
Other callers pass 0 to preserve existing behavior.

Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
 fs/ext4/inline.c | 40 +++++++++++++++++++++-------------------
 1 file changed, 21 insertions(+), 19 deletions(-)

diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 009a4e058793..e0b80273e322 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -385,7 +385,8 @@ static int ext4_prepare_inline_data(handle_t *handle, struct inode *inode,
 }
 
 static int ext4_destroy_inline_data_nolock(handle_t *handle,
-					   struct inode *inode)
+					   struct inode *inode,
+					   ext4_fsblk_t pblk)
 {
 	struct ext4_inode_info *ei = EXT4_I(inode);
 	struct ext4_xattr_ibody_find is = {
@@ -433,7 +434,21 @@ static int ext4_destroy_inline_data_nolock(handle_t *handle,
 		    S_ISREG(inode->i_mode) || S_ISLNK(inode->i_mode)) {
 			ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
 			ext4_ext_tree_init(handle, inode);
+			if (pblk) {
+				struct ext4_extent_header *eh;
+				struct ext4_extent *ex;
+
+				eh = ext_inode_hdr(inode);
+				ex = EXT_FIRST_EXTENT(eh);
+				ex->ee_block = cpu_to_le32(0);
+				ex->ee_len = cpu_to_le16(1);
+				ext4_ext_store_pblock(ex, pblk);
+				eh->eh_entries = cpu_to_le16(1);
+			}
 		}
+	} else if (pblk) {
+		/* indirect mapping: set i_data[0] directly */
+		EXT4_I(inode)->i_data[0] = cpu_to_le32(pblk);
 	}
 	ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
 
@@ -750,10 +765,11 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
 
 	/*
 	 * Data is safely in the allocated block.  Now destroy the inline
-	 * data (which also initializes the extent tree via
-	 * ext4_ext_tree_init) and then insert the pre-allocated block.
+	 * data and populate the extent entry atomically under i_data_sem
+	 * (inside ext4_destroy_inline_data_nolock).  This ensures no
+	 * concurrent reader sees an empty extent tree.
 	 */
-	error = ext4_destroy_inline_data_nolock(handle, inode);
+	error = ext4_destroy_inline_data_nolock(handle, inode, pblk);
 	if (error)
 		goto out_free_block;
 
@@ -763,20 +779,6 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
 		EXT4_I(inode)->i_disksize = inode->i_sb->s_blocksize;
 	}
 
-	/* Insert the pre-allocated block into the extent tree */
-	if (ext4_has_feature_extents(inode->i_sb)) {
-		struct ext4_extent_header *eh = ext_inode_hdr(inode);
-		struct ext4_extent *ex = EXT_FIRST_EXTENT(eh);
-
-		ex->ee_block = cpu_to_le32(0);
-		ex->ee_len = cpu_to_le16(1);
-		ext4_ext_store_pblock(ex, pblk);
-		eh->eh_entries = cpu_to_le16(1);
-	} else {
-		/* indirect mapping: set i_data[0] directly */
-		EXT4_I(inode)->i_data[0] = cpu_to_le32(pblk);
-	}
-
 	error = ext4_mark_inode_dirty(handle, inode);
 	goto out;
 
@@ -1419,7 +1421,7 @@ int ext4_destroy_inline_data(handle_t *handle, struct inode *inode)
 	int ret, no_expand;
 
 	ext4_write_lock_xattr(inode, &no_expand);
-	ret = ext4_destroy_inline_data_nolock(handle, inode);
+	ret = ext4_destroy_inline_data_nolock(handle, inode, 0);
 	ext4_write_unlock_xattr(inode, &no_expand);
 
 	return ret;
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files
  2026-07-27 10:54 [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Yun Zhou
                   ` (8 preceding siblings ...)
  2026-07-27 10:54 ` [RFC PATCH 9/9] ext4: populate extent entry atomically during inline data destroy Yun Zhou
@ 2026-07-27 15:03 ` Theodore Tso
  9 siblings, 0 replies; 11+ messages in thread
From: Theodore Tso @ 2026-07-27 15:03 UTC (permalink / raw)
  To: Yun Zhou
  Cc: adilger.kernel, libaokun, jack, ojaswin, ritesh.list, yi.zhang,
	linux-ext4, linux-kernel

On Mon, Jul 27, 2026 at 06:54:32PM -0500, Yun Zhou wrote:
> 
> The inline_data feature has been a persistent source of bugs in
> ext4 over the past three years...
> 
> The root cause is architectural: inline data requires special handling
> in write_begin/write_end, writepages, page_mkwrite, truncate, and
> directory operations, with complex locking interactions between
> xattr_sem, i_rwsem, i_data_sem, and page locks.
> 
> The benefit is minimal: inline data saves at most one 4K block per
> small file.  In practice, very few regular files are small enough to
> benefit from inline storage -- mostly only small directories gain from
> it.  Major distributions do not enable inline_data by default in
> mkfs.ext4.

I agree that inline_data feature has been a persistent source of bugs,
and is relatively minimal for data files.  Howvever, the regular files
that are small enough to support inline data come really boring filess
where we're not doing the really stressful things that fsx, fstress,
syzbot, etc. do to punish the file system.

I do think there will be some systems where there might be enough
small files (especially when the blocksize is 64k, and the inode size
is larger --- perhaps 4k) where it might be worthwhile to support the
"boring" inline data files, e.g., small config files, header files
etc., where the file is written once, and closed, and never modified
again.

So what if we had a mechanism where we're using delayed allocation, if
the are no open file descriptor, so races and deadlocks can be
avoided, and in that case, we can write it as an inline regular data
file.  Otherwise, we back it using a regular block.  I think this
would get us 95% of the benefits of inline data files, with much less
of the complexity cost.

What do you think of that approach?

					- Ted

^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-07-27 15:06 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-27 10:54 [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Yun Zhou
2026-07-27 10:54 ` [RFC PATCH 1/9] ext4: add deprecation warning for inline_data feature Yun Zhou
2026-07-27 10:54 ` [RFC PATCH 2/9] ext4: stop creating inline data for new regular files Yun Zhou
2026-07-27 10:54 ` [RFC PATCH 3/9] ext4: use safe convert path for inline data write overflow Yun Zhou
2026-07-27 10:54 ` [RFC PATCH 4/9] ext4: remove inline data write paths for regular files Yun Zhou
2026-07-27 10:54 ` [RFC PATCH 5/9] ext4: remove dead inline data write code Yun Zhou
2026-07-27 10:54 ` [RFC PATCH 6/9] ext4: allocate block before destroying inline data in conversion Yun Zhou
2026-07-27 10:54 ` [RFC PATCH 7/9] ext4: remove DA convert path for regular file inline data Yun Zhou
2026-07-27 10:54 ` [RFC PATCH 8/9] ext4: document inline_data deprecation Yun Zhou
2026-07-27 10:54 ` [RFC PATCH 9/9] ext4: populate extent entry atomically during inline data destroy Yun Zhou
2026-07-27 15:03 ` [RFC PATCH 0/9] ext4: phase out inline data write paths for regular files Theodore Tso

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.