[PATCH v4 14/31] ntfs: report attribute list update and recovery failures

From: Baolin Liu

Date: Tue Oct 20 2026 - 04:51:28 EST


From: Baolin Liu <liubaolin@xxxxxxxxxx>

Attribute-list building, repacking and updating can leave inconsistent
metadata without reporting it. Report missing required list attributes,
invalid list state, failed mapping restoration and cluster cleanup.

Preserve the original update error and report independent recovery
failures against the base file. Use EFSCORRUPTED for confirmed corruption
while retaining allocation failures as such.

Signed-off-by: Baolin Liu <liubaolin@xxxxxxxxxx>
---
fs/ntfs/attrlist.c | 38 ++++++++++++++++++++++++++++----------
fs/ntfs/inode.c | 43 +++++++++++++++++++++++++++++--------------
2 files changed, 57 insertions(+), 24 deletions(-)

diff --git a/fs/ntfs/attrlist.c b/fs/ntfs/attrlist.c
index 7e7440c5f2ba..de642af3bc30 100644
--- a/fs/ntfs/attrlist.c
+++ b/fs/ntfs/attrlist.c
@@ -39,8 +39,9 @@ int ntfs_attrlist_need(struct ntfs_inode *ni)
}

if (!ni->attr_list) {
+ ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
ntfs_debug("Corrupt in-memory struct.\n");
- return -EINVAL;
+ return -EFSCORRUPTED;
}

ale = (struct attr_list_entry *)ni->attr_list;
@@ -76,7 +77,7 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,
s64 old_alloc_size;
size_t old_rl_count, new_rl_count;
unsigned long flags;
- int err, restore_err;
+ int err, free_err, restore_err;
if (attr_ni->mft_no != FILE_MFT || !NInoNonResident(attr_ni) ||
min_alloc_size < 0)
return -EINVAL;
@@ -89,8 +90,10 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,
return err;

data_size = attr_ni->data_size;
- if (data_size < 0)
- return -EIO;
+ if (data_size < 0) {
+ ntfs_report_file_metadata_error(attr_vi, -EFSCORRUPTED);
+ return -EFSCORRUPTED;
+ }

if (data_size) {
data = kvmalloc(data_size, GFP_NOFS);
@@ -99,7 +102,11 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,

written = ntfs_inode_attr_pread(attr_vi, 0, data_size, data);
if (written != data_size) {
- err = written < 0 ? (int)written : -EIO;
+ err = written < 0 ? (int)written : -EFSCORRUPTED;
+ if (written >= 0 || written == -EIO)
+ ntfs_report_file_metadata_error(attr_vi,
+ written < 0 ?
+ (int)written : -EFSCORRUPTED);
goto out_free_data;
}
}
@@ -127,7 +134,9 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,
new_rl_count = 2;

