ntfs: allow index root relocation

Allow a resident attribute record to move to an extent MFT record when the
base record needs room for an attribute list. Retry the root conversion
after creating the list, but do not relocate a root that is already
external.

Roll the root back to the base record if persisting the attribute list
fails, and free extent MFT records left empty by relocation or rollback.
Also preserve bitmap allocation errors in index operations.

Fixes: af0db57d42 ("ntfs: update inode operations")
Reported-by: yi <691464208@qq.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
This commit is contained in:
Namjae Jeon 2026-08-10 23:23:17 +09:00
parent a83e82b0ec
commit 9badcfb91b
4 changed files with 137 additions and 20 deletions

View File

@ -3558,8 +3558,13 @@ int ntfs_attr_record_move_away(struct ntfs_attr_search_ctx *ctx, int extra)
unmap_mft_record(ni);
err = ntfs_attr_record_move_to(ctx, ni);
if (err)
if (err) {
ntfs_error(sb, "Couldn't move attribute to MFT record");
if (ntfs_mft_record_free(base_ni->vol, ni))
ntfs_error(sb, "Couldn't free empty MFT record");
else
ntfs_inode_close(ni);
}
return err;
}

View File

@ -616,6 +616,31 @@ static struct index_root *ntfs_ir_lookup2(struct ntfs_inode *ni, __le16 *name, u
return ir;
}
static int ntfs_ir_move_to_base(struct ntfs_index_context *icx)
{
struct ntfs_attr_search_ctx *ctx = NULL;
struct index_root *ir;
bool moved = false;
int ret = 0;
ir = ntfs_ir_lookup(icx->idx_ni, icx->name, icx->name_len, &ctx);
if (!ir)
return -ENOENT;
if (ctx->ntfs_ino->mft_no != icx->idx_ni->mft_no) {
ret = ntfs_attr_record_move_to(ctx, icx->idx_ni);
if (!ret) {
moved = true;
ret = ntfs_attrlist_update(icx->idx_ni);
}
}
ntfs_attr_put_search_ctx(ctx);
if (!ret && moved)
ret = ntfs_inode_free_empty_extents(icx->idx_ni);
return ret;
}
/*
* Find a key in the index block.
*/
@ -989,6 +1014,7 @@ static s64 ntfs_ibm_pos_to_vcn(struct ntfs_index_context *icx, s64 pos)
static int ntfs_ibm_add(struct ntfs_index_context *icx)
{
u8 bmp[8];
int ret;
ntfs_debug("Entering\n");
@ -998,10 +1024,11 @@ static int ntfs_ibm_add(struct ntfs_index_context *icx)
* AT_BITMAP must be at least 8 bytes.
*/
memset(bmp, 0, sizeof(bmp));
if (ntfs_attr_add(icx->idx_ni, AT_BITMAP, icx->name, icx->name_len,
bmp, sizeof(bmp))) {
ret = ntfs_attr_add(icx->idx_ni, AT_BITMAP, icx->name, icx->name_len,
bmp, sizeof(bmp));
if (ret) {
ntfs_error(icx->idx_ni->vol->sb, "Failed to add AT_BITMAP");
return -EINVAL;
return ret;
}
return 0;
@ -1074,6 +1101,7 @@ static s64 ntfs_ibm_get_free(struct ntfs_index_context *icx)
{
u8 *bm;
int bit;
int ret;
s64 vcn, byte, size;
ntfs_debug("Entering\n");
@ -1081,7 +1109,7 @@ 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 (!bm)
return (s64)-1;
return -EIO;
for (byte = 0; byte < size; byte++) {
if (bm[byte] == 255)
@ -1099,10 +1127,12 @@ static s64 ntfs_ibm_get_free(struct ntfs_index_context *icx)
out:
ntfs_debug("allocated vcn: %lld\n", vcn);
if (ntfs_ibm_set(icx, vcn))
vcn = (s64)-1;
ret = ntfs_ibm_set(icx, vcn);
kvfree(bm);
if (ret)
return ret;
return vcn;
}
@ -1275,7 +1305,7 @@ static int ntfs_ir_reparent(struct ntfs_index_context *icx)
new_ib_vcn = ntfs_ibm_get_free(icx);
if (new_ib_vcn < 0) {
ret = -EINVAL;
ret = (int)new_ib_vcn;
goto out;
}
@ -1346,19 +1376,42 @@ static int ntfs_ir_reparent(struct ntfs_index_context *icx)
* When there is no space to build a non-resident
* index, we may have to move the root to an extent
*/
if ((ret == -ENOSPC) && (ctx->al_entry || !ntfs_inode_add_attrlist(icx->idx_ni))) {
ntfs_attr_put_search_ctx(ctx);
ctx = NULL;
ir = ntfs_ir_lookup(icx->idx_ni, icx->name, icx->name_len, &ctx);
if (ir && !ntfs_attr_record_move_away(ctx, ix_root_size -
le32_to_cpu(ctx->attr->data.resident.value_length))) {
if (ntfs_attrlist_update(ctx->base_ntfs_ino ?
ctx->base_ntfs_ino : ctx->ntfs_ino))
if (ret == -ENOSPC) {
if (!ctx->al_entry) {
ret = ntfs_inode_add_attrlist(icx->idx_ni);
if (ret)
goto clear_bmp;
ntfs_attr_put_search_ctx(ctx);
ctx = NULL;
goto retry;
}
if (ctx->ntfs_ino->mft_no != icx->idx_ni->mft_no)
goto clear_bmp;
ret = ntfs_attr_record_move_away(ctx, ix_root_size -
le32_to_cpu(ctx->attr->data.resident.value_length));
if (ret)
goto clear_bmp;
ret = ntfs_attrlist_update(icx->idx_ni);
if (ret) {
int rollback_ret;
ntfs_attr_put_search_ctx(ctx);
ctx = NULL;
rollback_ret = ntfs_ir_move_to_base(icx);
if (rollback_ret)
ntfs_error(icx->idx_ni->vol->sb,
"Failed to roll back INDEX_ROOT relocation: %d",
rollback_ret);
goto clear_bmp;
}
ntfs_attr_put_search_ctx(ctx);
ctx = NULL;
goto retry;
}
clear_bmp:
ntfs_ibm_clear(icx, new_ib_vcn);
@ -1590,7 +1643,7 @@ static int ntfs_ib_split(struct ntfs_index_context *icx, struct index_block *ib)
median = ntfs_ie_get_median(&ib->index);
new_vcn = ntfs_ibm_get_free(icx);
if (new_vcn < 0) {
ret = -EINVAL;
ret = (int)new_vcn;
goto out;
}

View File

@ -3045,6 +3045,7 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
struct attr_list_entry *ale = NULL;
struct mft_record *ni_mrec;
u32 attr_al_len;
bool free_empty_extents = true;
if (!ni)
return -EINVAL;
@ -3144,6 +3145,7 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
ntfs_error(ni->vol->sb, "Couldn't add $ATTRIBUTE_LIST to MFT");
goto rollback;
}
free_empty_extents = false;
err = ntfs_attrlist_update(ni);
if (err < 0)
@ -3163,6 +3165,8 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
CASE_SENSITIVE, 0, NULL, 0, ctx)) {
if (ntfs_attr_record_rm(ctx))
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");
}
@ -3201,6 +3205,11 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
ni->attr_list_size = 0;
NInoClearAttrList(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");
goto err_out;
put_err_out:
ntfs_attr_put_search_ctx(ctx);
err_out:
@ -3287,6 +3296,55 @@ int ntfs_inode_close(struct ntfs_inode *ni)
return err;
}
/*
* ntfs_inode_free_empty_extents - free empty extent MFT records
* @ni: base inode whose empty extent records should be freed
*
* The caller must ensure that no on-disk attribute list references an empty
* extent record and must hold @ni->mrec_lock to serialize the extent array.
*/
int ntfs_inode_free_empty_extents(struct ntfs_inode *ni)
{
int err = 0, i = 0;
if (!ni || ni->nr_extents < 0)
return -EINVAL;
mutex_lock(&ni->extent_lock);
while (i < ni->nr_extents) {
struct ntfs_inode *ext_ni = ni->ext.extent_ntfs_inos[i];
struct mft_record *m;
int ret;
m = map_mft_record(ext_ni);
if (IS_ERR(m)) {
if (!err)
err = PTR_ERR(m);
i++;
continue;
}
if (le32_to_cpu(m->bytes_in_use) -
le16_to_cpu(m->attrs_offset) != 8) {
unmap_mft_record(ext_ni);
i++;
continue;
}
unmap_mft_record(ext_ni);
ret = ntfs_mft_record_free(ni->vol, ext_ni);
if (ret) {
if (!err)
err = ret;
i++;
continue;
}
ntfs_inode_close(ext_ni);
/* ntfs_inode_close() removed this entry from the extent array. */
}
mutex_unlock(&ni->extent_lock);
return err;
}
void ntfs_destroy_ext_inode(struct ntfs_inode *ni)
{
ntfs_debug("Entering.");
@ -3386,6 +3444,9 @@ int ntfs_inode_free_space(struct ntfs_inode *ni, int size)
* Chkdsk complain if $STANDARD_INFORMATION is not in the base MFT
* record.
*
* $INDEX_ROOT must remain resident, but its attribute record may be moved
* to an extent MFT record when the base record needs room for the list.
*
* Also we can't move $ATTRIBUTE_LIST from base MFT_RECORD, so position
* search context on first attribute after $STANDARD_INFORMATION and
* $ATTRIBUTE_LIST.
@ -3427,9 +3488,6 @@ int ntfs_inode_free_space(struct ntfs_inode *ni, int size)
ctx->attr->type == AT_DATA)
goto retry;
if (ctx->attr->type == AT_INDEX_ROOT)
goto retry;
record_size = le32_to_cpu(ctx->attr->length);
/* Move away attribute. */

View File

@ -338,6 +338,7 @@ int ntfs_get_block_mft_record(struct ntfs_inode *mft_ni, struct ntfs_inode *ni);
int __ntfs_write_inode(struct inode *vi, int sync);
int ntfs_inode_attach_all_extents(struct ntfs_inode *ni);
int ntfs_inode_add_attrlist(struct ntfs_inode *ni);
int ntfs_inode_free_empty_extents(struct ntfs_inode *ni);
void ntfs_destroy_ext_inode(struct ntfs_inode *ni);
int ntfs_inode_free_space(struct ntfs_inode *ni, int size);
s64 ntfs_inode_attr_pread(struct inode *vi, s64 pos, s64 count, u8 *buf);