[PATCH v4 3/4] ntfs: protect attribute-list creation/teardown with attr_list_persist_lock
Hyunchul Lee <[email protected]> Thu, 30 Jul 2026 13:56:14 +0900
| Newsgroups | dev.linux.lists.ntfs,org.kernel.vger.stable |
|---|---|
| Message-ID | <[email protected]> |
ntfs_attr_record_rm() and ntfs_inode_add_attrlist() update the in-memory attribute list without any lock, so a concurrent reader can observe an inconsistent trio. Teardown can also free a buffer that a concurrent ntfs_attrlist_entry_add()/rm() transaction still owns while it sleeps in ntfs_attrlist_update(). Protect both functions with attr_list_persist_lock for their whole publish/persist or teardown sequence, and with attr_list_lock (write) around the actual buffer pointer/size/gen updates. Reported-by: Cen Zhang <[email protected]> Link: https://lore.kernel.org/all/[email protected]/ Fixes: 495e90fa3348 ("ntfs: update attrib operations") Cc: [email protected] Tested-by: Cen Zhang <[email protected]> Signed-off-by: Hyunchul Lee <[email protected]> --- fs/ntfs/attrib.c | 47 +++++++++++++++++++++++++++++++++++++++++------ fs/ntfs/attrib.h | 2 +- fs/ntfs/inode.c | 36 ++++++++++++++++++++++++++++++++++-- fs/ntfs/namei.c | 2 +- fs/ntfs/super.c | 2 +- 5 files changed, 78 insertions(+), 11 deletions(-) diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c index e9004d410918..0fdcf6757d02 100644 --- a/fs/ntfs/attrib.c +++ b/fs/ntfs/attrib.c @@ -2774,12 +2774,15 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type, /* * ntfs_attr_record_rm - remove attribute extent - * @ctx: search context describing the attribute which should be removed + * @ctx: search context describing the attribute which should be removed + * @persist_locked: true if the caller already holds + * base_ni->attr_list_persist_lock for the in-flight + * transaction this removal is part of * * If this function succeed, user should reinit search context if he/she wants * use it anymore. */ -int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx) +int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx, bool persist_locked) { struct ntfs_inode *base_ni, *ni; __le32 type; @@ -2819,10 +2822,33 @@ int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx) /* Post $ATTRIBUTE_LIST delete setup. */ if (type == AT_ATTRIBUTE_LIST) { + /* + * attr_list_persist_lock serializes this in-memory attr_list + * teardown against a concurrent ntfs_attrlist_entry_add()/rm() + * transaction on the same inode. + * + * @persist_locked is true when we got here from + * ntfs_attr_update_mapping_pairs() rebuilding the mapping + * pairs of the $ATTRIBUTE_LIST attribute itself: that call + * only happens underneath ntfs_attrlist_update(), which is + * always invoked by ntfs_attrlist_entry_add()/rm() or + * ntfs_inode_add_attrlist() while already holding this same + * mutex. Taking it again here would deadlock the caller + * against itself, so skip the (re-)acquisition in that case + * and rely on the lock already held further up the stack. + */ + if (!persist_locked) + mutex_lock(&base_ni->attr_list_persist_lock); + down_write(&base_ni->attr_list_lock); if (NInoAttrList(base_ni) && base_ni->attr_list) kvfree(base_ni->attr_list); base_ni->attr_list = NULL; + base_ni->attr_list_size = 0; + base_ni->attr_list_gen++; NInoClearAttrList(base_ni); + up_write(&base_ni->attr_list_lock); + if (!persist_locked) + mutex_unlock(&base_ni->attr_list_persist_lock); } /* Free MFT record, if it doesn't contain attributes. */ @@ -2869,7 +2895,7 @@ int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx) kvfree(al_rl); } /* Remove attribute record itself. */ - if (ntfs_attr_record_rm(ctx)) { + if (ntfs_attr_record_rm(ctx, false)) { ntfs_debug("Couldn't remove attribute list. Succeed anyway.\n"); return 0; } @@ -4158,8 +4184,17 @@ int ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni, s64 from_vcn) if (le64_to_cpu(ctx->attr->data.non_resident.highest_vcn) != NTFS_VCN_DELETE_MARK) continue; - /* Remove unused attribute record. */ - err = ntfs_attr_record_rm(ctx); + /* + * Remove unused attribute record. When @ni is the + * $ATTRIBUTE_LIST attribute itself, we only get here + * underneath ntfs_attrlist_update(), whose caller + * (ntfs_attrlist_entry_add()/rm() or + * ntfs_inode_add_attrlist()) already holds + * base_ni->attr_list_persist_lock for this + * transaction, so tell ntfs_attr_record_rm() not to + * recurse into it. + */ + err = ntfs_attr_record_rm(ctx, ni->type == AT_ATTRIBUTE_LIST); if (err) { ntfs_error(sb, "Could not remove unused attr"); goto put_err_out; @@ -5417,7 +5452,7 @@ int ntfs_attr_rm(struct ntfs_inode *ni) } while (!(err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, CASE_SENSITIVE, 0, NULL, 0, ctx))) { - err = ntfs_attr_record_rm(ctx); + err = ntfs_attr_record_rm(ctx, false); if (err) { ntfs_error(sb, "Failed to remove attribute extent. Leaving inconstant metadata.\n"); diff --git a/fs/ntfs/attrib.h b/fs/ntfs/attrib.h index 2e5f87abd852..ec0c846febf1 100644 --- a/fs/ntfs/attrib.h +++ b/fs/ntfs/attrib.h @@ -161,7 +161,7 @@ int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name, u32 name_len); int ntfs_attr_remove(struct ntfs_inode *ni, const __le32 type, __le16 *name, u32 name_len); -int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx); +int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx, bool persist_locked); int ntfs_attr_record_move_to(struct ntfs_attr_search_ctx *ctx, struct ntfs_inode *ni); int ntfs_attr_add(struct ntfs_inode *ni, __le32 type, __le16 *name, u8 name_len, u8 *val, s64 size); diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c index f9d3e37f99e7..0297de482a36 100644 --- a/fs/ntfs/inode.c +++ b/fs/ntfs/inode.c @@ -3049,6 +3049,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 attrlist_locked = false; if (!ni) return -EINVAL; @@ -3124,9 +3125,14 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni) } /* Set in-memory attribute list. */ + mutex_lock(&ni->attr_list_persist_lock); + attrlist_locked = true; + down_write(&ni->attr_list_lock); ni->attr_list = al; ni->attr_list_size = al_len; + ni->attr_list_gen++; NInoSetAttrList(ni); + up_write(&ni->attr_list_lock); attr_al_len = offsetof(struct attr_record, data.resident.reserved) + 1 + ((al_len + 7) & ~7); @@ -3153,28 +3159,45 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni) if (err < 0) goto remove_attrlist_record; + mutex_unlock(&ni->attr_list_persist_lock); + attrlist_locked = false; ntfs_attr_put_search_ctx(ctx); unmap_mft_record(ni); return 0; remove_attrlist_record: - /* Prevent ntfs_attr_recorm_rm from freeing attribute list. */ + down_write(&ni->attr_list_lock); ni->attr_list = NULL; + ni->attr_list_gen++; NInoClearAttrList(ni); + up_write(&ni->attr_list_lock); + mutex_unlock(&ni->attr_list_persist_lock); + attrlist_locked = false; + /* 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)) + if (ntfs_attr_record_rm(ctx, false)) ntfs_error(ni->vol->sb, "Rollback failed to remove attrlist"); } else { ntfs_error(ni->vol->sb, "Rollback failed to find attrlist"); } + /* + * Without this lock, a concurrent ntfs_attrlist_entry_add()/rm() + * could replace or free @al out from under this loop. + */ + mutex_lock(&ni->attr_list_persist_lock); + attrlist_locked = true; + /* Setup back in-memory runlist. */ + down_write(&ni->attr_list_lock); ni->attr_list = al; ni->attr_list_size = al_len; + ni->attr_list_gen++; NInoSetAttrList(ni); + up_write(&ni->attr_list_lock); rollback: /* * Scan attribute list for attributes that placed not in the base MFT @@ -3201,10 +3224,19 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni) } /* Remove in-memory attribute list. */ + down_write(&ni->attr_list_lock); ni->attr_list = NULL; ni->attr_list_size = 0; + ni->attr_list_gen++; NInoClearAttrList(ni); NInoClearAttrListDirty(ni); + up_write(&ni->attr_list_lock); + + if (attrlist_locked) { + mutex_unlock(&ni->attr_list_persist_lock); + attrlist_locked = false; + } + put_err_out: ntfs_attr_put_search_ctx(ctx); err_out: diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index 5ff25e9aaa32..9c85caa00a0c 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -941,7 +941,7 @@ static int ntfs_delete(struct ntfs_inode *ni, struct ntfs_inode *dir_ni, if (err) goto err_out; - err = ntfs_attr_record_rm(actx); + err = ntfs_attr_record_rm(actx, false); if (err) goto err_out; diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c index 8abe7bee4c0d..6d28c4c25a05 100644 --- a/fs/ntfs/super.c +++ b/fs/ntfs/super.c @@ -485,7 +485,7 @@ int ntfs_write_volume_label(struct ntfs_volume *vol, char *label) ret = ntfs_attr_lookup(AT_VOLUME_NAME, NULL, 0, 0, 0, NULL, 0, ctx); if (!ret) - ret = ntfs_attr_record_rm(ctx); + ret = ntfs_attr_record_rm(ctx, false); else if (ret == -ENOENT) ret = 0; ntfs_attr_put_search_ctx(ctx); -- 2.43.0