if (new_rl_count != 2) {
- ntfs_cluster_free_from_rl(vol, new_rl);
+ free_err = ntfs_cluster_free_from_rl(vol, new_rl);
+ if (free_err)
+ ntfs_report_file_metadata_error(attr_vi, free_err);
kvfree(new_rl);
err = -ENOSPC;
goto out_free_data;
@@ -157,11 +166,12 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,
goto restore_old_runlist;

/* The new mapping is now authoritative; release the old data runs. */
- if (ntfs_cluster_free_from_rl(vol, old_rl)) {
+ free_err = ntfs_cluster_free_from_rl(vol, old_rl);
+ if (free_err) {
ntfs_error(vol->sb,
"Failed to free old ATTRIBUTE_LIST extent: inode %#llx",
(long long)attr_ni->mft_no);
- NVolSetErrors(vol);
+ ntfs_report_file_metadata_error(attr_vi, free_err);
}
kvfree(old_rl);
kvfree(data);
@@ -186,10 +196,12 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,
if (restore_err) {
ntfs_error(vol->sb, "Failed to restore ATTRIBUTE_LIST mapping pairs (%d)",
restore_err);
- NVolSetErrors(vol);
+ ntfs_report_file_metadata_error(attr_vi, restore_err);
}

- ntfs_cluster_free_from_rl(vol, new_rl);
+ free_err = ntfs_cluster_free_from_rl(vol, new_rl);
+ if (free_err)
+ ntfs_report_file_metadata_error(attr_vi, free_err);
kvfree(new_rl);
err = err ? err : restore_err;

@@ -218,6 +230,11 @@ int ntfs_attrlist_update_locked(struct ntfs_inode *base_ni,
attr_vi = ntfs_attr_iget(VFS_I(base_ni), AT_ATTRIBUTE_LIST, AT_UNNAMED, 0);
if (IS_ERR(attr_vi)) {
err = PTR_ERR(attr_vi);
+ if (err == -ENOENT) {
+ err = -EFSCORRUPTED;
+ ntfs_report_file_metadata_error(VFS_I(base_ni),
+ -EFSCORRUPTED);
+ }
return err;
}
attr_ni = NTFS_I(attr_vi);
@@ -293,6 +310,7 @@ int ntfs_attrlist_update_locked(struct ntfs_inode *base_ni,
base_ni->attr_list, false);
if (written != base_ni->attr_list_size) {
err = written < 0 ? (int)written : -EIO;
+ ntfs_report_file_metadata_error(attr_vi, err);
iput(attr_vi);
ntfs_error(base_ni->vol->sb,
"Failed to write attribute list of inode %#llx",
diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 6ed995b09a3f..564682f59203 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -3231,7 +3231,7 @@ int ntfs_inode_attach_all_extents(struct ntfs_inode *ni)
*/
int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
{
- int err;
+ int err, rollback_err;
struct ntfs_attr_search_ctx *ctx;
u8 *al = NULL, *aln;
int al_len = 0;
@@ -3252,7 +3252,7 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)

ni_mrec = map_mft_record(ni);
if (IS_ERR(ni_mrec))
- return -EIO;
+ return PTR_ERR(ni_mrec);

/* Form attribute list. */
ctx = ntfs_attr_get_search_ctx(ni, ni_mrec);
@@ -3267,7 +3267,9 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
int ale_size;

if (ctx->attr->type == AT_ATTRIBUTE_LIST) {
- err = -EIO;
+ err = -EFSCORRUPTED;
+ ntfs_report_file_metadata_error(VFS_I(ni),
+ -EFSCORRUPTED);
ntfs_error(ni->vol->sb, "Attribute list already present");
goto put_err_out;
}
@@ -3355,15 +3357,19 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
NInoClearAttrList(ni);
/* Remove $ATTRIBUTE_LIST record. */
ntfs_attr_reinit_search_ctx(ctx);
- if (!ntfs_attr_lookup(AT_ATTRIBUTE_LIST, NULL, 0,
- CASE_SENSITIVE, 0, NULL, 0, ctx)) {
- if (ntfs_attr_record_rm(ctx))
+ rollback_err = ntfs_attr_lookup(AT_ATTRIBUTE_LIST, NULL, 0,
+ CASE_SENSITIVE, 0, NULL, 0, ctx);
+ if (!rollback_err) {
+ rollback_err = ntfs_attr_record_rm(ctx);
+ if (rollback_err)
ntfs_error(ni->vol->sb, "Rollback failed to remove attrlist");
else
free_empty_extents = true;
} else {
ntfs_error(ni->vol->sb, "Rollback failed to find attrlist");
}
+ if (rollback_err)
+ ntfs_report_file_metadata_error(VFS_I(ni), rollback_err);

/* Setup back in-memory runlist. */
ni->attr_list = al;
@@ -3378,17 +3384,21 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
ale = (struct attr_list_entry *)al;
while ((u8 *)ale < al + al_len) {
if (MREF_LE(ale->mft_reference) != ni->mft_no) {
- if (!ntfs_attr_lookup(ale->type, ale->name,
- ale->name_length,
- CASE_SENSITIVE,
- le64_to_cpu(ale->lowest_vcn),
- NULL, 0, ctx)) {
- if (ntfs_attr_record_move_to(ctx, ni))
+ rollback_err = ntfs_attr_lookup(ale->type, ale->name,
+ ale->name_length,
+ CASE_SENSITIVE,
+ le64_to_cpu(ale->lowest_vcn),
+ NULL, 0, ctx);
+ if (!rollback_err) {
+ rollback_err = ntfs_attr_record_move_to(ctx, ni);
+ if (rollback_err)
ntfs_error(ni->vol->sb,
"Rollback failed to move attribute");
} else {
ntfs_error(ni->vol->sb, "Rollback failed to find attr");
}
+ if (rollback_err)
+ ntfs_report_file_metadata_error(VFS_I(ni), rollback_err);
ntfs_attr_reinit_search_ctx(ctx);
}
ale = (struct attr_list_entry *)((u8 *)ale + le16_to_cpu(ale->length));
@@ -3401,8 +3411,13 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
NInoClearAttrListDirty(ni);
ntfs_attr_put_search_ctx(ctx);
ctx = NULL;
- if (free_empty_extents && ntfs_inode_free_empty_extents(ni))
- ntfs_error(ni->vol->sb, "Rollback failed to free empty extent");
+ if (free_empty_extents) {
+ rollback_err = ntfs_inode_free_empty_extents(ni);
+ if (rollback_err) {
+ ntfs_report_file_metadata_error(VFS_I(ni), rollback_err);
+ ntfs_error(ni->vol->sb, "Rollback failed to free empty extent");
+ }
+ }
goto err_out;
put_err_out:
ntfs_attr_put_search_ctx(ctx);
--
2.51.0