From 18d4d2a3ce756af690a620a5199616ce2611c2c9 Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sun, 4 Oct 2026 17:56:06 +0800 Subject: [PATCH] ntfs: restart the zone search when the allocation hint fails 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 Signed-off-by: Matthias Goergens Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- 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)