[PATCH v3 2/3] f2fs: support encrypted inline data

From: LiaoYuanhong-vivo

Date: Mon Jun 15 2026 - 08:55:57 EST


F2FS normally disables inline data for encrypted regular files because the
inline payload is stored in the inode block and does not pass through the
regular fscrypt data I/O path. This wastes space for small encrypted files
on filesystems that otherwise use inline_data.

Add encrypted inline data support for encrypted regular files. When the
encrypted_inline_data on-disk feature is enabled, inline payloads of
encrypted regular files are stored as ciphertext in the inode block. They
are decrypted into page-cache plaintext on read and encrypted before being
copied back into the inode block on write.

F2FS keeps the on-disk format decision separate from fscrypt key
capability. It uses fscrypt_supports_data_unit_inplace() when deciding
whether a new file may keep inline_data. It calls
fscrypt_prepare_data_unit_inplace() only when the encrypted inline payload
is actually read or written.

Update inline-data size checks to use the encrypted inline capacity, since
the stored payload is rounded to the fscrypt contents alignment. If an
encrypted inline-data file is truncated from a non-zero offset, convert it
to normal data blocks first and then use the normal truncate path.
Recovery copies inline payloads as on-disk bytes.

Signed-off-by: LiaoYuanhong-vivo <liaoyuanhong@xxxxxxxx>
---
Changes in v3:
- Follow the updated fscrypt key support matrix.
- Clean up the inline-data conversion error path.

fs/f2fs/Kconfig | 14 +++++
fs/f2fs/data.c | 8 +--
fs/f2fs/f2fs.h | 37 ++++++++++++-
fs/f2fs/file.c | 24 ++++++++-
fs/f2fs/inline.c | 134 ++++++++++++++++++++++++++++++++++++++++++-----
fs/f2fs/super.c | 12 +++++
fs/f2fs/sysfs.c | 8 +++
7 files changed, 217 insertions(+), 20 deletions(-)

diff --git a/fs/f2fs/Kconfig b/fs/f2fs/Kconfig
index 5916a02fb46d..0220f23be56d 100644
--- a/fs/f2fs/Kconfig
+++ b/fs/f2fs/Kconfig
@@ -92,6 +92,20 @@ config F2FS_FAULT_INJECTION

If unsure, say N.

