* [PATCH 1/1] ext4: drop extra ea_inode ref in cached-block reuse path
@ 2026-08-12 4:45 Matthias Goergens
2026-08-12 5:05 ` sashiko-bot
2026-08-14 6:50 ` [PATCH v2] " Matthias Goergens
0 siblings, 2 replies; 4+ messages in thread
From: Matthias Goergens @ 2026-08-12 4:45 UTC (permalink / raw)
To: linux-ext4; +Cc: tytso, jack, Matthias Goergens
ext4_xattr_block_set() acquires a reference to the ea_inode via
ext4_xattr_inode_lookup_create() before constructing the new xattr
block. In the new-block allocation path this extra reference is dropped
after ext4_xattr_inode_inc_ref_all() accounts for every entry in the
block. But when mbcache supplies an identical existing block and that
block is reused, the cached block already carries the correct refcount
for its entries, and the extra reference is never dropped.
The cleanup block at the end of the function only calls
ext4_xattr_inode_dec_ref() when error is non-zero, so the success path
through cache reuse leaks one refcount. Each reuse event adds another;
eventually the saturated refcount prevents the ea_inode from being freed
when its last real reference is dropped.
Add the matching drop when the entry was not written directly to the old
physical buffer (bs->bh && s->base == bs->bh->b_data). In that case the
reference belongs to the old buffer, which the later release_block()
handles. When s->base is a clone or a fresh allocation and is discarded
in favour of the cached block, the extra reference must be dropped.
Found by the Sashiko AI review bot while reviewing the xattr
saturation-livelock fix.
Fixes: 0a46ef234756 ("ext4: do not create EA inode under buffer lock")
---
fs/ext4/xattr.c | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
index 982a1f831e228..cf26932a528b9 100644
--- a/fs/ext4/xattr.c
+++ b/fs/ext4/xattr.c
@@ -2107,6 +2107,26 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
mb_cache_entry_touch(ea_block_cache, ce);
mb_cache_entry_put(ea_block_cache, ce);
ce = NULL;
+ if (!(bs->bh && s->base == bs->bh->b_data) &&
+ ea_inode) {
+ /*
+ * The reused cached block already holds a
+ * reference to the ea_inode. Drop the extra
+ * reference from
+ * ext4_xattr_inode_lookup_create() unless the
+ * entry was written directly to the old
+ * physical buffer, in which case the reference
+ * belongs to that buffer.
+ */
+ error = ext4_xattr_inode_dec_ref(handle,
+ ea_inode);
+ if (error)
+ ext4_warning_inode(ea_inode,
+ "dec ref error=%d",
+ error);
+ iput(ea_inode);
+ ea_inode = NULL;
+ }
} else if (bs->bh && s->base == bs->bh->b_data) {
/* We were modifying this block in-place. */
ea_bdebug(bs->bh, "keeping this block");
--
2.55.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH 1/1] ext4: drop extra ea_inode ref in cached-block reuse path
2026-08-12 4:45 [PATCH 1/1] ext4: drop extra ea_inode ref in cached-block reuse path Matthias Goergens
@ 2026-08-12 5:05 ` sashiko-bot
2026-08-14 6:50 ` [PATCH v2] " Matthias Goergens
1 sibling, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-08-12 5:05 UTC (permalink / raw)
To: Matthias Goergens; +Cc: tytso, linux-ext4
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] On-disk reference counts for ea_inodes and physical blocks are leaked if block creation fails after calling ext4_xattr_inode_inc_ref_all().
--
--- Patch [1]: [PATCH 1/1] ext4: drop extra ea_inode ref in cached-block reuse path ---
commit 57ed7d8076824778cc2e749011823fc10096cd99
Author: Matthias Goergens <matthias.goergens@gmail.com>
ext4: drop extra ea_inode ref in cached-block reuse path
ext4_xattr_block_set() acquires a reference to the ea_inode via
ext4_xattr_inode_lookup_create() before constructing the new xattr
block. In the new-block allocation path this extra reference is dropped
after ext4_xattr_inode_inc_ref_all() accounts for every entry in the
block. But when mbcache supplies an identical existing block and that
block is reused, the cached block already carries the correct refcount
for its entries, and the extra reference is never dropped.
> diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
> index 6127e94ea4f5d..cec71c993286d 100644
> --- a/fs/ext4/xattr.c
> +++ b/fs/ext4/xattr.c
> @@ -2032,6 +2032,26 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
> mb_cache_entry_touch(ea_block_cache, ce);
> mb_cache_entry_put(ea_block_cache, ce);
> ce = NULL;
> + if (!(bs->bh && s->base == bs->bh->b_data) &&
> + ea_inode) {
[ ... ]
> + error = ext4_xattr_inode_dec_ref(handle,
> + ea_inode);
> + if (error)
> + ext4_warning_inode(ea_inode,
> + "dec ref error=%d",
> + error);
> + iput(ea_inode);
> + ea_inode = NULL;
> + }
> } else if (bs->bh && s->base == bs->bh->b_data) {
[Severity: High]
This is a pre-existing issue, but does this error path leak on-disk EA inode
reference counts, block quota, and physical blocks?
Further down in the new block allocation path of ext4_xattr_block_set(), the
code calls ext4_xattr_inode_inc_ref_all() to increment the on-disk reference
counts for all entries in the block:
error = ext4_xattr_inode_inc_ref_all(handle, inode,
ENTRY(header(s->base)+1));
if (error)
goto getblk_failed;
If a subsequent operation like ext4_handle_dirty_metadata() fails, such as
returning -EIO due to a synchronous write failure in nojournal mode:
error = ext4_handle_dirty_metadata(handle, inode,
new_bh);
if (error)
goto cleanup;
The code jumps directly to the cleanup label.
Will jumping to cleanup bypass block deallocation (getblk_failed) and
fail to revert the previously incremented EA inode references?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260812044556.2209787-1-matthias.goergens@gmail.com?part=1
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v2] ext4: drop extra ea_inode ref in cached-block reuse path
2026-08-12 4:45 [PATCH 1/1] ext4: drop extra ea_inode ref in cached-block reuse path Matthias Goergens
2026-08-12 5:05 ` sashiko-bot
@ 2026-08-14 6:50 ` Matthias Goergens
2026-08-14 7:04 ` sashiko-bot
1 sibling, 1 reply; 4+ messages in thread
From: Matthias Goergens @ 2026-08-14 6:50 UTC (permalink / raw)
To: linux-ext4; +Cc: tytso, jack
ext4_xattr_block_set() acquires a reference to the ea_inode via
ext4_xattr_inode_lookup_create() before constructing the new xattr
block. In the new-block allocation path this extra reference is dropped
after ext4_xattr_inode_inc_ref_all() accounts for every entry in the
block. But when mbcache supplies an identical existing block and that
block is reused, the cached block already carries the correct refcount
for its entries, and the extra reference is never dropped.
The cleanup block at the end of the function only calls
ext4_xattr_inode_dec_ref() when error is non-zero, so the success path
through cache reuse leaks one refcount. Each reuse event adds another;
eventually the saturated refcount prevents the ea_inode from being freed
when its last real reference is dropped.
Add the matching drop when the entry was not written directly to the old
physical buffer (bs->bh && s->base == bs->bh->b_data). In that case the
reference belongs to the old buffer, which the later release_block()
handles. When s->base is a clone or a fresh allocation and is discarded
in favour of the cached block, the extra reference must be dropped.
v2:
- The v1 dropped the reference best-effort: a failure was logged but
masked by the later "error = 0" on the success path, so the syscall
could succeed with the on-disk refcount one too high. Fail the
operation instead, and undo the locally-owned part of the reuse
through ext4_xattr_reuse_undo(), the same helper the success path
uses for the old block. Any reference that nonetheless persists
(e.g. under errors=continue) is repaired by e2fsck.
- A failed decrement is never retried: it may already have mutated
the refcount before failing (e.g. an EA inode inheriting S_SYNC from
the root directory can fail ext4_mark_iloc_dirty() after the
mutation in nojournal mode), so a retry could decrement twice. The
failure paths free the quota charge and drop the inode reference
directly.
- Propagate the same masked failure at the new-block branch's
identical drop: it has always been best-effort (warn and continue).
Found by the Sashiko AI review bot while reviewing the xattr
saturation-livelock fix.
Fixes: 0a46ef234756 ("ext4: do not create EA inode under buffer lock")
Signed-off-by: Matthias Goergens <matthias.goergens@gmail.com>
---
fs/ext4/xattr.c | 62 +++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 57 insertions(+), 5 deletions(-)
diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
index 982a1f831e228..0ce937d890042 100644
--- a/fs/ext4/xattr.c
+++ b/fs/ext4/xattr.c
@@ -1362,6 +1362,17 @@ ext4_xattr_release_block(handle_t *handle, struct inode *inode,
return;
}
+/* Undo the locally-owned part of a cached-block reuse. */
+static void ext4_xattr_reuse_undo(handle_t *handle, struct inode *inode,
+ struct buffer_head *bh)
+{
+ struct ext4_xattr_inode_array *ea_inode_array = NULL;
+
+ ext4_xattr_release_block(handle, inode, bh, &ea_inode_array,
+ 0 /* extra_credits */);
+ ext4_xattr_inode_array_free(ea_inode_array);
+}
+
/*
* Find the available free space for EAs. This also returns the total number of
* bytes used by EA entries.
@@ -2107,6 +2118,39 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
mb_cache_entry_touch(ea_block_cache, ce);
mb_cache_entry_put(ea_block_cache, ce);
ce = NULL;
+ if (!(bs->bh && s->base == bs->bh->b_data) &&
+ ea_inode) {
+ /*
+ * The reused block already holds its own
+ * reference; drop the extra one unless the
+ * entry went into the old buffer directly.
+ */
+ error = ext4_xattr_inode_dec_ref(handle,
+ ea_inode);
+ if (error) {
+ /*
+ * The decrement may already have run;
+ * do not retry it: free the charge,
+ * undo the reuse, and fail the op.
+ */
+ ext4_error_inode(inode, __func__,
+ __LINE__, 0,
+ "dec ref error=%d",
+ error);
+ ext4_xattr_inode_free_quota(inode,
+ ea_inode,
+ i_size_read(ea_inode));
+ iput(ea_inode);
+ ea_inode = NULL;
+ if (new_bh != bs->bh)
+ ext4_xattr_reuse_undo(handle,
+ inode,
+ new_bh);
+ goto cleanup;
+ }
+ iput(ea_inode);
+ ea_inode = NULL;
+ }
} else if (bs->bh && s->base == bs->bh->b_data) {
/* We were modifying this block in-place. */
ea_bdebug(bs->bh, "keeping this block");
@@ -2143,13 +2187,21 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
if (error)
goto getblk_failed;
if (ea_inode) {
- /* Drop the extra ref on ea_inode. */
+ /*
+ * Drop the extra ref on ea_inode; on
+ * failure the decrement may already have
+ * run, so just fail and free the block.
+ */
error = ext4_xattr_inode_dec_ref(handle,
ea_inode);
- if (error)
- ext4_warning_inode(ea_inode,
- "dec ref error=%d",
- error);
+ if (error) {
+ ext4_xattr_inode_free_quota(inode,
+ ea_inode,
+ i_size_read(ea_inode));
+ iput(ea_inode);
+ ea_inode = NULL;
+ goto getblk_failed;
+ }
iput(ea_inode);
ea_inode = NULL;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH v2] ext4: drop extra ea_inode ref in cached-block reuse path
2026-08-14 6:50 ` [PATCH v2] " Matthias Goergens
@ 2026-08-14 7:04 ` sashiko-bot
0 siblings, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-08-14 7:04 UTC (permalink / raw)
To: Matthias Goergens; +Cc: linux-ext4, tytso
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Permanent reference count leak for EA inodes when jumping to `getblk_failed` after `ext4_xattr_inode_inc_ref_all()` succeeds.
--
--- Patch [1]: [PATCH v2] ext4: drop extra ea_inode ref in cached-block reuse path ---
commit 6323662f6aec67da90a2777f14df4011e64b989b
Author: Matthias Goergens <matthias.goergens@gmail.com>
ext4: drop extra ea_inode ref in cached-block reuse path
This commit addresses an EA inode reference count leak when reusing an
identical cached block in ext4_xattr_block_set(). It also modifies the
new-block allocation path to fail the operation instead of just warning
if dropping the extra reference fails.
> diff --git a/fs/ext4/xattr.c b/fs/ext4/xattr.c
> index 6127e94ea4f5d..29b08b69da2be 100644
> --- a/fs/ext4/xattr.c
> +++ b/fs/ext4/xattr.c
[ ... ]
> @@ -2075,13 +2119,21 @@ ext4_xattr_block_set(handle_t *handle, struct inode *inode,
> if (error)
> goto getblk_failed;
> if (ea_inode) {
> - /* Drop the extra ref on ea_inode. */
> + /*
> + * Drop the extra ref on ea_inode; on
> + * failure the decrement may already have
> + * run, so just fail and free the block.
> + */
> error = ext4_xattr_inode_dec_ref(handle,
> ea_inode);
> - if (error)
> - ext4_warning_inode(ea_inode,
> - "dec ref error=%d",
> - error);
> + if (error) {
> + ext4_xattr_inode_free_quota(inode,
> + ea_inode,
> + i_size_read(ea_inode));
> + iput(ea_inode);
> + ea_inode = NULL;
> + goto getblk_failed;
[Severity: High]
This isn't a bug introduced by this patch, but this new goto getblk_failed
path appears to exacerbate a pre-existing reference count leak for EA inodes.
Just above this block, ext4_xattr_inode_inc_ref_all() successfully iterates
over all EA entries and increments their inode reference counts:
error = ext4_xattr_inode_inc_ref_all(handle, inode,
ENTRY(header(s->base)+1));
If the decrement of the extra reference fails, the code now jumps to
getblk_failed. The getblk_failed label frees the newly allocated physical
block and jumps to cleanup:
getblk_failed:
ext4_free_blocks(handle, inode, NULL, block, 1,
EXT4_FREE_BLOCKS_METADATA);
goto cleanup;
However, it completely skips rolling back the newly incremented EA inode
references via ext4_xattr_inode_dec_ref_all(). Because inc_ref_all already
called ext4_mark_iloc_dirty() for those EA inodes, won't the artificially
elevated reference counts be committed to the journal?
Since the error here doesn't mark the filesystem as containing errors,
could this silently leak these references without triggering an automatic
e2fsck on the next reboot?
> + }
> iput(ea_inode);
> ea_inode = NULL;
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260814065023.3466742-1-matthias.goergens@gmail.com?part=1
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-08-14 7:04 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-12 4:45 [PATCH 1/1] ext4: drop extra ea_inode ref in cached-block reuse path Matthias Goergens
2026-08-12 5:05 ` sashiko-bot
2026-08-14 6:50 ` [PATCH v2] " Matthias Goergens
2026-08-14 7:04 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox