[PATCH 2/3] ntfs: restart the zone search when the allocation hint fails
From: Matthias Goergens
Date: Sun Sep 27 2026 - 06:59:49 EST
When ntfs_cluster_alloc() is given a start_lcn, it first tries the
clusters from there on. If that does not satisfy the request, it moves
on to the zone's current position, but keeps the pass, the zone_end and
the has_guess state it had. That loses free clusters in two ways. If
the search had already moved on to pass 2, whose range ends at the hint,
it scans only from the zone position to the hint and misses every free
cluster between the start of the zone and the zone position. If no
cluster had been tested yet, has_guess is still set, so the cluster at
the zone position is tried as if it had been the hint, and when that
cluster is in use the rest of the bitmap buffer is skipped.
Both happen when the hint lies at or past the end of the volume, which
ntfs_attr_map_cluster() produces when it extrapolates from the last
allocated run across a hole. The allocator then finds nothing, shrinks
the MFT zone to nothing trying to satisfy the request, and fails with
-ENOSPC.
To reproduce on a 128 MiB volume with 4 KiB clusters, extend two files
in turn by one cluster at a time, alternating between fallocate(),
write() past EOF and truncate() up, so that the runs of each file are
separated by holes. When the volume is full, truncate both files back
to 1 MiB and start again. During the second round a one-cluster
fallocate() fails with ENOSPC while 40 MiB is free.
Start the zone over as if no hint had been given.
Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator")
Signed-off-by: Matthias Goergens <matthias.goergens@xxxxxxxxx>
---
fs/ntfs/lcnalloc.c | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/fs/ntfs/lcnalloc.c b/fs/ntfs/lcnalloc.c
index 0d6cd08ee2e76..30faf422a5424 100644
--- a/fs/ntfs/lcnalloc.c
+++ b/fs/ntfs/lcnalloc.c
@@ -506,13 +506,25 @@ struct runlist_element *ntfs_cluster_alloc(struct ntfs_volume *vol, const s64 st
}
if (!used_zone_pos) {
+ /*
+ * Leaving @start_lcn for the zone position starts the
+ * zone over as if no hint had been given, even if the
+ * search had already reached pass 2, whose range ends
+ * at @start_lcn.
+ */
used_zone_pos = 1;
- if (search_zone == 1)
+ has_guess = 0;
+ pass = 1;
+ if (search_zone == 1) {
zone_start = vol->mft_zone_pos;
- else if (search_zone == 2)
+ zone_end = vol->mft_zone_end;
+ } else if (search_zone == 2) {
zone_start = vol->data1_zone_pos;
- else
+ zone_end = vol->nr_clusters;
+ } else {
zone_start = vol->data2_zone_pos;
+ zone_end = vol->mft_zone_start;
+ }
if (!zone_start || zone_start == vol->mft_zone_start ||
zone_start == vol->mft_zone_end)
--
2.55.0