[PATCH v5 22/31] ntfs: report index update and recovery failures

From: Xuewen Wang

Date: Mon Nov 02 2026 - 02:40:38 EST


From: Dandan Wu <wudandan@xxxxxxxxxx>

Index bitmap updates, root relocation and entry deletion can fail
after metadata has changed without reporting the incomplete update.
Report failed cleanup and restoration against the index owner,
including ENOMEM that leaves an unfinished deletion.

Report malformed bitmap and tree state as EFSCORRUPTED. Preserve
independent recovery errors and keep early allocation failures distinct.

Report bitmap failures at the modifier's shared exit, without repeating
propagated read corruption during allocation. A failed clear still
reports the unfinished cleanup.

Signed-off-by: Dandan Wu <wudandan@xxxxxxxxxx>
---
Validation:
- Forty bitmap set/clear cases cover success and failures, checking
errno preservation and reporting at the shared exit.
- The thirty combined index/bitmap cases also described in patch 19
verify that allocation does not repeat propagated read-corruption
reports, while failed clearing reports its unfinished cleanup.
- All pass in extracted-source userspace harnesses with controlled
dependencies under ASan/UBSan. These check reporting calls, not kernel
FAN_FS_ERROR event delivery.

fs/ntfs/index.c | 83 +++++++++++++++++++++++++++++++++++++++----------
1 file changed, 67 insertions(+), 16 deletions(-)

diff --git a/fs/ntfs/index.c b/fs/ntfs/index.c
index 4a6084b884d9..69923fc46af6 100644
--- a/fs/ntfs/index.c
+++ b/fs/ntfs/index.c
@@ -1100,6 +1100,7 @@ static int ntfs_ibm_add(struct ntfs_index_context *icx)

static int ntfs_ibm_modify(struct ntfs_index_context *icx, s64 vcn, int set)
{
+ int report_err = 0;
u8 byte;
u64 pos = ntfs_ibm_vcn_to_pos(icx, vcn);
u32 bpos = pos / 8;
@@ -1112,8 +1113,13 @@ static int ntfs_ibm_modify(struct ntfs_index_context *icx, s64 vcn, int set)

bmp_vi = ntfs_attr_iget(VFS_I(icx->idx_ni), AT_BITMAP, icx->name, icx->name_len);
if (IS_ERR(bmp_vi)) {
+ if (PTR_ERR(bmp_vi) == -ENOENT) {
+ report_err = -EFSCORRUPTED;
+ bmp_vi = ERR_PTR(-EFSCORRUPTED);
+ }
ntfs_error(icx->idx_ni->vol->sb, "Failed to open $BITMAP attribute");
- return PTR_ERR(bmp_vi);
+ ret = PTR_ERR(bmp_vi);
+ goto out;
}

bmp_ni = NTFS_I(bmp_vi);
@@ -1129,8 +1135,14 @@ static int ntfs_ibm_modify(struct ntfs_index_context *icx, s64 vcn, int set)
}
}

- if (ntfs_inode_attr_pread(bmp_vi, bpos, 1, &byte) != 1) {
- ret = -EIO;
+ ret = ntfs_inode_attr_pread(bmp_vi, bpos, 1, &byte);
+ if (ret != 1) {
+ if (ret >= 0 || ret == -EINVAL) {
+ ret = -EFSCORRUPTED;
+ report_err = ret;
+ } else if (ret == -EIO) {
+ report_err = ret;
+ }
ntfs_error(icx->idx_ni->vol->sb, "Failed to read $BITMAP");
goto err;
}
@@ -1140,14 +1152,24 @@ static int ntfs_ibm_modify(struct ntfs_index_context *icx, s64 vcn, int set)
else
byte &= ~bit;

- if (ntfs_inode_attr_pwrite(bmp_vi, bpos, 1, &byte, false) != 1) {
- ret = -EIO;
+ ret = ntfs_inode_attr_pwrite(bmp_vi, bpos, 1, &byte, false);
+ if (ret != 1) {
+ if (ret >= 0)
+ ret = -EIO;
+ report_err = ret;
ntfs_error(icx->idx_ni->vol->sb, "Failed to write $Bitmap");
goto err;
}
+ ret = 0;

err:
iput(bmp_vi);
+out:
+ /* A failed clear leaves a block allocated, including during rollback. */
+ if (!set && ret)
+ report_err = ret;
+ if (report_err)
+ ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), report_err);
return ret;
}

@@ -1172,8 +1194,14 @@ static s64 ntfs_ibm_get_free(struct ntfs_index_context *icx)

