[f2fs-dev] [RFC PATCH v4 1/3] f2fs: introduce inline extent mapping for inode data blocks

Yongpeng Yang <[email protected]> Thu, 6 Aug 2026 21:19:02 +0800
Newsgroups net.sourceforge.lists.linux-f2fs-devel
Message-ID <[email protected]>
From: Yongpeng Yang <[email protected]>

Introduce an inline extent mapping mechanism that stores extent entries
directly within the inode's extra attribute area. This eliminates
indirect node lookups for frequently accessed file blocks.

Key design points:
- Inline extent area is placed after i_extra_end in the on-disk inode,
  within the i_extra_isize region. Its size is determined by the new
  field i_inline_ext_capacity (number of extent entries).
- Split the existing __le64 i_compr_blocks into __le32 i_compr_blocks +
  __le32 i_inline_ext_capacity to avoid enlarging the inode.
- Inline extent only caches mappings for indirect blocks (fofs >=
  ADDRS_PER_INODE). Direct block mappings continue to use i_addr[].
- Per-inode capacity is determined at file creation time based on
  extension matching rules.
- Supports a mount option inline_extent_size=<N> to set the default
  capacity for new inodes.
- Mutually exclusive with compression (compressed files cannot use
  inline extent).

The implementation includes:
- Binary search based extent lookup and insertion with merge/split
  support in fs/f2fs/iextent.c
- Integration with data block allocation and truncation paths
- Proper recovery handling (inline extent cleared during fsync replay)
- Kconfig option CONFIG_F2FS_INLINE_EXTENT to enable the feature

Test setup (Xiaomi smartphone, UFS 4.0 storage, f2fs):

  echo 1 > /sys/fs/f2fs/<dev>/inline_extent_enable
  echo 'mp4:256' > /sys/fs/f2fs/<dev>/inline_extent_extension_list
  fio --name=test --filename=data.mp4 --rw=write:4k --bs=64M \
      --size=8G --ioengine=libaio --direct=1
  sync
  fio --name=test --filename=data.mp4 --rw=write --bs=64M \
      --size=8G --ioengine=libaio --direct=1
  sync
  echo 3 > /proc/sys/vm/drop_caches
  fio --name=buffer-read --ioengine=libaio --rw=randread --bs=$BS \
      --size=8G --io_size=1G --numjobs=1 --filename=data.mp4

Results (random read bandwidth, MiB/s):
+---------------------------------------------------+
| BS     | baseline | inline ext | improvement      |
|--------+----------+------------+------------------|
| 4K     | 31.6     | 32.4       | +2.5%            |
| 8K     | 55.4     | 58.5       | +5.6%            |
| 32K    | 155.3    | 166.8      | +7.4%            |
| 64K    | 229.8    | 255.3      | +11.1%           |
| 128K   | 337.8    | 388        | +14.9%           |
+---------------------------------------------------+

Signed-off-by: Yongpeng Yang <[email protected]>
---
v4:
- Do not cache NEW_ADDR in the inline extent area.
- Simplify the insert path.
- Remove the inline-extent code-coverage statistics.
v3:
- Modify inline extent format.
- Split struct f2fs_inode->i_compr_blocks (was __le64) into __le32 and
__le32 i_inline_ext_capacity.
- Re-ran the performance tests.
v2:
- Bypass inline extent lookup for F2FS_GET_BLOCK_PRECACHE.
- Unify fofs range check to "fofs >= direct_blocks".
- Remove NULL_ADDR caching support; simplify merge/split logic.
- Change f2fs_iext_convert_to_inline_extent return type to bool.
- Rename __is_extent_mergeable to __is_iextent_mergeable.
- Remove inode parameter from f2fs_iext_sanity_check.
- Reduce #ifdef nesting in node.c.
- Add complete benchmark data (4K/8K/32K/64K).
- Code style fixes.
---
 fs/f2fs/Kconfig         |  18 ++
 fs/f2fs/Makefile        |   1 +
 fs/f2fs/data.c          | 146 ++++++++++-
 fs/f2fs/dir.c           |   1 +
 fs/f2fs/f2fs.h          |  44 +++-
 fs/f2fs/file.c          |   1 +
 fs/f2fs/iextent.c       | 528 ++++++++++++++++++++++++++++++++++++++++
 fs/f2fs/iextent.h       | 136 +++++++++++
 fs/f2fs/inline.c        |   1 +
 fs/f2fs/inode.c         |  41 +++-
 fs/f2fs/namei.c         |  67 +++++
 fs/f2fs/node.c          |  38 ++-
 fs/f2fs/node.h          |   4 +
 fs/f2fs/recovery.c      |  15 ++
 fs/f2fs/super.c         |  29 +++
 include/linux/f2fs_fs.h |   3 +-
 16 files changed, 1052 insertions(+), 21 deletions(-)
 create mode 100644 fs/f2fs/iextent.c
 create mode 100644 fs/f2fs/iextent.h

diff --git a/fs/f2fs/Kconfig b/fs/f2fs/Kconfig
index 5916a02fb46d..4a5d900090ee 100644
--- a/fs/f2fs/Kconfig
+++ b/fs/f2fs/Kconfig
@@ -150,3 +150,21 @@ config F2FS_UNFAIR_RWSEM
 	help
 	  Use unfair rw_semaphore, if system configured IO priority by block
 	  cgroup.
+
+config F2FS_INLINE_EXTENT
+	bool "F2FS inline extent"
+	depends on F2FS_FS
+	default y
+	help
+	  Support the inline extent feature: leverage the inode's data block
+	  address area to store extent-format mapping relationships, replacing
+	  individual block addresses with compact extent entries to optimize
+	  large file random reads.
+
+config F2FS_INLINE_EXTENT_DEBUG
+	bool "F2FS inline extent debug"
+	depends on F2FS_INLINE_EXTENT
+	default n
+	help
+	  Support inline extent debug to stat code coverage and extents
+	  consistency check.
diff --git a/fs/f2fs/Makefile b/fs/f2fs/Makefile
index 8a7322d229e4..ed75c0b71a93 100644
--- a/fs/f2fs/Makefile
+++ b/fs/f2fs/Makefile
@@ -4,6 +4,7 @@ obj-$(CONFIG_F2FS_FS) += f2fs.o
 f2fs-y		:= dir.o file.o inode.o namei.o hash.o super.o inline.o
 f2fs-y		+= checkpoint.o gc.o data.o node.o segment.o recovery.o
 f2fs-y		+= shrinker.o extent_cache.o sysfs.o
+f2fs-$(CONFIG_F2FS_INLINE_EXTENT) += iextent.o
 f2fs-$(CONFIG_F2FS_STAT_FS) += debug.o
 f2fs-$(CONFIG_F2FS_FS_XATTR) += xattr.o
 f2fs-$(CONFIG_F2FS_FS_POSIX_ACL) += acl.o
diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c
index 042ed8ad9cc3..5be05792ee24 100644
--- a/fs/f2fs/data.c
+++ b/fs/f2fs/data.c
@@ -26,6 +26,7 @@
 #include "node.h"
 #include "segment.h"
 #include "iostat.h"
+#include "iextent.h"
 #include <trace/events/f2fs.h>
 
 #define NUM_PREALLOC_POST_READ_CTXS	128
@@ -1212,11 +1213,55 @@ static void f2fs_submit_page_read(struct inode *inode, struct fsverity_info *vi,
 	f2fs_submit_read_bio(sbi, bio, DATA);
 }
 
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+static void __set_iext_data_blkaddr(struct dnode_of_data *dn, block_t blkaddr)
+{
+	block_t fofs = f2fs_start_bidx_of_node(ofs_of_node(dn->node_folio),
+						dn->inode) + dn->ofs_in_node;
+	int ret;
+
+	/*
+	 * Don't cache extent during recovery, be consistent with largest
+	 * extent.
+	 */
+	if (unlikely(is_sbi_flag_set(F2FS_I_SB(dn->inode), SBI_POR_DOING))) {
+		f2fs_bug_on(F2FS_I_SB(dn->inode),
+			EXT_ENTRY_COUNT(iext_get_header(dn->inode_folio)));
+		return;
+	}
+
+	f2fs_folio_wait_writeback(dn->inode_folio, NODE, true, true);
+	ret = f2fs_iext_update_data_blkaddr(dn->inode, dn->inode_folio, fofs,
+						blkaddr);
+	switch (ret) {
+	case F2FS_IEXT_INSERT_REMOVED:
+	case F2FS_IEXT_INSERT_NORMAL:
+		if (folio_mark_dirty(dn->inode_folio))
+			dn->node_changed = true;
+		break;
+	case F2FS_IEXT_INSERT_DROP:
+		break;
+	default:
+		f2fs_bug_on(F2FS_I_SB(dn->inode), 1);
+	}
+}
+#endif
+
 static void __set_data_blkaddr(struct dnode_of_data *dn, block_t blkaddr)
 {
 	__le32 *addr = get_dnode_addr(dn->inode, dn->node_folio);
 
 	dn->data_blkaddr = blkaddr;
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+	if (f2fs_iext_support_inline_extent(dn->inode, dn->inode_folio)
+			&& dn->inode_folio != dn->node_folio) {
+		f2fs_bug_on(F2FS_I_SB(dn->inode),
+				!dn->inode_folio_locked);
+		f2fs_bug_on(F2FS_I_SB(dn->inode),
+				!folio_test_locked(dn->inode_folio));
+		__set_iext_data_blkaddr(dn, blkaddr);
+	}
+#endif
 	addr[dn->ofs_in_node] = cpu_to_le32(dn->data_blkaddr);
 }
 
@@ -1655,6 +1700,60 @@ static bool map_is_mergeable(struct f2fs_sb_info *sbi,
 	return false;
 }
 
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+static bool f2fs_iext_map_blocks(struct inode *inode,
+		struct f2fs_map_blocks *map, int flag)
+{
+	struct f2fs_sb_info *sbi = F2FS_I_SB(inode);
+	unsigned int maxblocks = map->m_len;
+	pgoff_t pgoff = (pgoff_t)map->m_lblk;
+	block_t blkaddr;
+	struct folio *ifolio;
+	unsigned int len;
+	int ret;
+
+	if (f2fs_compressed_file(inode))
+		return false;
+
+	ifolio = f2fs_get_inode_folio(sbi, inode->i_ino);
+	if (IS_ERR(ifolio))
+		return false;
+	if (!f2fs_iext_support_inline_extent(inode, ifolio) ||
+			pgoff < ADDRS_PER_INODE(inode)) {
+		f2fs_folio_put(ifolio, true);
+		return false;
+	}
+	ret = f2fs_iext_lookup_blkaddr(inode, ifolio, pgoff, &blkaddr, &len);
+	f2fs_folio_put(ifolio, true);
+	if (ret)
+		return false;
+
+	map->m_pblk = blkaddr;
+	map->m_len = min_t(unsigned int, maxblocks, len);
+	map->m_flags = F2FS_MAP_MAPPED;
+	if (map->m_next_extent)
+		*map->m_next_extent = pgoff + map->m_len;
+
+	/* for hardware encryption, but to avoid potential issue in future */
+	if (flag == F2FS_GET_BLOCK_DIO)
+		f2fs_wait_on_block_writeback_range(inode,
+					map->m_pblk, map->m_len);
+
+	map->m_multidev_dio = f2fs_allow_multi_device_dio(sbi, flag);
+	if (map->m_multidev_dio) {
+		int bidx = f2fs_target_device_index(sbi, map->m_pblk);
+		struct f2fs_dev_info *dev = &sbi->devs[bidx];
+
+		map->m_bdev = dev->bdev;
+		map->m_len = min(map->m_len, dev->end_blk + 1 - map->m_pblk);
+		map->m_pblk -= dev->start_blk;
+	} else {
+		map->m_bdev = inode->i_sb->s_bdev;
+	}
+	return true;
+}
+#endif
+
 /*
  * f2fs_map_blocks() tries to find or build mapping relationship which
  * maps continuous logical blocks to physical blocks, and return such
@@ -1704,6 +1803,23 @@ int f2fs_map_blocks(struct inode *inode, struct f2fs_map_blocks *map, int flag)
 		goto map_more;
 	}
 
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+	/*
+	 * Precache need to load all mapping to read extent cache, so we need
+	 * to bypass inline extent.
+	 */
+	if (!map->m_may_create && flag != F2FS_GET_BLOCK_PRECACHE &&
+			f2fs_iext_map_blocks(inode, map, flag)) {
+		if (map->m_len == maxblocks ||
+			map->m_multidev_dio ||
+			flag != F2FS_GET_BLOCK_FIEMAP)
+			goto out;
+		pgofs =	(pgoff_t)map->m_lblk + map->m_len;
+		ofs = map->m_len;
+		goto map_more;
+	}
+#endif
+
 	map->m_bdev = inode->i_sb->s_bdev;
 	map->m_multidev_dio =
 		f2fs_allow_multi_device_dio(F2FS_I_SB(inode), flag);
@@ -3716,6 +3832,32 @@ void f2fs_write_failed(struct inode *inode, loff_t to)
 	}
 }
 
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+static bool f2fs_iext_get_data_blkaddr(struct inode *inode,
+			struct folio *ifolio, pgoff_t index, block_t *blkaddr)
+{
+	int ret;
+
+	if (f2fs_compressed_file(inode))
+		return false;
+	if (!f2fs_iext_support_inline_extent(inode, ifolio) ||
+			index < ADDRS_PER_INODE(inode))
+		return false;
+
+	ret = f2fs_iext_lookup_blkaddr(inode, ifolio, index, blkaddr, NULL);
+	if (ret)
+		return false;
+
+	return true;
+}
+#else
+static bool f2fs_iext_get_data_blkaddr(struct inode *inode,
+			struct folio *ifolio, pgoff_t index, block_t *blkaddr)
+{
+	return false;
+}
+#endif
+
 static int prepare_write_begin(struct f2fs_sb_info *sbi,
 			struct folio *folio, loff_t pos, unsigned int len,
 			block_t *blk_addr, bool *node_changed)
@@ -3776,7 +3918,9 @@ static int prepare_write_begin(struct f2fs_sb_info *sbi,
 	}
 
 	if (!f2fs_lookup_read_extent_cache_block(inode, index,
-						 &dn.data_blkaddr)) {
+				&dn.data_blkaddr) &&
+			!f2fs_iext_get_data_blkaddr(inode, ifolio, index,
+				&dn.data_blkaddr)) {
 		if (IS_DEVICE_ALIASING(inode)) {
 			err = -ENODATA;
 			goto out;
diff --git a/fs/f2fs/dir.c b/fs/f2fs/dir.c
index a9563f7fcd88..d7dd3c8c8684 100644
--- a/fs/f2fs/dir.c
+++ b/fs/f2fs/dir.c
@@ -16,6 +16,7 @@
 #include "node.h"
 #include "acl.h"
 #include "xattr.h"
+#include "iextent.h"
 #include <trace/events/f2fs.h>
 
 static inline bool f2fs_should_fallback_to_linear(struct inode *dir)
diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h
index 8011bbdf2c68..d5326681fc64 100644
--- a/fs/f2fs/f2fs.h
+++ b/fs/f2fs/f2fs.h
@@ -144,6 +144,7 @@ enum f2fs_mount_opt {
 	 * string rather than using the MS_LAZYTIME flag, so this must remain.
 	 */
 	F2FS_MOUNT_LAZYTIME,
+	F2FS_MOUNT_INLINE_EXTENT_SIZE,
 	F2FS_MOUNT_RESERVE_NODE,
 };
 
@@ -229,6 +230,7 @@ struct f2fs_mount_info {
 	kgid_t s_resgid;		/* reserved blocks for gid */
 	int active_logs;		/* # of active logs */
 	int inline_xattr_size;		/* inline xattr size */
+	int inline_extent_size;		/* default inline extent capacity */
 #ifdef CONFIG_F2FS_FAULT_INJECTION
 	struct f2fs_fault_info fault_info;	/* For fault injection */
 #endif
@@ -939,6 +941,7 @@ enum {
 	FI_ATOMIC_REPLACE,	/* indicate atomic replace */
 	FI_OPENED_FILE,		/* indicate file has been opened */
 	FI_DONATE_FINISHED,	/* indicate page donation of file has been finished */
+	FI_INLINE_EXTENT,	/* indicate file uses inline extent mapping */
 	FI_MAX,			/* max flag, never be used */
 };
 
@@ -998,6 +1001,7 @@ struct f2fs_inode_info {
 	int i_extra_isize;		/* size of extra space located in i_addr */
 	kprojid_t i_projid;		/* id for project quota */
 	int i_inline_xattr_size;	/* inline xattr size */
+	int i_inline_ext_capacity;	/* # of inline extent entries */
 	struct timespec64 i_crtime;	/* inode creation time */
 	struct timespec64 i_disk_time[3];/* inode disk times */
 
@@ -1136,6 +1140,8 @@ static inline void set_new_dnode(struct dnode_of_data *dn, struct inode *inode,
 	dn->inode_folio = ifolio;
 	dn->node_folio = nfolio;
 	dn->nid = nid;
+	if (ifolio != NULL)
+		dn->inode_folio_locked = folio_test_locked(ifolio);
 }
 
 /*
@@ -1796,6 +1802,10 @@ struct f2fs_sb_info {
 	struct f2fs_nm_info *nm_info;		/* node manager */
 	struct inode *node_inode;		/* cache node blocks */
 
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+	struct f2fs_iext_info *iext_info;
+#endif
+
 	/* for segment-related operations */
 	struct f2fs_sm_info *sm_info;		/* segment manager */
 
@@ -3133,12 +3143,15 @@ static inline void f2fs_put_page(struct page *page, bool unlock)
 	f2fs_folio_put(page_folio(page), unlock);
 }
 
+static inline __le32 *get_dnode_addr(struct inode *inode,
+					struct folio *node_folio);
 static inline void f2fs_put_dnode(struct dnode_of_data *dn)
 {
 	if (dn->node_folio)
 		f2fs_folio_put(dn->node_folio, true);
 	if (dn->inode_folio && dn->node_folio != dn->inode_folio)
-		f2fs_folio_put(dn->inode_folio, false);
+		f2fs_folio_put(dn->inode_folio,
+				dn->inode_folio_locked);
 	dn->node_folio = NULL;
 	dn->inode_folio = NULL;
 }
@@ -3636,6 +3649,27 @@ static inline bool f2fs_is_cow_file(struct inode *inode)
 	return is_inode_flag_set(inode, FI_COW_FILE);
 }
 
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+static inline bool f2fs_iext_support_inline_extent(struct inode *inode,
+			struct folio *ifolio)
+{
+	if (!inode) {
+		struct f2fs_inode *ri = (struct f2fs_inode *)ifolio;
+
+		if (!(ri->i_inline & F2FS_EXTRA_ATTR))
+			return false;
+		return le32_to_cpu(ri->i_inline_ext_capacity) > 0;
+	}
+	return is_inode_flag_set(inode, FI_INLINE_EXTENT);
+}
+#else
+static inline bool f2fs_iext_support_inline_extent(struct inode *inode,
+			struct folio *ifolio)
+{
+	return false;
+}
+#endif
+
 static inline void *inline_data_addr(struct inode *inode, struct folio *folio)
 {
 	__le32 *addr = get_dnode_addr(inode, folio);
@@ -3782,6 +3816,11 @@ static inline int get_inline_xattr_addrs(struct inode *inode)
 	return F2FS_I(inode)->i_inline_xattr_size;
 }
 
+static inline int get_inline_ext_capacity(struct inode *inode)
+{
+	return F2FS_I(inode)->i_inline_ext_capacity;
+}
+
 #define f2fs_get_inode_mode(i) \
 	((is_inode_flag_set(i, FI_ACL_MODE)) ? \
 	 (F2FS_I(i)->i_acl_mode) : ((i)->i_mode))
@@ -5003,7 +5042,8 @@ static inline bool f2fs_may_compress(struct inode *inode)
 {
 	if (IS_SWAPFILE(inode) || f2fs_is_pinned_file(inode) ||
 		f2fs_is_atomic_file(inode) || f2fs_has_inline_data(inode) ||
-		f2fs_is_mmap_file(inode))
+		f2fs_is_mmap_file(inode) ||
+		f2fs_iext_support_inline_extent(inode, NULL))
 		return false;
 	return S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode);
 }
diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c
index c54897a25981..4bb6729a7f56 100644
--- a/fs/f2fs/file.c
+++ b/fs/f2fs/file.c
@@ -33,6 +33,7 @@
 #include "acl.h"
 #include "gc.h"
 #include "iostat.h"
+#include "iextent.h"
 #include <trace/events/f2fs.h>
 #include <uapi/linux/f2fs.h>
 
diff --git a/fs/f2fs/iextent.c b/fs/f2fs/iextent.c
new file mode 100644
index 000000000000..e2eed9e79960
--- /dev/null
+++ b/fs/f2fs/iextent.c
@@ -0,0 +1,528 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * fs/f2fs/iextent.c
+ *
+ * Copyright (c) 2026 Xiaomi Technology Co., Ltd.
+ *             http://www.mi.com/
+ */
+#include <linux/f2fs_fs.h>
+
+#include "f2fs.h"
+#include "iextent.h"
+
+/*
+ * ASSERT - debug assertion for inline extent code.
+ * Uses f2fs_bug_on when sbi is available, falls back to WARN_ON_ONCE otherwise.
+ */
+#define ASSERT(sbi, condition) do {			\
+	if (!(sbi))					\
+		WARN_ON_ONCE(!(condition));		\
+	else						\
+		f2fs_bug_on((sbi), !(condition));	\
+} while (0)
+
+/*
+ * Borrowed from ext4_ext_binsearch_idx.
+ *
+ * __iext_binsearch_idx:
+ * binary search for the closest index of the given block
+ * the header must be checked before calling this
+ */
+static int __iext_binsearch_idx(struct inode *inode, struct folio *ifolio,
+			block_t block)
+{
+	struct f2fs_iext_header *eh = iext_get_header(ifolio);
+	struct f2fs_extent *r, *l, *m;
+
+	if (EXT_ENTRY_COUNT(eh) == 0)
+		return -1;
+
+	l = EXT_FIRST_INDEX(eh) + 1;
+	r = EXT_LAST_INDEX(eh);
+	while (l <= r) {
+		m = l + (r - l) / 2;
+		if (block < le32_to_cpu(m->fofs))
+			r = m - 1;
+		else
+			l = m + 1;
+	}
+
+#ifdef CONFIG_F2FS_INLINE_EXTENT_DEBUG
+	{
+		struct f2fs_sb_info *sbi = inode ? F2FS_I_SB(inode) : NULL;
+		struct f2fs_extent *chix, *ix;
+		int k;
+
+		chix = ix = EXT_FIRST_INDEX(eh);
+		for (k = 0; k < EXT_ENTRY_COUNT(eh); k++, ix++) {
+			if (k != 0 && le32_to_cpu(ix->fofs) <=
+					le32_to_cpu(ix[-1].fofs)) {
+				f2fs_debug(sbi, "k=%d, ix=0x%p, first=0x%p",
+						k, ix, EXT_FIRST_INDEX(eh));
+				f2fs_debug(sbi, "%u <= %u",
+						le32_to_cpu(ix->fofs),
+						le32_to_cpu(ix[-1].fofs));
+			}
+			ASSERT(sbi, !(k && le32_to_cpu(ix->fofs)
+						<= le32_to_cpu(ix[-1].fofs)));
+			if (block < le32_to_cpu(ix->fofs))
+				break;
+			chix = ix;
+		}
+		ASSERT(sbi, chix == l - 1);
+	}
+#endif
+
+	return l - EXT_FIRST_INDEX(eh) - 1;
+}
+
+static void __ext_add_one_entry(struct f2fs_iext_header *eh, int index,
+			struct f2fs_extent *ext)
+{
+	int inline_extents = EXT_ENTRY_COUNT(eh);
+
+	if (WARN_ON_ONCE(index < 0 || index > inline_extents))
+		return;
+
+	memmove(&eh->exts[index + 1], &eh->exts[index],
+			(inline_extents - index) * sizeof(struct f2fs_extent));
+	memcpy(&eh->exts[index], ext, sizeof(struct f2fs_extent));
+
+	eh->cnt = cpu_to_le32(inline_extents + 1);
+}
+
+static void __ext_del_one_entry(struct f2fs_iext_header *eh, int index)
+{
+	int inline_extents = EXT_ENTRY_COUNT(eh);
+
+	memmove(&eh->exts[index], &eh->exts[index + 1],
+			(inline_extents - index - 1) *
+			sizeof(struct f2fs_extent));
+
+	eh->cnt = cpu_to_le32(inline_extents - 1);
+}
+
+enum {
+	F2FS_EXT_HIT_LEFT = -1,
+	F2FS_EXT_HIT_MID = 0,
+	F2FS_EXT_HIT_RIGHT = 1,
+};
+
+static int __is_iextent_hit(struct f2fs_extent *ext, block_t fofs)
+{
+	block_t start = F2FS_EXT_LOGICAL_START(ext);
+	block_t end = F2FS_EXT_LOGICAL_END(ext);
+
+	if (fofs < start)
+		return F2FS_EXT_HIT_LEFT;
+	if (fofs > end)
+		return F2FS_EXT_HIT_RIGHT;
+	return F2FS_EXT_HIT_MID;
+}
+
+static bool __is_iextent_mergeable(struct f2fs_extent *left,
+			struct f2fs_extent *right)
+{
+	if (F2FS_EXT_LOGICAL_END(left) + 1 != F2FS_EXT_LOGICAL_START(right))
+		return false;
+	return F2FS_EXT_PHYSICAL_END(left) + 1
+				== F2FS_EXT_PHYSICAL_START(right);
+}
+
+static bool __iext_try_merge(struct f2fs_iext_header *eh, int i)
+{
+	struct f2fs_extent *a, *b;
+
+	if (i < 0 || i + 1 >= EXT_ENTRY_COUNT(eh))
+		return false;
+
+	a = &eh->exts[i];
+	b = &eh->exts[i + 1];
+	if (!__is_iextent_mergeable(a, b))
+		return false;
+
+	a->len = cpu_to_le32(F2FS_EXT_LEN(a) + F2FS_EXT_LEN(b));
+	__ext_del_one_entry(eh, i + 1);
+	return true;
+}
+
+/*
+ * Returns the array index at which a replacement {fofs, ...} extent should be
+ * inserted. For F2FS_EXT_HIT_LEFT/RIGHT (@fofs falls in a gap) nothing is
+ * modified.
+ */
+static int __iext_punch(struct f2fs_iext_header *eh, int index, int hit,
+			block_t fofs)
+{
+	struct f2fs_extent *ext = &eh->exts[index];
+	block_t start = F2FS_EXT_LOGICAL_START(ext);
+	block_t end = F2FS_EXT_LOGICAL_END(ext);
+	block_t blk = F2FS_EXT_PHYSICAL_START(ext);
+	unsigned int len = F2FS_EXT_LEN(ext);
+	struct f2fs_extent right;
+
+	switch (hit) {
+	case F2FS_EXT_HIT_LEFT:
+		return 0;
+	case F2FS_EXT_HIT_RIGHT:
+		return index + 1;
+	case F2FS_EXT_HIT_MID:
+		if (len == 1) {
+			__ext_del_one_entry(eh, index);
+			return index;
+		}
+		if (fofs == start) {
+			ext->fofs = cpu_to_le32(start + 1);
+			ext->blk = cpu_to_le32(blk + 1);
+			ext->len = cpu_to_le32(len - 1);
+			return index;
+		}
+		if (fofs == end) {
+			ext->len = cpu_to_le32(len - 1);
+			return index + 1;
+		}
+		/* split: keep [start, fofs-1], add [fofs+1, end] after it */
+		right.fofs = cpu_to_le32(fofs + 1);
+		right.len = cpu_to_le32(end - fofs);
+		right.blk = cpu_to_le32(blk + (fofs + 1 - start));
+		ext->len = cpu_to_le32(fofs - start);
+		__ext_add_one_entry(eh, index + 1, &right);
+		return index + 1;
+	}
+	return index;
+}
+
+/*
+ * -1 for a delete. 0 for a overwrite/head-or-tail delete.
+ * +1 for a head/tail overwrite or a middle delete, +2 for split.
+ */
+static int __iext_slot_delta(int hit, unsigned int len, block_t start,
+			block_t end, block_t fofs, block_t blkaddr)
+{
+	bool insert = (blkaddr != NULL_ADDR);
+
+	if (hit != F2FS_EXT_HIT_MID)
+		return insert ? 1 : 0;
+	if (len == 1)
+		return insert ? 0 : -1;
+	if (fofs == start || fofs == end)
+		return insert ? 1 : 0;
+	return insert ? 2 : 1;
+}
+
+/* Precondition: the covering extent has len > 1. */
+static void __iext_invalidate_one(struct f2fs_iext_header *eh, int index,
+			block_t fofs)
+{
+	struct f2fs_extent *ext = &eh->exts[index];
+	block_t start = F2FS_EXT_LOGICAL_START(ext);
+	block_t blk = F2FS_EXT_PHYSICAL_START(ext);
+	unsigned int len = F2FS_EXT_LEN(ext);
+
+	if (fofs == start) {
+		/* trim the head, dropping only @fofs */
+		ext->fofs = cpu_to_le32(start + 1);
+		ext->blk = cpu_to_le32(blk + 1);
+		ext->len = cpu_to_le32(len - 1);
+	} else {
+		/* drop [fofs, end] from the cache (also covers fofs == end) */
+		ext->len = cpu_to_le32(fofs - start);
+	}
+}
+
+/*
+ * Overwrite the mapping of a single logical block @fofs with @blkaddr
+ * (NULL_ADDR mean "invalidate the mapping").  Keeps the array sorted, merged
+ * and non-overlapping.
+ *
+ * Soundness rule: the inline area is a cache, so on the full/no-room paths we
+ * may fail to store the new mapping, but we must never leave a *stale* mapping
+ * for @fofs behind.  Hence whenever @fofs is covered we always remove its old
+ * mapping, even if the replacement cannot be inserted.
+ *
+ * The return value tells the caller whether the ifolio must be marked dirty:
+ *   F2FS_IEXT_INSERT_NORMAL/REMOVED - the area changed, mark dirty
+ *   F2FS_IEXT_INSERT_DROP           - nothing changed, no need to dirty
+ */
+int f2fs_iext_update_data_blkaddr(struct inode *inode, struct folio *ifolio,
+					block_t fofs, block_t blkaddr)
+{
+	struct f2fs_iext_header *eh = iext_get_header(ifolio);
+	int max_inline_extents = f2fs_iext_max_extents(inode);
+	struct f2fs_extent ext;
+	struct f2fs_extent *cur;
+	int index, hit, delta;
+	bool modified = false;
+	block_t start, end;
+	unsigned int len;
+
+	/* NEW_ADDR blocks are never cached. Treat it as a delete. */
+	if (blkaddr == NEW_ADDR)
+		blkaddr = NULL_ADDR;
+
+	ext.fofs = cpu_to_le32(fofs);
+	ext.blk = cpu_to_le32(blkaddr);
+	ext.len = cpu_to_le32(1);
+
+	if (EXT_ENTRY_COUNT(eh) == 0) {
+		if (blkaddr == NULL_ADDR || max_inline_extents < 1)
+			return F2FS_IEXT_INSERT_DROP;
+		__ext_add_one_entry(eh, 0, &ext);
+		return F2FS_IEXT_INSERT_NORMAL;
+	}
+
+	/* Fast path: contiguous append/merge with the last extent. */
+	if (blkaddr != NULL_ADDR) {
+		cur = EXT_LAST_INDEX(eh);
+		if (__is_iextent_mergeable(cur, &ext)) {
+			cur->len = cpu_to_le32(F2FS_EXT_LEN(cur) + 1);
+			return F2FS_IEXT_INSERT_NORMAL;
+		}
+	}
+
+	index = __iext_binsearch_idx(inode, ifolio, fofs);
+	cur = &eh->exts[index];
+	hit = __is_iextent_hit(cur, fofs);
+	start = F2FS_EXT_LOGICAL_START(cur);
+	end = F2FS_EXT_LOGICAL_END(cur);
+	len = F2FS_EXT_LEN(cur);
+
+	if (hit == F2FS_EXT_HIT_MID &&
+			F2FS_EXT_PHYSICAL_START(cur) + fofs - start == blkaddr)
+		return F2FS_IEXT_INSERT_DROP;
+
+	if (hit != F2FS_EXT_HIT_MID && blkaddr == NULL_ADDR)
+		return F2FS_IEXT_INSERT_DROP;
+
+	/*
+	 * Reserve room for the peak occupancy.  When the area is full, evict
+	 * from the tail.
+	 */
+	delta = __iext_slot_delta(hit, len, start, end, fofs, blkaddr);
+	while (EXT_ENTRY_COUNT(eh) + delta > max_inline_extents) {
+		struct f2fs_extent *last = EXT_LAST_INDEX(eh);
+
+		if (fofs >= F2FS_EXT_LOGICAL_START(last))
+			break;
+		__ext_del_one_entry(eh, EXT_ENTRY_COUNT(eh) - 1);
+		modified = true;
+	}
+
+	/*
+	 * Still no room: drop its stale mapping without splitting; otherwise
+	 * there is nothing to store.
+	 */
+	if (EXT_ENTRY_COUNT(eh) + delta > max_inline_extents) {
+		if (hit == F2FS_EXT_HIT_MID) {
+			__iext_invalidate_one(eh, index, fofs);
+			return F2FS_IEXT_INSERT_REMOVED;
+		}
+		return modified ? F2FS_IEXT_INSERT_REMOVED :
+				F2FS_IEXT_INSERT_DROP;
+	}
+
+	index = __iext_punch(eh, index, hit, fofs);
+	if (blkaddr == NULL_ADDR)
+		return F2FS_IEXT_INSERT_NORMAL;
+
+	__ext_add_one_entry(eh, index, &ext);
+
+	/* Coalesce the freshly inserted extent with its neighbours. */
+	if (index > 0 && __iext_try_merge(eh, index - 1))
+		index--;
+	__iext_try_merge(eh, index);
+
+	return F2FS_IEXT_INSERT_NORMAL;
+}
+
+int f2fs_iext_lookup_blkaddr(struct inode *inode, struct folio *ifolio,
+					block_t fofs, block_t *blkaddr,
+					unsigned int *len)
+{
+	struct f2fs_iext_header *eh = iext_get_header(ifolio);
+	struct f2fs_extent *last_ext = EXT_LAST_INDEX(eh);
+	struct f2fs_extent *ext;
+	int index, hit;
+	block_t blk_start, fofs_start;
+
+	if (EXT_ENTRY_COUNT(eh) == 0 || F2FS_EXT_LOGICAL_END(last_ext) < fofs)
+		return -ENOENT;
+
+	index = __iext_binsearch_idx(inode, ifolio, fofs);
+	if (index < 0)
+		return -ENOENT;
+	ext = &eh->exts[index];
+	hit = __is_iextent_hit(ext, fofs);
+	if (hit != F2FS_EXT_HIT_MID)
+		return -ENOENT;
+	blk_start = F2FS_EXT_PHYSICAL_START(ext);
+	fofs_start = F2FS_EXT_LOGICAL_START(ext);
+	*blkaddr = blk_start + fofs - fofs_start;
+	if (len)
+		*len = F2FS_EXT_LEN(ext) - (fofs - fofs_start);
+
+	return 0;
+}
+
+/* truncate extent starting from @fofs. */
+void f2fs_iext_truncate_from_blkaddr(struct inode *inode,
+					struct folio *ifolio, block_t fofs)
+{
+	struct f2fs_sb_info *sbi = F2FS_I_SB(inode);
+	int index, hit;
+	unsigned int len;
+	struct f2fs_iext_header *eh = iext_get_header(ifolio);
+	int inline_extents = EXT_ENTRY_COUNT(eh);
+	struct f2fs_extent *ext;
+	block_t fofs_start;
+
+	if (inline_extents == 0)
+		return;
+	index = __iext_binsearch_idx(inode, ifolio, fofs);
+	ext = &eh->exts[index];
+	hit = __is_iextent_hit(ext, fofs);
+	switch (hit) {
+	case F2FS_EXT_HIT_LEFT:
+		ASSERT(sbi, index == 0);
+		eh->cnt = cpu_to_le32(0);
+		break;
+	case F2FS_EXT_HIT_RIGHT:
+		eh->cnt = cpu_to_le32(index + 1);
+		break;
+	case F2FS_EXT_HIT_MID:
+		fofs_start = F2FS_EXT_LOGICAL_START(ext);
+		len = F2FS_EXT_LEN(ext);
+
+		if (len == 1 || fofs == fofs_start) {
+			eh->cnt = cpu_to_le32(index);
+			break;
+		}
+		ext->len = cpu_to_le32(fofs - fofs_start);
+		eh->cnt = cpu_to_le32(index + 1);
+		break;
+	default:
+		ASSERT(sbi, 0);
+	}
+}
+
+int f2fs_iext_update_extension_list(struct f2fs_sb_info *sbi, const char *name,
+					bool set, unsigned int capacity)
+{
+	struct f2fs_iext_info *iext_info = sbi->iext_info;
+	__u8 (*extlist)[F2FS_EXTENSION_LEN] = iext_info->extensions;
+	int count;
+	int i, ret = 0;
+	unsigned long flag;
+
+	if (strlen(name) >= F2FS_EXTENSION_LEN)
+		return -EINVAL;
+
+	spin_lock_irqsave(&iext_info->iext_ext_lock, flag);
+	count = iext_info->iext_ext_cnt;
+	if (set && count == IEXT_EXT_NUM) {
+		ret = -EINVAL;
+		goto out;
+	}
+	for (i = 0; i < count; i++) {
+		if (strcmp(name, extlist[i]))
+			continue;
+
+		if (set) {
+			/* Extension already exists — update its capacity. */
+			iext_info->ext_capacity[i] = capacity;
+			goto out;
+		}
+
+		/* Remove: shift both extensions and ext_capacity arrays. */
+		memcpy(extlist[i], extlist[i + 1],
+				F2FS_EXTENSION_LEN * (count - i - 1));
+		memset(extlist[count - 1], 0, F2FS_EXTENSION_LEN);
+		memmove(&iext_info->ext_capacity[i],
+				&iext_info->ext_capacity[i + 1],
+				sizeof(unsigned int) * (count - i - 1));
+		iext_info->ext_capacity[count - 1] = 0;
+		iext_info->iext_ext_cnt--;
+		goto out;
+	}
+
+	if (!set) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	memcpy(extlist[count], name, strlen(name));
+	iext_info->ext_capacity[count] = capacity;
+	iext_info->iext_ext_cnt++;
+out:
+	spin_unlock_irqrestore(&iext_info->iext_ext_lock, flag);
+	return ret;
+}
+
+bool f2fs_iext_sanity_check(struct folio *ifolio)
+{
+	struct f2fs_iext_header *eh = iext_get_header(ifolio);
+	struct f2fs_extent *ix;
+	int inline_extents = EXT_ENTRY_COUNT(eh);
+	int max_inline_extents =
+		le32_to_cpu(F2FS_NODE(ifolio)->i.i_inline_ext_capacity);
+	int extra_isize =
+		le16_to_cpu(F2FS_NODE(ifolio)->i.i_extra_isize);
+	int k;
+
+	if (!S_ISREG(le16_to_cpu(F2FS_NODE(ifolio)->i.i_mode)))
+		return false;
+	if (inline_extents > max_inline_extents)
+		return false;
+	if (extra_isize != F2FS_TOTAL_EXTRA_ATTR_SIZE +
+			max_inline_extents * sizeof (struct f2fs_extent) +
+			sizeof(struct f2fs_iext_header))
+		return false;
+
+	ix = EXT_FIRST_INDEX(eh);
+	for (k = 0; k < inline_extents; k++, ix++) {
+		if (F2FS_EXT_LEN(ix) == 0)
+			return false;
+		/* only real block addresses are cached */
+		if (F2FS_EXT_PHYSICAL_START(ix) == NULL_ADDR ||
+				F2FS_EXT_PHYSICAL_START(ix) == NEW_ADDR)
+			return false;
+		if (F2FS_EXT_LOGICAL_START(ix) > UINT_MAX - F2FS_EXT_LEN(ix))
+			return false;
+		if (F2FS_EXT_PHYSICAL_START(ix) > UINT_MAX - F2FS_EXT_LEN(ix))
+			return false;
+		if (k == 0)
+			continue;
+		if (F2FS_EXT_LOGICAL_START(ix) <=
+				F2FS_EXT_LOGICAL_END(&ix[-1]))
+			return false;
+	}
+	return true;
+}
+
+int f2fs_iext_info_init(struct f2fs_sb_info *sbi)
+{
+	struct f2fs_iext_info *iext_info;
+
+	iext_info = f2fs_kvzalloc(sbi, sizeof(struct f2fs_iext_info),
+				GFP_KERNEL);
+	if (!iext_info)
+		return -ENOMEM;
+
+	spin_lock_init(&iext_info->iext_ext_lock);
+	if (F2FS_OPTION(sbi).inline_extent_size)
+		iext_info->default_capacity =
+				F2FS_OPTION(sbi).inline_extent_size;
+	else
+		iext_info->default_capacity = F2FS_IEXT_DEF_CAPACITY;
+	iext_info->iext_enable = 1;
+	sbi->iext_info = iext_info;
+	f2fs_iext_update_extension_list(sbi, "*", true, 256);
+	return 0;
+}
+
+void f2fs_iext_info_destroy(struct f2fs_sb_info *sbi)
+{
+	if (sbi->iext_info == NULL)
+		return;
+	kvfree(sbi->iext_info);
+}
diff --git a/fs/f2fs/iextent.h b/fs/f2fs/iextent.h
new file mode 100644
index 000000000000..40b5aeaa8d52
--- /dev/null
+++ b/fs/f2fs/iextent.h
@@ -0,0 +1,136 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * fs/f2fs/iextent.h
+ *
+ * Copyright (c) 2026 Xiaomi Technology Co., Ltd.
+ *             http://www.mi.com/
+ *
+ */
+
+/*
+ * Inline extent mapping for f2fs inodes.
+ *
+ * The inline extent area is placed immediately after i_extra_end in the
+ * on-disk inode, within the i_extra_isize region.  Its size is determined
+ * by f2fs_inode->i_inline_ext_capacity (number of extent entries).
+ *
+ * On-disk layout:
+ *   [i_extra_isize .. i_extra_end] [f2fs_iext_header | f2fs_extent[cap]]
+ *   |<------------------- i_extra_isize (in bytes) ------------------->|
+ *
+ * Old kernels treat i_extra_isize as opaque and skip the whole region,
+ * so the inline extent area is invisible to them.
+ */
+#ifndef __F2FS_INLINE_EXTENT_H_
+#define __F2FS_INLINE_EXTENT_H_
+
+#include <linux/types.h>
+
+/*
+ * f2fs_iext_header: header for inline extent area in i_extra_attr region.
+ * Placed right after i_extra_end[0], before the extent array.
+ */
+struct f2fs_iext_header {
+	__le32 cnt;		/* current # of valid extents */
+	struct f2fs_extent exts[];
+} __packed;
+
+#define EXT_FIRST_INDEX(__hdr__) ((__hdr__)->exts)
+#define EXT_ENTRY_COUNT(__hdr__) (le32_to_cpu((__hdr__)->cnt))
+#define EXT_LAST_INDEX(__hdr__) \
+			(EXT_ENTRY_COUNT(__hdr__) ? \
+			&(__hdr__)->exts[EXT_ENTRY_COUNT(__hdr__) - 1] : \
+			NULL)
+
+#define F2FS_EXT_LEN(ext) (le32_to_cpu((ext)->len))
+#define F2FS_EXT_LOGICAL_START(ext) (le32_to_cpu((ext)->fofs))
+#define F2FS_EXT_LOGICAL_END(ext) \
+			((F2FS_EXT_LOGICAL_START(ext) + F2FS_EXT_LEN(ext)) - 1)
+#define F2FS_EXT_PHYSICAL_START(ext) (le32_to_cpu((ext)->blk))
+#define F2FS_EXT_PHYSICAL_END(ext) \
+			((F2FS_EXT_PHYSICAL_START(ext) + F2FS_EXT_LEN(ext)) - 1)
+
+#define IEXT_EXT_NUM 16
+#define F2FS_IEXT_DEF_CAPACITY	8	/* default inline extent capacity */
+struct f2fs_iext_info {
+	spinlock_t iext_ext_lock;
+	unsigned char iext_ext_cnt;	/* extension count */
+	unsigned char extensions[IEXT_EXT_NUM][F2FS_EXTENSION_LEN];
+	unsigned int ext_capacity[IEXT_EXT_NUM]; /* per-extension capacity */
+	unsigned int default_capacity;	/* default capacity from mount opt */
+	bool iext_enable;
+};
+
+enum {
+	/* insert extent correctly */
+	F2FS_IEXT_INSERT_NORMAL = 0,
+	/* didn't modify inode folio, because no space and fofs is too big */
+	F2FS_IEXT_INSERT_DROP,
+	/* modify inode folio, but didn't insert due to no space */
+	F2FS_IEXT_INSERT_REMOVED,
+};
+
+/*
+ * Get the inline extent header from an inode page.
+ * The header is located at i_extra_end (after the base extra attributes).
+ */
+static inline struct f2fs_iext_header *iext_get_header(struct folio *ifolio)
+{
+	struct f2fs_inode *ri = F2FS_INODE(ifolio);
+
+	return (struct f2fs_iext_header *)ri->i_extra_end;
+}
+
+static inline int f2fs_iext_max_extents(struct inode *inode)
+{
+	return get_inline_ext_capacity(inode);
+}
+
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+static inline void f2fs_iext_init_inline_extent(struct inode *inode,
+			struct folio *ifolio)
+{
+	struct f2fs_iext_header *eh = iext_get_header(ifolio);
+
+	eh->cnt = 0;
+}
+
+static inline bool f2fs_iext_is_enable(struct f2fs_sb_info *sbi)
+{
+	struct f2fs_iext_info *iext_info = sbi->iext_info;
+
+	return READ_ONCE(iext_info->iext_enable);
+}
+
+static inline void f2fs_iext_set_enable(struct f2fs_sb_info *sbi, bool enable)
+{
+	struct f2fs_iext_info *iext_info = sbi->iext_info;
+
+	WRITE_ONCE(iext_info->iext_enable, enable);
+}
+
+static inline unsigned int f2fs_iext_total_size(struct f2fs_inode_info *fi)
+{
+	return fi->i_inline_ext_capacity * sizeof(struct f2fs_extent) +
+			sizeof(struct f2fs_iext_header);
+}
+#else
+static inline unsigned int f2fs_iext_total_size(struct f2fs_inode_info *fi)
+{
+	return 0;
+}
+#endif
+
+int f2fs_iext_update_data_blkaddr(struct inode *inode, struct folio *ifolio,
+					block_t fofs, block_t blkaddr);
+int f2fs_iext_lookup_blkaddr(struct inode *inode, struct folio *ifolio,
+					block_t fofs, block_t *blkaddr,
+					unsigned int *len);
+void f2fs_iext_truncate_from_blkaddr(struct inode *inode,
+					struct folio *ifolio, block_t fofs);
+int f2fs_iext_update_extension_list(struct f2fs_sb_info *sbi, const char *name,
+					bool set, unsigned int capacity);
+bool f2fs_iext_sanity_check(struct folio *ifolio);
+int f2fs_iext_info_init(struct f2fs_sb_info *sbi);
+void f2fs_iext_info_destroy(struct f2fs_sb_info *sbi);
+#endif
diff --git a/fs/f2fs/inline.c b/fs/f2fs/inline.c
index e2f7bedf1552..8d62d25c8694 100644
--- a/fs/f2fs/inline.c
+++ b/fs/f2fs/inline.c
@@ -13,6 +13,7 @@
 
 #include "f2fs.h"
 #include "node.h"
+#include "iextent.h"
 #include <trace/events/f2fs.h>
 
 static bool support_inline_data(struct inode *inode)
diff --git a/fs/f2fs/inode.c b/fs/f2fs/inode.c
index c95e0b126da4..0b1d839289fd 100644
--- a/fs/f2fs/inode.c
+++ b/fs/f2fs/inode.c
@@ -17,6 +17,7 @@
 #include "node.h"
 #include "segment.h"
 #include "xattr.h"
+#include "iextent.h"
 
 #include <trace/events/f2fs.h>
 
@@ -208,11 +209,11 @@ static bool sanity_check_compress_inode(struct inode *inode,
 			__func__, inode->i_ino, ri->i_compress_algorithm);
 		return false;
 	}
-	if (le64_to_cpu(ri->i_compr_blocks) >
+	if (le32_to_cpu(ri->i_compr_blocks) >
 			SECTOR_TO_BLOCK(inode->i_blocks)) {
 		f2fs_warn(sbi,
-			"%s: inode (ino=%llx) has inconsistent i_compr_blocks:%llu, i_blocks:%llu, run fsck to fix",
-			__func__, inode->i_ino, le64_to_cpu(ri->i_compr_blocks),
+			"%s: inode (ino=%llx) has inconsistent i_compr_blocks:%u, i_blocks:%llu, run fsck to fix",
+			__func__, inode->i_ino, le32_to_cpu(ri->i_compr_blocks),
 			SECTOR_TO_BLOCK(inode->i_blocks));
 		return false;
 	}
@@ -307,12 +308,12 @@ static bool sanity_check_inode(struct inode *inode, struct folio *node_folio)
 				  __func__, inode->i_ino);
 			return false;
 		}
