From: Ian Bridges <icb@fastmail.org>
To: Joseph Qi <joseph.qi@linux.alibaba.com>
Cc: Heming Zhao <heming.zhao@suse.com>, Mark Fasheh <mark@fasheh.com>,
Joel Becker <jlbec@evilplan.org>,
"ocfs2-devel@lists.linux.dev" <ocfs2-devel@lists.linux.dev>,
"linux-kernel@vger.kernel.org" <linux-kernel@vger.kernel.org>
Subject: Re: [PATCH] ocfs2: fix NULL h_transaction deref in ocfs2_assure_trans_credits
Date: Sat, 13 Jun 2026 07:16:26 -0500 [thread overview]
Message-ID: <ai1KGobnHDtU2x_4@dev> (raw)
In-Reply-To: <dd6f703a-9877-468f-9a02-886e59770e25@linux.alibaba.com>
On Fri, Jun 12, 2026 at 09:19:31AM +0800, Joseph Qi wrote:
>
>
> On 6/11/26 10:46 PM, Ian Bridges wrote:
> > [BUG]
> > A direct write over unwritten extents can panic the kernel in
> > ocfs2_assure_trans_credits() when the journal aborts during DIO
> > completion. The crash is a general protection fault from a NULL pointer
> > dereference.
> >
> > [CAUSE]
> > ocfs2_dio_end_io_write() loops over a direct write's unwritten extents,
> > marking each written under a single journal handle. If the journal
> > aborts (for example after an I/O error) while the extent tree is being
> > updated, the handle is left aborted with its transaction pointer
> > cleared. The extent merge treats that failure as not critical and
> > reports success, so the loop keeps using the handle.
> > ocfs2_assure_trans_credits() reads the handle's remaining credits
> > without first checking whether the handle is aborted, and that read
> > dereferences the cleared transaction pointer.
> >
> > [FIX]
> > A journal abort is recorded in the handle itself, so callers are
> > expected to test the handle rather than rely on a returned error.
> > Make ocfs2_assure_trans_credits() do that, as the other ocfs2 journal
> > helpers already do, and return -EROFS when the handle is aborted.
> >
> > Fixes: be346c1a6eeb ("ocfs2: fix DIO failure due to insufficient transaction credits")
>
> It seems this is introduced by commit d647c5b2fbf8 ("ocfs2: split
> transactions in dio completion to avoid credit exhaustion")?
>
> Thanks,
> Joseph
The pattern of the bug appears present in the commit before d647c5b2fbf8.
(510a75028707).
The loop reuses one handle for every extent (fs/ocfs2/aops.c):
handle = ocfs2_start_trans(osb, credits);
...
list_for_each_entry(ue, &dwc->dw_zero_list, ue_node) {
ret = ocfs2_assure_trans_credits(handle, credits);
...
ret = ocfs2_mark_extent_written(inode, &et, handle, ...);
...
}
ocfs2_mark_extent_written() reaches a restart that sets h_transaction
to NULL. jbd2__journal_restart() (fs/jbd2/transaction.c) leaves it NULL
when start_this_handle() fails on an aborted journal:
stop_this_handle(handle);
handle->h_transaction = NULL;
...
ret = start_this_handle(journal, handle, gfp_mask);
ocfs2_extend_trans() (fs/ocfs2/journal.c) is what reaches that restart:
status = jbd2_journal_extend(handle, nblocks, 0);
...
if (status > 0) {
...
status = jbd2_journal_restart(handle, ...);
...
}
ocfs2_try_to_merge_extent() (fs/ocfs2/alloc.c) calls ocfs2_extend_trans()
through ocfs2_extend_rotate_transaction() and swallows its error, so
ocfs2_mark_extent_written() returns success with h_transaction left NULL:
if (ctxt->c_split_covers_rec) {
ret = ocfs2_extend_rotate_transaction(handle, 0,
jbd2_handle_buffer_credits(handle), path);
if (ret) {
mlog_errno(ret);
ret = 0;
goto out;
}
...
}
The next ocfs2_assure_trans_credits() call in the loop then reads that
NULL h_transaction and faults.
Ian
>
> > Reported-by: syzbot+e9c15ff790cea6a0cfae@syzkaller.appspotmail.com
> > Closes: https://syzkaller.appspot.com/bug?extid=e9c15ff790cea6a0cfae
> > Cc: stable@vger.kernel.org
> > Signed-off-by: Ian Bridges <icb@fastmail.org>
> > ---
> > This patch contains a proposed fix for a crash reported by syzbot
> > in ocfs2_assure_trans_credits().
> >
> > The file names and offsets in this description are from commit
> > 7cb1c5b32a2bfde961fff8d5204526b609bcb30a from this repo:
> > git://git.kernel.org/pub/scm/linux/kernel/git/gregkh/staging.git
> >
> > I also have a small test harness that reproduces the original panic,
> > which I can make available as well.
> >
> > The Bug
> >
> > OCFS2 supports unwritten extents. These are ranges that fallocate()
> > has allocated on disk but flagged as holding no data, so reads return
> > zeros until the range is written. Clearing that flag is a journalled
> > metadata change. For a direct write, OCFS2 makes that change when the
> > write completes rather than when the write is submitted.
> >
> > When a direct write to unwritten extents completes, ocfs2_dio_end_io()
> > (fs/ocfs2/aops.c:2401) calls ocfs2_dio_end_io_write()
> > (fs/ocfs2/aops.c:2266). That function opens one jbd2 handle and loops
> > over the extents the write covered (fs/ocfs2/aops.c:2319). For each one
> > it calls ocfs2_assure_trans_credits() (fs/ocfs2/aops.c:2334) and then
> > ocfs2_mark_extent_written() (fs/ocfs2/aops.c:2339). The same handle is
> > reused on each pass.
> >
> > ocfs2_assure_trans_credits() (fs/ocfs2/journal.c:474) makes sure the
> > handle still has enough journal credits for the next extent operation.
> > Its first action is:
> >
> > int old_nblks = jbd2_handle_buffer_credits(handle);
> >
> > jbd2_handle_buffer_credits() (include/linux/jbd2.h:1817) is an inline
> > accessor that reads handle->h_transaction->t_journal without a NULL
> > check. t_journal is at offset 0 of struct transaction_s, so a NULL
> > h_transaction makes this a read of address 0.
> >
> > A handle's h_transaction is set to NULL when a transaction restart
> > fails. The bug is that ocfs2_dio_end_io_write() can keep using such a
> > handle. The sequence is:
> >
> > 1. ocfs2_mark_extent_written() reaches ocfs2_try_to_merge_extent()
> > through ocfs2_change_extent_flag() and ocfs2_split_extent(). When the
> > marked extent merges with a neighbor (ctxt->c_split_covers_rec,
> > fs/ocfs2/alloc.c:3820), the merge reserves rotation credits with
> > ocfs2_extend_rotate_transaction() (fs/ocfs2/alloc.c:3822), which calls
> > ocfs2_extend_trans() (fs/ocfs2/journal.c:428).
> >
> > 2. ocfs2_extend_trans() cannot grow the running transaction, so it
> > restarts the handle with jbd2_journal_restart()
> > (fs/ocfs2/journal.c:454).
> >
> > 3. jbd2__journal_restart() (fs/jbd2/transaction.c) sets
> > handle->h_transaction to NULL, then calls start_this_handle() to
> > attach a new transaction. If the journal has aborted,
> > start_this_handle() returns an error (fs/jbd2/transaction.c:366) and
> > h_transaction stays NULL.
> >
> > 4. The error reaches ocfs2_try_to_merge_extent(), which ignores it.
> > At fs/ocfs2/alloc.c:3827 it resets ret to 0 and returns success, so
> > the loop does not stop.
> >
> > 5. The loop moves to the next extent and calls
> > ocfs2_assure_trans_credits(handle) again (fs/ocfs2/aops.c:2334), now
> > on the handle whose h_transaction is NULL.
> >
> > 6. ocfs2_assure_trans_credits() calls jbd2_handle_buffer_credits()
> > (fs/ocfs2/journal.c:476), which dereferences the NULL h_transaction.
> > This is the general protection fault syzbot reports.
> >
> > The Proposed Fix
> >
> > A failed transaction restart records the abort in the handle itself, as
> > a NULL h_transaction. It is not threaded back through return values, so
> > an intermediate caller that ignores the error, like
> > ocfs2_try_to_merge_extent() above, does not lose the abort. Each user is
> > instead expected to check the handle before touching it.
> >
> > ocfs2 already does this. ocfs2_journal_dirty() (fs/ocfs2/journal.c:834)
> > and ocfs2_update_inode_fsync_trans() (fs/ocfs2/journal.h:603) both test
> > is_handle_aborted() before they read handle->h_transaction.
> >
> > ocfs2_assure_trans_credits() is the one place that reads h_transaction,
> > through jbd2_handle_buffer_credits(), without that check. The fix adds
> > that check. is_handle_aborted() returns true when h_transaction is NULL,
> > so the NULL dereference cannot happen. Returning the error makes
> > ocfs2_dio_end_io_write() take its "goto commit" path and stop using the
> > handle.
> >
> > fs/ocfs2/journal.c | 6 +++++-
> > 1 file changed, 5 insertions(+), 1 deletion(-)
> >
> > diff --git a/fs/ocfs2/journal.c b/fs/ocfs2/journal.c
> > index f9bf3bac085d..64a26da8eb28 100644
> > --- a/fs/ocfs2/journal.c
> > +++ b/fs/ocfs2/journal.c
> > @@ -473,8 +473,12 @@ int ocfs2_extend_trans(handle_t *handle, int nblocks)
> > */
> > int ocfs2_assure_trans_credits(handle_t *handle, int nblocks)
> > {
> > - int old_nblks = jbd2_handle_buffer_credits(handle);
> > + int old_nblks;
> >
> > + if (is_handle_aborted(handle))
> > + return -EROFS;
> > +
> > + old_nblks = jbd2_handle_buffer_credits(handle);
> > trace_ocfs2_assure_trans_credits(old_nblks);
> > if (old_nblks >= nblocks)
> > return 0;
>
prev parent reply other threads:[~2026-06-13 12:16 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-11 14:46 [PATCH] ocfs2: fix NULL h_transaction deref in ocfs2_assure_trans_credits Ian Bridges
2026-06-12 1:19 ` Joseph Qi
2026-06-13 12:16 ` Ian Bridges [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=ai1KGobnHDtU2x_4@dev \
--to=icb@fastmail.org \
--cc=heming.zhao@suse.com \
--cc=jlbec@evilplan.org \
--cc=joseph.qi@linux.alibaba.com \
--cc=linux-kernel@vger.kernel.org \
--cc=mark@fasheh.com \
--cc=ocfs2-devel@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox