[PATCH v2 2/3] ntfs: restart the zone search when the allocation hint fails

From: Matthias Goergens

Date: Thu Oct 01 2026 - 10:54:13 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. A contiguous request
can get there with the run from the hint already allocated, when that
run reached the end of a bitmap buffer or of the zone. Return that run
instead, as when a cluster in use ends it; going on at the zone position
gives the request a second run that its caller does not expect. That
could happen before this patch too, whenever the cluster at the zone
position was free. ntfs_attr_map_cluster() reports and zeroes only the
first run, so after a fallocate() of 20 clusters over a hole, with the
hint 4 clusters before the end of the volume, 16 of them read back the
data of a deleted file.

Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator")
Suggested-by: Baolin Liu <liubaolin@xxxxxxxxxx>
Signed-off-by: Matthias Goergens <matthias.goergens@xxxxxxxxx>
---
v2: for a contiguous request, return the run from the hint instead of
going on at the zone position (Baolin Liu). This also fixes the stale
data after fallocate() described in the last paragraph.
---
fs/ntfs/lcnalloc.c | 25 ++++++++++++++++++++++---
1 file changed, 22 insertions(+), 3 deletions(-)

diff --git a/fs/ntfs/lcnalloc.c b/fs/ntfs/lcnalloc.c
index 0d6cd08ee2e7..c58e689fb585 100644
--- a/fs/ntfs/lcnalloc.c
+++ b/fs/ntfs/lcnalloc.c
@@ -506,13 +506,32 @@ struct runlist_element *ntfs_cluster_alloc(struct ntfs_volume *vol, const s64 st
}

if (!used_zone_pos) {
+ /*
+ * The run at @start_lcn reached the end of the buffer
+ * or the zone. A contiguous request gets that run
+ * alone, not a second one from the zone position.
+ */
+ if (is_contig && rlpos)
+ goto out;
+ /*
+ * 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.56.0