* Re: [PATCH v2] Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
2026-08-17 3:12 ` [PATCH v2] " Theodore Tso
@ 2026-08-17 8:24 ` shuo chen
2026-08-22 16:26 ` [PATCH] " shuo chen
2026-08-22 16:28 ` [PATCH V3] " shuo chen
2 siblings, 0 replies; 17+ messages in thread
From: shuo chen @ 2026-08-17 8:24 UTC (permalink / raw)
To: Theodore Tso; +Cc: adilger.kernel, libaokun, ack, ojaswin, yi.zhang, linux-ext4
On Sun, Aug 16, 2026 at 11:12:33PM -0400, Theodore Tso wrote:
> Since you are making changes to inline_data feature, you'd use it
> soemthing like this:
>
> % cd /usr/src/linux # replace with your kernel sources
> % install-kconfig
> % kbuild
> % kvm-xfstests -c ext4/inline -g auto
Thank you very much for your detailed guidance on the testing process.
I will fix the issues reported by Sashiko and Syzbot.
>
> You can also add some options to install-kconfig to enable extra
> debugging, of which the most interesting are --kasan and --lockdep. I
> would recommend doing your initial testing using just the plain
> install-kconfig, and then once you are getting clean run, do separate
> runs with the Kernel Address Sanitizer and the runtime locking
> correctness validator.
I'll run the tests following your recommended sequence.
>
> The way I do my testing using gce-xfstests is to use a command like
> this: "gce-xfstests ltm -c ext4/all -g auto" which will launch 12
> VM's, for a variety of ext4 test scenarios (e.g., with bigalloc, fast
> commit, 1k block sizes, etc.) You can of course try running
> "kvm-xfstests -c ext4/all -g auto" but this will likely take around
> 24-36 hours depending on how fast your local storage test devices.
> (Using gce-xfstest's lightweight test manager takes around 2 hours of
> wall clock time, since the test VM's run in parallel.)
Regarding gce-xfstests, I don't have Google Cloud access at the moment,
but I'll also try running "kvm-xfstests -c ext4/all -g auto" locally to
cover more scenarios.
I'll send the v3 patch along with the test results once everything is
ready.
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH] Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
2026-08-17 3:12 ` [PATCH v2] " Theodore Tso
2026-08-17 8:24 ` shuo chen
@ 2026-08-22 16:26 ` shuo chen
2026-08-22 16:43 ` sashiko-bot
2026-08-22 16:28 ` [PATCH V3] " shuo chen
2 siblings, 1 reply; 17+ messages in thread
From: shuo chen @ 2026-08-22 16:26 UTC (permalink / raw)
To: tytso
Cc: adilger.kernel, libaokun, ack, ojaswin, yi.zhang, linux-ext4,
pipishuo
From: pipishuo <1289151713@qq.com>
---
v2 -> v3:
-Add error recovery for ext4_find_extent() and ext4_ext_insert_extent()
-Add additional error handling code
---
Signed-off-by: pipishuo <1289151713@qq.com>
---
fs/ext4/inline.c | 179 ++++++++++++++++++++++++++++++++++++-----------
1 file changed, 139 insertions(+), 40 deletions(-)
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 8045e4ff270c..507106c72e82 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -14,6 +14,7 @@
#include "ext4.h"
#include "xattr.h"
#include "truncate.h"
+#include "ext4_extents.h"
#define EXT4_XATTR_SYSTEM_DATA "data"
#define EXT4_MIN_INLINE_DATA_SIZE ((sizeof(__le32) * EXT4_N_BLOCKS))
@@ -1070,21 +1071,115 @@ 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)
+static int ext4_set_inline_data_block(handle_t *handle, struct inode *inode, ext4_fsblk_t block,
+ unsigned int len, struct buffer_head *bh)
{
- int ret;
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ struct ext4_xattr_ibody_find is = {
+ .s = { .not_found = 0, },
+ };
+ struct ext4_xattr_info i = {
+ .name_index = EXT4_XATTR_INDEX_SYSTEM,
+ .name = EXT4_XATTR_SYSTEM_DATA,
+ .value = NULL,
+ .value_len = 0,
+ };
+ int error;
+ int inode_size;
+ void *inode_buf = NULL;
+ struct ext4_inode *raw_inode;
- 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;
+ inode_size = EXT4_INODE_SIZE(inode->i_sb);
+ inode_buf = kmalloc(inode_size, GFP_NOFS);
+ if (!inode_buf) {
+ error = -ENOMEM;
+ down_write(&ei->i_data_sem);
+ ext4_discard_preallocations(inode);
+ ext4_free_blocks(handle, inode, bh, block, len, EXT4_FREE_BLOCKS_FORGET);
+ up_write(&ei->i_data_sem);
+ return error;
}
- ext4_write_inline_data(inode, iloc, buf, 0, inline_size);
- ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ down_write(&ei->i_data_sem);
+ error = ext4_get_inode_loc(inode, &is.iloc);
+ if (error) {
+ ext4_discard_preallocations(inode);
+ ext4_free_blocks(handle, inode, bh, block, len, EXT4_FREE_BLOCKS_FORGET);
+ up_write(&ei->i_data_sem);
+ kfree(inode_buf);
+ return error;
+ }
+ raw_inode = ext4_raw_inode(&is.iloc);
+ BUFFER_TRACE(is.iloc.bh, "get_write_access");
+ error = ext4_journal_get_write_access(handle, inode->i_sb, is.iloc.bh, EXT4_JTR_NONE);
+ if (error) {
+ ext4_discard_preallocations(inode);
+ ext4_free_blocks(handle, inode, bh, block, len, EXT4_FREE_BLOCKS_FORGET);
+ goto out;
+ }
+ memcpy(inode_buf, (void *)raw_inode, inode_size);
+ memset((void *)raw_inode->i_block, 0, EXT4_MIN_INLINE_DATA_SIZE);
+ memset(ei->i_data, 0, EXT4_MIN_INLINE_DATA_SIZE);
+ if (ext4_has_feature_extents(inode->i_sb) &&
+ (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode) || S_ISLNK(inode->i_mode))) {
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ext4_ext_tree_init(handle, inode);
+ struct ext4_ext_path *path = ext4_find_extent(inode, 0, NULL, 0);
+
+ if (IS_ERR(path)) {
+ error = PTR_ERR(path);
+ goto recovery;
+ }
+ struct ext4_extent newex;
+
+ newex.ee_block = cpu_to_le32(0);
+ newex.ee_len = cpu_to_le16(1);
+ ext4_ext_store_pblock(&newex, block);
+ path = ext4_ext_insert_extent(handle, inode, path, &newex, 0);
+ if (IS_ERR(path)) {
+ error = PTR_ERR(path);
+ if (error == -EDQUOT || error == -ENOSPC) {
+ goto recovery;
+ } else {
+ ext4_forget(handle, 0, inode, bh, block);
+ goto nofree;
+ }
+ } else {
+ ext4_free_ext_path(path);
+ }
+ } else {
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(block);
+ }
+ error = ext4_xattr_ibody_find(inode, &i, &is);
+ if (error)
+ goto recovery;
+ if (!is.s.not_found)
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+recovery:
+ if (error) {
+ ext4_discard_preallocations(inode);
+ ext4_free_blocks(handle, inode, bh, block, len, EXT4_FREE_BLOCKS_FORGET);
+nofree:
+ memcpy((void *)raw_inode, inode_buf, inode_size);
+ memcpy(ei->i_data, raw_inode->i_block, EXT4_MIN_INLINE_DATA_SIZE);
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ } else {
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ get_bh(is.iloc.bh);
+ error = ext4_mark_iloc_dirty(handle, inode, &is.iloc);
+ EXT4_I(inode)->i_inline_off = 0;
+ EXT4_I(inode)->i_inline_size = 0;
+ ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ if (S_ISDIR(inode->i_mode)) {
+ i_size_write(inode, inode->i_sb->s_blocksize);
+ EXT4_I(inode)->i_disksize = inode->i_sb->s_blocksize;
+ }
+ brelse(bh);
+ }
+out:
+ brelse(is.iloc.bh);
+ up_write(&ei->i_data_sem);
+ kfree(inode_buf);
+ return error;
}
static int ext4_convert_inline_data_nolock(handle_t *handle,
@@ -1094,8 +1189,11 @@ 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;
int inline_size;
+ ext4_fsblk_t newblock = 0;
+ struct ext4_allocation_request ar;
+ struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+ unsigned int allocated_block;
inline_size = ext4_get_inline_size(inode);
buf = kmalloc(inline_size, GFP_NOFS);
@@ -1120,25 +1218,22 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
goto out;
}
- error = ext4_destroy_inline_data_nolock(handle, inode);
- 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);
+ memset(&ar, 0, sizeof(ar));
+ ar.inode = inode;
+ ar.logical = 0;
+ ar.len = 1;
+ if (S_ISREG(inode->i_mode))
+ ar.flags = EXT4_MB_HINT_DATA;
+ else
+ ar.flags = 0;
+ newblock = ext4_mb_new_blocks(handle, &ar, &error);
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);
+ goto out;
+ allocated_block = EXT4_C2B(sbi, ar.len);
+ data_bh = sb_getblk(inode->i_sb, newblock);
if (!data_bh) {
error = -ENOMEM;
- goto out_restore;
+ goto out_bh;
}
lock_buffer(data_bh);
@@ -1147,7 +1242,7 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
if (error) {
unlock_buffer(data_bh);
error = -EIO;
- goto out_restore;
+ goto out_bh;
}
memset(data_bh->b_data, 0, inode->i_sb->s_blocksize);
@@ -1159,24 +1254,28 @@ static int ext4_convert_inline_data_nolock(handle_t *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);
}
+out_bh:
+ if (error) {
+ int flags = 0;
-out_restore:
- if (error)
- ext4_restore_inline_data(handle, inode, iloc, buf, inline_size);
+ if (data_bh)
+ flags |= EXT4_FREE_BLOCKS_FORGET;
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ down_write(&ei->i_data_sem);
+ ext4_discard_preallocations(inode);
+ ext4_free_blocks(handle, inode, data_bh, newblock, allocated_block, flags);
+ up_write(&ei->i_data_sem);
+ } else {
+ error = ext4_set_inline_data_block(handle, inode,
+ newblock, allocated_block, data_bh);
+ }
out:
- brelse(data_bh);
kfree(buf);
return error;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 17+ messages in thread* Re: [PATCH] Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
2026-08-22 16:26 ` [PATCH] " shuo chen
@ 2026-08-22 16:43 ` sashiko-bot
0 siblings, 0 replies; 17+ messages in thread
From: sashiko-bot @ 2026-08-22 16:43 UTC (permalink / raw)
To: shuo chen; +Cc: tytso, linux-ext4
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] Data blocks are permanently leaked in the block bitmap when `ext4_ext_insert_extent` fails with an error other than EDQUOT or ENOSPC.
- [High] The extent insertion hardcodes a length of 1, breaking bigalloc support and leaking blocks if the cluster size is greater than the block size.
- [High] Error recovery path triggers a BUG_ON(count > 1) in ext4_free_blocks for regular files or bigalloc clusters.
- [High] Raw inode memory is reverted on error without marking the buffer dirty, leading to an inconsistent journal state.
- [High] The on-disk inode's size is not updated when converting an inline directory, leaving the directory with a truncated size on disk.
- [Medium] The patch implementation completely contradicts the commit message.
--
--- Patch [1]: [PATCH] Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir ---
commit 60b81af153ab511900118d63a628e53fb1229564
Author: pipishuo <1289151713@qq.com>
Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
This commit refactors inline data conversion and adds error recovery for
extent insertion during block allocation.
> Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
[Severity: Medium]
The commit message claims to add a check in ext4_readdir, but the diff
completely ignores ext4_readdir and instead refactors inline data
conversion in ext4_set_inline_data_block. Can the commit message be
updated to reflect the actual changes?
> diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
> index ceee69a66482b..259a6af13b0f6 100644
> --- a/fs/ext4/inline.c
> +++ b/fs/ext4/inline.c
[ ... ]
> @@ -1079,21 +1080,115 @@ static int ext4_update_inline_dir(handle_t *handle, struct inode *dir,
[ ... ]
> + struct ext4_extent newex;
> +
> + newex.ee_block = cpu_to_le32(0);
> + newex.ee_len = cpu_to_le16(1);
[Severity: High]
Does hardcoding the extent length to 1 break bigalloc support? If the
cluster size is greater than the block size, or if multiple blocks are
allocated, the len parameter is ignored. Will the additional allocated
blocks marked in the bitmap be permanently leaked since they aren't
added to the extent tree?
> + ext4_ext_store_pblock(&newex, block);
> + path = ext4_ext_insert_extent(handle, inode, path, &newex, 0);
> + if (IS_ERR(path)) {
> + error = PTR_ERR(path);
> + if (error == -EDQUOT || error == -ENOSPC) {
> + goto recovery;
> + } else {
> + ext4_forget(handle, 0, inode, bh, block);
> + goto nofree;
[Severity: High]
If ext4_ext_insert_extent() fails with an error other than EDQUOT or
ENOSPC, the code jumps to nofree. Does this permanently leak the
allocated data blocks in the block bitmap, since the ext4_free_blocks()
call in the recovery block is bypassed?
> + }
> + } else {
> + ext4_free_ext_path(path);
> + }
[ ... ]
> +recovery:
> + if (error) {
> + ext4_discard_preallocations(inode);
> + ext4_free_blocks(handle, inode, bh, block, len, EXT4_FREE_BLOCKS_FORGET);
> +nofree:
> + memcpy((void *)raw_inode, inode_buf, inode_size);
> + memcpy(ei->i_data, raw_inode->i_block, EXT4_MIN_INLINE_DATA_SIZE);
> + ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
[Severity: High]
This reverts the raw_inode memory on error, but is the buffer ever
marked dirty again? Because ext4_ext_tree_init() previously marked the
buffer dirty in the active transaction, doesn't overwriting the memory
here without calling ext4_mark_iloc_dirty() lead to an inconsistent
journal state?
> + } else {
> + ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
> + get_bh(is.iloc.bh);
> + error = ext4_mark_iloc_dirty(handle, inode, &is.iloc);
> + EXT4_I(inode)->i_inline_off = 0;
> + EXT4_I(inode)->i_inline_size = 0;
> + ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
> + if (S_ISDIR(inode->i_mode)) {
> + i_size_write(inode, inode->i_sb->s_blocksize);
> + EXT4_I(inode)->i_disksize = inode->i_sb->s_blocksize;
> + }
[Severity: High]
Because ext4_mark_iloc_dirty() is called before the S_ISDIR size
updates, does this cause the new size to be missed in the buffer update?
Will the on-disk inode retain the old truncated inline data size?
> + brelse(bh);
> + }
[ ... ]
> @@ -1103,8 +1198,11 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
[ ... ]
> +out_bh:
> + if (error) {
> + int flags = 0;
>
> -out_restore:
> - if (error)
> - ext4_restore_inline_data(handle, inode, iloc, buf, inline_size);
> + if (data_bh)
> + flags |= EXT4_FREE_BLOCKS_FORGET;
> + struct ext4_inode_info *ei = EXT4_I(inode);
>
> + down_write(&ei->i_data_sem);
> + ext4_discard_preallocations(inode);
> + ext4_free_blocks(handle, inode, data_bh, newblock, allocated_block, flags);
[Severity: High]
Does this trigger a BUG_ON in ext4_free_blocks() on error paths? If we
allocate multiple blocks (e.g., when bigalloc is enabled or
EXT4_MB_HINT_DATA is used), allocated_block can be > 1.
Calling ext4_free_blocks() with a non-NULL data_bh, a count > 1, and the
EXT4_FREE_BLOCKS_FORGET flag will hit the BUG_ON(count > 1) in
ext4_free_blocks().
> + up_write(&ei->i_data_sem);
> + } else {
> + error = ext4_set_inline_data_block(handle, inode,
> + newblock, allocated_block, data_bh);
> + }
--
Sashiko AI review · https://sashiko.dev/#/patchset/tencent_7FDF0CFD84843032AE9A95DCBD443746860A@qq.com?part=1
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH V3] Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
2026-08-17 3:12 ` [PATCH v2] " Theodore Tso
2026-08-17 8:24 ` shuo chen
2026-08-22 16:26 ` [PATCH] " shuo chen
@ 2026-08-22 16:28 ` shuo chen
2026-08-22 16:45 ` sashiko-bot
2 siblings, 1 reply; 17+ messages in thread
From: shuo chen @ 2026-08-22 16:28 UTC (permalink / raw)
To: tytso
Cc: adilger.kernel, libaokun, ack, ojaswin, yi.zhang, linux-ext4,
pipishuo
From: pipishuo <1289151713@qq.com>
Resend as v3 because I forgot to add the version number
---
v2 -> v3:
-Add error recovery for ext4_find_extent() and ext4_ext_insert_extent()
-Add additional error handling code
---
Signed-off-by: pipishuo <1289151713@qq.com>
---
fs/ext4/inline.c | 179 ++++++++++++++++++++++++++++++++++++-----------
1 file changed, 139 insertions(+), 40 deletions(-)
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 8045e4ff270c..507106c72e82 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -14,6 +14,7 @@
#include "ext4.h"
#include "xattr.h"
#include "truncate.h"
+#include "ext4_extents.h"
#define EXT4_XATTR_SYSTEM_DATA "data"
#define EXT4_MIN_INLINE_DATA_SIZE ((sizeof(__le32) * EXT4_N_BLOCKS))
@@ -1070,21 +1071,115 @@ 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)
+static int ext4_set_inline_data_block(handle_t *handle, struct inode *inode, ext4_fsblk_t block,
+ unsigned int len, struct buffer_head *bh)
{
- int ret;
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ struct ext4_xattr_ibody_find is = {
+ .s = { .not_found = 0, },
+ };
+ struct ext4_xattr_info i = {
+ .name_index = EXT4_XATTR_INDEX_SYSTEM,
+ .name = EXT4_XATTR_SYSTEM_DATA,
+ .value = NULL,
+ .value_len = 0,
+ };
+ int error;
+ int inode_size;
+ void *inode_buf = NULL;
+ struct ext4_inode *raw_inode;
- 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;
+ inode_size = EXT4_INODE_SIZE(inode->i_sb);
+ inode_buf = kmalloc(inode_size, GFP_NOFS);
+ if (!inode_buf) {
+ error = -ENOMEM;
+ down_write(&ei->i_data_sem);
+ ext4_discard_preallocations(inode);
+ ext4_free_blocks(handle, inode, bh, block, len, EXT4_FREE_BLOCKS_FORGET);
+ up_write(&ei->i_data_sem);
+ return error;
}
- ext4_write_inline_data(inode, iloc, buf, 0, inline_size);
- ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ down_write(&ei->i_data_sem);
+ error = ext4_get_inode_loc(inode, &is.iloc);
+ if (error) {
+ ext4_discard_preallocations(inode);
+ ext4_free_blocks(handle, inode, bh, block, len, EXT4_FREE_BLOCKS_FORGET);
+ up_write(&ei->i_data_sem);
+ kfree(inode_buf);
+ return error;
+ }
+ raw_inode = ext4_raw_inode(&is.iloc);
+ BUFFER_TRACE(is.iloc.bh, "get_write_access");
+ error = ext4_journal_get_write_access(handle, inode->i_sb, is.iloc.bh, EXT4_JTR_NONE);
+ if (error) {
+ ext4_discard_preallocations(inode);
+ ext4_free_blocks(handle, inode, bh, block, len, EXT4_FREE_BLOCKS_FORGET);
+ goto out;
+ }
+ memcpy(inode_buf, (void *)raw_inode, inode_size);
+ memset((void *)raw_inode->i_block, 0, EXT4_MIN_INLINE_DATA_SIZE);
+ memset(ei->i_data, 0, EXT4_MIN_INLINE_DATA_SIZE);
+ if (ext4_has_feature_extents(inode->i_sb) &&
+ (S_ISDIR(inode->i_mode) || S_ISREG(inode->i_mode) || S_ISLNK(inode->i_mode))) {
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ ext4_ext_tree_init(handle, inode);
+ struct ext4_ext_path *path = ext4_find_extent(inode, 0, NULL, 0);
+
+ if (IS_ERR(path)) {
+ error = PTR_ERR(path);
+ goto recovery;
+ }
+ struct ext4_extent newex;
+
+ newex.ee_block = cpu_to_le32(0);
+ newex.ee_len = cpu_to_le16(1);
+ ext4_ext_store_pblock(&newex, block);
+ path = ext4_ext_insert_extent(handle, inode, path, &newex, 0);
+ if (IS_ERR(path)) {
+ error = PTR_ERR(path);
+ if (error == -EDQUOT || error == -ENOSPC) {
+ goto recovery;
+ } else {
+ ext4_forget(handle, 0, inode, bh, block);
+ goto nofree;
+ }
+ } else {
+ ext4_free_ext_path(path);
+ }
+ } else {
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(block);
+ }
+ error = ext4_xattr_ibody_find(inode, &i, &is);
+ if (error)
+ goto recovery;
+ if (!is.s.not_found)
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+recovery:
+ if (error) {
+ ext4_discard_preallocations(inode);
+ ext4_free_blocks(handle, inode, bh, block, len, EXT4_FREE_BLOCKS_FORGET);
+nofree:
+ memcpy((void *)raw_inode, inode_buf, inode_size);
+ memcpy(ei->i_data, raw_inode->i_block, EXT4_MIN_INLINE_DATA_SIZE);
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+ } else {
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ get_bh(is.iloc.bh);
+ error = ext4_mark_iloc_dirty(handle, inode, &is.iloc);
+ EXT4_I(inode)->i_inline_off = 0;
+ EXT4_I(inode)->i_inline_size = 0;
+ ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+ if (S_ISDIR(inode->i_mode)) {
+ i_size_write(inode, inode->i_sb->s_blocksize);
+ EXT4_I(inode)->i_disksize = inode->i_sb->s_blocksize;
+ }
+ brelse(bh);
+ }
+out:
+ brelse(is.iloc.bh);
+ up_write(&ei->i_data_sem);
+ kfree(inode_buf);
+ return error;
}
static int ext4_convert_inline_data_nolock(handle_t *handle,
@@ -1094,8 +1189,11 @@ 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;
int inline_size;
+ ext4_fsblk_t newblock = 0;
+ struct ext4_allocation_request ar;
+ struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+ unsigned int allocated_block;
inline_size = ext4_get_inline_size(inode);
buf = kmalloc(inline_size, GFP_NOFS);
@@ -1120,25 +1218,22 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
goto out;
}
- error = ext4_destroy_inline_data_nolock(handle, inode);
- 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);
+ memset(&ar, 0, sizeof(ar));
+ ar.inode = inode;
+ ar.logical = 0;
+ ar.len = 1;
+ if (S_ISREG(inode->i_mode))
+ ar.flags = EXT4_MB_HINT_DATA;
+ else
+ ar.flags = 0;
+ newblock = ext4_mb_new_blocks(handle, &ar, &error);
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);
+ goto out;
+ allocated_block = EXT4_C2B(sbi, ar.len);
+ data_bh = sb_getblk(inode->i_sb, newblock);
if (!data_bh) {
error = -ENOMEM;
- goto out_restore;
+ goto out_bh;
}
lock_buffer(data_bh);
@@ -1147,7 +1242,7 @@ static int ext4_convert_inline_data_nolock(handle_t *handle,
if (error) {
unlock_buffer(data_bh);
error = -EIO;
- goto out_restore;
+ goto out_bh;
}
memset(data_bh->b_data, 0, inode->i_sb->s_blocksize);
@@ -1159,24 +1254,28 @@ static int ext4_convert_inline_data_nolock(handle_t *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);
}
+out_bh:
+ if (error) {
+ int flags = 0;
-out_restore:
- if (error)
- ext4_restore_inline_data(handle, inode, iloc, buf, inline_size);
+ if (data_bh)
+ flags |= EXT4_FREE_BLOCKS_FORGET;
+ struct ext4_inode_info *ei = EXT4_I(inode);
+ down_write(&ei->i_data_sem);
+ ext4_discard_preallocations(inode);
+ ext4_free_blocks(handle, inode, data_bh, newblock, allocated_block, flags);
+ up_write(&ei->i_data_sem);
+ } else {
+ error = ext4_set_inline_data_block(handle, inode,
+ newblock, allocated_block, data_bh);
+ }
out:
- brelse(data_bh);
kfree(buf);
return error;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 17+ messages in thread* Re: [PATCH V3] Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
2026-08-22 16:28 ` [PATCH V3] " shuo chen
@ 2026-08-22 16:45 ` sashiko-bot
2026-08-27 8:50 ` shuo chen
0 siblings, 1 reply; 17+ messages in thread
From: sashiko-bot @ 2026-08-22 16:45 UTC (permalink / raw)
To: shuo chen; +Cc: tytso, linux-ext4
Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [Critical] Misordered metadata update causes corrupt directory `i_size` on disk.
- [Critical] Unconditional BUG_ON() crash in `ext4_free_blocks()` on error paths for allocations > 1 block.
- [High] Permanent block leak when `ext4_ext_insert_extent` fails with an error other than EDQUOT or ENOSPC.
- [High] Concurrent inode metadata changes are overwritten during error recovery.
- [High] Permanent space leak due to hardcoded extent length of 1 block.
- [High] Missing `i_blocks` accounting for newly allocated physical blocks.
- [Medium] The commit message describes a completely different change.
--
--- Patch [1]: [PATCH V3] Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir ---
commit 1cb52ec17c1b86718dd26a2d224b02e260c85e40
Author: pipishuo <1289151713@qq.com>
Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
The commit message describes adding a state check in ext4_readdir, but the
patch refactors ext4_convert_inline_data_nolock and introduces a new block
allocation mechanism for inline data conversion.
> Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
>
> Resend as v3 because I forgot to add the version number
[Severity: Medium]
Is the commit message mismatched with the actual code changes? The subject
and message describe adding a state check in ext4_readdir, but the patch
entirely refactors ext4_convert_inline_data_nolock and introduces a new
block allocation mechanism for inline data conversion without mentioning it.
> --- a/fs/ext4/inline.c
> +++ b/fs/ext4/inline.c
[ ... ]
> @@ -1079,21 +1080,115 @@ 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)
> +static int ext4_set_inline_data_block(handle_t *handle, struct inode *inode, ext4_fsblk_t block,
> + unsigned int len, struct buffer_head *bh)
> {
[ ... ]
> + struct ext4_extent newex;
> +
> + newex.ee_block = cpu_to_le32(0);
> + newex.ee_len = cpu_to_le16(1);
[Severity: High]
Does this permanently leak physical blocks if the allocator returns more
than 1 block? When ext4_mb_new_blocks() returns a larger allocation
(e.g., due to cluster normalization on bigalloc filesystems), hardcoding
the extent length to 1 abandons the extra blocks. Should this use the
allocated length (len parameter) instead?
> + ext4_ext_store_pblock(&newex, block);
> + path = ext4_ext_insert_extent(handle, inode, path, &newex, 0);
[Severity: High]
Does this miss accounting for the newly allocated physical blocks?
Manually inserting the extent bypasses the normal ext4_map_blocks()
pipeline, meaning ext4_inode_add_physical_space() is never called to
increment inode->i_blocks. Could this lead to stat() inconsistencies and
fsck failures?
> + if (IS_ERR(path)) {
> + error = PTR_ERR(path);
> + if (error == -EDQUOT || error == -ENOSPC) {
> + goto recovery;
> + } else {
> + ext4_forget(handle, 0, inode, bh, block);
> + goto nofree;
> + }
> + } else {
> + ext4_free_ext_path(path);
> + }
> + } else {
> + EXT4_I(inode)->i_data[0] = cpu_to_le32(block);
> + }
> + error = ext4_xattr_ibody_find(inode, &i, &is);
> + if (error)
> + goto recovery;
> + if (!is.s.not_found)
> + error = ext4_xattr_ibody_set(handle, inode, &i, &is);
> +recovery:
> + if (error) {
> + ext4_discard_preallocations(inode);
> + ext4_free_blocks(handle, inode, bh, block, len, EXT4_FREE_BLOCKS_FORGET);
[Severity: Critical]
Can this trigger a BUG_ON() crash in ext4_free_blocks() on bigalloc
filesystems? If the length (len) is greater than 1, calling
ext4_free_blocks() with both a valid buffer_head (bh) and the
EXT4_FREE_BLOCKS_FORGET flag will unconditionally hit the
BUG_ON(count > 1) check inside ext4_free_blocks().
> +nofree:
[Severity: High]
Does the jump to the nofree label earlier permanently leak the allocated
blocks if the extent insertion fails with an error other than -EDQUOT or
-ENOSPC? Jumping here bypasses the ext4_free_blocks() call during error
recovery. Because ext4_forget() only removes the buffer from the journal
without freeing the physical blocks in the buddy bitmap, the blocks appear
to be permanently leaked.
> + memcpy((void *)raw_inode, inode_buf, inode_size);
[Severity: High]
Could this non-atomic memcpy() overwrite concurrent metadata updates?
The i_data_sem protects the block mapping, but background flushers or
other operations might have updated the inode's timestamps or extended
attributes in the raw_inode while we were allocating blocks. Blindly
copying back the old inode_buf could silently discard those changes.
> + memcpy(ei->i_data, raw_inode->i_block, EXT4_MIN_INLINE_DATA_SIZE);
> + ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
> + } else {
> + ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
> + get_bh(is.iloc.bh);
> + error = ext4_mark_iloc_dirty(handle, inode, &is.iloc);
> + EXT4_I(inode)->i_inline_off = 0;
> + EXT4_I(inode)->i_inline_size = 0;
> + ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
> + if (S_ISDIR(inode->i_mode)) {
> + i_size_write(inode, inode->i_sb->s_blocksize);
> + EXT4_I(inode)->i_disksize = inode->i_sb->s_blocksize;
> + }
[Severity: Critical]
Does this permanently save a stale directory size to disk?
The raw metadata buffer is marked dirty via ext4_mark_iloc_dirty()
before the VFS inode size and i_disksize are updated. Since this patch
removed the later ext4_mark_inode_dirty() call, is there a risk the
updated size fields never make it to the on-disk raw_inode?
> + brelse(bh);
> + }
[ ... ]
> @@ -1168,24 +1263,28 @@ static int ext4_convert_inline_data_nolock(handle_t *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);
> }
> +out_bh:
> + if (error) {
> + int flags = 0;
>
> -out_restore:
> - if (error)
> - ext4_restore_inline_data(handle, inode, iloc, buf, inline_size);
> + if (data_bh)
> + flags |= EXT4_FREE_BLOCKS_FORGET;
> + struct ext4_inode_info *ei = EXT4_I(inode);
>
> + down_write(&ei->i_data_sem);
> + ext4_discard_preallocations(inode);
> + ext4_free_blocks(handle, inode, data_bh, newblock, allocated_block, flags);
[Severity: Critical]
Similarly, could this also trigger the BUG_ON(count > 1) in
ext4_free_blocks() on failure? If allocated_block is greater than 1
(e.g., bigalloc) and data_bh is valid, flags will include
EXT4_FREE_BLOCKS_FORGET, which crashes when freeing multiple blocks
with a provided buffer_head.
--
Sashiko AI review · https://sashiko.dev/#/patchset/tencent_5FCFA1D9100C4691D5AA92A597B846A8F007@qq.com?part=1
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH V3] Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
2026-08-22 16:45 ` sashiko-bot
@ 2026-08-27 8:50 ` shuo chen
0 siblings, 0 replies; 17+ messages in thread
From: shuo chen @ 2026-08-27 8:50 UTC (permalink / raw)
To: sashiko-reviews; +Cc: tytso, linux-ext4
On Sat, Aug 22, 2026 at 04:45:43PM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
> - [Critical] Misordered metadata update causes corrupt directory `i_size` on disk.
> - [Critical] Unconditional BUG_ON() crash in `ext4_free_blocks()` on error paths for allocations > 1 block.
> - [High] Permanent block leak when `ext4_ext_insert_extent` fails with an error other than EDQUOT or ENOSPC.
> - [High] Concurrent inode metadata changes are overwritten during error recovery.
> - [High] Permanent space leak due to hardcoded extent length of 1 block.
> - [High] Missing `i_blocks` accounting for newly allocated physical blocks.
> - [Medium] The commit message describes a completely different change.
> --
>
> --- Patch [1]: [PATCH V3] Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir ---
> commit 1cb52ec17c1b86718dd26a2d224b02e260c85e40
> Author: pipishuo <1289151713@qq.com>
> Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
>
> The commit message describes adding a state check in ext4_readdir, but the
> patch refactors ext4_convert_inline_data_nolock and introduces a new block
> allocation mechanism for inline data conversion.
>
> > Add EXT4_STATE_MAY_INLINE_DATA check in ext4_readdir
> >
> > Resend as v3 because I forgot to add the version number
>
> [Severity: Medium]
> Is the commit message mismatched with the actual code changes? The subject
> and message describe adding a state check in ext4_readdir, but the patch
> entirely refactors ext4_convert_inline_data_nolock and introduces a new
> block allocation mechanism for inline data conversion without mentioning it.
Got it
>
> > --- a/fs/ext4/inline.c
> > +++ b/fs/ext4/inline.c
> [ ... ]
> > @@ -1079,21 +1080,115 @@ 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)
> > +static int ext4_set_inline_data_block(handle_t *handle, struct inode *inode, ext4_fsblk_t block,
> > + unsigned int len, struct buffer_head *bh)
> > {
> [ ... ]
> > + struct ext4_extent newex;
> > +
> > + newex.ee_block = cpu_to_le32(0);
> > + newex.ee_len = cpu_to_le16(1);
>
> [Severity: High]
> Does this permanently leak physical blocks if the allocator returns more
> than 1 block? When ext4_mb_new_blocks() returns a larger allocation
> (e.g., due to cluster normalization on bigalloc filesystems), hardcoding
> the extent length to 1 abandons the extra blocks. Should this use the
> allocated length (len parameter) instead?
The excess blocks will be stored in i_prealloc_node, so setting the length
to 1 is fine.
>
> > + ext4_ext_store_pblock(&newex, block);
> > + path = ext4_ext_insert_extent(handle, inode, path, &newex, 0);
>
> [Severity: High]
> Does this miss accounting for the newly allocated physical blocks?
> Manually inserting the extent bypasses the normal ext4_map_blocks()
> pipeline, meaning ext4_inode_add_physical_space() is never called to
> increment inode->i_blocks. Could this lead to stat() inconsistencies and
> fsck failures?
I don't have a deep understanding of the i_blocks update mechanism,
but I still believe that i_blocks should not be updated here.
>
> > + if (IS_ERR(path)) {
> > + error = PTR_ERR(path);
> > + if (error == -EDQUOT || error == -ENOSPC) {
> > + goto recovery;
> > + } else {
> > + ext4_forget(handle, 0, inode, bh, block);
> > + goto nofree;
> > + }
> > + } else {
> > + ext4_free_ext_path(path);
> > + }
> > + } else {
> > + EXT4_I(inode)->i_data[0] = cpu_to_le32(block);
> > + }
> > + error = ext4_xattr_ibody_find(inode, &i, &is);
> > + if (error)
> > + goto recovery;
> > + if (!is.s.not_found)
> > + error = ext4_xattr_ibody_set(handle, inode, &i, &is);
> > +recovery:
> > + if (error) {
> > + ext4_discard_preallocations(inode);
> > + ext4_free_blocks(handle, inode, bh, block, len, EXT4_FREE_BLOCKS_FORGET);
>
> [Severity: Critical]
> Can this trigger a BUG_ON() crash in ext4_free_blocks() on bigalloc
> filesystems? If the length (len) is greater than 1, calling
> ext4_free_blocks() with both a valid buffer_head (bh) and the
> EXT4_FREE_BLOCKS_FORGET flag will unconditionally hit the
> BUG_ON(count > 1) check inside ext4_free_blocks().
Got it
>
> > +nofree:
>
> [Severity: High]
> Does the jump to the nofree label earlier permanently leak the allocated
> blocks if the extent insertion fails with an error other than -EDQUOT or
> -ENOSPC? Jumping here bypasses the ext4_free_blocks() call during error
> recovery. Because ext4_forget() only removes the buffer from the journal
> without freeing the physical blocks in the buddy bitmap, the blocks appear
> to be permanently leaked.
Got it
>
> > + memcpy((void *)raw_inode, inode_buf, inode_size);
>
> [Severity: High]
> Could this non-atomic memcpy() overwrite concurrent metadata updates?
> The i_data_sem protects the block mapping, but background flushers or
> other operations might have updated the inode's timestamps or extended
> attributes in the raw_inode while we were allocating blocks. Blindly
> copying back the old inode_buf could silently discard those changes.
Got it
>
> > + memcpy(ei->i_data, raw_inode->i_block, EXT4_MIN_INLINE_DATA_SIZE);
> > + ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
> > + } else {
> > + ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
> > + get_bh(is.iloc.bh);
> > + error = ext4_mark_iloc_dirty(handle, inode, &is.iloc);
> > + EXT4_I(inode)->i_inline_off = 0;
> > + EXT4_I(inode)->i_inline_size = 0;
> > + ext4_clear_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
> > + if (S_ISDIR(inode->i_mode)) {
> > + i_size_write(inode, inode->i_sb->s_blocksize);
> > + EXT4_I(inode)->i_disksize = inode->i_sb->s_blocksize;
> > + }
>
> [Severity: Critical]
> Does this permanently save a stale directory size to disk?
> The raw metadata buffer is marked dirty via ext4_mark_iloc_dirty()
> before the VFS inode size and i_disksize are updated. Since this patch
> removed the later ext4_mark_inode_dirty() call, is there a risk the
> updated size fields never make it to the on-disk raw_inode?
Got it
>
> > + brelse(bh);
> > + }
> [ ... ]
> > @@ -1168,24 +1263,28 @@ static int ext4_convert_inline_data_nolock(handle_t *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);
> > }
> > +out_bh:
> > + if (error) {
> > + int flags = 0;
> >
> > -out_restore:
> > - if (error)
> > - ext4_restore_inline_data(handle, inode, iloc, buf, inline_size);
> > + if (data_bh)
> > + flags |= EXT4_FREE_BLOCKS_FORGET;
> > + struct ext4_inode_info *ei = EXT4_I(inode);
> >
> > + down_write(&ei->i_data_sem);
> > + ext4_discard_preallocations(inode);
> > + ext4_free_blocks(handle, inode, data_bh, newblock, allocated_block, flags);
>
> [Severity: Critical]
> Similarly, could this also trigger the BUG_ON(count > 1) in
> ext4_free_blocks() on failure? If allocated_block is greater than 1
> (e.g., bigalloc) and data_bh is valid, flags will include
> EXT4_FREE_BLOCKS_FORGET, which crashes when freeing multiple blocks
> with a provided buffer_head.
Got it
I'll fix these issues and send V4.
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/tencent_5FCFA1D9100C4691D5AA92A597B846A8F007@qq.com?part=1
^ permalink raw reply [flat|nested] 17+ messages in thread