bm = ntfs_attr_readall(icx->idx_ni, AT_BITMAP, icx->name, icx->name_len,
&size);
- if (IS_ERR(bm))
+ if (IS_ERR(bm)) {
+ if (PTR_ERR(bm) == -ENOENT) {
+ ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+ -EFSCORRUPTED);
+ bm = ERR_PTR(-EFSCORRUPTED);
+ }
return PTR_ERR(bm);
+ }

for (byte = 0; byte < size; byte++) {
if (bm[byte] == 255)
@@ -1477,10 +1505,13 @@ static int ntfs_ir_reparent(struct ntfs_index_context *icx)
ntfs_attr_put_search_ctx(ctx);
ctx = NULL;
rollback_ret = ntfs_ir_move_to_base(icx);
- if (rollback_ret)
+ if (rollback_ret) {
+ ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+ rollback_ret);
ntfs_error(icx->idx_ni->vol->sb,
"Failed to roll back INDEX_ROOT relocation: %d",
rollback_ret);
+ }
goto clear_bmp;
}

@@ -1948,8 +1979,11 @@ static int ntfs_ih_reparent_end(struct ntfs_index_context *icx, struct index_hea

ie = ntfs_ie_get_by_pos(ih, ntfs_icx_parent_pos(icx));
ie_prev = ntfs_ie_prev(ih, ie);
- if (!ie_prev)
- return -EIO;
+ if (!ie_prev) {
+ ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+ -EFSCORRUPTED);
+ return -EFSCORRUPTED;
+ }
ntfs_ie_set_vcn(ie, ntfs_ie_get_vcn(ie_prev));

return ntfs_ih_takeout(icx, ih, ie_prev, ib);
@@ -1976,8 +2010,10 @@ static int ntfs_index_rm_leaf(struct ntfs_index_context *icx)
parent_ih = &icx->ir->index;
else {
ib = kvzalloc(icx->block_size, GFP_NOFS);
- if (!ib)
- return -ENOMEM;
+ if (!ib) {
+ ret = -ENOMEM;
+ goto out;
+ }

ret = ntfs_ib_read(icx, ntfs_icx_parent_vcn(icx), ib);
if (ret)
@@ -2004,12 +2040,16 @@ static int ntfs_index_rm_leaf(struct ntfs_index_context *icx)

ret = ntfs_ih_reparent_end(icx, parent_ih, ib);
out:
+ /* The allocation bit is clear, but the parent update did not finish. */
+ if (ret)
+ ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), ret);
kvfree(ib);
return ret;
}

static int ntfs_index_rm_node(struct ntfs_index_context *icx)
{
+ int report_err = 0;
int entry_pos, pindex;
s64 vcn;
struct index_block *ib = NULL;
@@ -2055,7 +2095,8 @@ static int ntfs_index_rm_node(struct ntfs_index_context *icx)
goto descend;

if (ntfs_ih_zero_entry(&ib->index)) {
- ret = -EIO;
+ report_err = -EFSCORRUPTED;
+ ret = -EFSCORRUPTED;
ntfs_error(icx->idx_ni->vol->sb, "Empty index block");
goto out;
}
@@ -2100,10 +2141,14 @@ static int ntfs_index_rm_node(struct ntfs_index_context *icx)
ntfs_ie_delete(ih, entry);
ntfs_ie_insert(ih, ie, entry);

- if (icx->is_in_root)
+ if (icx->is_in_root) {
ret = ntfs_ir_truncate(icx, new_size);
- else
+ /* The old key has been replaced without removing its successor. */
+ if (ret)
+ report_err = ret;
+ } else {
ret = ntfs_icx_ib_write(icx);
+ }
if (ret)
goto out2;

@@ -2118,6 +2163,8 @@ static int ntfs_index_rm_node(struct ntfs_index_context *icx)
kfree(ie);
out:
kvfree(ib);
+ if (report_err)
+ ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), report_err);
return ret;
}

@@ -2152,10 +2199,14 @@ int ntfs_index_rm(struct ntfs_index_context *icx)
} else if (icx->is_in_root || !ntfs_ih_one_entry(ih)) {
ntfs_ie_delete(ih, icx->entry);

- if (icx->is_in_root)
+ if (icx->is_in_root) {
ret = ntfs_ir_truncate(icx, le32_to_cpu(ih->index_length));
- else
+ /* The entry is already removed and will not be restored. */
+ if (ret)
+ ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), ret);
+ } else {
ret = ntfs_icx_ib_write(icx);
+ }
if (ret)
goto err_out;
} else {