+config F2FS_FS_ENCRYPTED_INLINE_DATA
+ bool "F2FS encrypted inline data support"
+ depends on F2FS_FS && FS_ENCRYPTION
+ help
+ Allow encrypted regular files to keep inline data inside the inode
+ while encrypting that inode-managed payload in software.
+
+ This does not change normal data block encryption. Normal data
+ blocks continue to use the existing fscrypt path, such as blk-crypto
+ when inline encryption is enabled.
+
+ Filesystems carrying the encrypted_inline_data incompat feature
+ require this option in order to be mounted correctly.
+
config F2FS_FS_COMPRESSION
bool "F2FS compression feature"
depends on F2FS_FS
diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c
index 657fd5986c73..9371ffa7c96d 100644
--- a/fs/f2fs/data.c
+++ b/fs/f2fs/data.c
@@ -3700,7 +3700,7 @@ static int prepare_write_begin(struct f2fs_sb_info *sbi,

/* f2fs_lock_op avoids race between write CP and convert_inline_page */
if (f2fs_has_inline_data(inode)) {
- if (pos + len > MAX_INLINE_DATA(inode))
+ if (pos + len > f2fs_max_inline_data(inode))
flag = F2FS_GET_BLOCK_DEFAULT;
f2fs_map_lock(sbi, &lc, flag);
locked = true;
@@ -3720,8 +3720,10 @@ static int prepare_write_begin(struct f2fs_sb_info *sbi,
set_new_dnode(&dn, inode, ifolio, ifolio, 0);

if (f2fs_has_inline_data(inode)) {
- if (pos + len <= MAX_INLINE_DATA(inode)) {
- f2fs_do_read_inline_data(folio, ifolio);
+ if (pos + len <= f2fs_max_inline_data(inode)) {
+ err = f2fs_do_read_inline_data(folio, ifolio);
+ if (err)
+ goto out;
set_inode_flag(inode, FI_DATA_EXIST);
if (inode->i_nlink)
folio_set_f2fs_inline(ifolio);
diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h
index 832b2f8beb11..0a2d75baf23e 100644
--- a/fs/f2fs/f2fs.h
+++ b/fs/f2fs/f2fs.h
@@ -276,6 +276,7 @@ struct f2fs_mount_info {
#define F2FS_FEATURE_RO 0x00004000
#define F2FS_FEATURE_DEVICE_ALIAS 0x00008000
#define F2FS_FEATURE_PACKED_SSA 0x00010000
+#define F2FS_FEATURE_ENCRYPTED_INLINE_DATA 0x00020000

#define __F2FS_HAS_FEATURE(raw_super, mask) \
((raw_super->feature & cpu_to_le32(mask)) != 0)
@@ -4502,7 +4503,7 @@ extern struct kmem_cache *f2fs_inode_entry_slab;
bool f2fs_may_inline_data(struct inode *inode);
bool f2fs_sanity_check_inline_data(struct inode *inode, struct folio *ifolio);
bool f2fs_may_inline_dentry(struct inode *inode);
-void f2fs_do_read_inline_data(struct folio *folio, struct folio *ifolio);
+int f2fs_do_read_inline_data(struct folio *folio, struct folio *ifolio);
void f2fs_truncate_inline_inode(struct inode *inode, struct folio *ifolio,
u64 from);
int f2fs_read_inline_data(struct inode *inode, struct folio *folio);
@@ -4595,6 +4596,39 @@ static inline bool f2fs_encrypted_file(struct inode *inode)
return IS_ENCRYPTED(inode) && S_ISREG(inode->i_mode);
}

+static inline bool f2fs_sb_has_encrypted_inline_data(struct f2fs_sb_info *sbi);
+
+static inline bool f2fs_uses_encrypted_inline_data(struct inode *inode)
+{
+#ifdef CONFIG_F2FS_FS_ENCRYPTED_INLINE_DATA
+ /*
+ * When the filesystem allows encrypted inline data, inline payloads
+ * in encrypted regular files are interpreted as ciphertext.
+ */
+ return f2fs_sb_has_encrypted_inline_data(F2FS_I_SB(inode)) &&
+ f2fs_encrypted_file(inode);
+#else
+ return false;
+#endif
+}
+
+static inline unsigned int f2fs_max_inline_data(struct inode *inode)
+{
+ unsigned int max_bytes = MAX_INLINE_DATA(inode);
+
+ /*
+ * Encrypted inline data is rounded up to the fscrypt contents
+ * alignment before being stored back into the inode. This is an
+ * on-disk layout constraint, so it must not depend on whether the
+ * inode's key has been prepared yet.
+ */
+#ifdef CONFIG_F2FS_FS_ENCRYPTED_INLINE_DATA
+ if (f2fs_uses_encrypted_inline_data(inode))
+ max_bytes = round_down(max_bytes, FSCRYPT_CONTENTS_ALIGNMENT);
+#endif
+ return max_bytes;
+}
+
static inline void f2fs_set_encrypted_inode(struct inode *inode)
{
#ifdef CONFIG_FS_ENCRYPTION
@@ -4827,6 +4861,7 @@ F2FS_FEATURE_FUNCS(compression, COMPRESSION);
F2FS_FEATURE_FUNCS(readonly, RO);
F2FS_FEATURE_FUNCS(device_alias, DEVICE_ALIAS);
F2FS_FEATURE_FUNCS(packed_ssa, PACKED_SSA);
+F2FS_FEATURE_FUNCS(encrypted_inline_data, ENCRYPTED_INLINE_DATA);

#ifdef CONFIG_BLK_DEV_ZONED
static inline bool f2fs_zone_is_seq(struct f2fs_sb_info *sbi, int devi,
diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c
index 71385ca4163d..ec243bb9039b 100644
--- a/fs/f2fs/file.c
+++ b/fs/f2fs/file.c
@@ -825,12 +825,32 @@ int f2fs_do_truncate_blocks(struct inode *inode, u64 from, bool lock)
}

if (f2fs_has_inline_data(inode)) {
+ if (f2fs_uses_encrypted_inline_data(inode) && from) {
+ f2fs_folio_put(ifolio, true);
+ if (lock)
+ f2fs_unlock_op(sbi, &lc);
+
+ err = f2fs_convert_inline_inode(inode);
+
+ if (lock)
+ f2fs_lock_op(sbi, &lc);
+ if (err)
+ goto out;
+
+ ifolio = f2fs_get_inode_folio(sbi, inode->i_ino);
+ if (IS_ERR(ifolio)) {
+ err = PTR_ERR(ifolio);
+ goto out;
+ }
+ goto truncate_blocks;
+ }
f2fs_truncate_inline_inode(inode, ifolio, from);
f2fs_folio_put(ifolio, true);
truncate_page = true;
goto out;
}

+truncate_blocks:
set_new_dnode(&dn, inode, ifolio, NULL, 0);
err = f2fs_get_dnode_of_data(&dn, free_from, LOOKUP_NODE_RA);
if (err) {
@@ -1147,7 +1167,7 @@ int f2fs_setattr(struct mnt_idmap *idmap, struct dentry *dentry,
if (attr->ia_valid & ATTR_SIZE) {
loff_t old_size = i_size_read(inode);

- if (attr->ia_size > MAX_INLINE_DATA(inode)) {
+ if (attr->ia_size > f2fs_max_inline_data(inode)) {
/*
* should convert inline inode before i_size_write to
* keep smaller than inline_data size with inline flag.
@@ -5007,7 +5027,7 @@ static int f2fs_preallocate_blocks(struct kiocb *iocb, struct iov_iter *iter,

if (f2fs_has_inline_data(inode)) {
/* If the data will fit inline, don't bother. */
- if (pos + count <= MAX_INLINE_DATA(inode))
+ if (pos + count <= f2fs_max_inline_data(inode))
return 0;
ret = f2fs_convert_inline_inode(inode);
if (ret)
diff --git a/fs/f2fs/inline.c b/fs/f2fs/inline.c
index 099f72089701..85846f2e7866 100644
--- a/fs/f2fs/inline.c
+++ b/fs/f2fs/inline.c
@@ -21,7 +21,7 @@ static bool support_inline_data(struct inode *inode)
return false;
if (!S_ISREG(inode->i_mode) && !S_ISLNK(inode->i_mode))
return false;
- if (i_size_read(inode) > MAX_INLINE_DATA(inode))
+ if (i_size_read(inode) > f2fs_max_inline_data(inode))
return false;
return true;
}
@@ -31,6 +31,9 @@ bool f2fs_may_inline_data(struct inode *inode)
if (!support_inline_data(inode))
return false;

+ if (f2fs_uses_encrypted_inline_data(inode))
+ return fscrypt_supports_data_unit_inplace(inode);
+
return !f2fs_post_read_required(inode);
}

@@ -65,7 +68,9 @@ bool f2fs_sanity_check_inline_data(struct inode *inode, struct folio *ifolio)
* been synchronized to inmem fields.
*/
return (S_ISREG(inode->i_mode) &&
- (file_is_encrypt(inode) || file_is_verity(inode) ||
+ ((file_is_encrypt(inode) &&
+ !f2fs_sb_has_encrypted_inline_data(F2FS_I_SB(inode))) ||
+ file_is_verity(inode) ||
(F2FS_I(inode)->i_flags & F2FS_COMPR_FL)));
}

@@ -80,22 +85,66 @@ bool f2fs_may_inline_dentry(struct inode *inode)
return true;
}

-void f2fs_do_read_inline_data(struct folio *folio, struct folio *ifolio)
+int f2fs_do_read_inline_data(struct folio *folio, struct folio *ifolio)
{
struct inode *inode = folio->mapping->host;
+ unsigned int len = min_t(loff_t, i_size_read(inode),
+ f2fs_max_inline_data(inode));

if (folio_test_uptodate(folio))
- return;
+ return 0;

f2fs_bug_on(F2FS_I_SB(inode), folio->index);

- folio_zero_segment(folio, MAX_INLINE_DATA(inode), folio_size(folio));
+ if (f2fs_uses_encrypted_inline_data(inode)) {
+ struct page *tmp_page;
+ void *kaddr;
+ int err;
+
+ folio_zero_segment(folio, 0, folio_size(folio));
+
+ /*
+ * Decrypt through a temporary page because inline data occupies
+ * only a byte range inside the inode folio.
+ */
+ tmp_page = alloc_page(GFP_NOFS | __GFP_ZERO);
+ if (!tmp_page)
+ return -ENOMEM;
+
+ len = round_up(len, FSCRYPT_CONTENTS_ALIGNMENT);
+ if (len) {
+ err = fscrypt_prepare_data_unit_inplace(inode);
+ if (err) {
+ __free_page(tmp_page);
+ return err;
+ }
+ memcpy_to_page(tmp_page, 0, inline_data_addr(inode, ifolio),
+ len);
+ err = fscrypt_crypt_data_unit_inplace(inode, tmp_page,
+ len, 0, 0,
+ false);
+ if (err) {
+ __free_page(tmp_page);
+ return err;
+ }
+ }

- /* Copy the whole inline data block */
- memcpy_to_folio(folio, 0, inline_data_addr(inode, ifolio),
- MAX_INLINE_DATA(inode));
+ kaddr = kmap_local_page(tmp_page);
+ memcpy_to_folio(folio, 0, kaddr,
+ min_t(loff_t, i_size_read(inode),
+ f2fs_max_inline_data(inode)));
+ kunmap_local(kaddr);
+ __free_page(tmp_page);
+ } else {
+ folio_zero_segment(folio, MAX_INLINE_DATA(inode),
+ folio_size(folio));
+ /* Copy the whole inline data block */
+ memcpy_to_folio(folio, 0, inline_data_addr(inode, ifolio),
+ MAX_INLINE_DATA(inode));
+ }
if (!folio_test_uptodate(folio))
folio_mark_uptodate(folio);
+ return 0;
}

void f2fs_truncate_inline_inode(struct inode *inode, struct folio *ifolio,
@@ -119,6 +168,7 @@ void f2fs_truncate_inline_inode(struct inode *inode, struct folio *ifolio,
int f2fs_read_inline_data(struct inode *inode, struct folio *folio)
{
struct folio *ifolio;
+ int ret = 0;

ifolio = f2fs_get_inode_folio(F2FS_I_SB(inode), inode->i_ino);
if (IS_ERR(ifolio)) {
@@ -134,7 +184,13 @@ int f2fs_read_inline_data(struct inode *inode, struct folio *folio)
if (folio->index)
folio_zero_segment(folio, 0, folio_size(folio));
else
- f2fs_do_read_inline_data(folio, ifolio);
+ ret = f2fs_do_read_inline_data(folio, ifolio);
+
+ if (!folio->index && ret) {
+ f2fs_folio_put(ifolio, true);
+ folio_unlock(folio);
+ return ret;
+ }

if (!folio_test_uptodate(folio))
folio_mark_uptodate(folio);
@@ -186,7 +242,12 @@ int f2fs_convert_inline_folio(struct dnode_of_data *dn, struct folio *folio)

f2fs_bug_on(F2FS_F_SB(folio), folio_test_writeback(folio));

- f2fs_do_read_inline_data(folio, dn->inode_folio);
+ err = f2fs_do_read_inline_data(folio, dn->inode_folio);
+ if (err) {
+ f2fs_truncate_data_blocks_range(dn, 1);
+ f2fs_put_dnode(dn);
+ return err;
+ }
folio_mark_dirty(folio);

/* clear dirty state */
@@ -267,6 +328,8 @@ int f2fs_write_inline_data(struct inode *inode, struct folio *folio)
{
struct f2fs_sb_info *sbi = F2FS_I_SB(inode);
struct folio *ifolio;
+ void *inline_addr;
+ int err = 0;

ifolio = f2fs_get_inode_folio(sbi, inode->i_ino);
if (IS_ERR(ifolio))
@@ -280,8 +343,50 @@ int f2fs_write_inline_data(struct inode *inode, struct folio *folio)
f2fs_bug_on(F2FS_I_SB(inode), folio->index);

f2fs_folio_wait_writeback(ifolio, NODE, true, true);
- memcpy_from_folio(inline_data_addr(inode, ifolio),
- folio, 0, MAX_INLINE_DATA(inode));
+ inline_addr = inline_data_addr(inode, ifolio);
+
+ if (f2fs_uses_encrypted_inline_data(inode)) {
+ struct page *tmp_page;
+ void *kaddr;
+ unsigned int len = min_t(loff_t, i_size_read(inode),
+ f2fs_max_inline_data(inode));
+
+ tmp_page = alloc_page(GFP_NOFS | __GFP_ZERO);
+ if (!tmp_page) {
+ err = -ENOMEM;
+ goto out;
+ }
+
+ len = round_up(len, FSCRYPT_CONTENTS_ALIGNMENT);
+ if (len) {
+ err = fscrypt_prepare_data_unit_inplace(inode);
+ if (err) {
+ __free_page(tmp_page);
+ goto out;
+ }
+ kaddr = kmap_local_page(tmp_page);
+ memcpy_from_folio(kaddr, folio, 0,
+ min_t(loff_t, i_size_read(inode),
+ f2fs_max_inline_data(inode)));
+ kunmap_local(kaddr);
+ err = fscrypt_crypt_data_unit_inplace(inode, tmp_page,
+ len, 0, 0,
+ true);
+ }
+ if (!err) {
+ memset(inline_addr, 0, MAX_INLINE_DATA(inode));
+ if (len) {
+ kaddr = kmap_local_page(tmp_page);
+ memcpy(inline_addr, kaddr, len);
+ kunmap_local(kaddr);
+ }
+ }
+ __free_page(tmp_page);
+ if (err)
+ goto out;
+ } else {
+ memcpy_from_folio(inline_addr, folio, 0, MAX_INLINE_DATA(inode));
+ }
folio_mark_dirty(ifolio);

f2fs_clear_page_cache_dirty_tag(folio);
@@ -290,8 +395,9 @@ int f2fs_write_inline_data(struct inode *inode, struct folio *folio)
set_inode_flag(inode, FI_DATA_EXIST);

folio_clear_f2fs_inline(ifolio);
+out:
f2fs_folio_put(ifolio, true);
- return 0;
+ return err;
}

int f2fs_recover_inline_data(struct inode *inode, struct folio *nfolio)
@@ -826,7 +932,7 @@ int f2fs_inline_data_fiemap(struct inode *inode,
return PTR_ERR(ifolio);
f2fs_folio_wait_writeback(ifolio, NODE, true, true);
}
- ilen = min_t(size_t, MAX_INLINE_DATA(inode), i_size_read(inode));
+ ilen = min_t(size_t, f2fs_max_inline_data(inode), i_size_read(inode));
if (start >= ilen)
goto out;
if (start + len < ilen)
diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c
index c6afdbd6e1cd..9eddcde7939c 100644
--- a/fs/f2fs/super.c
+++ b/fs/f2fs/super.c
@@ -1549,6 +1549,18 @@ static int f2fs_check_opt_consistency(struct fs_context *fc,
return -EINVAL;
}

+ if (f2fs_sb_has_encrypted_inline_data(sbi)) {
+ if (!IS_ENABLED(CONFIG_F2FS_FS_ENCRYPTED_INLINE_DATA)) {
+ f2fs_err(sbi,
+ "encrypted_inline_data requires CONFIG_F2FS_FS_ENCRYPTED_INLINE_DATA");
+ return -EINVAL;
+ }
+ if (!f2fs_sb_has_encrypt(sbi)) {
+ f2fs_err(sbi, "encrypted inline_data requires encryption feature");
+ return -EINVAL;
+ }
+ }
+
/*
* The BLKZONED feature indicates that the drive was formatted with
* zone alignment optimization. This is optional for host-aware
diff --git a/fs/f2fs/sysfs.c b/fs/f2fs/sysfs.c
index 665687244c93..600eaee75926 100644
--- a/fs/f2fs/sysfs.c
+++ b/fs/f2fs/sysfs.c
@@ -1399,6 +1399,9 @@ F2FS_FEATURE_RO_ATTR(pin_file);
F2FS_FEATURE_RO_ATTR(linear_lookup);
#endif
F2FS_FEATURE_RO_ATTR(packed_ssa);
+#ifdef CONFIG_F2FS_FS_ENCRYPTED_INLINE_DATA
+F2FS_FEATURE_RO_ATTR(encrypted_inline_data);
+#endif
F2FS_FEATURE_RO_ATTR(fserror);

#define ATTR_LIST(name) (&f2fs_attr_##name.attr)
@@ -1567,6 +1570,9 @@ static struct attribute *f2fs_feat_attrs[] = {
BASE_ATTR_LIST(linear_lookup),
#endif
BASE_ATTR_LIST(packed_ssa),
+#ifdef CONFIG_F2FS_FS_ENCRYPTED_INLINE_DATA
+ BASE_ATTR_LIST(encrypted_inline_data),
+#endif
BASE_ATTR_LIST(fserror),
NULL,
};
@@ -1604,6 +1610,7 @@ F2FS_SB_FEATURE_RO_ATTR(compression, COMPRESSION);
F2FS_SB_FEATURE_RO_ATTR(readonly, RO);
F2FS_SB_FEATURE_RO_ATTR(device_alias, DEVICE_ALIAS);
F2FS_SB_FEATURE_RO_ATTR(packed_ssa, PACKED_SSA);
+F2FS_SB_FEATURE_RO_ATTR(encrypted_inline_data, ENCRYPTED_INLINE_DATA);

static struct attribute *f2fs_sb_feat_attrs[] = {
ATTR_LIST(sb_encryption),
@@ -1622,6 +1629,7 @@ static struct attribute *f2fs_sb_feat_attrs[] = {
ATTR_LIST(sb_readonly),
ATTR_LIST(sb_device_alias),
ATTR_LIST(sb_packed_ssa),
+ ATTR_LIST(sb_encrypted_inline_data),
NULL,
};
ATTRIBUTE_GROUPS(f2fs_sb_feat);
--
2.34.1