From: Matthias Goergens <matthias.goergens@gmail.com>
To: Namjae Jeon <linkinjeon@kernel.org>, Hyunchul Lee <hyc.lee@gmail.com>
Cc: ntfs@lists.linux.dev, linux-kernel@vger.kernel.org
Subject: [PATCH v2 1/2] ntfs: balance the $MFT runlist lock in data extension error paths
Date: Wed, 30 Sep 2026 11:35:55 +0800 [thread overview]
Message-ID: <20260930033556.169300-2-matthias.goergens@gmail.com> (raw)
In-Reply-To: <20260930033556.169300-1-matthias.goergens@gmail.com>
ntfs_mft_data_extend_allocation_nolock() drops the $MFT runlist lock
before allocating clusters, and every path into undo_alloc arrives
without it, except a map_mft_record() failure, which takes it first.
undo_alloc never releases it, so the lock leaks and the next $MFT
extension and $MFT writeback hang. The restore_undo_alloc failure
path, on the other hand, releases the lock without holding it. And
undo_alloc frees the clusters with ntfs_cluster_free() and truncates
the runlist without the lock, although both require it.
Enter undo_alloc without the lock on every path. Under the lock, copy
the new runs and truncate the runlist; after dropping it, free the
clusters from the copy with ntfs_cluster_free_from_rl(). That keeps
lcnbmp_lock outside the runlist lock, as the function documents. Like
the runlist merge failure above, this does not discard the clusters,
which were never written. If the truncation fails, the runlist may
still reference the clusters, so they are not freed. In that case, and
if the copy cannot be allocated, the clusters stay allocated and the
volume is marked for chkdsk.
A shorter fix would keep calling ntfs_cluster_free() on the live
runlist without the lock, relying on $MFT's runlist being fully mapped
and changed only by this function under mrec_lock. That breaks
ntfs_cluster_free()'s documented locking rule on an argument about the
rest of the driver, so this patch does not do that.
Fixes: 115380f9a2f9 ("ntfs: update mft operations")
Cc: stable@vger.kernel.org
Signed-off-by: Matthias Goergens <matthias.goergens@gmail.com>
---
v2: do not free the clusters if ntfs_rl_truncate_nolock() fails
(Hyunchul Lee).
Without this patch, a forced map_mft_record() failure gives "WARNING:
lock held when returning to user space" for the $MFT runlist lock, and
the next file create and $MFT writeback block on it. A forced lookup
failure in restore_undo_alloc gives "bad unlock balance". With it, the
create fails with EIO, lockdep stays quiet and the volume keeps working,
also when the copy allocation is forced to fail. With the truncation
forced to fail, nothing is freed that $MFT's runlist still maps. $MFT
growth on a fragmented volume behaves as before.
fs/ntfs/mft.c | 59 ++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 51 insertions(+), 8 deletions(-)
diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c
index e01e367a588d..8edc65911aa2 100644
--- a/fs/ntfs/mft.c
+++ b/fs/ntfs/mft.c
@@ -1764,6 +1764,36 @@ static int ntfs_mft_bitmap_extend_initialized_nolock(struct ntfs_volume *vol)
return ret;
}
+/*
+ * ntfs_mft_copy_tail - copy the runs of a runlist from a vcn onwards
+ * @rl: runlist to copy from
+ * @vcn: first vcn to copy
+ *
+ * Return a terminated copy of the runs of @rl covering @vcn and everything
+ * after it, NULL if there are none, or ERR_PTR(-ENOMEM). The caller must
+ * hold the runlist lock and free the copy with kfree().
+ */
+static struct runlist_element *ntfs_mft_copy_tail(struct runlist_element *rl, s64 vcn)
+{
+ struct runlist_element *end, *copy;
+ s64 delta;
+
+ rl = ntfs_rl_find_vcn_nolock(rl, vcn);
+ if (!rl || !rl->length)
+ return NULL;
+ for (end = rl; end->length; end++)
+ ;
+ copy = kmemdup(rl, (end - rl + 1) * sizeof(*rl), GFP_NOFS);
+ if (!copy)
+ return ERR_PTR(-ENOMEM);
+ delta = vcn - copy->vcn;
+ copy->vcn = vcn;
+ copy->length -= delta;
+ if (copy->lcn >= 0)
+ copy->lcn += delta;
+ return copy;
+}
+
/*
* ntfs_mft_data_extend_allocation_nolock - extend mft data attribute
* @vol: volume on which to extend the mft data attribute
@@ -1791,7 +1821,7 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
s64 min_nr, nr, ll;
unsigned long flags;
struct ntfs_inode *mft_ni;
- struct runlist_element *rl, *rl2;
+ struct runlist_element *rl, *rl2, *tail_rl;
struct ntfs_attr_search_ctx *ctx = NULL;
struct mft_record *mrec;
struct attr_record *a = NULL;
@@ -1904,7 +1934,6 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
if (IS_ERR(mrec)) {
ntfs_error(vol->sb, "Failed to map mft record.");
ret = PTR_ERR(mrec);
- down_write(&mft_ni->runlist.lock);
goto undo_alloc;
}
ctx = ntfs_attr_get_search_ctx(mft_ni, mrec);
@@ -2015,7 +2044,6 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
write_unlock_irqrestore(&mft_ni->size_lock, flags);
ntfs_attr_put_search_ctx(ctx);
unmap_mft_record(mft_ni);
- up_write(&mft_ni->runlist.lock);
/*
* The only thing that is now wrong is ->allocated_size of the
* base attribute extent which chkdsk should be able to fix.
@@ -2026,15 +2054,30 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
ctx->attr->data.non_resident.highest_vcn =
cpu_to_le64(old_last_vcn - 1);
undo_alloc:
- if (ntfs_cluster_free(mft_ni, old_last_vcn, -1, ctx) < 0) {
- ntfs_error(vol->sb, "Failed to free clusters from mft data attribute.%s", es);
- NVolSetErrors(vol);
- }
-
+ /*
+ * Entered without the runlist lock. Take the new runs off the
+ * runlist under it, and free their clusters from a copy once it is
+ * dropped, as lcnbmp_lock nests outside it (see above). If the
+ * runlist cannot be truncated or the copy cannot be allocated, the
+ * clusters stay allocated until chkdsk.
+ */
+ down_write(&mft_ni->runlist.lock);
+ tail_rl = ntfs_mft_copy_tail(mft_ni->runlist.rl, old_last_vcn);
if (ntfs_rl_truncate_nolock(vol, &mft_ni->runlist, old_last_vcn)) {
ntfs_error(vol->sb, "Failed to truncate mft data attribute runlist.%s", es);
NVolSetErrors(vol);
+ /* The runlist may still reference the clusters, so keep them. */
+ if (!IS_ERR(tail_rl))
+ kfree(tail_rl);
+ tail_rl = NULL;
+ }
+ up_write(&mft_ni->runlist.lock);
+ if (IS_ERR(tail_rl) || ntfs_cluster_free_from_rl(vol, tail_rl)) {
+ ntfs_error(vol->sb, "Failed to free clusters from mft data attribute.%s", es);
+ NVolSetErrors(vol);
}
+ if (!IS_ERR(tail_rl))
+ kfree(tail_rl);
if (mp_extended && ntfs_attr_update_mapping_pairs(mft_ni, 0)) {
ntfs_error(vol->sb, "Failed to restore mapping pairs.%s",
es);
--
2.55.0
next prev parent reply other threads:[~2026-09-30 3:36 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-30 3:35 [PATCH v2 0/2] ntfs: fix the undo path of $MFT data extension Matthias Goergens
2026-09-30 3:35 ` Matthias Goergens [this message]
2026-09-30 8:14 ` [PATCH v2 1/2] ntfs: balance the $MFT runlist lock in data extension error paths Hyunchul Lee
2026-09-30 3:35 ` [PATCH v2 2/2] ntfs: do not use a stale runlist pointer when undoing $MFT extension Matthias Goergens
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=20260930033556.169300-2-matthias.goergens@gmail.com \
--to=matthias.goergens@gmail.com \
--cc=hyc.lee@gmail.com \
--cc=linkinjeon@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=ntfs@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
all inboxes | Powered by JetHome®