-		if (fi->i_extra_isize > F2FS_TOTAL_EXTRA_ATTR_SIZE ||
+		if (fi->i_extra_isize > F2FS_TOTAL_EXTRA_ATTR_SIZE +
+			f2fs_iext_total_size(fi) ||
 			fi->i_extra_isize < F2FS_MIN_EXTRA_ATTR_SIZE ||
 			fi->i_extra_isize % sizeof(__le32)) {
-			f2fs_warn(sbi, "%s: inode (ino=%llx) has corrupted i_extra_isize: %d, max: %zu",
-				  __func__, inode->i_ino, fi->i_extra_isize,
-				  F2FS_TOTAL_EXTRA_ATTR_SIZE);
+			f2fs_warn(sbi, "%s: inode (ino=%llx) has corrupted i_extra_isize: %d",
+				  __func__, inode->i_ino, fi->i_extra_isize);
 			return false;
 		}
 		if (f2fs_sb_has_compression(sbi) &&
@@ -477,6 +478,23 @@ static int do_read_inode(struct inode *inode)
 		fi->i_inline_xattr_size = 0;
 	}
 
+	if (f2fs_has_extra_attr(inode) &&
+			F2FS_FITS_IN_INODE(ri, fi->i_extra_isize,
+					i_inline_ext_capacity)) {
+		fi->i_inline_ext_capacity =
+				le32_to_cpu(ri->i_inline_ext_capacity);
+		/* Compressed files must not use inline extents. */
+		if (f2fs_compressed_file(inode) && fi->i_inline_ext_capacity) {
+			f2fs_warn(F2FS_I_SB(inode),
+				"%s: compressed inode (ino=%llx) has i_inline_ext_capacity=%u, clearing",
+				__func__, inode->i_ino,
+				fi->i_inline_ext_capacity);
+			return -EFSCORRUPTED;
+		}
+		if (fi->i_inline_ext_capacity)
+			set_inode_flag(inode, FI_INLINE_EXTENT);
+	}
+
 	if (!sanity_check_inode(inode, node_folio)) {
 		f2fs_folio_put(node_folio, true);
 		set_sbi_flag(sbi, SBI_NEED_FSCK);
@@ -525,7 +543,7 @@ static int do_read_inode(struct inode *inode)
 			unsigned short compress_flag;
 
 			atomic_set(&fi->i_compr_blocks,
-					le64_to_cpu(ri->i_compr_blocks));
+					le32_to_cpu(ri->i_compr_blocks));
 			fi->i_compress_algorithm = ri->i_compress_algorithm;
 			fi->i_log_cluster_size = ri->i_log_cluster_size;
 			compress_flag = le16_to_cpu(ri->i_compress_flag);
@@ -753,7 +771,7 @@ void f2fs_update_inode(struct inode *inode, struct folio *node_folio)
 							i_compress_flag)) {
 			unsigned short compress_flag;
 
-			ri->i_compr_blocks = cpu_to_le64(
+			ri->i_compr_blocks = cpu_to_le32(
 					atomic_read(&fi->i_compr_blocks));
 			ri->i_compress_algorithm = fi->i_compress_algorithm;
 			compress_flag = fi->i_compress_flag |
@@ -762,6 +780,11 @@ void f2fs_update_inode(struct inode *inode, struct folio *node_folio)
 			ri->i_compress_flag = cpu_to_le16(compress_flag);
 			ri->i_log_cluster_size = fi->i_log_cluster_size;
 		}
+
+		if (F2FS_FITS_IN_INODE(ri, fi->i_extra_isize,
+						i_inline_ext_capacity))
+			ri->i_inline_ext_capacity =
+				cpu_to_le32(fi->i_inline_ext_capacity);
 	}
 
 	__set_inode_rdev(inode, node_folio);
diff --git a/fs/f2fs/namei.c b/fs/f2fs/namei.c
index cac03b8e91a1..2b285afe2dbc 100644
--- a/fs/f2fs/namei.c
+++ b/fs/f2fs/namei.c
@@ -20,6 +20,7 @@
 #include "segment.h"
 #include "xattr.h"
 #include "acl.h"
+#include "iextent.h"
 #include <trace/events/f2fs.h>
 
 static inline bool is_extension_exist(const unsigned char *s, const char *sub,
@@ -70,6 +71,13 @@ static inline bool is_compress_extension(const unsigned char *s, const char *sub
 	return is_extension_exist(s, sub, true, true);
 }
 
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+static inline bool is_iext_extension(const unsigned char *s, const char *sub)
+{
+	return is_extension_exist(s, sub, true, true);
+}
+#endif
+
 int f2fs_update_extension_list(struct f2fs_sb_info *sbi, const char *name,
 							bool hot, bool set)
 {
@@ -231,6 +239,60 @@ static void set_file_temperature(struct f2fs_sb_info *sbi, struct inode *inode,
 		file_set_hot(inode);
 }
 
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+static void set_iext_new_inode(struct f2fs_sb_info *sbi, struct inode *inode,
+				const unsigned char *name)
+{
+	struct f2fs_iext_info *iext_info = sbi->iext_info;
+	unsigned char (*ext)[F2FS_EXTENSION_LEN] = iext_info->extensions;
+	unsigned long flag;
+	unsigned int capacity = 0;
+	int i;
+
+	if (f2fs_compressed_file(inode))
+		return;
+
+	if (!S_ISREG(inode->i_mode))
+		return;
+
+	if (!f2fs_iext_is_enable(sbi))
+		return;
+
+	/* This name comes only from normal files. */
+	if (!name)
+		return;
+
+	spin_lock_irqsave(&iext_info->iext_ext_lock, flag);
+	/* Check extension list for matching capacity. */
+	for (i = 0; i < iext_info->iext_ext_cnt; i++) {
+		if (is_iext_extension(name, ext[i])) {
+			unsigned int addrs_of_iext;
+
+			if (iext_info->ext_capacity[i])
+				capacity = iext_info->ext_capacity[i];
+			else
+				capacity = iext_info->default_capacity;
+			addrs_of_iext = (capacity * sizeof(struct f2fs_extent) +
+				sizeof(struct f2fs_iext_header)) /
+				sizeof(__le32);
+			if (addrs_per_page(inode, true) <= addrs_of_iext)
+				capacity = 0;
+			break;
+		}
+	}
+	spin_unlock_irqrestore(&iext_info->iext_ext_lock, flag);
+
+	if (capacity) {
+		struct f2fs_inode_info *fi = F2FS_I(inode);
+
+		fi->i_inline_ext_capacity = capacity;
+		fi->i_extra_isize += sizeof(struct f2fs_iext_header) +
+				capacity * sizeof(struct f2fs_extent);
+		set_inode_flag(inode, FI_INLINE_EXTENT);
+	}
+}
+#endif
+
 static struct inode *f2fs_new_inode(struct mnt_idmap *idmap,
 						struct inode *dir, umode_t mode,
 						const char *name)
@@ -327,6 +389,11 @@ static struct inode *f2fs_new_inode(struct mnt_idmap *idmap,
 	/* Check compression first. */
 	set_compress_new_inode(sbi, dir, inode, name);
 
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+	/* must be set after compress flag set. */
+	if (is_inode_flag_set(inode, FI_EXTRA_ATTR))
+		set_iext_new_inode(sbi, inode, name);
+#endif
 	/* Should enable inline_data after compression set */
 	if (test_opt(sbi, INLINE_DATA) && f2fs_may_inline_data(inode))
 		set_inode_flag(inode, FI_INLINE_DATA);
diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c
index 38917e4a7319..c72b343e351b 100644
--- a/fs/f2fs/node.c
+++ b/fs/f2fs/node.c
@@ -19,6 +19,7 @@
 #include "segment.h"
 #include "xattr.h"
 #include "iostat.h"
+#include "iextent.h"
 #include <trace/events/f2fs.h>
 
 #define on_f2fs_build_free_nids(nm_i) mutex_is_locked(&(nm_i)->build_lock)
@@ -819,6 +820,7 @@ int f2fs_get_dnode_of_data(struct dnode_of_data *dn, pgoff_t index, int mode)
 	nid_t nids[4];
 	int level, i = 0;
 	int err = 0;
+	bool lock_ifolio = false;
 
 	level = get_node_path(dn->inode, index, offset, noffset);
 	if (level < 0)
@@ -846,6 +848,8 @@ int f2fs_get_dnode_of_data(struct dnode_of_data *dn, pgoff_t index, int mode)
 		nids[1] = get_nid(parent, offset[0], true);
 	dn->inode_folio = nfolio[0];
 	dn->inode_folio_locked = true;
+	if (f2fs_iext_support_inline_extent(dn->inode, NULL))
+		lock_ifolio = true;
 
 	/* get indirect or direct nodes */
 	for (i = 1; i <= level; i++) {
@@ -888,8 +892,10 @@ int f2fs_get_dnode_of_data(struct dnode_of_data *dn, pgoff_t index, int mode)
 			done = true;
 		}
 		if (i == 1) {
-			dn->inode_folio_locked = false;
-			folio_unlock(parent);
+			if (!lock_ifolio) {
+				dn->inode_folio_locked = false;
+				folio_unlock(parent);
+			}
 		} else {
 			f2fs_folio_put(parent, true);
 		}
@@ -899,7 +905,8 @@ int f2fs_get_dnode_of_data(struct dnode_of_data *dn, pgoff_t index, int mode)
 						NODE_TYPE_NON_INODE);
 			if (IS_ERR(nfolio[i])) {
 				err = PTR_ERR(nfolio[i]);
-				f2fs_folio_put(nfolio[0], false);
+				f2fs_folio_put(nfolio[0],
+						dn->inode_folio_locked);
 				goto release_out;
 			}
 		}
@@ -945,7 +952,7 @@ int f2fs_get_dnode_of_data(struct dnode_of_data *dn, pgoff_t index, int mode)
 release_pages:
 	f2fs_folio_put(parent, true);
 	if (i > 1)
-		f2fs_folio_put(nfolio[0], false);
+		f2fs_folio_put(nfolio[0], dn->inode_folio_locked);
 release_out:
 	dn->inode_folio = NULL;
 	dn->node_folio = NULL;
@@ -1194,6 +1201,7 @@ int f2fs_truncate_inode_blocks(struct inode *inode, pgoff_t from)
 	unsigned int nofs = 0;
 	struct dnode_of_data dn;
 	struct folio *folio;
+	bool lock_ifolio = false;
 
 	trace_f2fs_truncate_inode_blocks_enter(inode, from);
 
@@ -1217,7 +1225,11 @@ int f2fs_truncate_inode_blocks(struct inode *inode, pgoff_t from)
 	}
 
 	set_new_dnode(&dn, inode, folio, NULL, 0);
-	folio_unlock(folio);
+	if (f2fs_iext_support_inline_extent(inode, NULL)) {
+		lock_ifolio = true;
+		f2fs_iext_truncate_from_blkaddr(inode, folio, from);
+	} else
+		folio_unlock(folio);
 
 	switch (level) {
 	case 0:
@@ -1282,17 +1294,19 @@ int f2fs_truncate_inode_blocks(struct inode *inode, pgoff_t from)
 		if (err < 0)
 			goto fail;
 		if (offset[1] == 0 && get_nid(folio, offset[0], true)) {
-			folio_lock(folio);
+			if (!lock_ifolio)
+				folio_lock(folio);
 			BUG_ON(!is_node_folio(folio));
 			set_nid(folio, offset[0], 0, true);
-			folio_unlock(folio);
+			if (!lock_ifolio)
+				folio_unlock(folio);
 		}
 		offset[1] = 0;
 		offset[0]++;
 		nofs += err;
 	}
 fail:
-	f2fs_folio_put(folio, false);
+	f2fs_folio_put(folio, lock_ifolio);
 	trace_f2fs_truncate_inode_blocks_exit(inode, err);
 	return err > 0 ? 0 : err;
 }
@@ -1612,6 +1626,14 @@ static struct folio *__get_node_folio(struct f2fs_sb_info *sbi, pgoff_t nid,
 		err = -EFSBADCRC;
 		goto out_err;
 	}
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+	if (IS_INODE(folio) &&
+			f2fs_iext_support_inline_extent(NULL, folio) &&
+			!f2fs_iext_sanity_check(folio)) {
+		err = -EINVAL;
+		goto out_err;
+	}
+#endif
 page_hit:
 	err = f2fs_sanity_check_node_footer(sbi, folio, nid, ntype, false);
 	if (!err)
diff --git a/fs/f2fs/node.h b/fs/f2fs/node.h
index 5e114f352099..28c10787e5bb 100644
--- a/fs/f2fs/node.h
+++ b/fs/f2fs/node.h
@@ -259,6 +259,10 @@ static inline unsigned int ofs_of_node(const struct folio *node_folio)
 	return flag >> OFFSET_BIT_SHIFT;
 }
 
+int f2fs_iext_data_blkaddr(struct inode *inode,
+			struct folio *node_folio, unsigned int offset,
+			block_t *blkaddr);
+
 static inline __u64 cpver_of_node(const struct folio *node_folio)
 {
 	struct f2fs_node *rn = F2FS_NODE(node_folio);
diff --git a/fs/f2fs/recovery.c b/fs/f2fs/recovery.c
index 89af8407b667..8ab5c8ea2077 100644
--- a/fs/f2fs/recovery.c
+++ b/fs/f2fs/recovery.c
@@ -13,6 +13,7 @@
 #include "f2fs.h"
 #include "node.h"
 #include "segment.h"
+#include "iextent.h"
 
 /*
  * Roll forward recovery scenarios.
@@ -665,6 +666,20 @@ static int do_recover_data(struct f2fs_sb_info *sbi, struct inode *inode,
 		}
 		goto out;
 	}
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+	if (f2fs_iext_support_inline_extent(inode,
+			dn.inode_folio)) {
+		f2fs_bug_on(F2FS_I_SB(inode),
+				!dn.inode_folio_locked);
+		f2fs_bug_on(F2FS_I_SB(inode),
+				!folio_test_locked(dn.inode_folio));
+		f2fs_folio_wait_writeback(dn.inode_folio, NODE, true, true);
+		if (EXT_ENTRY_COUNT(iext_get_header(dn.inode_folio)) > 0) {
+			f2fs_iext_init_inline_extent(inode, dn.inode_folio);
+			folio_mark_dirty(dn.inode_folio);
+		}
+	}
+#endif
 
 	f2fs_folio_wait_writeback(dn.node_folio, NODE, true, true);
 
diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c
index afca2dad4da8..3696b5d8bb80 100644
--- a/fs/f2fs/super.c
+++ b/fs/f2fs/super.c
@@ -37,6 +37,7 @@
 #include "xattr.h"
 #include "gc.h"
 #include "iostat.h"
+#include "iextent.h"
 
 #define CREATE_TRACE_POINTS
 #include <trace/events/f2fs.h>
@@ -235,6 +236,7 @@ enum {
 	Opt_jqfmt,
 	Opt_checkpoint,
 	Opt_lookup_mode,
+	Opt_inline_extent_size,
 	Opt_err,
 };
 
@@ -320,6 +322,7 @@ static const struct fs_parameter_spec f2fs_param_specs[] = {
 	fsparam_s32("inline_xattr_size", Opt_inline_xattr_size),
 	fsparam_flag_no("inline_data", Opt_inline_data),
 	fsparam_flag_no("inline_dentry", Opt_inline_dentry),
+	fsparam_u32("inline_extent_size", Opt_inline_extent_size),
 	fsparam_flag_no("flush_merge", Opt_flush_merge),
 	fsparam_flag_no("barrier", Opt_barrier),
 	fsparam_flag("fastboot", Opt_fastboot),
@@ -404,6 +407,7 @@ static match_table_t f2fs_checkpoint_tokens = {
 #define F2FS_SPEC_errors			(1 << 23)
 #define F2FS_SPEC_lookup_mode			(1 << 24)
 #define F2FS_SPEC_reserve_node			(1 << 25)
+#define F2FS_SPEC_inline_extent_size		(1 << 26)
 
 struct f2fs_fs_context {
 	struct f2fs_mount_info info;
@@ -850,6 +854,13 @@ static int f2fs_parse_param(struct fs_context *fc, struct fs_parameter *param)
 		F2FS_CTX_INFO(ctx).inline_xattr_size = result.int_32;
 		ctx->spec_mask |= F2FS_SPEC_inline_xattr_size;
 		break;
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+	case Opt_inline_extent_size:
+		ctx_set_opt(ctx, F2FS_MOUNT_INLINE_EXTENT_SIZE);
+		F2FS_CTX_INFO(ctx).inline_extent_size = result.uint_32;
+		ctx->spec_mask |= F2FS_SPEC_inline_extent_size;
+		break;
+#endif
 #else
 	case Opt_user_xattr:
 	case Opt_inline_xattr:
@@ -1763,6 +1774,8 @@ static void f2fs_apply_options(struct fs_context *fc, struct super_block *sb)
 		F2FS_OPTION(sbi).errors = F2FS_CTX_INFO(ctx).errors;
 	if (ctx->spec_mask & F2FS_SPEC_lookup_mode)
 		F2FS_OPTION(sbi).lookup_mode = F2FS_CTX_INFO(ctx).lookup_mode;
+	if (ctx->spec_mask & F2FS_SPEC_inline_extent_size)
+		F2FS_OPTION(sbi).inline_extent_size = F2FS_CTX_INFO(ctx).inline_extent_size;
 
 	f2fs_apply_compression(fc, sb);
 	f2fs_apply_test_dummy_encryption(fc, sb);
@@ -2432,6 +2445,11 @@ static int f2fs_show_options(struct seq_file *seq, struct dentry *root)
 		seq_puts(seq, ",inline_dentry");
 	else
 		seq_puts(seq, ",noinline_dentry");
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+	if (test_opt(sbi, INLINE_EXTENT_SIZE))
+		seq_printf(seq, ",inline_extent_size=%u",
+				F2FS_OPTION(sbi).inline_extent_size);
+#endif
 	if (test_opt(sbi, FLUSH_MERGE))
 		seq_puts(seq, ",flush_merge");
 	else
@@ -5139,6 +5157,11 @@ static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc)
 	/* disallow all the data/node/meta page writes */
 	set_sbi_flag(sbi, SBI_POR_DOING);
 
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+	err = f2fs_iext_info_init(sbi);
+	if (err)
+		goto free_bio_info;
+#endif
 	err = f2fs_init_write_merge_io(sbi);
 	if (err)
 		goto free_bio_info;
@@ -5499,6 +5522,9 @@ static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc)
 	utf8_unload(sb->s_encoding);
 	sb->s_encoding = NULL;
 #endif
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+	f2fs_iext_info_destroy(sbi);
+#endif
 free_options:
 #ifdef CONFIG_QUOTA
 	for (i = 0; i < MAXQUOTAS; i++)
@@ -5595,6 +5621,9 @@ static void kill_f2fs_super(struct super_block *sb)
 		destroy_device_list(sbi);
 #ifdef CONFIG_DEBUG_LOCK_ALLOC
 		lockdep_unregister_key(&sbi->cp_global_sem_key);
+#endif
+#ifdef CONFIG_F2FS_INLINE_EXTENT
+		f2fs_iext_info_destroy(sbi);
 #endif
 		kfree(sbi);
 		sb->s_fs_info = NULL;
diff --git a/include/linux/f2fs_fs.h b/include/linux/f2fs_fs.h
index bb2b6cd5d507..e8d224fa926f 100644
--- a/include/linux/f2fs_fs.h
+++ b/include/linux/f2fs_fs.h
@@ -330,7 +330,8 @@ struct f2fs_inode {
 			__le32 i_inode_checksum;/* inode meta checksum */
 			__le64 i_crtime;	/* creation time */
 			__le32 i_crtime_nsec;	/* creation time in nano scale */
-			__le64 i_compr_blocks;	/* # of compressed blocks */
+			__le32 i_compr_blocks;	/* # of compressed blocks */
+			__le32 i_inline_ext_capacity;	/* # of inline extent entries */
 			__u8 i_compress_algorithm;	/* compress algorithm */
 			__u8 i_log_cluster_size;	/* log of cluster size */
 			__le16 i_compress_flag;		/* compress flag */
-- 
2.43.0

_______________________________________________
Linux-f2fs-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/linux-f2fs-devel