[PATCH v3 3/3] ntfs: balance the $MFT runlist lock in data extension error paths
From: Matthias Goergens
Date: Fri Oct 02 2026 - 00:35:59 EST
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. Merge the new clusters
with ntfs_runlists_merge_keep_src() and keep the runlist that
ntfs_cluster_alloc() returned until the extension is complete. On
failure, undo_alloc truncates $MFT's runlist under the lock and, after
dropping it, frees the clusters from the kept runlist with
ntfs_cluster_free_from_rl(). That keeps lcnbmp_lock outside the runlist
lock, the order given by the comment before the cluster allocation.
Unlike ntfs_cluster_free() on a volume mounted with -o discard, this
does not issue discards for the clusters, which were never written; the
runlist merge failure path already frees them the same way. If the
truncation fails, the runlist may still reference the clusters, so they
stay allocated and the volume is marked for chkdsk; at this call site
the truncation only shrinks the runlist and cannot fail today.
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@xxxxxxxxxxxxxxx
Signed-off-by: Matthias Goergens <matthias.goergens@xxxxxxxxx>
---
v3: free the clusters from the runlist that ntfs_cluster_alloc()
returned, kept intact by the merge from patch 2, instead of from a copy
of the new runs made with ntfs_mft_copy_tail() in undo_alloc (Hyunchul
Lee). The undo path no longer allocates, so the case where the copy
failed and the clusters stayed allocated is gone. A copy made before
the merge would also have worked, but would add an allocation to every
$MFT extension.
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 the injected error, lockdep stays quiet and the
volume keeps working, also when the merge is forced to fail. With the
truncation forced to fail, nothing is freed that $MFT's runlist still
maps. A debug check in undo_alloc found, in every undo of these runs,
that the kept runlist describes exactly the runs taken off $MFT's
runlist. $MFT growth on a nearly full, fragmented volume behaves as
before.
fs/ntfs/mft.c | 50 ++++++++++++++++++++++++++++++++++----------------
1 file changed, 34 insertions(+), 16 deletions(-)
diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c
index c2339acc6319..89288b794348 100644
--- a/fs/ntfs/mft.c
+++ b/fs/ntfs/mft.c
@@ -1791,7 +1791,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, *alloc_rl;
struct ntfs_attr_search_ctx *ctx = NULL;
struct mft_record *mrec;
struct attr_record *a = NULL;
@@ -1859,15 +1859,15 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
up_write(&mft_ni->runlist.lock);
do {
- rl2 = ntfs_cluster_alloc(vol, old_last_vcn, nr, lcn, MFT_ZONE,
- true, false, false);
- if (!IS_ERR(rl2))
+ alloc_rl = ntfs_cluster_alloc(vol, old_last_vcn, nr, lcn,
+ MFT_ZONE, true, false, false);
+ if (!IS_ERR(alloc_rl))
break;
- if (PTR_ERR(rl2) != -ENOSPC || nr == min_nr) {
+ if (PTR_ERR(alloc_rl) != -ENOSPC || nr == min_nr) {
ntfs_error(vol->sb,
"Failed to allocate the minimal number of clusters (%lli) for the mft data attribute.",
nr);
- return PTR_ERR(rl2);
+ return PTR_ERR(alloc_rl);
}
/*
* There is not enough space to do the allocation, but there
@@ -1878,17 +1878,22 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
ntfs_debug("Retrying mft data allocation with minimal cluster count %lli.", nr);
} while (1);
+ /*
+ * Keep @alloc_rl until the extension is complete: it describes exactly
+ * the new clusters, which undo_alloc frees if a later step fails.
+ */
down_write(&mft_ni->runlist.lock);
- rl = ntfs_runlists_merge(&mft_ni->runlist, rl2, 0, &new_rl_count);
+ rl = ntfs_runlists_merge_keep_src(&mft_ni->runlist, alloc_rl, 0,
+ &new_rl_count);
if (IS_ERR(rl)) {
up_write(&mft_ni->runlist.lock);
ntfs_error(vol->sb, "Failed to merge runlists for mft data attribute.");
- if (ntfs_cluster_free_from_rl(vol, rl2)) {
+ if (ntfs_cluster_free_from_rl(vol, alloc_rl)) {
ntfs_error(vol->sb,
"Failed to deallocate clusters from the mft data attribute.%s", es);
NVolSetErrors(vol);
}
- kvfree(rl2);
+ kvfree(alloc_rl);
return PTR_ERR(rl);
}
mft_ni->runlist.rl = rl;
@@ -1904,7 +1909,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);
@@ -2002,6 +2006,7 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
mark_mft_record_dirty(ctx->ntfs_ino);
ntfs_attr_put_search_ctx(ctx);
unmap_mft_record(mft_ni);
+ kvfree(alloc_rl);
ntfs_debug("Done.");
return 0;
restore_undo_alloc:
@@ -2015,7 +2020,7 @@ 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);
+ kvfree(alloc_rl);
/*
* The only thing that is now wrong is ->allocated_size of the
* base attribute extent which chkdsk should be able to fix.
@@ -2026,15 +2031,28 @@ 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 @alloc_rl once it
+ * is dropped, as lcnbmp_lock nests outside it (see above).
+ */
+ down_write(&mft_ni->runlist.lock);
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 leave them
+ * allocated until chkdsk.
+ */
+ kvfree(alloc_rl);
+ alloc_rl = NULL;
+ }
+ up_write(&mft_ni->runlist.lock);
+ if (ntfs_cluster_free_from_rl(vol, alloc_rl)) {
+ ntfs_error(vol->sb, "Failed to free clusters from mft data attribute.%s", es);
+ NVolSetErrors(vol);
}
+ kvfree(alloc_rl);
if (mp_extended && ntfs_attr_update_mapping_pairs(mft_ni, 0)) {
ntfs_error(vol->sb, "Failed to restore mapping pairs.%s",
es);
--
2.56.0