* Re: [PATCH ext4 v3] ext4: fix out-of-bounds read in ext4_read_inline_dir()
From: Jan Kara @ 2026-06-15 9:27 UTC (permalink / raw)
To: Xiang Mei
Cc: linux-ext4, Jan Kara, Theodore Ts'o, Andreas Dilger,
Baokun Li, Ojaswin Mujoo, Ritesh Harjani, Zhang Yi, Weiming Shi
In-Reply-To: <20260613221836.556019-1-xmei5@asu.edu>
On Sat 13-06-26 15:18:36, Xiang Mei wrote:
> ext4_read_inline_dir() can read a dirent header past the end of its inline
> buffer, triggering a slab-out-of-bounds read during getdents64():
>
> BUG: KASAN: slab-out-of-bounds in __ext4_check_dir_entry
> Read of size 2 at addr ffff88800f3dd23c by task exploit/148
> ...
> __ext4_check_dir_entry
> ext4_read_inline_dir
> iterate_dir
>
> The dirent payload lives in a buffer of exactly inline_size bytes:
>
> dir_buf = kmalloc(inline_size, GFP_NOFS);
>
> but iteration runs in a position space extra_offset bytes larger
> (extra_size = extra_offset + inline_size) so the synthetic "." and ".."
> land at their block-dir offsets. A dirent is formed at "dir_buf + pos -
> extra_offset", yet the loop bound (ctx->pos < extra_size) and the
> ext4_check_dir_entry() length argument both use the larger extra_size. A
> ctx->pos that is valid in extra_size space but whose de lies past
> inline_size is therefore accepted, and the rescan loop's rec_len probe
> and ext4_check_dir_entry() dereference de->rec_len before the entry is
> rejected.
>
> Bound the dirent header by inline_size in both loops: break out of the
> rescan loop once a minimum-size header no longer fits, reject such a
> position in the main loop before forming de, and pass inline_size rather
> than extra_size to ext4_check_dir_entry().
>
> Fixes: c4d8b0235aa9 ("ext4: fix readdir error in case inline_data+^dir_index.")
> Reported-by: Weiming Shi <bestswngs@gmail.com>
> Assisted-by: Claude:claude-opus-4-8
> Signed-off-by: Xiang Mei <xmei5@asu.edu>
Looks mostly good, just one simplification suggestion below:
> diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
> index 8045e4ff270c..1266c8827cca 100644
> --- a/fs/ext4/inline.c
> +++ b/fs/ext4/inline.c
> @@ -1454,6 +1454,9 @@ int ext4_read_inline_dir(struct file *file,
> /* for other entry, the real offset in
> * the buf has to be tuned accordingly.
> */
> + if (i - extra_offset + ext4_dir_rec_len(1, NULL) >
> + inline_size)
> + break;
Since 'i' lives in "dir logical offset space", it might be simpler to check
this as:
if (i + ext4_dir_rec_len(1, NULL) > extra_size)
> de = (struct ext4_dir_entry_2 *)
> (dir_buf + i - extra_offset);
> /* It's too expensive to do a full
> @@ -1488,10 +1491,18 @@ int ext4_read_inline_dir(struct file *file,
> continue;
> }
>
> + /*
> + * de lives at dir_buf + ctx->pos - extra_offset, within the
> + * kmalloc(inline_size) buffer. Make sure its header fits before
> + * ext4_check_dir_entry() dereferences de->rec_len.
> + */
> + if (ctx->pos - extra_offset + ext4_dir_rec_len(1, NULL) >
> + inline_size)
Similarly here this could be checked as:
if (ctx->pos + ext4_dir_rec_len(1, NULL) > extra_size)
which is both more consistent with the loop termination condition and saves
the conversion from logical offset to physical offset.
Otherwise the patch looks good.
Honza
> + goto out;
> de = (struct ext4_dir_entry_2 *)
> (dir_buf + ctx->pos - extra_offset);
> if (ext4_check_dir_entry(inode, file, de, iloc.bh, dir_buf,
> - extra_size, ctx->pos))
> + inline_size, ctx->pos))
> goto out;
> if (le32_to_cpu(de->inode)) {
> if (!dir_emit(ctx, de->name, de->name_len,
> --
> 2.43.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply
* [PATCH v3 0/2] ext4: optimize ext4_mb_prefetch
From: Bohdan Trach @ 2026-06-15 10:03 UTC (permalink / raw)
To: Theodore Ts'o, Andreas Dilger, Baokun Li, Jan Kara,
Ojaswin Mujoo, Ritesh Harjani (IBM), Zhang Yi
Cc: mchehab+huawei, bohdan.trach, lilith.oberhauser, Bohdan Trach,
linux-ext4, linux-kernel
v3:
Rebase to latest ext4/dev.
Added R-b by Jan Kara for patch 1/2.
v2:
Fix issues found by Jan Kara, added R-b for patch 2/2.
Extend commit message of patch 1/2 a bit.
v1:
https://lore.kernel.org/linux-ext4/20260521125931.16474-1-bohdan.trach@huaweicloud.com/
Original cover letter below:
Dear Ted,
We have been profiling scalability of some rocksdb-related workloads on
ext4 file system and have found a case where significant time ends up
being spent in ext4_mb_prefetch() function. This happens because
ext4_mb_scan_groups_linear() path is triggered in ext4_mb_scan_groups().
We have noticed that on larger, filled disks, this function can take
lots of time.
We have added a test for this issue to our fork of will-it-scale [1],
which you can use to reproduce the issue.(the actual workload does a few
writes after fallocate, they have been dropped to better illustrate the
issue).
1) https://github.com/open-s4c/will-it-scale/blob/master/tests/fallocate3.c
On this series, we optimize this code path:
Patch 1: change EXT4_MB_GRP_TEST_AND_SET_READ() to reduce the rate of
atomic RMW operation via test_and_set_bit, which has quite
high cost on large multicore CPUs, especially under
contention for the group's flag cache lines.
As this bit is only ever set, but never unset, it should be
possible to reduce the cost of this check by calling
test_bit[_acquire]() first.
Patch 2: restructure the ext4_mb_prefetch loop operations such that
ext4_group_desc is fetched only after the checks based on
ext4_group_info succeed.
This series has been tested with
kvm-xfstests -c ext4/all -g auto
and did not introduce any new issues.
Performance test: we have used a our will-it-scale drop-in test we have
provided above, and used three machines for running it:
- Kunpeng 920 (arm64, 96 CPUs * 1 socket, 128G RAM, SAS HDD: Seagate
Exos 10E2400 1.2TB)
- Kunpeng 920b (arm64, 80 CPUs * 2 sockets, 502G RAM, SATA SSD: Huawei
ES3000 V6 0.96TB)
- AMD 9654 (x86_64, 96 CPUs * 2 sockets, 1.5T RAM, NVME SSD: Samsung SSD
970 EVO Plus 1TB)
We have performed tests with existing file systems, as well as more limited
tests with a fixed-size file systems.
Benchmark on an existing file system for Kunpeng 920 (842G FS, 31% space
used) with the patch based on kernel 7.0.6:
| thr. | base | patched | improv. |
| | perf | perf | |
|------|------|---------|--------------|
| 1 | 1286 | 1608 | +25.0388802 |
| 2 | 1673 | 1680 | +0.4184100 |
| 4 | 1698 | 1712 | +0.8244994 |
| 8 | 1721 | 1730 | +0.5229518 |
| 16 | 1739 | 2313 | +33.0074756 |
| 32 | 1742 | 3571 | +104.9942595 |
| 64 | 1735 | 3427 | +97.5216138 |
| 96 | 1688 | 1814 | +7.4644550 |
Benchmark on an existing file system for Kunpeng 920b (802G ext4 FS, 68%
space used) with the patch based on kernel 6.6:
| thr. | base | patched | improv. |
| | perf | perf | |
|------|------|---------|----------|
| 1 | 1613 | 1625 | +0.74% |
| 2 | 1620 | 2603 | +60.67% |
| 4 | 1624 | 4894 | +201.35% |
| 8 | 2505 | 8328 | +232.45% |
| 16 | 4736 | 11632 | +145.60% |
| 32 | 7784 | 13124 | +68.60% |
| 64 | 8094 | 8636 | +6.69% |
| 128 | 6914 | 7890 | +14.11% |
Benchmark on an existing file system for AMD 9654 (15T FS, 6% space
used), kernel 7.1-rc3. This shows the performance impact on a mostly
free file system.
| thr. | base | patched | improv. |
| | perf | perf | |
|------|-------|---------|------------|
| 1 | 30901 | 31191 | +0.9384810 |
| 2 | 50874 | 50504 | -0.7272870 |
| 4 | 66068 | 64108 | -2.9666404 |
| 8 | 63963 | 61927 | -3.1830902 |
| 16 | 47809 | 47044 | -1.6001171 |
| 32 | 42441 | 42326 | -0.2709644 |
| 64 | 39773 | 39929 | +0.3922259 |
| 128 | 37065 | 36413 | -1.7590719 |
We have also performed the test with kernel 6.6 on both Kunpeng920b and
AMD 9654 with much smaller FS image (133G) to have more controlled
benchmarking environment, although this reduces the measured benefits as
well compared to a bigger FS with more groups to iterate over:
AMD 9654 performance:
| thr. | base | patched | improv. |
| | perf | perf | |
|------|----------------------------|
| 25% full file system: |
|------|----------------------------|
| 1 | 5964 | 6778 | +13.64% |
| 2 | 11811 | 13415 | +13.58% |
| 4 | 20111 | 23570 | +17.19% |
| 8 | 30083 | 36296 | +20.65% |
| 16 | 27781 | 38302 | +37.87% |
| 32 | 28325 | 36930 | +30.37% |
| 64 | 26044 | 29952 | +15.00% |
| 128 | 19969 | 20882 | +4.57% |
|------|----------------------------|
| 50% full file system: |
|------|----------------------------|
| 1 | 4093 | 7380 | +80.30% |
| 2 | 13168 | 13906 | +5.60% |
| 4 | 21440 | 22623 | +5.51% |
| 8 | 30523 | 32360 | +6.01% |
| 16 | 27502 | 34017 | +23.68% |
| 32 | 27189 | 32480 | +19.46% |
| 64 | 24146 | 26463 | +9.59% |
| 128 | 18386 | 18631 | +1.33% |
|------|----------------------------|
| 75% full file system: |
|------|----------------------------|
| 1 | 5738 | 7208 | +25.61% |
| 2 | 13869 | 15309 | +10.38% |
| 4 | 21803 | 23447 | +7.54% |
| 8 | 29004 | 30766 | +6.07% |
| 16 | 25542 | 30584 | +19.74% |
| 32 | 24242 | 28631 | +18.10% |
| 64 | 20631 | 22833 | +10.67% |
| 128 | 14603 | 15086 | +3.30% |
Kunpeng K920b performance:
| thr. | base | patched | improv. |
| | perf | perf | |
|------|---------------------------|
| 25% full file system: |
|------|---------------------------|
| 1 | 5398 | 7025 | +30.14% |
| 2 | 7451 | 12299 | +65.06% |
| 4 | 12574 | 20899 | +66.20% |
| 8 | 18645 | 27694 | +48.53% |
| 16 | 25088 | 31739 | +26.51% |
| 32 | 26699 | 27632 | +3.49% |
| 64 | 14943 | 19547 | +30.81% |
| 128 | 13047 | 14544 | +11.47% |
|------|---------------------------|
| 50% full file system: |
|------|---------------------------|
| 1 | 4881 | 6618 | +35.58% |
| 2 | 6544 | 11660 | +78.17% |
| 4 | 11156 | 19506 | +74.84% |
| 8 | 16842 | 25835 | +53.39% |
| 16 | 23305 | 29260 | +25.55% |
| 32 | 24622 | 25303 | +2.76% |
| 64 | 13814 | 17707 | +28.18% |
| 128 | 12061 | 13180 | +9.27% |
|------|---------------------------|
| 75% full file system: |
|------|---------------------------|
| 1 | 7037 | 10580 | +50.34% |
| 2 | 9216 | 9075 | -1.52% |
| 4 | 14534 | 22076 | +51.89% |
| 8 | 19341 | 25936 | +34.09% |
| 16 | 23592 | 27409 | +16.17% |
| 32 | 23680 | 23078 | -2.54% |
| 64 | 12836 | 15902 | +23.88% |
| 128 | 9614 | 10341 | +7.56% |
Thanks,
Bohdan.
Bohdan Trach (2):
ext4: avoid RWM atomic in EXT4_MB_GRP_TEST_AND_SET_READ
ext4: get ext4_group_desc in ext4_mb_prefetch only when necessary
fs/ext4/ext4.h | 8 +++++++-
fs/ext4/mballoc.c | 21 +++++++++++----------
2 files changed, 18 insertions(+), 11 deletions(-)
--
2.43.0
^ permalink raw reply
* [PATCH v3 1/2] ext4: avoid RWM atomic in EXT4_MB_GRP_TEST_AND_SET_READ
From: Bohdan Trach @ 2026-06-15 10:03 UTC (permalink / raw)
To: Theodore Ts'o, Andreas Dilger, Baokun Li, Jan Kara,
Ojaswin Mujoo, Ritesh Harjani (IBM), Zhang Yi
Cc: mchehab+huawei, bohdan.trach, lilith.oberhauser, Bohdan Trach,
linux-ext4, linux-kernel
In-Reply-To: <20260615100331.163997-1-bohdan.trach@huaweicloud.com>
EXT4_MB_GRP_TEST_AND_SET_READ uses test_and_set_bit function which
issues an atomic write. This can cause high overhead due to cache
contention when multiple threads iterate over groups in a tight loop,
as is the case for ext4_mb_prefetch(). We have seen this to be a
problem for Kunpeng 920b CPUs which uses a single ARM LSE instruction
for this purpose.
Avoid this unconditional atomic write by testing the bit first without
changing its value. This is OK for this use case as this bit is never
unset.
This change significantly reduces costs of fallocate() operations which
trigger linear group scans on large multicore machines where
test_and_set_bit issues an atomic write operation unconditionally.
Signed-off-by: Bohdan Trach <bohdan.trach@huaweicloud.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
fs/ext4/ext4.h | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index ddc903738c6b..7cb2f86296c8 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3639,7 +3639,13 @@ struct ext4_group_info {
#define EXT4_MB_GRP_CLEAR_TRIMMED(grp) \
(clear_bit(EXT4_GROUP_INFO_WAS_TRIMMED_BIT, &((grp)->bb_state)))
#define EXT4_MB_GRP_TEST_AND_SET_READ(grp) \
- (test_and_set_bit(EXT4_GROUP_INFO_BBITMAP_READ_BIT, &((grp)->bb_state)))
+ (ext4_mb_grp_test_and_set_read((grp)))
+
+static inline int ext4_mb_grp_test_and_set_read(struct ext4_group_info *grp)
+{
+ return (test_bit(EXT4_GROUP_INFO_BBITMAP_READ_BIT, &grp->bb_state) ||
+ test_and_set_bit(EXT4_GROUP_INFO_BBITMAP_READ_BIT, &grp->bb_state));
+}
#define EXT4_MAX_CONTENTION 8
#define EXT4_CONTENTION_THRESHOLD 2
--
2.43.0
^ permalink raw reply related
* [PATCH v3 2/2] ext4: get ext4_group_desc in ext4_mb_prefetch only when necessary
From: Bohdan Trach @ 2026-06-15 10:03 UTC (permalink / raw)
To: Theodore Ts'o, Andreas Dilger, Baokun Li, Jan Kara,
Ojaswin Mujoo, Ritesh Harjani (IBM), Zhang Yi
Cc: mchehab+huawei, bohdan.trach, lilith.oberhauser, Bohdan Trach,
linux-ext4, linux-kernel
In-Reply-To: <20260615100331.163997-1-bohdan.trach@huaweicloud.com>
Getting ext4_group_desc structure can contribute to the cost of
ext4_mb_prefetch() without any need, as most groups fail the
!EXT4_MB_GRP_TEST_AND_SET_READ check.
Optimize ext4_mb_prefetch by getting the group description only when
necessary.
The result is further increase in performance of fallocate() system call
path that triggers ext4_mb_prefetch() via a linear group scan.
Signed-off-by: Bohdan Trach <bohdan.trach@huaweicloud.com>
Reviewed-by: Jan Kara <jack@suse.cz>
---
fs/ext4/mballoc.c | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/fs/ext4/mballoc.c b/fs/ext4/mballoc.c
index ed1bd00e11cd..06171a11db12 100644
--- a/fs/ext4/mballoc.c
+++ b/fs/ext4/mballoc.c
@@ -2861,8 +2861,6 @@ ext4_group_t ext4_mb_prefetch(struct super_block *sb, ext4_group_t group,
blk_start_plug(&plug);
while (nr-- > 0) {
- struct ext4_group_desc *gdp = ext4_get_group_desc(sb, group,
- NULL);
struct ext4_group_info *grp = ext4_get_group_info(sb, group);
/*
@@ -2872,14 +2870,17 @@ ext4_group_t ext4_mb_prefetch(struct super_block *sb, ext4_group_t group,
* prefetch once, so we avoid getblk() call, which can
* be expensive.
*/
- if (gdp && grp && !EXT4_MB_GRP_TEST_AND_SET_READ(grp) &&
- EXT4_MB_GRP_NEED_INIT(grp) &&
- ext4_free_group_clusters(sb, gdp) > 0 ) {
- bh = ext4_read_block_bitmap_nowait(sb, group, true);
- if (!IS_ERR_OR_NULL(bh)) {
- if (!buffer_uptodate(bh) && cnt)
- (*cnt)++;
- brelse(bh);
+ if (grp && !EXT4_MB_GRP_TEST_AND_SET_READ(grp) &&
+ EXT4_MB_GRP_NEED_INIT(grp)) {
+ struct ext4_group_desc *gdp = ext4_get_group_desc(sb, group, NULL);
+
+ if (gdp && ext4_free_group_clusters(sb, gdp) > 0) {
+ bh = ext4_read_block_bitmap_nowait(sb, group, true);
+ if (!IS_ERR_OR_NULL(bh)) {
+ if (!buffer_uptodate(bh) && cnt)
+ (*cnt)++;
+ brelse(bh);
+ }
}
}
if (++group >= ngroups)
--
2.43.0
^ permalink raw reply related
* [PATCH v5 2/3] ext4: set EXT4_STATE_NO_EXPAND in ext4_evict_inode
From: Yun Zhou @ 2026-06-15 11:53 UTC (permalink / raw)
To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
yi.zhang, ebiggers, yun.zhou
Cc: linux-ext4, linux-kernel
In-Reply-To: <20260615115317.3549469-1-yun.zhou@windriver.com>
An inode being evicted will never need its extra isize expanded. Set
EXT4_STATE_NO_EXPAND before ext4_mark_inode_dirty() in ext4_evict_inode()
to make this explicit and prevent any unnecessary work in
ext4_try_to_expand_extra_isize().
This also provides defense-in-depth for the s_writepages_rwsem deadlock
during mount-time orphan cleanup, ensuring the expand path is blocked
for inodes under eviction regardless of how they are reached.
Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
fs/ext4/inode.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index be1e3eaa4f23..60c91c098fa0 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -264,6 +264,7 @@ void ext4_evict_inode(struct inode *inode)
if (ext4_inode_is_fast_symlink(inode))
memset(EXT4_I(inode)->i_data, 0, sizeof(EXT4_I(inode)->i_data));
inode->i_size = 0;
+ ext4_set_inode_state(inode, EXT4_STATE_NO_EXPAND);
err = ext4_mark_inode_dirty(handle, inode);
if (err) {
ext4_warning(inode->i_sb,
--
2.43.0
^ permalink raw reply related
* [PATCH v5 3/3] ext4: defer iput() on ea_inodes to reduce lock holding scope
From: Yun Zhou @ 2026-06-15 11:53 UTC (permalink / raw)
To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
yi.zhang, ebiggers, yun.zhou
Cc: linux-ext4, linux-kernel
In-Reply-To: <20260615115317.3549469-1-yun.zhou@windriver.com>
ext4_xattr_block_set(), ext4_xattr_ibody_set() and ext4_xattr_set_entry()
call iput() on ea_inodes while xattr_sem and a jbd2 handle are held.
This creates an unnecessarily wide lock scope and, without the preceding
!SB_ACTIVE guard, could trigger write_inode_now() -> s_writepages_rwsem
creating a circular dependency.
Structurally eliminate iput-under-xattr_sem by collecting ea_inodes into
a deferred-iput array and releasing them after locks are dropped:
1. Convert all iput(ea_inode) calls within ext4_xattr_set_entry(),
ext4_xattr_ibody_set(), and ext4_xattr_block_set() to deferred iput
via ext4_expand_inode_array(). Thread the ea_inode_array through
ext4_xattr_set_handle(), ext4_xattr_move_to_block(), and
ext4_expand_extra_isize_ea() as an output parameter.
2. Callers that own the journal handle (ext4_xattr_set,
ext4_expand_extra_isize) free the array AFTER ext4_journal_stop().
For ext4_try_to_expand_extra_isize (called from __ext4_mark_inode_dirty
with the caller's handle), free after xattr_sem release -- safe
because the preceding patches block the !SB_ACTIVE path.
3. Callers that cannot control the handle lifetime (ext4_initxattrs,
__ext4_set_acl, ext4_set_context, inline data ops) pass NULL.
ext4_expand_inode_array() falls back to synchronous iput() when
ea_inode_array is NULL -- safe because these callers only run with
SB_ACTIVE where iput cannot trigger write_inode_now().
4. Use __GFP_NOFAIL in ext4_expand_inode_array() to guarantee the
deferred-iput array never fails to grow, eliminating any fallback
to synchronous iput() under locks.
5. Pass ea_inode_array directly to ext4_xattr_release_block() in
ext4_xattr_block_set() instead of using a local array freed
synchronously under xattr_sem.
Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
fs/ext4/acl.c | 2 +-
fs/ext4/crypto.c | 4 +-
fs/ext4/inline.c | 8 ++--
fs/ext4/inode.c | 16 +++++--
fs/ext4/xattr.c | 93 ++++++++++++++++++++++++----------------
fs/ext4/xattr.h | 10 +++--
fs/ext4/xattr_security.c | 3 +-
7 files changed, 85 insertions(+), 51 deletions(-)
diff --git a/fs/ext4/acl.c b/fs/ext4/acl.c
index 3bffe862f954..21de8276b558 100644
--- a/fs/ext4/acl.c
+++ b/fs/ext4/acl.c
@@ -215,7 +215,7 @@ __ext4_set_acl(handle_t *handle, struct inode *inode, int type,
}
error = ext4_xattr_set_handle(handle, inode, name_index, "",
- value, size, xattr_flags);
+ value, size, xattr_flags, NULL);
kfree(value);
if (!error)
diff --git a/fs/ext4/crypto.c b/fs/ext4/crypto.c
index f41f320f4437..bca760751c1d 100644
--- a/fs/ext4/crypto.c
+++ b/fs/ext4/crypto.c
@@ -173,7 +173,7 @@ static int ext4_set_context(struct inode *inode, const void *ctx, size_t len,
res = ext4_xattr_set_handle(handle, inode,
EXT4_XATTR_INDEX_ENCRYPTION,
EXT4_XATTR_NAME_ENCRYPTION_CONTEXT,
- ctx, len, XATTR_CREATE);
+ ctx, len, XATTR_CREATE, NULL);
if (!res) {
ext4_set_inode_flag(inode, EXT4_INODE_ENCRYPT);
ext4_clear_inode_state(inode,
@@ -202,7 +202,7 @@ static int ext4_set_context(struct inode *inode, const void *ctx, size_t len,
res = ext4_xattr_set_handle(handle, inode, EXT4_XATTR_INDEX_ENCRYPTION,
EXT4_XATTR_NAME_ENCRYPTION_CONTEXT,
- ctx, len, 0);
+ ctx, len, 0, NULL);
if (!res) {
ext4_set_inode_flag(inode, EXT4_INODE_ENCRYPT);
/*
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 8045e4ff270c..2bf2b771929d 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -309,7 +309,7 @@ static int ext4_create_inline_data(handle_t *handle,
goto out;
}
- error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is, NULL);
if (error) {
if (error == -ENOSPC)
ext4_clear_inode_state(inode,
@@ -386,7 +386,7 @@ static int ext4_update_inline_data(handle_t *handle, struct inode *inode,
i.value = value;
i.value_len = len;
- error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is, NULL);
if (error)
goto out;
@@ -469,7 +469,7 @@ static int ext4_destroy_inline_data_nolock(handle_t *handle,
if (error)
goto out;
- error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is, NULL);
if (error)
goto out;
@@ -1917,7 +1917,7 @@ int ext4_inline_data_truncate(struct inode *inode, int *has_inline)
i.value = value;
i.value_len = i_size > EXT4_MIN_INLINE_DATA_SIZE ?
i_size - EXT4_MIN_INLINE_DATA_SIZE : 0;
- err = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ err = ext4_xattr_ibody_set(handle, inode, &i, &is, NULL);
if (err)
goto out_error;
}
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 60c91c098fa0..a1b0f47f8f4f 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -6409,7 +6409,8 @@ ext4_reserve_inode_write(handle_t *handle, struct inode *inode,
static int __ext4_expand_extra_isize(struct inode *inode,
unsigned int new_extra_isize,
struct ext4_iloc *iloc,
- handle_t *handle, int *no_expand)
+ handle_t *handle, int *no_expand,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_inode *raw_inode;
struct ext4_xattr_ibody_header *header;
@@ -6454,7 +6455,7 @@ static int __ext4_expand_extra_isize(struct inode *inode,
/* try to expand with EAs present */
error = ext4_expand_extra_isize_ea(inode, new_extra_isize,
- raw_inode, handle);
+ raw_inode, handle, ea_inode_array);
if (error) {
/*
* Inode size expansion failed; don't try again
@@ -6476,6 +6477,7 @@ static int ext4_try_to_expand_extra_isize(struct inode *inode,
{
int no_expand;
int error;
+ struct ext4_xattr_inode_array *ea_inode_array = NULL;
if (ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND))
return -EOVERFLOW;
@@ -6507,8 +6509,11 @@ static int ext4_try_to_expand_extra_isize(struct inode *inode,
return -EBUSY;
error = __ext4_expand_extra_isize(inode, new_extra_isize, &iloc,
- handle, &no_expand);
+ handle, &no_expand,
+ &ea_inode_array);
ext4_write_unlock_xattr(inode, &no_expand);
+ /* Safe with caller's handle active: !SB_ACTIVE is blocked above */
+ ext4_xattr_inode_array_free(ea_inode_array);
return error;
}
@@ -6520,6 +6525,7 @@ int ext4_expand_extra_isize(struct inode *inode,
handle_t *handle;
int no_expand;
int error, rc;
+ struct ext4_xattr_inode_array *ea_inode_array = NULL;
if (ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND)) {
brelse(iloc->bh);
@@ -6545,7 +6551,8 @@ int ext4_expand_extra_isize(struct inode *inode,
}
error = __ext4_expand_extra_isize(inode, new_extra_isize, iloc,
- handle, &no_expand);
+ handle, &no_expand,
+ &ea_inode_array);
rc = ext4_mark_iloc_dirty(handle, inode, iloc);
if (!error)
@@ -6554,6 +6561,7 @@ int ext4_expand_extra_isize(struct inode *inode,
out_unlock:
ext4_write_unlock_xattr(inode, &no_expand);
ext4_journal_stop(handle);
+ ext4_xattr_inode_array_free(ea_inode_array);
return error;
}
diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
index 982a1f831e22..4eb83917e6b4 100644
--- a/fs/ext4/xattr.c
+++ b/fs/ext4/xattr.c
@@ -1630,7 +1630,8 @@ static int ext4_xattr_set_entry(struct ext4_xattr_info *i,
struct ext4_xattr_search *s,
handle_t *handle, struct inode *inode,
struct inode *new_ea_inode,
- bool is_block)
+ bool is_block,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_entry *last, *next;
struct ext4_xattr_entry *here = s->here;
@@ -1848,7 +1849,7 @@ static int ext4_xattr_set_entry(struct ext4_xattr_info *i,
ret = 0;
out:
- iput(old_ea_inode);
+ ext4_expand_inode_array(ea_inode_array, old_ea_inode);
return ret;
}
@@ -1898,7 +1899,8 @@ ext4_xattr_block_find(struct inode *inode, struct ext4_xattr_info *i,
static int
ext4_xattr_block_set(handle_t *handle, struct inode *inode,
struct ext4_xattr_info *i,
- struct ext4_xattr_block_find *bs)
+ struct ext4_xattr_block_find *bs,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct super_block *sb = inode->i_sb;
struct buffer_head *new_bh = NULL;
@@ -1961,7 +1963,8 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
}
ea_bdebug(bs->bh, "modifying in-place");
error = ext4_xattr_set_entry(i, s, handle, inode,
- ea_inode, true /* is_block */);
+ ea_inode, true /* is_block */,
+ ea_inode_array);
ext4_xattr_block_csum_set(inode, bs->bh);
unlock_buffer(bs->bh);
if (error == -EFSCORRUPTED)
@@ -2030,7 +2033,7 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
}
error = ext4_xattr_set_entry(i, s, handle, inode, ea_inode,
- true /* is_block */);
+ true /* is_block */, ea_inode_array);
if (error == -EFSCORRUPTED)
goto bad_block;
if (error)
@@ -2150,7 +2153,8 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
ext4_warning_inode(ea_inode,
"dec ref error=%d",
error);
- iput(ea_inode);
+ ext4_expand_inode_array(ea_inode_array,
+ ea_inode);
ea_inode = NULL;
}
@@ -2182,12 +2186,9 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
/* Drop the previous xattr block. */
if (bs->bh && bs->bh != new_bh) {
- struct ext4_xattr_inode_array *ea_inode_array = NULL;
-
ext4_xattr_release_block(handle, inode, bs->bh,
- &ea_inode_array,
+ ea_inode_array,
0 /* extra_credits */);
- ext4_xattr_inode_array_free(ea_inode_array);
}
error = 0;
@@ -2203,7 +2204,7 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
ext4_xattr_inode_free_quota(inode, ea_inode,
i_size_read(ea_inode));
}
- iput(ea_inode);
+ ext4_expand_inode_array(ea_inode_array, ea_inode);
}
if (ce)
mb_cache_entry_put(ea_block_cache, ce);
@@ -2253,7 +2254,8 @@ int ext4_xattr_ibody_find(struct inode *inode, struct ext4_xattr_info *i,
int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
struct ext4_xattr_info *i,
- struct ext4_xattr_ibody_find *is)
+ struct ext4_xattr_ibody_find *is,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_ibody_header *header;
struct ext4_xattr_search *s = &is->s;
@@ -2273,7 +2275,7 @@ int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
return PTR_ERR(ea_inode);
}
error = ext4_xattr_set_entry(i, s, handle, inode, ea_inode,
- false /* is_block */);
+ false /* is_block */, ea_inode_array);
if (error) {
if (ea_inode) {
int error2;
@@ -2285,7 +2287,7 @@ int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
ext4_xattr_inode_free_quota(inode, ea_inode,
i_size_read(ea_inode));
- iput(ea_inode);
+ ext4_expand_inode_array(ea_inode_array, ea_inode);
}
return error;
}
@@ -2297,7 +2299,7 @@ int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
header->h_magic = cpu_to_le32(0);
ext4_clear_inode_state(inode, EXT4_STATE_XATTR);
}
- iput(ea_inode);
+ ext4_expand_inode_array(ea_inode_array, ea_inode);
return 0;
}
@@ -2348,7 +2350,8 @@ static struct buffer_head *ext4_xattr_get_block(struct inode *inode)
int
ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
const char *name, const void *value, size_t value_len,
- int flags)
+ int flags,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_info i = {
.name_index = name_index,
@@ -2428,9 +2431,11 @@ ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
if (!value) {
if (!is.s.not_found)
- error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is,
+ ea_inode_array);
else if (!bs.s.not_found)
- error = ext4_xattr_block_set(handle, inode, &i, &bs);
+ error = ext4_xattr_block_set(handle, inode, &i, &bs,
+ ea_inode_array);
} else {
error = 0;
/* Xattr value did not change? Save us some work and bail out */
@@ -2444,10 +2449,12 @@ ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
EXT4_XATTR_MIN_LARGE_EA_SIZE(inode->i_sb->s_blocksize)))
i.in_inode = 1;
retry_inode:
- error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is,
+ ea_inode_array);
if (!error && !bs.s.not_found) {
i.value = NULL;
- error = ext4_xattr_block_set(handle, inode, &i, &bs);
+ error = ext4_xattr_block_set(handle, inode, &i, &bs,
+ ea_inode_array);
} else if (error == -ENOSPC) {
if (EXT4_I(inode)->i_file_acl && !bs.s.base) {
brelse(bs.bh);
@@ -2456,11 +2463,12 @@ ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
if (error)
goto cleanup;
}
- error = ext4_xattr_block_set(handle, inode, &i, &bs);
+ error = ext4_xattr_block_set(handle, inode, &i, &bs,
+ ea_inode_array);
if (!error && !is.s.not_found) {
i.value = NULL;
error = ext4_xattr_ibody_set(handle, inode, &i,
- &is);
+ &is, ea_inode_array);
} else if (error == -ENOSPC) {
/*
* Xattr does not fit in the block, store at
@@ -2539,6 +2547,7 @@ ext4_xattr_set(struct inode *inode, int name_index, const char *name,
{
handle_t *handle;
struct super_block *sb = inode->i_sb;
+ struct ext4_xattr_inode_array *ea_inode_array = NULL;
int error, retries = 0;
int credits;
@@ -2559,10 +2568,13 @@ ext4_xattr_set(struct inode *inode, int name_index, const char *name,
int error2;
error = ext4_xattr_set_handle(handle, inode, name_index, name,
- value, value_len, flags);
+ value, value_len, flags,
+ &ea_inode_array);
ext4_fc_mark_ineligible(inode->i_sb, EXT4_FC_REASON_XATTR,
handle);
error2 = ext4_journal_stop(handle);
+ ext4_xattr_inode_array_free(ea_inode_array);
+ ea_inode_array = NULL;
if (error == -ENOSPC &&
ext4_should_retry_alloc(sb, &retries))
goto retry;
@@ -2604,7 +2616,8 @@ static void ext4_xattr_shift_entries(struct ext4_xattr_entry *entry,
*/
static int ext4_xattr_move_to_block(handle_t *handle, struct inode *inode,
struct ext4_inode *raw_inode,
- struct ext4_xattr_entry *entry)
+ struct ext4_xattr_entry *entry,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_ibody_find *is = NULL;
struct ext4_xattr_block_find *bs = NULL;
@@ -2668,14 +2681,14 @@ static int ext4_xattr_move_to_block(handle_t *handle, struct inode *inode,
goto out;
/* Move ea entry from the inode into the block */
- error = ext4_xattr_block_set(handle, inode, &i, bs);
+ error = ext4_xattr_block_set(handle, inode, &i, bs, ea_inode_array);
if (error)
goto out;
/* Remove the chosen entry from the inode */
i.value = NULL;
i.value_len = 0;
- error = ext4_xattr_ibody_set(handle, inode, &i, is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, is, ea_inode_array);
out:
kfree(b_entry_name);
@@ -2694,7 +2707,8 @@ static int ext4_xattr_move_to_block(handle_t *handle, struct inode *inode,
static int ext4_xattr_make_inode_space(handle_t *handle, struct inode *inode,
struct ext4_inode *raw_inode,
int isize_diff, size_t ifree,
- size_t bfree, int *total_ino)
+ size_t bfree, int *total_ino,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
struct ext4_xattr_entry *small_entry;
@@ -2744,7 +2758,7 @@ static int ext4_xattr_make_inode_space(handle_t *handle, struct inode *inode,
total_size += EXT4_XATTR_SIZE(
le32_to_cpu(entry->e_value_size));
error = ext4_xattr_move_to_block(handle, inode, raw_inode,
- entry);
+ entry, ea_inode_array);
if (error)
return error;
@@ -2761,7 +2775,8 @@ static int ext4_xattr_make_inode_space(handle_t *handle, struct inode *inode,
* Returns 0 on success or negative error number on failure.
*/
int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
- struct ext4_inode *raw_inode, handle_t *handle)
+ struct ext4_inode *raw_inode, handle_t *handle,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_ibody_header *header;
struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
@@ -2833,7 +2848,7 @@ int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
error = ext4_xattr_make_inode_space(handle, inode, raw_inode,
isize_diff, ifree, bfree,
- &total_ino);
+ &total_ino, ea_inode_array);
if (error) {
if (error == -ENOSPC && !tried_min_extra_isize &&
s_min_extra_isize) {
@@ -2869,19 +2884,27 @@ int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
/* Add the large xattr @inode into @ea_inode_array for deferred iput().
* If @ea_inode_array is new or full it will be grown and the old
* contents copied over.
+ *
+ * If @inode is NULL this is a no-op. If @ea_inode_array is NULL the
+ * caller guarantees SB_ACTIVE so synchronous iput is safe.
*/
static int
ext4_expand_inode_array(struct ext4_xattr_inode_array **ea_inode_array,
struct inode *inode)
{
+ if (!inode)
+ return 0;
+ if (!ea_inode_array) {
+ iput(inode);
+ return 0;
+ }
if (*ea_inode_array == NULL) {
/*
* Start with 15 inodes, so it fits into a power-of-two size.
*/
(*ea_inode_array) = kmalloc_flex(**ea_inode_array, inodes,
- EIA_MASK, GFP_NOFS);
- if (*ea_inode_array == NULL)
- return -ENOMEM;
+ EIA_MASK,
+ GFP_NOFS | __GFP_NOFAIL);
(*ea_inode_array)->count = 0;
} else if (((*ea_inode_array)->count & EIA_MASK) == EIA_MASK) {
/* expand the array once all 15 + n * 16 slots are full */
@@ -2889,9 +2912,7 @@ ext4_expand_inode_array(struct ext4_xattr_inode_array **ea_inode_array,
new_array = kmalloc_flex(**ea_inode_array, inodes,
(*ea_inode_array)->count + EIA_INCR,
- GFP_NOFS);
- if (new_array == NULL)
- return -ENOMEM;
+ GFP_NOFS | __GFP_NOFAIL);
memcpy(new_array, *ea_inode_array,
struct_size(*ea_inode_array, inodes,
(*ea_inode_array)->count));
diff --git a/fs/ext4/xattr.h b/fs/ext4/xattr.h
index 1fedf44d4fb6..6771d00d3fa4 100644
--- a/fs/ext4/xattr.h
+++ b/fs/ext4/xattr.h
@@ -179,7 +179,9 @@ extern ssize_t ext4_listxattr(struct dentry *, char *, size_t);
extern int ext4_xattr_get(struct inode *, int, const char *, void *, size_t);
extern int ext4_xattr_set(struct inode *, int, const char *, const void *, size_t, int);
-extern int ext4_xattr_set_handle(handle_t *, struct inode *, int, const char *, const void *, size_t, int);
+extern int ext4_xattr_set_handle(handle_t *, struct inode *, int, const char *,
+ const void *, size_t, int,
+ struct ext4_xattr_inode_array **ea_inode_array);
extern int ext4_xattr_set_credits(struct inode *inode, size_t value_len,
bool is_create, int *credits);
extern int __ext4_xattr_set_credits(struct super_block *sb, struct inode *inode,
@@ -192,7 +194,8 @@ extern int ext4_xattr_delete_inode(handle_t *handle, struct inode *inode,
extern void ext4_xattr_inode_array_free(struct ext4_xattr_inode_array *array);
extern int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
- struct ext4_inode *raw_inode, handle_t *handle);
+ struct ext4_inode *raw_inode, handle_t *handle,
+ struct ext4_xattr_inode_array **ea_inode_array);
extern void ext4_evict_ea_inode(struct inode *inode);
extern const struct xattr_handler * const ext4_xattr_handlers[];
@@ -204,7 +207,8 @@ extern int ext4_xattr_ibody_get(struct inode *inode, int name_index,
void *buffer, size_t buffer_size);
extern int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
struct ext4_xattr_info *i,
- struct ext4_xattr_ibody_find *is);
+ struct ext4_xattr_ibody_find *is,
+ struct ext4_xattr_inode_array **ea_inode_array);
extern struct mb_cache *ext4_xattr_create_cache(void);
extern void ext4_xattr_destroy_cache(struct mb_cache *);
diff --git a/fs/ext4/xattr_security.c b/fs/ext4/xattr_security.c
index 776cf11d24ca..6b7ab6e703ad 100644
--- a/fs/ext4/xattr_security.c
+++ b/fs/ext4/xattr_security.c
@@ -44,7 +44,8 @@ ext4_initxattrs(struct inode *inode, const struct xattr *xattr_array,
err = ext4_xattr_set_handle(handle, inode,
EXT4_XATTR_INDEX_SECURITY,
xattr->name, xattr->value,
- xattr->value_len, XATTR_CREATE);
+ xattr->value_len, XATTR_CREATE,
+ NULL);
if (err < 0)
break;
}
--
2.43.0
^ permalink raw reply related
* [PATCH v5 1/3] ext4: skip extra isize expansion during mount to prevent deadlock
From: Yun Zhou @ 2026-06-15 11:53 UTC (permalink / raw)
To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
yi.zhang, ebiggers, yun.zhou
Cc: linux-ext4, linux-kernel
In-Reply-To: <20260615115317.3549469-1-yun.zhou@windriver.com>
ext4_try_to_expand_extra_isize() is called from __ext4_mark_inode_dirty()
while holding an active jbd2 handle. During mount (!SB_ACTIVE), the
expand path may move xattrs to external blocks and release ea_inodes via
iput(). When !SB_ACTIVE, iput() calls write_inode_now() which acquires
s_writepages_rwsem, creating a circular lock dependency:
s_writepages_rwsem --> jbd2_handle --> xattr_sem --> s_writepages_rwsem
This can be triggered via:
ext4_process_orphan() -> ext4_truncate() -> ext4_mark_inode_dirty()
-> ext4_try_to_expand_extra_isize()
or:
ext4_evict_inode() -> ext4_mark_inode_dirty()
-> ext4_try_to_expand_extra_isize()
Skip expansion when !SB_ACTIVE. This is a minor loss of functionality
(extra isize won't grow for these inodes during mount), which e2fsck
can resolve later if needed.
Reported-by: syzbot+5d19358d7eb30ffb0cc5@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=5d19358d7eb30ffb0cc5
Fixes: c8585c6fcaf2 ("ext4: fix races between changing inode journal mode and ext4_writepages")
Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
fs/ext4/inode.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index cd7588a3fa45..be1e3eaa4f23 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -6479,6 +6479,16 @@ static int ext4_try_to_expand_extra_isize(struct inode *inode,
if (ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND))
return -EOVERFLOW;
+ /*
+ * Skip expansion during mount (!SB_ACTIVE). Expanding extra isize
+ * may move xattrs to external blocks and release ea_inodes via iput.
+ * When !SB_ACTIVE, iput triggers write_inode_now() which acquires
+ * s_writepages_rwsem, causing a deadlock with the caller's active
+ * jbd2 handle (lock order: s_writepages_rwsem -> jbd2_handle).
+ */
+ if (unlikely(!(inode->i_sb->s_flags & SB_ACTIVE)))
+ return -EBUSY;
+
/*
* In nojournal mode, we can immediately attempt to expand
* the inode. When journaled, we first need to obtain extra
--
2.43.0
^ permalink raw reply related
* [PATCH v5 0/3] ext4: fix xattr iput deadlock with s_writepages_rwsem
From: Yun Zhou @ 2026-06-15 11:53 UTC (permalink / raw)
To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
yi.zhang, ebiggers, yun.zhou
Cc: linux-ext4, linux-kernel
This series fixes a circular lock dependency reported by syzbot:
s_writepages_rwsem --> jbd2_handle --> xattr_sem --> s_writepages_rwsem
The deadlock occurs when iput() on an ea_inode triggers write_inode_now()
while xattr_sem and a jbd2 handle are held. The triggering path is
during mount-time orphan cleanup (!SB_ACTIVE) where iput_final() calls
write_inode_now() synchronously.
Patch 1 blocks the deadlock by skipping extra isize expansion when
!SB_ACTIVE -- this prevents the xattr manipulation path from being
entered during mount.
Patch 2 is a belt-and-suspenders semantic improvement: an inode under
eviction never needs extra isize expansion.
Patch 3 is a structural improvement: defer all ea_inode iput() calls
until after xattr_sem is released, reducing lock nesting depth and
preventing future code paths from reintroducing the lock ordering issue.
Link: https://syzkaller.appspot.com/bug?extid=5d19358d7eb30ffb0cc5
v5:
- Split into 3 patches for easier review.
- Add explicit !SB_ACTIVE early-return in ext4_try_to_expand_extra_isize()
to block ALL mount-time paths (ext4_process_orphan -> ext4_truncate ->
ext4_mark_inode_dirty), not just the eviction path. v4 only relied on
EXT4_STATE_NO_EXPAND which doesn't cover orphan truncation.
v4:
- Comprehensive rewrite of the deferred iput mechanism.
- Thread ea_inode_array through ext4_expand_extra_isize_ea() and
ext4_xattr_move_to_block() so ALL ea_inode iputs in the expand
path are deferred, not just those in ext4_xattr_block_set().
- Add NULL safety to ext4_expand_inode_array(): when ea_inode_array
pointer is NULL, fall back to synchronous iput (for callers like
ext4_initxattrs that only run with SB_ACTIVE).
- Use __GFP_NOFAIL to guarantee deferred array growth, eliminating
fallback to synchronous iput under locks.
- Update ext4_xattr_ibody_set() and ext4_xattr_set_entry() signatures
to accept ea_inode_array, converting ALL iput(ea_inode) calls.
- Set EXT4_STATE_NO_EXPAND in ext4_evict_inode() before
ext4_mark_inode_dirty().
v3:
- Check ext4_expand_inode_array() return value; fallback to
direct iput() on ENOMEM to prevent inode leak.
- Make ext4_xattr_set_handle() take an optional ea_inode_array
output parameter so callers can free after ext4_journal_stop(),
avoiding the jbd2_handle vs s_writepages_rwsem AB-BA.
- Pass ea_inode_array directly to ext4_xattr_release_block()
instead of using a local array freed under xattr_sem.
- Move ext4_xattr_inode_array_free() after ext4_journal_stop()
v2:
- Defer iput() in ext4_xattr_block_set() via ea_inode_array,
freed after xattr_sem is released. Fixes the root cause.
v1:
- Set EXT4_STATE_NO_EXPAND in ext4_evict_inode() to skip expand
on inodes being deleted. Only fixes the syzbot reproducer, not
the underlying lock ordering violation.
Yun Zhou (3):
ext4: skip extra isize expansion during mount to prevent deadlock
ext4: set EXT4_STATE_NO_EXPAND in ext4_evict_inode
ext4: defer iput() on ea_inodes to reduce lock holding scope
fs/ext4/acl.c | 2 +-
fs/ext4/crypto.c | 4 +-
fs/ext4/inline.c | 8 ++--
fs/ext4/inode.c | 26 +++++++++--
fs/ext4/xattr.c | 93 ++++++++++++++++++++++++----------------
fs/ext4/xattr.h | 10 +++--
fs/ext4/xattr_security.c | 3 +-
7 files changed, 95 insertions(+), 51 deletions(-)
--
2.43.0
^ permalink raw reply
* [GIT PULL] udf, isofs, ext2, quota fixes and cleanups for 7.2-rc1
From: Jan Kara @ 2026-06-15 14:51 UTC (permalink / raw)
To: Linus Torvalds; +Cc: linux-fsdevel, linux-ext4
Hello Linus,
could you please pull from
git://git.kernel.org/pub/scm/linux/kernel/git/jack/linux-fs.git fs_for_v7.2-rc1
to get:
* Assorted udf & isofs fixes for maliciously formatted devices
* Cleanups to use kmalloc() instead of __get_free_page()
* Removal of deprecated DAX code from ext2
Top of the tree is 5163e6ee1ea7. The full shortlog is:
Ashwin Gundarapu (1):
ext2: Remove deprecated DAX support
Bryam Vargas (3):
isofs: bound Rock Ridge symlink components to the SL record
udf: validate sparing table length as an entry count, not a byte count
udf: validate VAT header length against the VAT inode size
Danila Chernetsov (1):
ext2: fix ignored return value of generic_write_sync()
Jan Kara (1):
udf: validate VAT inode size for old VAT format
Michael Bommarito (1):
udf: validate free block extents against the partition length
Mike Rapoport (Microsoft) (2):
quota: allocate dquot_hash with kmalloc()
isofs: replace __get_free_page() with kmalloc()
The diffstat is
fs/ext2/ext2.h | 4 --
fs/ext2/file.c | 125 ++++---------------------------------------------------
fs/ext2/inode.c | 60 ++------------------------
fs/ext2/super.c | 39 ++---------------
fs/isofs/dir.c | 5 ++-
fs/isofs/rock.c | 11 +++++
fs/quota/dquot.c | 11 +++--
fs/udf/balloc.c | 5 ++-
fs/udf/super.c | 16 ++++++-
9 files changed, 51 insertions(+), 225 deletions(-)
Thanks
Honza
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply
* Re: [PATCH v5 1/3] ext4: skip extra isize expansion during mount to prevent deadlock
From: Jan Kara @ 2026-06-15 16:20 UTC (permalink / raw)
To: Yun Zhou
Cc: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
yi.zhang, ebiggers, linux-ext4, linux-kernel
In-Reply-To: <20260615115317.3549469-2-yun.zhou@windriver.com>
On Mon 15-06-26 19:53:15, Yun Zhou wrote:
> ext4_try_to_expand_extra_isize() is called from __ext4_mark_inode_dirty()
> while holding an active jbd2 handle. During mount (!SB_ACTIVE), the
> expand path may move xattrs to external blocks and release ea_inodes via
> iput(). When !SB_ACTIVE, iput() calls write_inode_now() which acquires
> s_writepages_rwsem, creating a circular lock dependency:
>
> s_writepages_rwsem --> jbd2_handle --> xattr_sem --> s_writepages_rwsem
>
> This can be triggered via:
>
> ext4_process_orphan() -> ext4_truncate() -> ext4_mark_inode_dirty()
> -> ext4_try_to_expand_extra_isize()
>
> or:
>
> ext4_evict_inode() -> ext4_mark_inode_dirty()
> -> ext4_try_to_expand_extra_isize()
>
> Skip expansion when !SB_ACTIVE. This is a minor loss of functionality
> (extra isize won't grow for these inodes during mount), which e2fsck
> can resolve later if needed.
>
> Reported-by: syzbot+5d19358d7eb30ffb0cc5@syzkaller.appspotmail.com
> Closes: https://syzkaller.appspot.com/bug?extid=5d19358d7eb30ffb0cc5
> Fixes: c8585c6fcaf2 ("ext4: fix races between changing inode journal mode and ext4_writepages")
> Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
Yeah, looks as a sensible compromise. Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> fs/ext4/inode.c | 10 ++++++++++
> 1 file changed, 10 insertions(+)
>
> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index cd7588a3fa45..be1e3eaa4f23 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
> @@ -6479,6 +6479,16 @@ static int ext4_try_to_expand_extra_isize(struct inode *inode,
> if (ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND))
> return -EOVERFLOW;
>
> + /*
> + * Skip expansion during mount (!SB_ACTIVE). Expanding extra isize
> + * may move xattrs to external blocks and release ea_inodes via iput.
> + * When !SB_ACTIVE, iput triggers write_inode_now() which acquires
> + * s_writepages_rwsem, causing a deadlock with the caller's active
> + * jbd2 handle (lock order: s_writepages_rwsem -> jbd2_handle).
> + */
> + if (unlikely(!(inode->i_sb->s_flags & SB_ACTIVE)))
> + return -EBUSY;
> +
> /*
> * In nojournal mode, we can immediately attempt to expand
> * the inode. When journaled, we first need to obtain extra
> --
> 2.43.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply
* Re: [PATCH v5 2/3] ext4: set EXT4_STATE_NO_EXPAND in ext4_evict_inode
From: Jan Kara @ 2026-06-15 16:28 UTC (permalink / raw)
To: Yun Zhou
Cc: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
yi.zhang, ebiggers, linux-ext4, linux-kernel
In-Reply-To: <20260615115317.3549469-3-yun.zhou@windriver.com>
On Mon 15-06-26 19:53:16, Yun Zhou wrote:
> An inode being evicted will never need its extra isize expanded. Set
> EXT4_STATE_NO_EXPAND before ext4_mark_inode_dirty() in ext4_evict_inode()
> to make this explicit and prevent any unnecessary work in
> ext4_try_to_expand_extra_isize().
>
> This also provides defense-in-depth for the s_writepages_rwsem deadlock
> during mount-time orphan cleanup, ensuring the expand path is blocked
> for inodes under eviction regardless of how they are reached.
>
> Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
Looks good. I'd move the setting somewhat earlier (just before
dquot_initialize()) so that it obviously covers all the inode dirtying that
can happen while deleting the inode. But otherwise feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> fs/ext4/inode.c | 1 +
> 1 file changed, 1 insertion(+)
>
> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index be1e3eaa4f23..60c91c098fa0 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
> @@ -264,6 +264,7 @@ void ext4_evict_inode(struct inode *inode)
> if (ext4_inode_is_fast_symlink(inode))
> memset(EXT4_I(inode)->i_data, 0, sizeof(EXT4_I(inode)->i_data));
> inode->i_size = 0;
> + ext4_set_inode_state(inode, EXT4_STATE_NO_EXPAND);
> err = ext4_mark_inode_dirty(handle, inode);
> if (err) {
> ext4_warning(inode->i_sb,
> --
> 2.43.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply
* [PATCH v6 1/3] ext4: skip extra isize expansion during mount to prevent deadlock
From: Yun Zhou @ 2026-06-15 16:30 UTC (permalink / raw)
To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
yi.zhang, ebiggers, yun.zhou
Cc: linux-ext4, linux-kernel
In-Reply-To: <20260615163038.1223293-1-yun.zhou@windriver.com>
ext4_try_to_expand_extra_isize() is called from __ext4_mark_inode_dirty()
while holding an active jbd2 handle. During mount (!SB_ACTIVE), the
expand path may move xattrs to external blocks and release ea_inodes via
iput(). When !SB_ACTIVE, iput() calls write_inode_now() which acquires
s_writepages_rwsem, creating a circular lock dependency:
s_writepages_rwsem --> jbd2_handle --> xattr_sem --> s_writepages_rwsem
This can be triggered via:
ext4_process_orphan() -> ext4_truncate() -> ext4_mark_inode_dirty()
-> ext4_try_to_expand_extra_isize()
or:
ext4_evict_inode() -> ext4_mark_inode_dirty()
-> ext4_try_to_expand_extra_isize()
Skip expansion when !SB_ACTIVE. This is a minor loss of functionality
(extra isize won't grow for these inodes during mount), which e2fsck
can resolve later if needed.
Reported-by: syzbot+5d19358d7eb30ffb0cc5@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=5d19358d7eb30ffb0cc5
Fixes: c8585c6fcaf2 ("ext4: fix races between changing inode journal mode and ext4_writepages")
Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
fs/ext4/inode.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index cd7588a3fa45..be1e3eaa4f23 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -6479,6 +6479,16 @@ static int ext4_try_to_expand_extra_isize(struct inode *inode,
if (ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND))
return -EOVERFLOW;
+ /*
+ * Skip expansion during mount (!SB_ACTIVE). Expanding extra isize
+ * may move xattrs to external blocks and release ea_inodes via iput.
+ * When !SB_ACTIVE, iput triggers write_inode_now() which acquires
+ * s_writepages_rwsem, causing a deadlock with the caller's active
+ * jbd2 handle (lock order: s_writepages_rwsem -> jbd2_handle).
+ */
+ if (unlikely(!(inode->i_sb->s_flags & SB_ACTIVE)))
+ return -EBUSY;
+
/*
* In nojournal mode, we can immediately attempt to expand
* the inode. When journaled, we first need to obtain extra
--
2.43.0
^ permalink raw reply related
* [PATCH v6 2/3] ext4: set EXT4_STATE_NO_EXPAND in ext4_evict_inode
From: Yun Zhou @ 2026-06-15 16:30 UTC (permalink / raw)
To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
yi.zhang, ebiggers, yun.zhou
Cc: linux-ext4, linux-kernel
In-Reply-To: <20260615163038.1223293-1-yun.zhou@windriver.com>
An inode being evicted will never need its extra isize expanded. Set
EXT4_STATE_NO_EXPAND before ext4_mark_inode_dirty() in ext4_evict_inode()
to make this explicit and prevent any unnecessary work in
ext4_try_to_expand_extra_isize().
This also provides defense-in-depth for the s_writepages_rwsem deadlock
during mount-time orphan cleanup, ensuring the expand path is blocked
for inodes under eviction regardless of how they are reached.
Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
fs/ext4/inode.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index be1e3eaa4f23..60c91c098fa0 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -264,6 +264,7 @@ void ext4_evict_inode(struct inode *inode)
if (ext4_inode_is_fast_symlink(inode))
memset(EXT4_I(inode)->i_data, 0, sizeof(EXT4_I(inode)->i_data));
inode->i_size = 0;
+ ext4_set_inode_state(inode, EXT4_STATE_NO_EXPAND);
err = ext4_mark_inode_dirty(handle, inode);
if (err) {
ext4_warning(inode->i_sb,
--
2.43.0
^ permalink raw reply related
* [PATCH v6 0/3] ext4: fix xattr iput deadlock with s_writepages_rwsem
From: Yun Zhou @ 2026-06-15 16:30 UTC (permalink / raw)
To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
yi.zhang, ebiggers, yun.zhou
Cc: linux-ext4, linux-kernel
This series fixes a circular lock dependency reported by syzbot:
s_writepages_rwsem --> jbd2_handle --> xattr_sem --> s_writepages_rwsem
The deadlock occurs when iput() on an ea_inode triggers write_inode_now()
while xattr_sem and a jbd2 handle are held. The triggering path is
during mount-time orphan cleanup (!SB_ACTIVE) where iput_final() calls
write_inode_now() synchronously.
Patch 1 blocks the deadlock by skipping extra isize expansion when
!SB_ACTIVE -- this prevents the xattr manipulation path from being
entered during mount.
Patch 2 is a belt-and-suspenders semantic improvement: an inode under
eviction never needs extra isize expansion.
Patch 3 is a structural improvement: defer all ea_inode iput() calls
until after xattr_sem is released, reducing lock nesting depth and
preventing future code paths from reintroducing the lock ordering issue.
Link: https://syzkaller.appspot.com/bug?extid=5d19358d7eb30ffb0cc5
v6:
- ext4_inline_data_truncate(): use local ea_inode_array instead of
passing NULL, freed after ext4_journal_stop(). Fixes a deadlock
reachable via crafted filesystem where inline data xattr entry has
e_value_inum set: orphan cleanup -> ext4_truncate ->
ext4_inline_data_truncate -> iput under !SB_ACTIVE.
v5:
- Split into 3 patches for easier review.
- Add explicit !SB_ACTIVE early-return in ext4_try_to_expand_extra_isize()
to block ALL mount-time paths (ext4_process_orphan -> ext4_truncate ->
ext4_mark_inode_dirty), not just the eviction path. v4 only relied on
EXT4_STATE_NO_EXPAND which doesn't cover orphan truncation.
v4:
- Comprehensive rewrite of the deferred iput mechanism.
- Thread ea_inode_array through ext4_expand_extra_isize_ea() and
ext4_xattr_move_to_block() so ALL ea_inode iputs in the expand
path are deferred, not just those in ext4_xattr_block_set().
- Add NULL safety to ext4_expand_inode_array(): when ea_inode_array
pointer is NULL, fall back to synchronous iput (for callers like
ext4_initxattrs that only run with SB_ACTIVE).
- Use __GFP_NOFAIL to guarantee deferred array growth, eliminating
fallback to synchronous iput under locks.
- Update ext4_xattr_ibody_set() and ext4_xattr_set_entry() signatures
to accept ea_inode_array, converting ALL iput(ea_inode) calls.
- Set EXT4_STATE_NO_EXPAND in ext4_evict_inode() before
ext4_mark_inode_dirty().
v3:
- Check ext4_expand_inode_array() return value; fallback to
direct iput() on ENOMEM to prevent inode leak.
- Make ext4_xattr_set_handle() take an optional ea_inode_array
output parameter so callers can free after ext4_journal_stop(),
avoiding the jbd2_handle vs s_writepages_rwsem AB-BA.
- Pass ea_inode_array directly to ext4_xattr_release_block()
instead of using a local array freed under xattr_sem.
- Move ext4_xattr_inode_array_free() after ext4_journal_stop()
v2:
- Defer iput() in ext4_xattr_block_set() via ea_inode_array,
freed after xattr_sem is released. Fixes the root cause.
v1:
- Set EXT4_STATE_NO_EXPAND in ext4_evict_inode() to skip expand
on inodes being deleted. Only fixes the syzbot reproducer, not
the underlying lock ordering violation.
Yun Zhou (3):
ext4: skip extra isize expansion during mount to prevent deadlock
ext4: set EXT4_STATE_NO_EXPAND in ext4_evict_inode
ext4: defer iput() on ea_inodes to reduce lock holding scope
fs/ext4/acl.c | 2 +-
fs/ext4/crypto.c | 4 +-
fs/ext4/inline.c | 10 +++--
fs/ext4/inode.c | 27 ++++++++++--
fs/ext4/xattr.c | 93 ++++++++++++++++++++++++----------------
fs/ext4/xattr.h | 10 +++--
fs/ext4/xattr_security.c | 3 +-
7 files changed, 98 insertions(+), 51 deletions(-)
--
2.43.0
^ permalink raw reply
* [PATCH v6 3/3] ext4: defer iput() on ea_inodes to reduce lock holding scope
From: Yun Zhou @ 2026-06-15 16:30 UTC (permalink / raw)
To: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
yi.zhang, ebiggers, yun.zhou
Cc: linux-ext4, linux-kernel
In-Reply-To: <20260615163038.1223293-1-yun.zhou@windriver.com>
ext4_xattr_block_set(), ext4_xattr_ibody_set() and ext4_xattr_set_entry()
call iput() on ea_inodes while xattr_sem and a jbd2 handle are held.
This creates an unnecessarily wide lock scope and, without the preceding
!SB_ACTIVE guard, could trigger write_inode_now() -> s_writepages_rwsem
creating a circular dependency.
Structurally eliminate iput-under-xattr_sem by collecting ea_inodes into
a deferred-iput array and releasing them after locks are dropped:
1. Convert all iput(ea_inode) calls within ext4_xattr_set_entry(),
ext4_xattr_ibody_set(), and ext4_xattr_block_set() to deferred iput
via ext4_expand_inode_array(). Thread the ea_inode_array through
ext4_xattr_set_handle(), ext4_xattr_move_to_block(), and
ext4_expand_extra_isize_ea() as an output parameter.
2. Callers that own the journal handle (ext4_xattr_set,
ext4_expand_extra_isize, ext4_inline_data_truncate) free the array
AFTER ext4_journal_stop(). For ext4_try_to_expand_extra_isize
(called from __ext4_mark_inode_dirty with the caller's handle),
free after xattr_sem release -- safe because the preceding patches
block the !SB_ACTIVE path.
3. Callers that cannot control the handle lifetime (ext4_initxattrs,
__ext4_set_acl, ext4_set_context, ext4_create_inline_data,
ext4_update_inline_data, ext4_destroy_inline_data_nolock) pass NULL.
ext4_expand_inode_array() falls back to synchronous iput() when
ea_inode_array is NULL -- safe because these callers only run with
SB_ACTIVE where iput cannot trigger write_inode_now().
4. Use __GFP_NOFAIL in ext4_expand_inode_array() to guarantee the
deferred-iput array never fails to grow, eliminating any fallback
to synchronous iput() under locks.
5. Pass ea_inode_array directly to ext4_xattr_release_block() in
ext4_xattr_block_set() instead of using a local array freed
synchronously under xattr_sem.
Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
---
fs/ext4/acl.c | 2 +-
fs/ext4/crypto.c | 4 +-
fs/ext4/inline.c | 10 +++--
fs/ext4/inode.c | 16 +++++--
fs/ext4/xattr.c | 93 ++++++++++++++++++++++++----------------
fs/ext4/xattr.h | 10 +++--
fs/ext4/xattr_security.c | 3 +-
7 files changed, 87 insertions(+), 51 deletions(-)
diff --git a/fs/ext4/acl.c b/fs/ext4/acl.c
index 3bffe862f954..21de8276b558 100644
--- a/fs/ext4/acl.c
+++ b/fs/ext4/acl.c
@@ -215,7 +215,7 @@ __ext4_set_acl(handle_t *handle, struct inode *inode, int type,
}
error = ext4_xattr_set_handle(handle, inode, name_index, "",
- value, size, xattr_flags);
+ value, size, xattr_flags, NULL);
kfree(value);
if (!error)
diff --git a/fs/ext4/crypto.c b/fs/ext4/crypto.c
index f41f320f4437..bca760751c1d 100644
--- a/fs/ext4/crypto.c
+++ b/fs/ext4/crypto.c
@@ -173,7 +173,7 @@ static int ext4_set_context(struct inode *inode, const void *ctx, size_t len,
res = ext4_xattr_set_handle(handle, inode,
EXT4_XATTR_INDEX_ENCRYPTION,
EXT4_XATTR_NAME_ENCRYPTION_CONTEXT,
- ctx, len, XATTR_CREATE);
+ ctx, len, XATTR_CREATE, NULL);
if (!res) {
ext4_set_inode_flag(inode, EXT4_INODE_ENCRYPT);
ext4_clear_inode_state(inode,
@@ -202,7 +202,7 @@ static int ext4_set_context(struct inode *inode, const void *ctx, size_t len,
res = ext4_xattr_set_handle(handle, inode, EXT4_XATTR_INDEX_ENCRYPTION,
EXT4_XATTR_NAME_ENCRYPTION_CONTEXT,
- ctx, len, 0);
+ ctx, len, 0, NULL);
if (!res) {
ext4_set_inode_flag(inode, EXT4_INODE_ENCRYPT);
/*
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 8045e4ff270c..6e2b242f8062 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -309,7 +309,7 @@ static int ext4_create_inline_data(handle_t *handle,
goto out;
}
- error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is, NULL);
if (error) {
if (error == -ENOSPC)
ext4_clear_inode_state(inode,
@@ -386,7 +386,7 @@ static int ext4_update_inline_data(handle_t *handle, struct inode *inode,
i.value = value;
i.value_len = len;
- error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is, NULL);
if (error)
goto out;
@@ -469,7 +469,7 @@ static int ext4_destroy_inline_data_nolock(handle_t *handle,
if (error)
goto out;
- error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is, NULL);
if (error)
goto out;
@@ -1847,6 +1847,7 @@ int ext4_inline_data_truncate(struct inode *inode, int *has_inline)
int inline_size, value_len, needed_blocks, no_expand, err = 0;
size_t i_size;
void *value = NULL;
+ struct ext4_xattr_inode_array *ea_inode_array = NULL;
struct ext4_xattr_ibody_find is = {
.s = { .not_found = -ENODATA, },
};
@@ -1917,7 +1918,7 @@ int ext4_inline_data_truncate(struct inode *inode, int *has_inline)
i.value = value;
i.value_len = i_size > EXT4_MIN_INLINE_DATA_SIZE ?
i_size - EXT4_MIN_INLINE_DATA_SIZE : 0;
- err = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ err = ext4_xattr_ibody_set(handle, inode, &i, &is, &ea_inode_array);
if (err)
goto out_error;
}
@@ -1950,6 +1951,7 @@ int ext4_inline_data_truncate(struct inode *inode, int *has_inline)
ext4_handle_sync(handle);
}
ext4_journal_stop(handle);
+ ext4_xattr_inode_array_free(ea_inode_array);
return err;
}
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 60c91c098fa0..a1b0f47f8f4f 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -6409,7 +6409,8 @@ ext4_reserve_inode_write(handle_t *handle, struct inode *inode,
static int __ext4_expand_extra_isize(struct inode *inode,
unsigned int new_extra_isize,
struct ext4_iloc *iloc,
- handle_t *handle, int *no_expand)
+ handle_t *handle, int *no_expand,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_inode *raw_inode;
struct ext4_xattr_ibody_header *header;
@@ -6454,7 +6455,7 @@ static int __ext4_expand_extra_isize(struct inode *inode,
/* try to expand with EAs present */
error = ext4_expand_extra_isize_ea(inode, new_extra_isize,
- raw_inode, handle);
+ raw_inode, handle, ea_inode_array);
if (error) {
/*
* Inode size expansion failed; don't try again
@@ -6476,6 +6477,7 @@ static int ext4_try_to_expand_extra_isize(struct inode *inode,
{
int no_expand;
int error;
+ struct ext4_xattr_inode_array *ea_inode_array = NULL;
if (ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND))
return -EOVERFLOW;
@@ -6507,8 +6509,11 @@ static int ext4_try_to_expand_extra_isize(struct inode *inode,
return -EBUSY;
error = __ext4_expand_extra_isize(inode, new_extra_isize, &iloc,
- handle, &no_expand);
+ handle, &no_expand,
+ &ea_inode_array);
ext4_write_unlock_xattr(inode, &no_expand);
+ /* Safe with caller's handle active: !SB_ACTIVE is blocked above */
+ ext4_xattr_inode_array_free(ea_inode_array);
return error;
}
@@ -6520,6 +6525,7 @@ int ext4_expand_extra_isize(struct inode *inode,
handle_t *handle;
int no_expand;
int error, rc;
+ struct ext4_xattr_inode_array *ea_inode_array = NULL;
if (ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND)) {
brelse(iloc->bh);
@@ -6545,7 +6551,8 @@ int ext4_expand_extra_isize(struct inode *inode,
}
error = __ext4_expand_extra_isize(inode, new_extra_isize, iloc,
- handle, &no_expand);
+ handle, &no_expand,
+ &ea_inode_array);
rc = ext4_mark_iloc_dirty(handle, inode, iloc);
if (!error)
@@ -6554,6 +6561,7 @@ int ext4_expand_extra_isize(struct inode *inode,
out_unlock:
ext4_write_unlock_xattr(inode, &no_expand);
ext4_journal_stop(handle);
+ ext4_xattr_inode_array_free(ea_inode_array);
return error;
}
diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
index 982a1f831e22..4eb83917e6b4 100644
--- a/fs/ext4/xattr.c
+++ b/fs/ext4/xattr.c
@@ -1630,7 +1630,8 @@ static int ext4_xattr_set_entry(struct ext4_xattr_info *i,
struct ext4_xattr_search *s,
handle_t *handle, struct inode *inode,
struct inode *new_ea_inode,
- bool is_block)
+ bool is_block,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_entry *last, *next;
struct ext4_xattr_entry *here = s->here;
@@ -1848,7 +1849,7 @@ static int ext4_xattr_set_entry(struct ext4_xattr_info *i,
ret = 0;
out:
- iput(old_ea_inode);
+ ext4_expand_inode_array(ea_inode_array, old_ea_inode);
return ret;
}
@@ -1898,7 +1899,8 @@ ext4_xattr_block_find(struct inode *inode, struct ext4_xattr_info *i,
static int
ext4_xattr_block_set(handle_t *handle, struct inode *inode,
struct ext4_xattr_info *i,
- struct ext4_xattr_block_find *bs)
+ struct ext4_xattr_block_find *bs,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct super_block *sb = inode->i_sb;
struct buffer_head *new_bh = NULL;
@@ -1961,7 +1963,8 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
}
ea_bdebug(bs->bh, "modifying in-place");
error = ext4_xattr_set_entry(i, s, handle, inode,
- ea_inode, true /* is_block */);
+ ea_inode, true /* is_block */,
+ ea_inode_array);
ext4_xattr_block_csum_set(inode, bs->bh);
unlock_buffer(bs->bh);
if (error == -EFSCORRUPTED)
@@ -2030,7 +2033,7 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
}
error = ext4_xattr_set_entry(i, s, handle, inode, ea_inode,
- true /* is_block */);
+ true /* is_block */, ea_inode_array);
if (error == -EFSCORRUPTED)
goto bad_block;
if (error)
@@ -2150,7 +2153,8 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
ext4_warning_inode(ea_inode,
"dec ref error=%d",
error);
- iput(ea_inode);
+ ext4_expand_inode_array(ea_inode_array,
+ ea_inode);
ea_inode = NULL;
}
@@ -2182,12 +2186,9 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
/* Drop the previous xattr block. */
if (bs->bh && bs->bh != new_bh) {
- struct ext4_xattr_inode_array *ea_inode_array = NULL;
-
ext4_xattr_release_block(handle, inode, bs->bh,
- &ea_inode_array,
+ ea_inode_array,
0 /* extra_credits */);
- ext4_xattr_inode_array_free(ea_inode_array);
}
error = 0;
@@ -2203,7 +2204,7 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
ext4_xattr_inode_free_quota(inode, ea_inode,
i_size_read(ea_inode));
}
- iput(ea_inode);
+ ext4_expand_inode_array(ea_inode_array, ea_inode);
}
if (ce)
mb_cache_entry_put(ea_block_cache, ce);
@@ -2253,7 +2254,8 @@ int ext4_xattr_ibody_find(struct inode *inode, struct ext4_xattr_info *i,
int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
struct ext4_xattr_info *i,
- struct ext4_xattr_ibody_find *is)
+ struct ext4_xattr_ibody_find *is,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_ibody_header *header;
struct ext4_xattr_search *s = &is->s;
@@ -2273,7 +2275,7 @@ int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
return PTR_ERR(ea_inode);
}
error = ext4_xattr_set_entry(i, s, handle, inode, ea_inode,
- false /* is_block */);
+ false /* is_block */, ea_inode_array);
if (error) {
if (ea_inode) {
int error2;
@@ -2285,7 +2287,7 @@ int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
ext4_xattr_inode_free_quota(inode, ea_inode,
i_size_read(ea_inode));
- iput(ea_inode);
+ ext4_expand_inode_array(ea_inode_array, ea_inode);
}
return error;
}
@@ -2297,7 +2299,7 @@ int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
header->h_magic = cpu_to_le32(0);
ext4_clear_inode_state(inode, EXT4_STATE_XATTR);
}
- iput(ea_inode);
+ ext4_expand_inode_array(ea_inode_array, ea_inode);
return 0;
}
@@ -2348,7 +2350,8 @@ static struct buffer_head *ext4_xattr_get_block(struct inode *inode)
int
ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
const char *name, const void *value, size_t value_len,
- int flags)
+ int flags,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_info i = {
.name_index = name_index,
@@ -2428,9 +2431,11 @@ ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
if (!value) {
if (!is.s.not_found)
- error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is,
+ ea_inode_array);
else if (!bs.s.not_found)
- error = ext4_xattr_block_set(handle, inode, &i, &bs);
+ error = ext4_xattr_block_set(handle, inode, &i, &bs,
+ ea_inode_array);
} else {
error = 0;
/* Xattr value did not change? Save us some work and bail out */
@@ -2444,10 +2449,12 @@ ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
EXT4_XATTR_MIN_LARGE_EA_SIZE(inode->i_sb->s_blocksize)))
i.in_inode = 1;
retry_inode:
- error = ext4_xattr_ibody_set(handle, inode, &i, &is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, &is,
+ ea_inode_array);
if (!error && !bs.s.not_found) {
i.value = NULL;
- error = ext4_xattr_block_set(handle, inode, &i, &bs);
+ error = ext4_xattr_block_set(handle, inode, &i, &bs,
+ ea_inode_array);
} else if (error == -ENOSPC) {
if (EXT4_I(inode)->i_file_acl && !bs.s.base) {
brelse(bs.bh);
@@ -2456,11 +2463,12 @@ ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
if (error)
goto cleanup;
}
- error = ext4_xattr_block_set(handle, inode, &i, &bs);
+ error = ext4_xattr_block_set(handle, inode, &i, &bs,
+ ea_inode_array);
if (!error && !is.s.not_found) {
i.value = NULL;
error = ext4_xattr_ibody_set(handle, inode, &i,
- &is);
+ &is, ea_inode_array);
} else if (error == -ENOSPC) {
/*
* Xattr does not fit in the block, store at
@@ -2539,6 +2547,7 @@ ext4_xattr_set(struct inode *inode, int name_index, const char *name,
{
handle_t *handle;
struct super_block *sb = inode->i_sb;
+ struct ext4_xattr_inode_array *ea_inode_array = NULL;
int error, retries = 0;
int credits;
@@ -2559,10 +2568,13 @@ ext4_xattr_set(struct inode *inode, int name_index, const char *name,
int error2;
error = ext4_xattr_set_handle(handle, inode, name_index, name,
- value, value_len, flags);
+ value, value_len, flags,
+ &ea_inode_array);
ext4_fc_mark_ineligible(inode->i_sb, EXT4_FC_REASON_XATTR,
handle);
error2 = ext4_journal_stop(handle);
+ ext4_xattr_inode_array_free(ea_inode_array);
+ ea_inode_array = NULL;
if (error == -ENOSPC &&
ext4_should_retry_alloc(sb, &retries))
goto retry;
@@ -2604,7 +2616,8 @@ static void ext4_xattr_shift_entries(struct ext4_xattr_entry *entry,
*/
static int ext4_xattr_move_to_block(handle_t *handle, struct inode *inode,
struct ext4_inode *raw_inode,
- struct ext4_xattr_entry *entry)
+ struct ext4_xattr_entry *entry,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_ibody_find *is = NULL;
struct ext4_xattr_block_find *bs = NULL;
@@ -2668,14 +2681,14 @@ static int ext4_xattr_move_to_block(handle_t *handle, struct inode *inode,
goto out;
/* Move ea entry from the inode into the block */
- error = ext4_xattr_block_set(handle, inode, &i, bs);
+ error = ext4_xattr_block_set(handle, inode, &i, bs, ea_inode_array);
if (error)
goto out;
/* Remove the chosen entry from the inode */
i.value = NULL;
i.value_len = 0;
- error = ext4_xattr_ibody_set(handle, inode, &i, is);
+ error = ext4_xattr_ibody_set(handle, inode, &i, is, ea_inode_array);
out:
kfree(b_entry_name);
@@ -2694,7 +2707,8 @@ static int ext4_xattr_move_to_block(handle_t *handle, struct inode *inode,
static int ext4_xattr_make_inode_space(handle_t *handle, struct inode *inode,
struct ext4_inode *raw_inode,
int isize_diff, size_t ifree,
- size_t bfree, int *total_ino)
+ size_t bfree, int *total_ino,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
struct ext4_xattr_entry *small_entry;
@@ -2744,7 +2758,7 @@ static int ext4_xattr_make_inode_space(handle_t *handle, struct inode *inode,
total_size += EXT4_XATTR_SIZE(
le32_to_cpu(entry->e_value_size));
error = ext4_xattr_move_to_block(handle, inode, raw_inode,
- entry);
+ entry, ea_inode_array);
if (error)
return error;
@@ -2761,7 +2775,8 @@ static int ext4_xattr_make_inode_space(handle_t *handle, struct inode *inode,
* Returns 0 on success or negative error number on failure.
*/
int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
- struct ext4_inode *raw_inode, handle_t *handle)
+ struct ext4_inode *raw_inode, handle_t *handle,
+ struct ext4_xattr_inode_array **ea_inode_array)
{
struct ext4_xattr_ibody_header *header;
struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
@@ -2833,7 +2848,7 @@ int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
error = ext4_xattr_make_inode_space(handle, inode, raw_inode,
isize_diff, ifree, bfree,
- &total_ino);
+ &total_ino, ea_inode_array);
if (error) {
if (error == -ENOSPC && !tried_min_extra_isize &&
s_min_extra_isize) {
@@ -2869,19 +2884,27 @@ int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
/* Add the large xattr @inode into @ea_inode_array for deferred iput().
* If @ea_inode_array is new or full it will be grown and the old
* contents copied over.
+ *
+ * If @inode is NULL this is a no-op. If @ea_inode_array is NULL the
+ * caller guarantees SB_ACTIVE so synchronous iput is safe.
*/
static int
ext4_expand_inode_array(struct ext4_xattr_inode_array **ea_inode_array,
struct inode *inode)
{
+ if (!inode)
+ return 0;
+ if (!ea_inode_array) {
+ iput(inode);
+ return 0;
+ }
if (*ea_inode_array == NULL) {
/*
* Start with 15 inodes, so it fits into a power-of-two size.
*/
(*ea_inode_array) = kmalloc_flex(**ea_inode_array, inodes,
- EIA_MASK, GFP_NOFS);
- if (*ea_inode_array == NULL)
- return -ENOMEM;
+ EIA_MASK,
+ GFP_NOFS | __GFP_NOFAIL);
(*ea_inode_array)->count = 0;
} else if (((*ea_inode_array)->count & EIA_MASK) == EIA_MASK) {
/* expand the array once all 15 + n * 16 slots are full */
@@ -2889,9 +2912,7 @@ ext4_expand_inode_array(struct ext4_xattr_inode_array **ea_inode_array,
new_array = kmalloc_flex(**ea_inode_array, inodes,
(*ea_inode_array)->count + EIA_INCR,
- GFP_NOFS);
- if (new_array == NULL)
- return -ENOMEM;
+ GFP_NOFS | __GFP_NOFAIL);
memcpy(new_array, *ea_inode_array,
struct_size(*ea_inode_array, inodes,
(*ea_inode_array)->count));
diff --git a/fs/ext4/xattr.h b/fs/ext4/xattr.h
index 1fedf44d4fb6..6771d00d3fa4 100644
--- a/fs/ext4/xattr.h
+++ b/fs/ext4/xattr.h
@@ -179,7 +179,9 @@ extern ssize_t ext4_listxattr(struct dentry *, char *, size_t);
extern int ext4_xattr_get(struct inode *, int, const char *, void *, size_t);
extern int ext4_xattr_set(struct inode *, int, const char *, const void *, size_t, int);
-extern int ext4_xattr_set_handle(handle_t *, struct inode *, int, const char *, const void *, size_t, int);
+extern int ext4_xattr_set_handle(handle_t *, struct inode *, int, const char *,
+ const void *, size_t, int,
+ struct ext4_xattr_inode_array **ea_inode_array);
extern int ext4_xattr_set_credits(struct inode *inode, size_t value_len,
bool is_create, int *credits);
extern int __ext4_xattr_set_credits(struct super_block *sb, struct inode *inode,
@@ -192,7 +194,8 @@ extern int ext4_xattr_delete_inode(handle_t *handle, struct inode *inode,
extern void ext4_xattr_inode_array_free(struct ext4_xattr_inode_array *array);
extern int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
- struct ext4_inode *raw_inode, handle_t *handle);
+ struct ext4_inode *raw_inode, handle_t *handle,
+ struct ext4_xattr_inode_array **ea_inode_array);
extern void ext4_evict_ea_inode(struct inode *inode);
extern const struct xattr_handler * const ext4_xattr_handlers[];
@@ -204,7 +207,8 @@ extern int ext4_xattr_ibody_get(struct inode *inode, int name_index,
void *buffer, size_t buffer_size);
extern int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
struct ext4_xattr_info *i,
- struct ext4_xattr_ibody_find *is);
+ struct ext4_xattr_ibody_find *is,
+ struct ext4_xattr_inode_array **ea_inode_array);
extern struct mb_cache *ext4_xattr_create_cache(void);
extern void ext4_xattr_destroy_cache(struct mb_cache *);
diff --git a/fs/ext4/xattr_security.c b/fs/ext4/xattr_security.c
index 776cf11d24ca..6b7ab6e703ad 100644
--- a/fs/ext4/xattr_security.c
+++ b/fs/ext4/xattr_security.c
@@ -44,7 +44,8 @@ ext4_initxattrs(struct inode *inode, const struct xattr *xattr_array,
err = ext4_xattr_set_handle(handle, inode,
EXT4_XATTR_INDEX_SECURITY,
xattr->name, xattr->value,
- xattr->value_len, XATTR_CREATE);
+ xattr->value_len, XATTR_CREATE,
+ NULL);
if (err < 0)
break;
}
--
2.43.0
^ permalink raw reply related
* Re: [PATCH v5 3/3] ext4: defer iput() on ea_inodes to reduce lock holding scope
From: Jan Kara @ 2026-06-15 17:02 UTC (permalink / raw)
To: Yun Zhou
Cc: tytso, adilger.kernel, libaokun, jack, ojaswin, ritesh.list,
yi.zhang, ebiggers, linux-ext4, linux-kernel
In-Reply-To: <20260615115317.3549469-4-yun.zhou@windriver.com>
On Mon 15-06-26 19:53:17, Yun Zhou wrote:
> ext4_xattr_block_set(), ext4_xattr_ibody_set() and ext4_xattr_set_entry()
> call iput() on ea_inodes while xattr_sem and a jbd2 handle are held.
> This creates an unnecessarily wide lock scope and, without the preceding
> !SB_ACTIVE guard, could trigger write_inode_now() -> s_writepages_rwsem
> creating a circular dependency.
>
> Structurally eliminate iput-under-xattr_sem by collecting ea_inodes into
> a deferred-iput array and releasing them after locks are dropped:
>
> 1. Convert all iput(ea_inode) calls within ext4_xattr_set_entry(),
> ext4_xattr_ibody_set(), and ext4_xattr_block_set() to deferred iput
> via ext4_expand_inode_array(). Thread the ea_inode_array through
> ext4_xattr_set_handle(), ext4_xattr_move_to_block(), and
> ext4_expand_extra_isize_ea() as an output parameter.
>
> 2. Callers that own the journal handle (ext4_xattr_set,
> ext4_expand_extra_isize) free the array AFTER ext4_journal_stop().
> For ext4_try_to_expand_extra_isize (called from __ext4_mark_inode_dirty
> with the caller's handle), free after xattr_sem release -- safe
> because the preceding patches block the !SB_ACTIVE path.
>
> 3. Callers that cannot control the handle lifetime (ext4_initxattrs,
> __ext4_set_acl, ext4_set_context, inline data ops) pass NULL.
> ext4_expand_inode_array() falls back to synchronous iput() when
> ea_inode_array is NULL -- safe because these callers only run with
> SB_ACTIVE where iput cannot trigger write_inode_now().
>
> 4. Use __GFP_NOFAIL in ext4_expand_inode_array() to guarantee the
> deferred-iput array never fails to grow, eliminating any fallback
> to synchronous iput() under locks.
>
> 5. Pass ea_inode_array directly to ext4_xattr_release_block() in
> ext4_xattr_block_set() instead of using a local array freed
> synchronously under xattr_sem.
>
> Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
Frankly, I hate how many places this touches and how this complicates
things, in particular when we don't really need this to fix the deadlock
because the previous two patches addressed the really problematic places.
That being said I fully agree dropping of ea_inodes xattr refs is a mess,
fragile, and there are potentially more deadlocks hiding there. The
architecture that might be more seamless would be something like having a
list of ea inodes to drop anchored in the sb and just queue work to drop
the list from a separate context to avoid the locking dependencies or doing
unexpected modifications in a transaction. That would allow even
simplifying the current code...
Honza
> ---
> fs/ext4/acl.c | 2 +-
> fs/ext4/crypto.c | 4 +-
> fs/ext4/inline.c | 8 ++--
> fs/ext4/inode.c | 16 +++++--
> fs/ext4/xattr.c | 93 ++++++++++++++++++++++++----------------
> fs/ext4/xattr.h | 10 +++--
> fs/ext4/xattr_security.c | 3 +-
> 7 files changed, 85 insertions(+), 51 deletions(-)
>
> diff --git a/fs/ext4/acl.c b/fs/ext4/acl.c
> index 3bffe862f954..21de8276b558 100644
> --- a/fs/ext4/acl.c
> +++ b/fs/ext4/acl.c
> @@ -215,7 +215,7 @@ __ext4_set_acl(handle_t *handle, struct inode *inode, int type,
> }
>
> error = ext4_xattr_set_handle(handle, inode, name_index, "",
> - value, size, xattr_flags);
> + value, size, xattr_flags, NULL);
>
> kfree(value);
> if (!error)
> diff --git a/fs/ext4/crypto.c b/fs/ext4/crypto.c
> index f41f320f4437..bca760751c1d 100644
> --- a/fs/ext4/crypto.c
> +++ b/fs/ext4/crypto.c
> @@ -173,7 +173,7 @@ static int ext4_set_context(struct inode *inode, const void *ctx, size_t len,
> res = ext4_xattr_set_handle(handle, inode,
> EXT4_XATTR_INDEX_ENCRYPTION,
> EXT4_XATTR_NAME_ENCRYPTION_CONTEXT,
> - ctx, len, XATTR_CREATE);
> + ctx, len, XATTR_CREATE, NULL);
> if (!res) {
> ext4_set_inode_flag(inode, EXT4_INODE_ENCRYPT);
> ext4_clear_inode_state(inode,
> @@ -202,7 +202,7 @@ static int ext4_set_context(struct inode *inode, const void *ctx, size_t len,
>
> res = ext4_xattr_set_handle(handle, inode, EXT4_XATTR_INDEX_ENCRYPTION,
> EXT4_XATTR_NAME_ENCRYPTION_CONTEXT,
> - ctx, len, 0);
> + ctx, len, 0, NULL);
> if (!res) {
> ext4_set_inode_flag(inode, EXT4_INODE_ENCRYPT);
> /*
> diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
> index 8045e4ff270c..2bf2b771929d 100644
> --- a/fs/ext4/inline.c
> +++ b/fs/ext4/inline.c
> @@ -309,7 +309,7 @@ static int ext4_create_inline_data(handle_t *handle,
> goto out;
> }
>
> - error = ext4_xattr_ibody_set(handle, inode, &i, &is);
> + error = ext4_xattr_ibody_set(handle, inode, &i, &is, NULL);
> if (error) {
> if (error == -ENOSPC)
> ext4_clear_inode_state(inode,
> @@ -386,7 +386,7 @@ static int ext4_update_inline_data(handle_t *handle, struct inode *inode,
> i.value = value;
> i.value_len = len;
>
> - error = ext4_xattr_ibody_set(handle, inode, &i, &is);
> + error = ext4_xattr_ibody_set(handle, inode, &i, &is, NULL);
> if (error)
> goto out;
>
> @@ -469,7 +469,7 @@ static int ext4_destroy_inline_data_nolock(handle_t *handle,
> if (error)
> goto out;
>
> - error = ext4_xattr_ibody_set(handle, inode, &i, &is);
> + error = ext4_xattr_ibody_set(handle, inode, &i, &is, NULL);
> if (error)
> goto out;
>
> @@ -1917,7 +1917,7 @@ int ext4_inline_data_truncate(struct inode *inode, int *has_inline)
> i.value = value;
> i.value_len = i_size > EXT4_MIN_INLINE_DATA_SIZE ?
> i_size - EXT4_MIN_INLINE_DATA_SIZE : 0;
> - err = ext4_xattr_ibody_set(handle, inode, &i, &is);
> + err = ext4_xattr_ibody_set(handle, inode, &i, &is, NULL);
> if (err)
> goto out_error;
> }
> diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
> index 60c91c098fa0..a1b0f47f8f4f 100644
> --- a/fs/ext4/inode.c
> +++ b/fs/ext4/inode.c
> @@ -6409,7 +6409,8 @@ ext4_reserve_inode_write(handle_t *handle, struct inode *inode,
> static int __ext4_expand_extra_isize(struct inode *inode,
> unsigned int new_extra_isize,
> struct ext4_iloc *iloc,
> - handle_t *handle, int *no_expand)
> + handle_t *handle, int *no_expand,
> + struct ext4_xattr_inode_array **ea_inode_array)
> {
> struct ext4_inode *raw_inode;
> struct ext4_xattr_ibody_header *header;
> @@ -6454,7 +6455,7 @@ static int __ext4_expand_extra_isize(struct inode *inode,
>
> /* try to expand with EAs present */
> error = ext4_expand_extra_isize_ea(inode, new_extra_isize,
> - raw_inode, handle);
> + raw_inode, handle, ea_inode_array);
> if (error) {
> /*
> * Inode size expansion failed; don't try again
> @@ -6476,6 +6477,7 @@ static int ext4_try_to_expand_extra_isize(struct inode *inode,
> {
> int no_expand;
> int error;
> + struct ext4_xattr_inode_array *ea_inode_array = NULL;
>
> if (ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND))
> return -EOVERFLOW;
> @@ -6507,8 +6509,11 @@ static int ext4_try_to_expand_extra_isize(struct inode *inode,
> return -EBUSY;
>
> error = __ext4_expand_extra_isize(inode, new_extra_isize, &iloc,
> - handle, &no_expand);
> + handle, &no_expand,
> + &ea_inode_array);
> ext4_write_unlock_xattr(inode, &no_expand);
> + /* Safe with caller's handle active: !SB_ACTIVE is blocked above */
> + ext4_xattr_inode_array_free(ea_inode_array);
>
> return error;
> }
> @@ -6520,6 +6525,7 @@ int ext4_expand_extra_isize(struct inode *inode,
> handle_t *handle;
> int no_expand;
> int error, rc;
> + struct ext4_xattr_inode_array *ea_inode_array = NULL;
>
> if (ext4_test_inode_state(inode, EXT4_STATE_NO_EXPAND)) {
> brelse(iloc->bh);
> @@ -6545,7 +6551,8 @@ int ext4_expand_extra_isize(struct inode *inode,
> }
>
> error = __ext4_expand_extra_isize(inode, new_extra_isize, iloc,
> - handle, &no_expand);
> + handle, &no_expand,
> + &ea_inode_array);
>
> rc = ext4_mark_iloc_dirty(handle, inode, iloc);
> if (!error)
> @@ -6554,6 +6561,7 @@ int ext4_expand_extra_isize(struct inode *inode,
> out_unlock:
> ext4_write_unlock_xattr(inode, &no_expand);
> ext4_journal_stop(handle);
> + ext4_xattr_inode_array_free(ea_inode_array);
> return error;
> }
>
> diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
> index 982a1f831e22..4eb83917e6b4 100644
> --- a/fs/ext4/xattr.c
> +++ b/fs/ext4/xattr.c
> @@ -1630,7 +1630,8 @@ static int ext4_xattr_set_entry(struct ext4_xattr_info *i,
> struct ext4_xattr_search *s,
> handle_t *handle, struct inode *inode,
> struct inode *new_ea_inode,
> - bool is_block)
> + bool is_block,
> + struct ext4_xattr_inode_array **ea_inode_array)
> {
> struct ext4_xattr_entry *last, *next;
> struct ext4_xattr_entry *here = s->here;
> @@ -1848,7 +1849,7 @@ static int ext4_xattr_set_entry(struct ext4_xattr_info *i,
>
> ret = 0;
> out:
> - iput(old_ea_inode);
> + ext4_expand_inode_array(ea_inode_array, old_ea_inode);
> return ret;
> }
>
> @@ -1898,7 +1899,8 @@ ext4_xattr_block_find(struct inode *inode, struct ext4_xattr_info *i,
> static int
> ext4_xattr_block_set(handle_t *handle, struct inode *inode,
> struct ext4_xattr_info *i,
> - struct ext4_xattr_block_find *bs)
> + struct ext4_xattr_block_find *bs,
> + struct ext4_xattr_inode_array **ea_inode_array)
> {
> struct super_block *sb = inode->i_sb;
> struct buffer_head *new_bh = NULL;
> @@ -1961,7 +1963,8 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
> }
> ea_bdebug(bs->bh, "modifying in-place");
> error = ext4_xattr_set_entry(i, s, handle, inode,
> - ea_inode, true /* is_block */);
> + ea_inode, true /* is_block */,
> + ea_inode_array);
> ext4_xattr_block_csum_set(inode, bs->bh);
> unlock_buffer(bs->bh);
> if (error == -EFSCORRUPTED)
> @@ -2030,7 +2033,7 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
> }
>
> error = ext4_xattr_set_entry(i, s, handle, inode, ea_inode,
> - true /* is_block */);
> + true /* is_block */, ea_inode_array);
> if (error == -EFSCORRUPTED)
> goto bad_block;
> if (error)
> @@ -2150,7 +2153,8 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
> ext4_warning_inode(ea_inode,
> "dec ref error=%d",
> error);
> - iput(ea_inode);
> + ext4_expand_inode_array(ea_inode_array,
> + ea_inode);
> ea_inode = NULL;
> }
>
> @@ -2182,12 +2186,9 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
>
> /* Drop the previous xattr block. */
> if (bs->bh && bs->bh != new_bh) {
> - struct ext4_xattr_inode_array *ea_inode_array = NULL;
> -
> ext4_xattr_release_block(handle, inode, bs->bh,
> - &ea_inode_array,
> + ea_inode_array,
> 0 /* extra_credits */);
> - ext4_xattr_inode_array_free(ea_inode_array);
> }
> error = 0;
>
> @@ -2203,7 +2204,7 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
> ext4_xattr_inode_free_quota(inode, ea_inode,
> i_size_read(ea_inode));
> }
> - iput(ea_inode);
> + ext4_expand_inode_array(ea_inode_array, ea_inode);
> }
> if (ce)
> mb_cache_entry_put(ea_block_cache, ce);
> @@ -2253,7 +2254,8 @@ int ext4_xattr_ibody_find(struct inode *inode, struct ext4_xattr_info *i,
>
> int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
> struct ext4_xattr_info *i,
> - struct ext4_xattr_ibody_find *is)
> + struct ext4_xattr_ibody_find *is,
> + struct ext4_xattr_inode_array **ea_inode_array)
> {
> struct ext4_xattr_ibody_header *header;
> struct ext4_xattr_search *s = &is->s;
> @@ -2273,7 +2275,7 @@ int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
> return PTR_ERR(ea_inode);
> }
> error = ext4_xattr_set_entry(i, s, handle, inode, ea_inode,
> - false /* is_block */);
> + false /* is_block */, ea_inode_array);
> if (error) {
> if (ea_inode) {
> int error2;
> @@ -2285,7 +2287,7 @@ int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
>
> ext4_xattr_inode_free_quota(inode, ea_inode,
> i_size_read(ea_inode));
> - iput(ea_inode);
> + ext4_expand_inode_array(ea_inode_array, ea_inode);
> }
> return error;
> }
> @@ -2297,7 +2299,7 @@ int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
> header->h_magic = cpu_to_le32(0);
> ext4_clear_inode_state(inode, EXT4_STATE_XATTR);
> }
> - iput(ea_inode);
> + ext4_expand_inode_array(ea_inode_array, ea_inode);
> return 0;
> }
>
> @@ -2348,7 +2350,8 @@ static struct buffer_head *ext4_xattr_get_block(struct inode *inode)
> int
> ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
> const char *name, const void *value, size_t value_len,
> - int flags)
> + int flags,
> + struct ext4_xattr_inode_array **ea_inode_array)
> {
> struct ext4_xattr_info i = {
> .name_index = name_index,
> @@ -2428,9 +2431,11 @@ ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
>
> if (!value) {
> if (!is.s.not_found)
> - error = ext4_xattr_ibody_set(handle, inode, &i, &is);
> + error = ext4_xattr_ibody_set(handle, inode, &i, &is,
> + ea_inode_array);
> else if (!bs.s.not_found)
> - error = ext4_xattr_block_set(handle, inode, &i, &bs);
> + error = ext4_xattr_block_set(handle, inode, &i, &bs,
> + ea_inode_array);
> } else {
> error = 0;
> /* Xattr value did not change? Save us some work and bail out */
> @@ -2444,10 +2449,12 @@ ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
> EXT4_XATTR_MIN_LARGE_EA_SIZE(inode->i_sb->s_blocksize)))
> i.in_inode = 1;
> retry_inode:
> - error = ext4_xattr_ibody_set(handle, inode, &i, &is);
> + error = ext4_xattr_ibody_set(handle, inode, &i, &is,
> + ea_inode_array);
> if (!error && !bs.s.not_found) {
> i.value = NULL;
> - error = ext4_xattr_block_set(handle, inode, &i, &bs);
> + error = ext4_xattr_block_set(handle, inode, &i, &bs,
> + ea_inode_array);
> } else if (error == -ENOSPC) {
> if (EXT4_I(inode)->i_file_acl && !bs.s.base) {
> brelse(bs.bh);
> @@ -2456,11 +2463,12 @@ ext4_xattr_set_handle(handle_t *handle, struct inode *inode, int name_index,
> if (error)
> goto cleanup;
> }
> - error = ext4_xattr_block_set(handle, inode, &i, &bs);
> + error = ext4_xattr_block_set(handle, inode, &i, &bs,
> + ea_inode_array);
> if (!error && !is.s.not_found) {
> i.value = NULL;
> error = ext4_xattr_ibody_set(handle, inode, &i,
> - &is);
> + &is, ea_inode_array);
> } else if (error == -ENOSPC) {
> /*
> * Xattr does not fit in the block, store at
> @@ -2539,6 +2547,7 @@ ext4_xattr_set(struct inode *inode, int name_index, const char *name,
> {
> handle_t *handle;
> struct super_block *sb = inode->i_sb;
> + struct ext4_xattr_inode_array *ea_inode_array = NULL;
> int error, retries = 0;
> int credits;
>
> @@ -2559,10 +2568,13 @@ ext4_xattr_set(struct inode *inode, int name_index, const char *name,
> int error2;
>
> error = ext4_xattr_set_handle(handle, inode, name_index, name,
> - value, value_len, flags);
> + value, value_len, flags,
> + &ea_inode_array);
> ext4_fc_mark_ineligible(inode->i_sb, EXT4_FC_REASON_XATTR,
> handle);
> error2 = ext4_journal_stop(handle);
> + ext4_xattr_inode_array_free(ea_inode_array);
> + ea_inode_array = NULL;
> if (error == -ENOSPC &&
> ext4_should_retry_alloc(sb, &retries))
> goto retry;
> @@ -2604,7 +2616,8 @@ static void ext4_xattr_shift_entries(struct ext4_xattr_entry *entry,
> */
> static int ext4_xattr_move_to_block(handle_t *handle, struct inode *inode,
> struct ext4_inode *raw_inode,
> - struct ext4_xattr_entry *entry)
> + struct ext4_xattr_entry *entry,
> + struct ext4_xattr_inode_array **ea_inode_array)
> {
> struct ext4_xattr_ibody_find *is = NULL;
> struct ext4_xattr_block_find *bs = NULL;
> @@ -2668,14 +2681,14 @@ static int ext4_xattr_move_to_block(handle_t *handle, struct inode *inode,
> goto out;
>
> /* Move ea entry from the inode into the block */
> - error = ext4_xattr_block_set(handle, inode, &i, bs);
> + error = ext4_xattr_block_set(handle, inode, &i, bs, ea_inode_array);
> if (error)
> goto out;
>
> /* Remove the chosen entry from the inode */
> i.value = NULL;
> i.value_len = 0;
> - error = ext4_xattr_ibody_set(handle, inode, &i, is);
> + error = ext4_xattr_ibody_set(handle, inode, &i, is, ea_inode_array);
>
> out:
> kfree(b_entry_name);
> @@ -2694,7 +2707,8 @@ static int ext4_xattr_move_to_block(handle_t *handle, struct inode *inode,
> static int ext4_xattr_make_inode_space(handle_t *handle, struct inode *inode,
> struct ext4_inode *raw_inode,
> int isize_diff, size_t ifree,
> - size_t bfree, int *total_ino)
> + size_t bfree, int *total_ino,
> + struct ext4_xattr_inode_array **ea_inode_array)
> {
> struct ext4_xattr_ibody_header *header = IHDR(inode, raw_inode);
> struct ext4_xattr_entry *small_entry;
> @@ -2744,7 +2758,7 @@ static int ext4_xattr_make_inode_space(handle_t *handle, struct inode *inode,
> total_size += EXT4_XATTR_SIZE(
> le32_to_cpu(entry->e_value_size));
> error = ext4_xattr_move_to_block(handle, inode, raw_inode,
> - entry);
> + entry, ea_inode_array);
> if (error)
> return error;
>
> @@ -2761,7 +2775,8 @@ static int ext4_xattr_make_inode_space(handle_t *handle, struct inode *inode,
> * Returns 0 on success or negative error number on failure.
> */
> int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
> - struct ext4_inode *raw_inode, handle_t *handle)
> + struct ext4_inode *raw_inode, handle_t *handle,
> + struct ext4_xattr_inode_array **ea_inode_array)
> {
> struct ext4_xattr_ibody_header *header;
> struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
> @@ -2833,7 +2848,7 @@ int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
>
> error = ext4_xattr_make_inode_space(handle, inode, raw_inode,
> isize_diff, ifree, bfree,
> - &total_ino);
> + &total_ino, ea_inode_array);
> if (error) {
> if (error == -ENOSPC && !tried_min_extra_isize &&
> s_min_extra_isize) {
> @@ -2869,19 +2884,27 @@ int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
> /* Add the large xattr @inode into @ea_inode_array for deferred iput().
> * If @ea_inode_array is new or full it will be grown and the old
> * contents copied over.
> + *
> + * If @inode is NULL this is a no-op. If @ea_inode_array is NULL the
> + * caller guarantees SB_ACTIVE so synchronous iput is safe.
> */
> static int
> ext4_expand_inode_array(struct ext4_xattr_inode_array **ea_inode_array,
> struct inode *inode)
> {
> + if (!inode)
> + return 0;
> + if (!ea_inode_array) {
> + iput(inode);
> + return 0;
> + }
> if (*ea_inode_array == NULL) {
> /*
> * Start with 15 inodes, so it fits into a power-of-two size.
> */
> (*ea_inode_array) = kmalloc_flex(**ea_inode_array, inodes,
> - EIA_MASK, GFP_NOFS);
> - if (*ea_inode_array == NULL)
> - return -ENOMEM;
> + EIA_MASK,
> + GFP_NOFS | __GFP_NOFAIL);
> (*ea_inode_array)->count = 0;
> } else if (((*ea_inode_array)->count & EIA_MASK) == EIA_MASK) {
> /* expand the array once all 15 + n * 16 slots are full */
> @@ -2889,9 +2912,7 @@ ext4_expand_inode_array(struct ext4_xattr_inode_array **ea_inode_array,
>
> new_array = kmalloc_flex(**ea_inode_array, inodes,
> (*ea_inode_array)->count + EIA_INCR,
> - GFP_NOFS);
> - if (new_array == NULL)
> - return -ENOMEM;
> + GFP_NOFS | __GFP_NOFAIL);
> memcpy(new_array, *ea_inode_array,
> struct_size(*ea_inode_array, inodes,
> (*ea_inode_array)->count));
> diff --git a/fs/ext4/xattr.h b/fs/ext4/xattr.h
> index 1fedf44d4fb6..6771d00d3fa4 100644
> --- a/fs/ext4/xattr.h
> +++ b/fs/ext4/xattr.h
> @@ -179,7 +179,9 @@ extern ssize_t ext4_listxattr(struct dentry *, char *, size_t);
>
> extern int ext4_xattr_get(struct inode *, int, const char *, void *, size_t);
> extern int ext4_xattr_set(struct inode *, int, const char *, const void *, size_t, int);
> -extern int ext4_xattr_set_handle(handle_t *, struct inode *, int, const char *, const void *, size_t, int);
> +extern int ext4_xattr_set_handle(handle_t *, struct inode *, int, const char *,
> + const void *, size_t, int,
> + struct ext4_xattr_inode_array **ea_inode_array);
> extern int ext4_xattr_set_credits(struct inode *inode, size_t value_len,
> bool is_create, int *credits);
> extern int __ext4_xattr_set_credits(struct super_block *sb, struct inode *inode,
> @@ -192,7 +194,8 @@ extern int ext4_xattr_delete_inode(handle_t *handle, struct inode *inode,
> extern void ext4_xattr_inode_array_free(struct ext4_xattr_inode_array *array);
>
> extern int ext4_expand_extra_isize_ea(struct inode *inode, int new_extra_isize,
> - struct ext4_inode *raw_inode, handle_t *handle);
> + struct ext4_inode *raw_inode, handle_t *handle,
> + struct ext4_xattr_inode_array **ea_inode_array);
> extern void ext4_evict_ea_inode(struct inode *inode);
>
> extern const struct xattr_handler * const ext4_xattr_handlers[];
> @@ -204,7 +207,8 @@ extern int ext4_xattr_ibody_get(struct inode *inode, int name_index,
> void *buffer, size_t buffer_size);
> extern int ext4_xattr_ibody_set(handle_t *handle, struct inode *inode,
> struct ext4_xattr_info *i,
> - struct ext4_xattr_ibody_find *is);
> + struct ext4_xattr_ibody_find *is,
> + struct ext4_xattr_inode_array **ea_inode_array);
>
> extern struct mb_cache *ext4_xattr_create_cache(void);
> extern void ext4_xattr_destroy_cache(struct mb_cache *);
> diff --git a/fs/ext4/xattr_security.c b/fs/ext4/xattr_security.c
> index 776cf11d24ca..6b7ab6e703ad 100644
> --- a/fs/ext4/xattr_security.c
> +++ b/fs/ext4/xattr_security.c
> @@ -44,7 +44,8 @@ ext4_initxattrs(struct inode *inode, const struct xattr *xattr_array,
> err = ext4_xattr_set_handle(handle, inode,
> EXT4_XATTR_INDEX_SECURITY,
> xattr->name, xattr->value,
> - xattr->value_len, XATTR_CREATE);
> + xattr->value_len, XATTR_CREATE,
> + NULL);
> if (err < 0)
> break;
> }
> --
> 2.43.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply
* [PATCH] ext4: Remove ext4_end_buffer_io_sync()
From: Matthew Wilcox (Oracle) @ 2026-06-15 18:25 UTC (permalink / raw)
To: Theodore Ts'o
Cc: Matthew Wilcox (Oracle), Harshad Shirwadkar, Andreas Dilger,
Baokun Li, Jan Kara, Ojaswin Mujoo, Ritesh Harjani, Zhang Yi,
linux-ext4
There's no need for a custom end_io routine here. We lose some
tracing of I/O completions, but we gain better error handling.
Well, consistent error handling anyway.
Signed-off-by: Matthew Wilcox (Oracle) <willy@infradead.org>
---
fs/ext4/fast_commit.c | 21 +--------------------
1 file changed, 1 insertion(+), 20 deletions(-)
diff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c
index 5773b85e43cb..6b00a7285344 100644
--- a/fs/ext4/fast_commit.c
+++ b/fs/ext4/fast_commit.c
@@ -184,25 +184,6 @@
#include <trace/events/ext4.h>
static struct kmem_cache *ext4_fc_dentry_cachep;
-static void ext4_end_buffer_io_sync(struct bio *bio)
-{
- struct buffer_head *bh;
- bool uptodate = bio_endio_bh(bio, &bh);
-
- BUFFER_TRACE(bh, "");
- if (uptodate) {
- ext4_debug("%s: Block %lld up-to-date",
- __func__, bh->b_blocknr);
- set_buffer_uptodate(bh);
- } else {
- ext4_debug("%s: Block %lld not up-to-date",
- __func__, bh->b_blocknr);
- clear_buffer_uptodate(bh);
- }
-
- unlock_buffer(bh);
-}
-
static inline void ext4_fc_reset_inode(struct inode *inode)
{
struct ext4_inode_info *ei = EXT4_I(inode);
@@ -662,7 +643,7 @@ static void ext4_fc_submit_bh(struct super_block *sb, bool is_tail)
lock_buffer(bh);
set_buffer_dirty(bh);
set_buffer_uptodate(bh);
- bh_submit(bh, REQ_OP_WRITE | write_flags, ext4_end_buffer_io_sync);
+ bh_submit(bh, REQ_OP_WRITE | write_flags, bh_end_write);
EXT4_SB(sb)->s_fc_bh = NULL;
}
--
2.47.3
^ permalink raw reply related
* [PATCH ext4 v4] ext4: fix out-of-bounds read in ext4_read_inline_dir()
From: Xiang Mei @ 2026-06-15 19:05 UTC (permalink / raw)
To: linux-ext4, Jan Kara
Cc: Theodore Ts'o, Andreas Dilger, Baokun Li, Ojaswin Mujoo,
Ritesh Harjani, Zhang Yi, Weiming Shi, Xiang Mei
ext4_read_inline_dir() can read a dirent header past the end of its inline
buffer, triggering a slab-out-of-bounds read during getdents64():
BUG: KASAN: slab-out-of-bounds in __ext4_check_dir_entry
Read of size 2 at addr ffff88800f3dd23c by task exploit/148
...
__ext4_check_dir_entry
ext4_read_inline_dir
iterate_dir
The dirent payload lives in a buffer of exactly inline_size bytes:
dir_buf = kmalloc(inline_size, GFP_NOFS);
but iteration runs in a position space extra_offset bytes larger
(extra_size = extra_offset + inline_size) so the synthetic "." and ".."
land at their block-dir offsets. A dirent is formed at "dir_buf + pos -
extra_offset", yet the ext4_check_dir_entry() length argument uses the
larger extra_size. A position whose dirent header would extend past
extra_size is therefore accepted, and the rescan loop's rec_len probe and
ext4_check_dir_entry() dereference de->rec_len before the entry is rejected.
Reject a position whose minimum-size dirent header would not fit within
extra_size before forming de, in both the rescan and main loops, and pass
inline_size rather than extra_size to ext4_check_dir_entry() so the length
check matches the physical buffer.
Fixes: c4d8b0235aa9 ("ext4: fix readdir error in case inline_data+^dir_index.")
Reported-by: Weiming Shi <bestswngs@gmail.com>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Xiang Mei <xmei5@asu.edu>
---
v4: use Jan's suggested logical-space bound (i/ctx->pos + rec_len >
extra_size), consistent with the loop condition.
v3: dropped the unreachable ctx->pos < extra_offset underflow check.
v2: validate against inline_size instead of extra_size.
fs/ext4/inline.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
index 8045e4ff270c..f1f7104d3dac 100644
--- a/fs/ext4/inline.c
+++ b/fs/ext4/inline.c
@@ -1454,6 +1454,8 @@ int ext4_read_inline_dir(struct file *file,
/* for other entry, the real offset in
* the buf has to be tuned accordingly.
*/
+ if (i + ext4_dir_rec_len(1, NULL) > extra_size)
+ break;
de = (struct ext4_dir_entry_2 *)
(dir_buf + i - extra_offset);
/* It's too expensive to do a full
@@ -1488,10 +1490,17 @@ int ext4_read_inline_dir(struct file *file,
continue;
}
+ /*
+ * de lives at dir_buf + ctx->pos - extra_offset, within the
+ * kmalloc(inline_size) buffer. Make sure its header fits before
+ * ext4_check_dir_entry() dereferences de->rec_len.
+ */
+ if (ctx->pos + ext4_dir_rec_len(1, NULL) > extra_size)
+ goto out;
de = (struct ext4_dir_entry_2 *)
(dir_buf + ctx->pos - extra_offset);
if (ext4_check_dir_entry(inode, file, de, iloc.bh, dir_buf,
- extra_size, ctx->pos))
+ inline_size, ctx->pos))
goto out;
if (le32_to_cpu(de->inode)) {
if (!dir_emit(ctx, de->name, de->name_len,
--
2.43.0
^ permalink raw reply related
* Re: [PATCH ext4 v3] ext4: fix out-of-bounds read in ext4_read_inline_dir()
From: Xiang Mei @ 2026-06-15 19:06 UTC (permalink / raw)
To: Jan Kara
Cc: linux-ext4, Theodore Ts'o, Andreas Dilger, Baokun Li,
Ojaswin Mujoo, Ritesh Harjani, Zhang Yi, Weiming Shi
In-Reply-To: <cohsy6t4qa7pxub7knepgd6nckybqx2xt7ps33zh7fwkx6p3ac@dswnh7vxc572>
On Mon, Jun 15, 2026 at 2:27 AM Jan Kara <jack@suse.cz> wrote:
>
> On Sat 13-06-26 15:18:36, Xiang Mei wrote:
> > ext4_read_inline_dir() can read a dirent header past the end of its inline
> > buffer, triggering a slab-out-of-bounds read during getdents64():
> >
> > BUG: KASAN: slab-out-of-bounds in __ext4_check_dir_entry
> > Read of size 2 at addr ffff88800f3dd23c by task exploit/148
> > ...
> > __ext4_check_dir_entry
> > ext4_read_inline_dir
> > iterate_dir
> >
> > The dirent payload lives in a buffer of exactly inline_size bytes:
> >
> > dir_buf = kmalloc(inline_size, GFP_NOFS);
> >
> > but iteration runs in a position space extra_offset bytes larger
> > (extra_size = extra_offset + inline_size) so the synthetic "." and ".."
> > land at their block-dir offsets. A dirent is formed at "dir_buf + pos -
> > extra_offset", yet the loop bound (ctx->pos < extra_size) and the
> > ext4_check_dir_entry() length argument both use the larger extra_size. A
> > ctx->pos that is valid in extra_size space but whose de lies past
> > inline_size is therefore accepted, and the rescan loop's rec_len probe
> > and ext4_check_dir_entry() dereference de->rec_len before the entry is
> > rejected.
> >
> > Bound the dirent header by inline_size in both loops: break out of the
> > rescan loop once a minimum-size header no longer fits, reject such a
> > position in the main loop before forming de, and pass inline_size rather
> > than extra_size to ext4_check_dir_entry().
> >
> > Fixes: c4d8b0235aa9 ("ext4: fix readdir error in case inline_data+^dir_index.")
> > Reported-by: Weiming Shi <bestswngs@gmail.com>
> > Assisted-by: Claude:claude-opus-4-8
> > Signed-off-by: Xiang Mei <xmei5@asu.edu>
>
> Looks mostly good, just one simplification suggestion below:
>
> > diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
> > index 8045e4ff270c..1266c8827cca 100644
> > --- a/fs/ext4/inline.c
> > +++ b/fs/ext4/inline.c
> > @@ -1454,6 +1454,9 @@ int ext4_read_inline_dir(struct file *file,
> > /* for other entry, the real offset in
> > * the buf has to be tuned accordingly.
> > */
> > + if (i - extra_offset + ext4_dir_rec_len(1, NULL) >
> > + inline_size)
> > + break;
>
> Since 'i' lives in "dir logical offset space", it might be simpler to check
> this as:
> if (i + ext4_dir_rec_len(1, NULL) > extra_size)
>
>
> > de = (struct ext4_dir_entry_2 *)
> > (dir_buf + i - extra_offset);
> > /* It's too expensive to do a full
> > @@ -1488,10 +1491,18 @@ int ext4_read_inline_dir(struct file *file,
> > continue;
> > }
> >
> > + /*
> > + * de lives at dir_buf + ctx->pos - extra_offset, within the
> > + * kmalloc(inline_size) buffer. Make sure its header fits before
> > + * ext4_check_dir_entry() dereferences de->rec_len.
> > + */
> > + if (ctx->pos - extra_offset + ext4_dir_rec_len(1, NULL) >
> > + inline_size)
>
> Similarly here this could be checked as:
>
> if (ctx->pos + ext4_dir_rec_len(1, NULL) > extra_size)
>
> which is both more consistent with the loop termination condition and saves
> the conversion from logical offset to physical offset.
>
> Otherwise the patch looks good.
Thanks so much for the tips and your review. V4 has been sent.
Xiang
>
> Honza
>
> > + goto out;
> > de = (struct ext4_dir_entry_2 *)
> > (dir_buf + ctx->pos - extra_offset);
> > if (ext4_check_dir_entry(inode, file, de, iloc.bh, dir_buf,
> > - extra_size, ctx->pos))
> > + inline_size, ctx->pos))
> > goto out;
> > if (le32_to_cpu(de->inode)) {
> > if (!dir_emit(ctx, de->name, de->name_len,
> > --
> > 2.43.0
> >
> --
> Jan Kara <jack@suse.com>
> SUSE Labs, CR
^ permalink raw reply
* Re: [PATCH v3 0/3] f2fs: support encrypted inline data
From: Eric Biggers @ 2026-06-15 19:37 UTC (permalink / raw)
To: LiaoYuanhong-vivo
Cc: Jaegeuk Kim, Chao Yu, Jonathan Corbet, Shuah Khan,
Theodore Y. Ts'o, open list:F2FS FILE SYSTEM, open list,
open list:DOCUMENTATION,
open list:FSCRYPT: FILE SYSTEM LEVEL ENCRYPTION SUPPORT,
linux-ext4
In-Reply-To: <20260615125517.362294-1-liaoyuanhong@vivo.com>
[+Cc linux-ext4@vger.kernel.org]
On Mon, Jun 15, 2026 at 08:55:12PM +0800, LiaoYuanhong-vivo wrote:
> F2FS currently disables inline data for encrypted regular files because the
> inline payload is stored in the inode block and does not go through the
> regular bio-based fscrypt path. This wastes space for small encrypted
> files on Android devices using F2FS inlinecrypt.
>
> This series adds an encrypted_inline_data on-disk feature for F2FS.
> With this feature enabled, encrypted regular files may keep small contents
> in the inode block. The inline payload is encrypted before being stored in
> the inode and decrypted back into page-cache plaintext on read.
>
> The fscrypt changes are scoped to filesystem-managed data-unit crypto.
> F2FS first asks fscrypt whether the inode's key/policy supports this path.
> It prepares the software transform only when encrypted inline payloads are
> read or written. Inlinecrypt support is limited to v2 IV_INO_LBLK_64 and
> IV_INO_LBLK_32 policies, including the hardware-wrapped key configurations
> supported by fscrypt. Per-file inlinecrypt keys and DIRECT_KEY policies
> are not supported for encrypted inline data.
I still think we should hold off on this, for the reasons I gave at
https://lore.kernel.org/r/20260515184124.GA4903@quark/
As soon as you start using hardware-wrapped keys this will become
irrelevant, as it can't be used in that case. I see you added "support"
for that case anyway by deriving contents encryption keys from the
sw_secret. But that bypasses the security model, which isn't okay.
I'm also working to simplify ext4 and f2fs's file contents encryption
implementation by standardizing on blk-crypto. That aligns well with
what btrfs encryption is going to do as well. So this isn't a great
time to be making f2fs's file contents encryption implementation even
more complex by going in a different direction.
If there was demand for this feature from the ext4 side for
general-purpose Linux distros as well, that would make it a bit more
appealing, as it would show broader demand. But with the proposal being
f2fs-specific and effectively just for Android devices that *don't* use
wrapped keys, that feels too narrow for the added complexity.
This proposal also lacks test cases in xfstests and/or Android's
vts_kernel_encryption_test that verify that the inline data is actually
being encrypted correctly. Those tests are essential, and we *must not*
add new file contents encryption implementations without such tests.
- Eric
^ permalink raw reply
* Re: [PATCH ext4 v4] ext4: fix out-of-bounds read in ext4_read_inline_dir()
From: Jan Kara @ 2026-06-15 20:35 UTC (permalink / raw)
To: Xiang Mei
Cc: linux-ext4, Jan Kara, Theodore Ts'o, Andreas Dilger,
Baokun Li, Ojaswin Mujoo, Ritesh Harjani, Zhang Yi, Weiming Shi
In-Reply-To: <20260615190519.946736-1-xmei5@asu.edu>
On Mon 15-06-26 12:05:19, Xiang Mei wrote:
> ext4_read_inline_dir() can read a dirent header past the end of its inline
> buffer, triggering a slab-out-of-bounds read during getdents64():
>
> BUG: KASAN: slab-out-of-bounds in __ext4_check_dir_entry
> Read of size 2 at addr ffff88800f3dd23c by task exploit/148
> ...
> __ext4_check_dir_entry
> ext4_read_inline_dir
> iterate_dir
>
> The dirent payload lives in a buffer of exactly inline_size bytes:
>
> dir_buf = kmalloc(inline_size, GFP_NOFS);
>
> but iteration runs in a position space extra_offset bytes larger
> (extra_size = extra_offset + inline_size) so the synthetic "." and ".."
> land at their block-dir offsets. A dirent is formed at "dir_buf + pos -
> extra_offset", yet the ext4_check_dir_entry() length argument uses the
> larger extra_size. A position whose dirent header would extend past
> extra_size is therefore accepted, and the rescan loop's rec_len probe and
> ext4_check_dir_entry() dereference de->rec_len before the entry is rejected.
>
> Reject a position whose minimum-size dirent header would not fit within
> extra_size before forming de, in both the rescan and main loops, and pass
> inline_size rather than extra_size to ext4_check_dir_entry() so the length
> check matches the physical buffer.
>
> Fixes: c4d8b0235aa9 ("ext4: fix readdir error in case inline_data+^dir_index.")
> Reported-by: Weiming Shi <bestswngs@gmail.com>
> Assisted-by: Claude:claude-opus-4-8
> Signed-off-by: Xiang Mei <xmei5@asu.edu>
Looks good. Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> v4: use Jan's suggested logical-space bound (i/ctx->pos + rec_len >
> extra_size), consistent with the loop condition.
> v3: dropped the unreachable ctx->pos < extra_offset underflow check.
> v2: validate against inline_size instead of extra_size.
>
> fs/ext4/inline.c | 11 ++++++++++-
> 1 file changed, 10 insertions(+), 1 deletion(-)
>
> diff --git a/fs/ext4/inline.c b/fs/ext4/inline.c
> index 8045e4ff270c..f1f7104d3dac 100644
> --- a/fs/ext4/inline.c
> +++ b/fs/ext4/inline.c
> @@ -1454,6 +1454,8 @@ int ext4_read_inline_dir(struct file *file,
> /* for other entry, the real offset in
> * the buf has to be tuned accordingly.
> */
> + if (i + ext4_dir_rec_len(1, NULL) > extra_size)
> + break;
> de = (struct ext4_dir_entry_2 *)
> (dir_buf + i - extra_offset);
> /* It's too expensive to do a full
> @@ -1488,10 +1490,17 @@ int ext4_read_inline_dir(struct file *file,
> continue;
> }
>
> + /*
> + * de lives at dir_buf + ctx->pos - extra_offset, within the
> + * kmalloc(inline_size) buffer. Make sure its header fits before
> + * ext4_check_dir_entry() dereferences de->rec_len.
> + */
> + if (ctx->pos + ext4_dir_rec_len(1, NULL) > extra_size)
> + goto out;
> de = (struct ext4_dir_entry_2 *)
> (dir_buf + ctx->pos - extra_offset);
> if (ext4_check_dir_entry(inode, file, de, iloc.bh, dir_buf,
> - extra_size, ctx->pos))
> + inline_size, ctx->pos))
> goto out;
> if (le32_to_cpu(de->inode)) {
> if (!dir_emit(ctx, de->name, de->name_len,
> --
> 2.43.0
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply
* Re: [PATCH] ext4: Remove ext4_end_buffer_io_sync()
From: Jan Kara @ 2026-06-15 20:41 UTC (permalink / raw)
To: Matthew Wilcox (Oracle)
Cc: Theodore Ts'o, Harshad Shirwadkar, Andreas Dilger, Baokun Li,
Jan Kara, Ojaswin Mujoo, Ritesh Harjani, Zhang Yi, linux-ext4
In-Reply-To: <20260615182527.2208479-1-willy@infradead.org>
On Mon 15-06-26 19:25:25, Matthew Wilcox (Oracle) wrote:
> There's no need for a custom end_io routine here. We lose some
> tracing of I/O completions, but we gain better error handling.
> Well, consistent error handling anyway.
>
> Signed-off-by: Matthew Wilcox (Oracle) <willy@infradead.org>
Yes, I think this is better. I was also considering that when revieweing
your patch series but decided to leave that as a future cleanup. The future
is now! :) Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> fs/ext4/fast_commit.c | 21 +--------------------
> 1 file changed, 1 insertion(+), 20 deletions(-)
>
> diff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c
> index 5773b85e43cb..6b00a7285344 100644
> --- a/fs/ext4/fast_commit.c
> +++ b/fs/ext4/fast_commit.c
> @@ -184,25 +184,6 @@
> #include <trace/events/ext4.h>
> static struct kmem_cache *ext4_fc_dentry_cachep;
>
> -static void ext4_end_buffer_io_sync(struct bio *bio)
> -{
> - struct buffer_head *bh;
> - bool uptodate = bio_endio_bh(bio, &bh);
> -
> - BUFFER_TRACE(bh, "");
> - if (uptodate) {
> - ext4_debug("%s: Block %lld up-to-date",
> - __func__, bh->b_blocknr);
> - set_buffer_uptodate(bh);
> - } else {
> - ext4_debug("%s: Block %lld not up-to-date",
> - __func__, bh->b_blocknr);
> - clear_buffer_uptodate(bh);
> - }
> -
> - unlock_buffer(bh);
> -}
> -
> static inline void ext4_fc_reset_inode(struct inode *inode)
> {
> struct ext4_inode_info *ei = EXT4_I(inode);
> @@ -662,7 +643,7 @@ static void ext4_fc_submit_bh(struct super_block *sb, bool is_tail)
> lock_buffer(bh);
> set_buffer_dirty(bh);
> set_buffer_uptodate(bh);
> - bh_submit(bh, REQ_OP_WRITE | write_flags, ext4_end_buffer_io_sync);
> + bh_submit(bh, REQ_OP_WRITE | write_flags, bh_end_write);
> EXT4_SB(sb)->s_fc_bh = NULL;
> }
>
> --
> 2.47.3
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply
* Re: [GIT PULL] udf, isofs, ext2, quota fixes and cleanups for 7.2-rc1
From: pr-tracker-bot @ 2026-06-16 7:08 UTC (permalink / raw)
To: Jan Kara; +Cc: Linus Torvalds, linux-fsdevel, linux-ext4
In-Reply-To: <jpb2wjkfh7j73dpinuvwe5qddkr54px64vmvx6bclzd5ter6hk@mjwwwgohsyxy>
The pull request you sent on Mon, 15 Jun 2026 16:51:01 +0200:
> git://git.kernel.org/pub/scm/linux/kernel/git/jack/linux-fs.git fs_for_v7.2-rc1
has been merged into torvalds/linux.git:
https://git.kernel.org/torvalds/c/974b3dec2bfb4b2726a6194105cb048a9dab0626
Thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/prtracker.html
^ permalink raw reply
* Re: [PATCH v3] ext4: fix circular lock dependency in ext4_ext_migrate
From: Zhou, Yun @ 2026-06-16 7:51 UTC (permalink / raw)
To: jack
Cc: linux-ext4, linux-kernel, tytso, adilger.kernel, libaokun,
ojaswin, ritesh.list, yi.zhang, ebiggers
In-Reply-To: <20260612005330.1930804-1-yun.zhou@windriver.com>
> Move iput(tmp_inode) after ext4_writepages_up_write() to avoid a
> circular lock dependency between s_writepages_rwsem and sb_internal
> (freeze protection).
>
> The deadlock scenario:
>
> CPU0 (EXT4_IOC_MIGRATE) CPU1 (orphan cleanup during mount)
> ---- ----
> ext4_ext_migrate()
> ext4_writepages_down_write()
> s_writepages_rwsem (write)
> ext4_evict_inode()
> sb_start_intwrite() [sb_internal]
> ...
> ext4_writepages()
> s_writepages_rwsem (read) [BLOCKED]
> iput(tmp_inode)
> ext4_evict_inode()
> sb_start_intwrite() [BLOCKED]
>
> The tmp_inode is a temporary inode with nlink=0 created solely for
> building the extent tree. Its eviction does not require
> s_writepages_rwsem protection, so deferring iput() until after
> releasing the rwsem is safe.
>
> Reported-by: syzbot+212e8f62790f8e0bc63b@syzkaller.appspotmail.com
> Closes: https://syzkaller.appspot.com/bug?extid=212e8f62790f8e0bc63b
> Fixes: cb85f4d23f79 ("ext4: fix race between writepages and enabling EXT4_EXTENTS_FL")
> Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
> Reviewed-by: Jan Kara <jack@suse.cz>
> ---
> v3: fixes Reported-by tag and Closes tag.
>
> v2: remove redundant null pointer check for iput(tmp_inode).
>
> fs/ext4/migrate.c | 3 ++-
> 1 file changed, 2 insertions(+), 1 deletion(-)
>
Hi Honza,
Thank you very much for taking the time to review these patches and
providing your valuable suggestions. I am eager to solve these
long-standing deadlock issues on Syzkaller, but I do not have much
community experience. I'd like to know, regarding this patch, should I
launch a new RR thread or continue waiting? BR, Yun
^ permalink raw reply
* Re: [PATCH v3] ext4: fix circular lock dependency in ext4_ext_migrate
From: Jan Kara @ 2026-06-16 9:07 UTC (permalink / raw)
To: Zhou, Yun
Cc: jack, linux-ext4, linux-kernel, tytso, adilger.kernel, libaokun,
ojaswin, ritesh.list, yi.zhang, ebiggers
In-Reply-To: <00673f65-cfd4-4042-93cf-cb04ad1d92fb@windriver.com>
Hello Yun!
On Tue 16-06-26 15:51:13, Zhou, Yun wrote:
> > Move iput(tmp_inode) after ext4_writepages_up_write() to avoid a
> > circular lock dependency between s_writepages_rwsem and sb_internal
> > (freeze protection).
> >
> > The deadlock scenario:
> >
> > CPU0 (EXT4_IOC_MIGRATE) CPU1 (orphan cleanup during mount)
> > ---- ----
> > ext4_ext_migrate()
> > ext4_writepages_down_write()
> > s_writepages_rwsem (write)
> > ext4_evict_inode()
> > sb_start_intwrite() [sb_internal]
> > ...
> > ext4_writepages()
> > s_writepages_rwsem (read) [BLOCKED]
> > iput(tmp_inode)
> > ext4_evict_inode()
> > sb_start_intwrite() [BLOCKED]
> >
> > The tmp_inode is a temporary inode with nlink=0 created solely for
> > building the extent tree. Its eviction does not require
> > s_writepages_rwsem protection, so deferring iput() until after
> > releasing the rwsem is safe.
> >
> > Reported-by: syzbot+212e8f62790f8e0bc63b@syzkaller.appspotmail.com
> > Closes: https://syzkaller.appspot.com/bug?extid=212e8f62790f8e0bc63b
> > Fixes: cb85f4d23f79 ("ext4: fix race between writepages and enabling EXT4_EXTENTS_FL")
> > Signed-off-by: Yun Zhou <yun.zhou@windriver.com>
> > Reviewed-by: Jan Kara <jack@suse.cz>
> > ---
> > v3: fixes Reported-by tag and Closes tag.
> >
> > v2: remove redundant null pointer check for iput(tmp_inode).
> >
> > fs/ext4/migrate.c | 3 ++-
> > 1 file changed, 2 insertions(+), 1 deletion(-)
>
> Thank you very much for taking the time to review these patches and
> providing your valuable suggestions. I am eager to solve these long-standing
> deadlock issues on Syzkaller, but I do not have much community experience.
> I'd like to know, regarding this patch, should I launch a new RR thread or
> continue waiting? BR, Yun
Please keep waiting. On Sunday the merge window for 7.2-rc1 has opened.
That means that about week before and at least for the next week,
maintainers often aren't taking any patches in their trees. I expect Ted to
pick your patch later when he collects fixes to send for 7.2-rc2 or so - he
sends email about that as a reply to the patch. If nothing happens for next
two weeks, I suggest you send a ping asking whether the patch didn't get
lost as a reply to your patch submission. Thanks for your fixes!
Honza
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox