| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* | |
| * Attribute list attribute handling code. | |
| * Part of this file is based on code from the NTFS-3G. | |
| * | |
| * Copyright (c) 2004-2005 Anton Altaparmakov | |
| * Copyright (c) 2004-2005 Yura Pakhuchiy | |
| * Copyright (c) 2006 Szabolcs Szakacsits | |
| * Copyright (c) 2025 LG Electronics Co., Ltd. | |
| */ | |
| #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 | |
| * @ni: opened ntfs inode for which perform check | |
| * | |
| * Check whether all are attributes belong to one MFT record, in that case | |
| * attribute list is not needed. | |
| * | |
| * Return 1 if inode need attribute list, 0 if not, or -errno on error. | |
| */ | |
| int ntfs_attrlist_need(struct ntfs_inode *ni) | |
| { | |
| struct attr_list_entry *ale; | |
| if (!ni) { | |
| ntfs_debug("Invalid arguments.\n"); | |
| return -EINVAL; | |
| } | |
| ntfs_debug("Entering for inode 0x%llx.\n", (long long) ni->mft_no); | |
| if (!NInoAttrList(ni)) { | |
| ntfs_debug("Inode haven't got attribute list.\n"); | |
| return -EINVAL; | |
| } | |
| if (!ni->attr_list) { | |
| ntfs_debug("Corrupt in-memory struct.\n"); | |
| return -EINVAL; | |
| } | |
| ale = (struct attr_list_entry *)ni->attr_list; | |
| while ((u8 *)ale < ni->attr_list + ni->attr_list_size) { | |
| if (MREF_LE(ale->mft_reference) != ni->mft_no) | |
| return 1; | |
| ale = (struct attr_list_entry *)((u8 *)ale + le16_to_cpu(ale->length)); | |
| } | |
| 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_inode *locked_ni) | |
| { | |
| 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; | |
| /* The buffered I/O below can reacquire the attribute runlist lock. */ | |
| if (attr_ni == locked_ni) | |
| return -ENOSPC; | |
| 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_locked(attr_ni, 0, locked_ni); | |
| 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_locked( | |
| attr_ni, 0, locked_ni); | |
| 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_locked(struct ntfs_inode *base_ni, | |
| struct ntfs_inode *locked_ni) | |
| { | |
| struct inode *attr_vi; | |
| struct ntfs_inode *attr_ni; | |
| s64 written; | |
| int err, retry_err; | |
| /* | |
| * generic_shutdown_super() clears SB_ACTIVE before evicting cached | |
| * inodes. Do not look up the attribute-list inode after SB_ACTIVE has | |
| * been cleared; it may already be I_FREEING, and waiting on it can | |
| * self-deadlock. | |
| */ | |
| if (!(VFS_I(base_ni)->i_sb->s_flags & SB_ACTIVE)) | |
| return -EIO; | |
| 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); | |
| return err; | |
| } | |
| attr_ni = NTFS_I(attr_vi); | |
| /* Truncation and page-cache writes can reacquire this runlist lock. */ | |
| if (attr_ni == locked_ni) { | |
| iput(attr_vi); | |
| return -ENOSPC; | |
| } | |
| err = ntfs_attr_truncate_i_locked( | |
| attr_ni, base_ni->attr_list_size, HOLES_NO, locked_ni); | |
| 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, locked_ni); | |
| 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_locked( | |
| attr_ni, base_ni->attr_list_size, | |
| HOLES_NO, locked_ni); | |
| if (retry_err) { | |
| ntfs_error(base_ni->vol->sb, | |
| "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 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_locked( | |
| attr_ni, base_ni->attr_list_size, | |
| NTFS_MAX_ATTR_LIST_SIZE, locked_ni); | |
| if (retry_err == -ENOSPC) { | |
| retry_err = ntfs_attrlist_repack( | |
| attr_vi, attr_ni, | |
| NTFS_MAX_ATTR_LIST_SIZE, locked_ni); | |
| 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); | |
| if (NInoNonResident(attr_ni) && !NInoAttrListNonResident(base_ni)) | |
| NInoSetAttrListNonResident(base_ni); | |
| 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 err; | |
| } | |
| NInoSetAttrListDirty(base_ni); | |
| iput(attr_vi); | |
| return 0; | |
| } | |
| int ntfs_attrlist_update(struct ntfs_inode *base_ni) | |
| { | |
| return ntfs_attrlist_update_locked(base_ni, NULL); | |
| } | |
| /* | |
| * ntfs_attrlist_entry_add - add an attribute list attribute entry | |
| * @ni: opened ntfs inode, which contains that attribute | |
| * @attr: attribute record to add to attribute list | |
| * | |
| * Return 0 on success and -errno on error. | |
| */ | |
| int ntfs_attrlist_entry_add(struct ntfs_inode *ni, struct attr_record *attr) | |
| { | |
| struct attr_list_entry *ale; | |
| __le64 mref; | |
| struct ntfs_attr_search_ctx *ctx; | |
| u8 *new_al; | |
| int entry_len, entry_offset, err; | |
| struct mft_record *ni_mrec; | |
| u8 *old_al; | |
| __le64 lowest_vcn; | |
| if (!ni || !attr) { | |
| ntfs_debug("Invalid arguments.\n"); | |
| return -EINVAL; | |
| } | |
| ntfs_debug("Entering for inode 0x%llx, attr 0x%x.\n", | |
| ni->mft_no, (unsigned int) le32_to_cpu(attr->type)); | |
| ni_mrec = map_mft_record(ni); | |
| if (IS_ERR(ni_mrec)) { | |
| ntfs_debug("Invalid arguments.\n"); | |
| return -EIO; | |
| } | |
| mref = MK_LE_MREF(ni->mft_no, le16_to_cpu(ni_mrec->sequence_number)); | |
| unmap_mft_record(ni); | |
| if (ni->nr_extents == -1) | |
| ni = ni->ext.base_ntfs_ino; | |
| if (!NInoAttrList(ni)) { | |
| ntfs_debug("Attribute list isn't present.\n"); | |
| return -ENOENT; | |
| } | |
| /* Determine size and allocate memory for new attribute list. */ | |
| entry_len = (sizeof(struct attr_list_entry) + sizeof(__le16) * | |
| attr->name_length + 7) & ~7; | |
| new_al = kvzalloc(ni->attr_list_size + entry_len, GFP_NOFS); | |
| if (!new_al) | |
| return -ENOMEM; | |
| /* Find place for the new entry. */ | |
| ctx = ntfs_attr_get_search_ctx(ni, NULL); | |
| if (!ctx) { | |
| err = -ENOMEM; | |
| ntfs_error(ni->vol->sb, "Failed to get search context"); | |
| goto err_out; | |
| } | |
| if (attr->non_resident) | |
| lowest_vcn = attr->data.non_resident.lowest_vcn; | |
| else | |
| lowest_vcn = 0; | |
| err = ntfs_attr_lookup(attr->type, (attr->name_length) ? (__le16 *) | |
| ((u8 *)attr + le16_to_cpu(attr->name_offset)) : | |
| AT_UNNAMED, attr->name_length, CASE_SENSITIVE, | |
| le64_to_cpu(lowest_vcn), | |
| (attr->non_resident) ? NULL : ((u8 *)attr + | |
| le16_to_cpu(attr->data.resident.value_offset)), (attr->non_resident) ? | |
| 0 : le32_to_cpu(attr->data.resident.value_length), ctx); | |
| if (!err) { | |
| /* Found some extent, check it to be before new extent. */ | |
| if (ctx->al_entry->lowest_vcn == lowest_vcn) { | |
| err = -EEXIST; | |
| ntfs_debug("Such attribute already present in the attribute list.\n"); | |
| ntfs_attr_put_search_ctx(ctx); | |
| goto err_out; | |
| } | |
| /* Add new entry after this extent. */ | |
| ale = (struct attr_list_entry *)((u8 *)ctx->al_entry + | |
| le16_to_cpu(ctx->al_entry->length)); | |
| } else { | |
| /* Check for real errors. */ | |
| if (err != -ENOENT) { | |
| ntfs_debug("Attribute lookup failed.\n"); | |
| ntfs_attr_put_search_ctx(ctx); | |
| goto err_out; | |
| } | |
| /* No previous extents found. */ | |
| ale = ctx->al_entry; | |
| } | |
| /* Don't need it anymore, @ctx->al_entry points to @ni->attr_list. */ | |
| ntfs_attr_put_search_ctx(ctx); | |
| /* Determine new entry offset. */ | |
| entry_offset = ((u8 *)ale - ni->attr_list); | |
| /* Set pointer to new entry. */ | |
| ale = (struct attr_list_entry *)(new_al + entry_offset); | |
| memset(ale, 0, entry_len); | |
| /* Form new entry. */ | |
| ale->type = attr->type; | |
| ale->length = cpu_to_le16(entry_len); | |
| ale->name_length = attr->name_length; | |
| ale->name_offset = offsetof(struct attr_list_entry, name); | |
| if (attr->non_resident) | |
| ale->lowest_vcn = attr->data.non_resident.lowest_vcn; | |
| else | |
| ale->lowest_vcn = 0; | |
| ale->mft_reference = mref; | |
| ale->instance = attr->instance; | |
| memcpy(ale->name, (u8 *)attr + le16_to_cpu(attr->name_offset), | |
| attr->name_length * sizeof(__le16)); | |
| /* Copy entries from old attribute list to new. */ | |
| memcpy(new_al, ni->attr_list, entry_offset); | |
| memcpy(new_al + entry_offset + entry_len, ni->attr_list + | |
| entry_offset, ni->attr_list_size - entry_offset); | |
| /* Set new runlist. */ | |
| old_al = ni->attr_list; | |
| ni->attr_list = new_al; | |
| ni->attr_list_size = ni->attr_list_size + entry_len; | |
| err = ntfs_attrlist_update(ni); | |
| if (err) { | |
| ni->attr_list = old_al; | |
| ni->attr_list_size -= entry_len; | |
| goto err_out; | |
| } | |
| kvfree(old_al); | |
| return 0; | |
| err_out: | |
| kvfree(new_al); | |
| return err; | |
| } | |
| /* | |
| * ntfs_attrlist_entry_rm - remove an attribute list attribute entry | |
| * @ctx: attribute search context describing the attribute list entry | |
| * | |
| * Remove the attribute list entry @ctx->al_entry from the attribute list. | |
| * | |
| * Return 0 on success and -errno on error. | |
| */ | |
| int ntfs_attrlist_entry_rm(struct ntfs_attr_search_ctx *ctx) | |
| { | |
| u8 *new_al; | |
| int new_al_len; | |
| struct ntfs_inode *base_ni; | |
| struct attr_list_entry *ale; | |
| if (!ctx || !ctx->ntfs_ino || !ctx->al_entry) { | |
| ntfs_debug("Invalid arguments.\n"); | |
| return -EINVAL; | |
| } | |
| if (ctx->base_ntfs_ino) | |
| base_ni = ctx->base_ntfs_ino; | |
| else | |
| base_ni = ctx->ntfs_ino; | |
| ale = ctx->al_entry; | |
| ntfs_debug("Entering for inode 0x%llx, attr 0x%x, lowest_vcn %lld.\n", | |
| (long long)ctx->ntfs_ino->mft_no, | |
| (unsigned int)le32_to_cpu(ctx->al_entry->type), | |
| (long long)le64_to_cpu(ctx->al_entry->lowest_vcn)); | |
| if (!NInoAttrList(base_ni)) { | |
| ntfs_debug("Attribute list isn't present.\n"); | |
| return -ENOENT; | |
| } | |
| /* Allocate memory for new attribute list. */ | |
| new_al_len = base_ni->attr_list_size - le16_to_cpu(ale->length); | |
| new_al = kvzalloc(new_al_len, GFP_NOFS); | |
| if (!new_al) | |
| return -ENOMEM; | |
| /* Copy entries from old attribute list to new. */ | |
| memcpy(new_al, base_ni->attr_list, (u8 *)ale - base_ni->attr_list); | |
| memcpy(new_al + ((u8 *)ale - base_ni->attr_list), (u8 *)ale + le16_to_cpu( | |
| ale->length), new_al_len - ((u8 *)ale - base_ni->attr_list)); | |
| /* Set new runlist. */ | |
| kvfree(base_ni->attr_list); | |
| base_ni->attr_list = new_al; | |
| base_ni->attr_list_size = new_al_len; | |
| return ntfs_attrlist_update(base_ni); | |
| } |