ntfs: repack $MFT/$ATTRIBUTE LIST

Repack the non-resident $MFT/$ATTRIBUTE_LIST into a contiguous run
when its mapping pairs no longer fit in the base MFT record. Propagate
allocation and writeback errors, and check synchronous replacement writes.

Fixes: 495e90fa33 ("ntfs: update attrib operations")
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
This commit is contained in:
Namjae Jeon 2026-09-06 11:12:47 +09:00
parent 6d8c197c99
commit b1d732e62a
3 changed files with 236 additions and 23 deletions

View File

@ -3844,13 +3844,13 @@ int ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni, s64 from_vcn)
*/
if (ni->type == AT_ATTRIBUTE_LIST) {
ntfs_attr_put_search_ctx(ctx);
if (ntfs_inode_free_space(base_ni, mp_size -
cur_max_mp_size)) {
ntfs_debug("Attribute list is too big. Defragment the volume\n");
return -ENOSPC;
}
if (ntfs_attrlist_update(base_ni))
return -EIO;
err = ntfs_inode_free_space(base_ni, mp_size -
cur_max_mp_size);
if (err)
return err;
err = ntfs_attrlist_update(base_ni);
if (err)
return err;
goto retry;
}
@ -4522,13 +4522,39 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz
ntfs_bytes_to_cluster(vol, ni->allocated_size),
first_free_vcn -
ntfs_bytes_to_cluster(vol, ni->allocated_size),
lcn_seek_from, DATA_ZONE, false, false, false);
lcn_seek_from, DATA_ZONE, false,
ni->type == AT_ATTRIBUTE_LIST, false);
if (IS_ERR(rl)) {
ntfs_debug("Cluster allocation failed (%lld)",
(long long)first_free_vcn -
ntfs_bytes_to_cluster(vol, ni->allocated_size));
return PTR_ERR(rl);
}
/*
* A contiguous ATTRIBUTE_LIST allocation keeps its mapping
* pairs small enough to fit in the base MFT record. The
* allocator can return a short run when contiguity was
* requested, so discard it and retry normally if necessary.
*/
if (ni->type == AT_ATTRIBUTE_LIST &&
(rl->vcn != ntfs_bytes_to_cluster(vol,
ni->allocated_size) ||
rl->length != first_free_vcn -
ntfs_bytes_to_cluster(vol, ni->allocated_size) ||
rl[1].length)) {
ntfs_cluster_free_from_rl(vol, rl);
kvfree(rl);
rl = ntfs_cluster_alloc(vol,
ntfs_bytes_to_cluster(vol,
ni->allocated_size),
first_free_vcn -
ntfs_bytes_to_cluster(vol,
ni->allocated_size),
lcn_seek_from, DATA_ZONE, false,
false, false);
if (IS_ERR(rl))
return PTR_ERR(rl);
}
}
if (!NInoCompressed(ni)) {
@ -4921,7 +4947,8 @@ int ntfs_attr_expand(struct ntfs_inode *ni, const s64 newsize, const s64 preallo
ntfs_debug("Entering for inode 0x%llx, attr 0x%x, size %lld\n",
(unsigned long long)ni->mft_no, ni->type, newsize);
if (ni->data_size == newsize) {
if (ni->data_size == newsize &&
(!prealloc_size || prealloc_size <= ni->allocated_size)) {
ntfs_debug("Size is already ok\n");
return 0;
}
@ -4936,7 +4963,7 @@ int ntfs_attr_expand(struct ntfs_inode *ni, const s64 newsize, const s64 preallo
}
if (NInoNonResident(ni)) {
if (newsize > ni->data_size)
if (newsize > ni->data_size || prealloc_size > ni->allocated_size)
err = ntfs_non_resident_attr_expand(ni, newsize, prealloc_size,
NVolDisableSparse(ni->vol) ?
HOLES_NO : HOLES_OK, true);

View File

@ -12,6 +12,9 @@
#include "mft.h"
#include "attrib.h"
#include "attrlist.h"
#include "lcnalloc.h"
#define NTFS_MAX_ATTR_LIST_SIZE (256 * 1024)
/*
* ntfs_attrlist_need - check whether inode need attribute list
@ -51,11 +54,151 @@ int ntfs_attrlist_need(struct ntfs_inode *ni)
return 0;
}
/*
* Repack the $MFT/$ATTRIBUTE_LIST data into one run.
*
* The mapping pairs for an $ATTRIBUTE_LIST must remain in the base MFT
* record. Once that record has no room left, extending a fragmented list
* can require one more mapping-pairs byte than the record can hold. There
* is no attribute that can legally be moved out in that state: $STANDARD_
* INFORMATION, $ATTRIBUTE_LIST, and the first $MFT/$DATA extent all have to
* stay in the base record. Move the list data to one contiguous run. The
* caller supplies the minimum allocation size so a recovery can use the
* smallest useful run while normal updates can still request the maximum
* legal list size as a reserve.
*/
static int ntfs_attrlist_repack(struct inode *attr_vi,
struct ntfs_inode *attr_ni, s64 min_alloc_size)
{
struct ntfs_volume *vol = attr_ni->vol;
struct runlist_element *old_rl, *new_rl;
u8 *data = NULL;
s64 data_size, alloc_size, nr_clusters, written;
s64 old_alloc_size;
size_t old_rl_count, new_rl_count;
unsigned long flags;
int err, restore_err;
if (attr_ni->mft_no != FILE_MFT || !NInoNonResident(attr_ni) ||
min_alloc_size < 0)
return -EINVAL;
err = ntfs_attr_map_whole_runlist(attr_ni);
if (err)
return err;
data_size = attr_ni->data_size;
if (data_size < 0)
return -EIO;
if (data_size) {
data = kvmalloc(data_size, GFP_NOFS);
if (!data)
return -ENOMEM;
written = ntfs_inode_attr_pread(attr_vi, 0, data_size, data);
if (written != data_size) {
err = written < 0 ? (int)written : -EIO;
goto out_free_data;
}
}
old_alloc_size = attr_ni->allocated_size;
alloc_size = max_t(s64, old_alloc_size, min_alloc_size);
nr_clusters = ntfs_bytes_to_cluster(vol,
alloc_size + vol->cluster_size - 1);
if (nr_clusters <= 0) {
err = -EFBIG;
goto out_free_data;
}
/* A single run keeps the mapping pairs at the minimum size. */
new_rl = ntfs_cluster_alloc(vol, 0, nr_clusters, -1, DATA_ZONE,
true, true, false);
if (IS_ERR(new_rl)) {
err = PTR_ERR(new_rl);
goto out_free_data;
}
new_rl_count = 0;
if (new_rl->vcn == 0 && new_rl->length == nr_clusters &&
!new_rl[1].length)
new_rl_count = 2;
if (new_rl_count != 2) {
ntfs_cluster_free_from_rl(vol, new_rl);
kvfree(new_rl);
err = -ENOSPC;
goto out_free_data;
}
old_rl = attr_ni->runlist.rl;
old_rl_count = attr_ni->runlist.count;
down_write(&attr_ni->runlist.lock);
attr_ni->runlist.rl = new_rl;
attr_ni->runlist.count = new_rl_count;
up_write(&attr_ni->runlist.lock);
write_lock_irqsave(&attr_ni->size_lock, flags);
attr_ni->allocated_size = ntfs_cluster_to_bytes(vol, nr_clusters);
write_unlock_irqrestore(&attr_ni->size_lock, flags);
/* Populate the replacement extent before publishing its mapping pairs. */
if (data_size) {
written = ntfs_inode_attr_pwrite(attr_vi, 0, data_size, data, true);
if (written != data_size) {
err = written < 0 ? (int)written : -EIO;
goto restore_old_runlist;
}
}
err = ntfs_attr_update_mapping_pairs(attr_ni, 0);
if (err)
goto restore_old_runlist;
/* The new mapping is now authoritative; release the old data runs. */
if (ntfs_cluster_free_from_rl(vol, old_rl)) {
ntfs_error(vol->sb,
"Failed to free old ATTRIBUTE_LIST extent: inode %#llx",
(long long)attr_ni->mft_no);
NVolSetErrors(vol);
}
kvfree(old_rl);
kvfree(data);
return 0;
restore_old_runlist:
down_write(&attr_ni->runlist.lock);
attr_ni->runlist.rl = old_rl;
attr_ni->runlist.count = old_rl_count;
up_write(&attr_ni->runlist.lock);
write_lock_irqsave(&attr_ni->size_lock, flags);
attr_ni->allocated_size = old_alloc_size;
write_unlock_irqrestore(&attr_ni->size_lock, flags);
restore_err = ntfs_attr_update_mapping_pairs(attr_ni, 0);
if (restore_err) {
ntfs_error(vol->sb, "Failed to restore ATTRIBUTE_LIST mapping pairs (%d)",
restore_err);
NVolSetErrors(vol);
}
ntfs_cluster_free_from_rl(vol, new_rl);
kvfree(new_rl);
err = err ? err : restore_err;
out_free_data:
kvfree(data);
return err;
}
int ntfs_attrlist_update(struct ntfs_inode *base_ni)
{
struct inode *attr_vi;
struct ntfs_inode *attr_ni;
int err;
s64 written;
int err, retry_err;
/*
* generic_shutdown_super() clears SB_ACTIVE before evicting cached
@ -74,21 +217,55 @@ int ntfs_attrlist_update(struct ntfs_inode *base_ni)
attr_ni = NTFS_I(attr_vi);
err = ntfs_attr_truncate_i(attr_ni, base_ni->attr_list_size, HOLES_NO);
if (err == -ENOSPC && attr_ni->mft_no == FILE_MFT) {
err = ntfs_attr_truncate(attr_ni, 0);
if (err || ntfs_attr_truncate_i(attr_ni, base_ni->attr_list_size, HOLES_NO) != 0) {
if (err == -ENOSPC && attr_ni->mft_no == FILE_MFT &&
NInoNonResident(attr_ni)) {
retry_err = ntfs_attrlist_repack(attr_vi, attr_ni,
base_ni->attr_list_size);
if (retry_err) {
ntfs_error(base_ni->vol->sb, "Failed to repack attribute list");
iput(attr_vi);
return retry_err;
}
retry_err = ntfs_attr_truncate_i(attr_ni, base_ni->attr_list_size,
HOLES_NO);
if (retry_err) {
ntfs_error(base_ni->vol->sb,
"Failed to truncate attribute list of inode %#llx",
(long long)base_ni->mft_no);
return -EIO;
"Failed to resize attribute list after repack");
iput(attr_vi);
return retry_err;
}
} else if (err) {
iput(attr_vi);
ntfs_error(base_ni->vol->sb,
"Failed to truncate attribute list of inode %#llx",
(long long)base_ni->mft_no);
return -EIO;
return err;
}
/*
* Reserve the maximum legal list size while the MFT metadata area is
* still easy to allocate contiguously. This prevents a later list entry
* from needing another mapping-pairs byte in the full base MFT record.
* Failure to obtain the optional reserve must not reject the current
* metadata update; the repack retry above remains available if needed.
*/
if (base_ni->mft_no == FILE_MFT && NInoNonResident(attr_ni) &&
attr_ni->allocated_size < NTFS_MAX_ATTR_LIST_SIZE) {
retry_err = ntfs_attr_expand(attr_ni, base_ni->attr_list_size,
NTFS_MAX_ATTR_LIST_SIZE);
if (retry_err == -ENOSPC) {
retry_err = ntfs_attrlist_repack(attr_vi, attr_ni,
NTFS_MAX_ATTR_LIST_SIZE);
if (retry_err == -ENOSPC)
retry_err = 0;
}
if (retry_err) {
ntfs_error(base_ni->vol->sb,
"Failed to reserve attribute list space");
iput(attr_vi);
return retry_err;
}
}
i_size_write(attr_vi, base_ni->attr_list_size);
@ -96,14 +273,15 @@ int ntfs_attrlist_update(struct ntfs_inode *base_ni)
if (NInoNonResident(attr_ni) && !NInoAttrListNonResident(base_ni))
NInoSetAttrListNonResident(base_ni);
if (ntfs_inode_attr_pwrite(attr_vi, 0, base_ni->attr_list_size,
base_ni->attr_list, false) !=
base_ni->attr_list_size) {
written = ntfs_inode_attr_pwrite(attr_vi, 0, base_ni->attr_list_size,
base_ni->attr_list, false);
if (written != base_ni->attr_list_size) {
err = written < 0 ? (int)written : -EIO;
iput(attr_vi);
ntfs_error(base_ni->vol->sb,
"Failed to write attribute list of inode %#llx",
(long long)base_ni->mft_no);
return -EIO;
return err;
}
NInoSetAttrListDirty(base_ni);

View File

@ -3745,6 +3745,7 @@ static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi,
u64 rl_length = 0;
s64 vcn;
struct runlist_element *rl;
int bio_err;
lcn_count = max_t(s64, 1, ntfs_bytes_to_cluster(vol, attr_len));
vcn = ntfs_pidx_to_cluster(vol, folio->index);
@ -3787,8 +3788,15 @@ static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi,
goto err_unlock_folio;
}
submit_bio_wait(bio);
bio_err = submit_bio_wait(bio);
bio_put(bio);
if (bio_err) {
ntfs_error(vi->i_sb,
"Synchronous attribute write failed (%d)",
bio_err);
ret = bio_err;
goto err_unlock_folio;
}
vcn += rl_length;
offset += length;
} while (lcn_count != 0);