summaryrefslogtreecommitdiff
path: root/fs/ntfs/attrib.c
diff options
context:
space:
mode:
authorGreg Kroah-Hartman <gregkh@linuxfoundation.org>2026-07-24 16:21:27 +0200
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>2026-07-24 16:21:27 +0200
commit8f9aa2c90530ab92301a82231ae44f3722becd93 (patch)
treefb282e955b0a880b07131a135257fe3ec764e928 /fs/ntfs/attrib.c
parent93467b31bec6da512b51544e5e4584f2745e995e (diff)
parent155b42bec9cbb6b8cdc47dd9bd09503a81fbe493 (diff)
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Diffstat (limited to 'fs/ntfs/attrib.c')
-rw-r--r--fs/ntfs/attrib.c345
1 files changed, 252 insertions, 93 deletions
diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index 421c6cdcbb53..0f1d0b54cfb5 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -174,7 +174,10 @@ int ntfs_map_runlist_nolock(struct ntfs_inode *ni, s64 vcn, struct ntfs_attr_sea
err = -EIO;
goto err_out;
}
- WARN_ON(!ctx->attr->non_resident);
+ if (unlikely(!ctx->attr->non_resident)) {
+ err = -EIO;
+ goto err_out;
+ }
}
a = ctx->attr;
/*
@@ -588,6 +591,8 @@ static u32 ntfs_resident_attr_min_value_length(const __le32 type)
sizeof(__le16) * 1;
case AT_VOLUME_INFORMATION:
return sizeof(struct volume_information);
+ case AT_INDEX_ROOT:
+ return sizeof(struct index_root);
case AT_EA_INFORMATION:
return sizeof(struct ea_information);
default:
@@ -595,6 +600,154 @@ static u32 ntfs_resident_attr_min_value_length(const __le32 type)
}
}
+static bool ntfs_attr_type_is_resident_only(const __le32 type)
+{
+ switch (type) {
+ case AT_STANDARD_INFORMATION:
+ case AT_FILE_NAME:
+ case AT_OBJECT_ID:
+ case AT_VOLUME_NAME:
+ case AT_VOLUME_INFORMATION:
+ case AT_INDEX_ROOT:
+ case AT_EA_INFORMATION:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool ntfs_file_name_attr_value_is_valid(const u8 *value, const u32 value_length)
+{
+ const struct file_name_attr *fn;
+ u32 file_name_size;
+
+ fn = (const struct file_name_attr *)value;
+ file_name_size = fn->file_name_length * sizeof(__le16);
+
+ return file_name_size <=
+ value_length - offsetof(struct file_name_attr, file_name);
+}
+
+static bool ntfs_volume_name_attr_value_is_valid(const u32 value_length)
+{
+ if (value_length & 1)
+ return false;
+
+ return value_length <= NTFS_MAX_LABEL_LEN * sizeof(__le16);
+}
+
+static bool ntfs_index_root_attr_value_is_valid(const u8 *value, const u32 value_length)
+{
+ const struct index_root *ir;
+ u32 index_size;
+ u32 entries_offset;
+ u32 index_length;
+ u32 allocated_size;
+
+ ir = (const struct index_root *)value;
+ index_size = value_length - offsetof(struct index_root, index);
+ entries_offset = le32_to_cpu(ir->index.entries_offset);
+ index_length = le32_to_cpu(ir->index.index_length);
+ allocated_size = le32_to_cpu(ir->index.allocated_size);
+
+ if ((entries_offset | index_length | allocated_size) & 7 ||
+ entries_offset < sizeof(struct index_header) ||
+ entries_offset > index_length ||
+ index_length > allocated_size ||
+ allocated_size > index_size ||
+ index_length - entries_offset < sizeof(struct index_entry_header))
+ return false;
+
+ return true;
+}
+
+struct ntfs_resident_attr_value {
+ const u8 *data;
+ u32 len;
+};
+
+static bool ntfs_resident_attr_value_get(const struct attr_record *a,
+ struct ntfs_resident_attr_value *value)
+{
+ u32 attr_len;
+ u16 value_offset;
+
+ attr_len = le32_to_cpu(a->length);
+ if (attr_len < offsetof(struct attr_record, data.resident.reserved) +
+ sizeof(a->data.resident.reserved))
+ return false;
+
+ value->len = le32_to_cpu(a->data.resident.value_length);
+ value_offset = le16_to_cpu(a->data.resident.value_offset);
+
+ if (value->len > attr_len || value_offset > attr_len - value->len)
+ return false;
+
+ value->data = (const u8 *)a + value_offset;
+ return true;
+}
+
+static bool ntfs_non_resident_attr_value_is_valid(const struct attr_record *a)
+{
+ u32 attr_len;
+ u32 min_len;
+ u16 mp_offset;
+
+ attr_len = le32_to_cpu(a->length);
+ min_len = offsetof(struct attr_record, data.non_resident.initialized_size) +
+ sizeof(a->data.non_resident.initialized_size);
+ if (attr_len < min_len)
+ return false;
+
+ mp_offset = le16_to_cpu(a->data.non_resident.mapping_pairs_offset);
+ return mp_offset >= min_len && mp_offset <= attr_len;
+}
+
+static bool ntfs_attr_value_is_valid(struct ntfs_volume *vol,
+ const struct attr_record *a,
+ const u64 mft_no)
+{
+ struct ntfs_resident_attr_value value;
+ u32 min_len;
+
+ if (a->non_resident) {
+ if (ntfs_attr_type_is_resident_only(a->type))
+ goto corrupt;
+ if (!ntfs_non_resident_attr_value_is_valid(a))
+ goto corrupt;
+ return true;
+ }
+
+ if (!ntfs_resident_attr_value_get(a, &value))
+ goto corrupt;
+
+ min_len = ntfs_resident_attr_min_value_length(a->type);
+ if (min_len && value.len < min_len)
+ goto corrupt;
+
+ switch (a->type) {
+ case AT_FILE_NAME:
+ if (!ntfs_file_name_attr_value_is_valid(value.data, value.len))
+ goto corrupt;
+ break;
+ case AT_VOLUME_NAME:
+ if (!ntfs_volume_name_attr_value_is_valid(value.len))
+ goto corrupt;
+ break;
+ case AT_INDEX_ROOT:
+ if (!ntfs_index_root_attr_value_is_valid(value.data, value.len))
+ goto corrupt;
+ break;
+ }
+ return true;
+
+corrupt:
+ ntfs_error(vol->sb,
+ "Corrupt %#x attribute in MFT record %llu\n",
+ le32_to_cpu(a->type), mft_no);
+ return false;
+}
+
/*
* ntfs_attr_find - find (next) attribute in mft record
* @type: attribute type to find
@@ -705,8 +858,11 @@ static int ntfs_attr_find(const __le32 type, const __le16 *name,
}
}
- if (type == AT_UNUSED)
+ if (type == AT_UNUSED) {
+ if (!ntfs_attr_value_is_valid(vol, a, ctx->ntfs_ino->mft_no))
+ break;
return 0;
+ }
if (a->type != type)
continue;
/*
@@ -747,37 +903,8 @@ static int ntfs_attr_find(const __le32 type, const __le16 *name,
}
}
- /* Validate attribute's value offset/length */
- if (!a->non_resident) {
- u32 min_len;
- u32 value_length = le32_to_cpu(a->data.resident.value_length);
- u16 value_offset = le16_to_cpu(a->data.resident.value_offset);
-
- if (value_length > le32_to_cpu(a->length) ||
- value_offset > le32_to_cpu(a->length) - value_length)
- break;
-
- min_len = ntfs_resident_attr_min_value_length(a->type);
- if (min_len && value_length < min_len) {
- ntfs_error(vol->sb,
- "Too small %#x resident attribute value in MFT record %lld\n",
- le32_to_cpu(a->type), (long long)ctx->ntfs_ino->mft_no);
- break;
- }
- } else {
- u32 min_len;
- u16 mp_offset;
-
- min_len = offsetof(struct attr_record, data.non_resident.initialized_size) +
- sizeof(a->data.non_resident.initialized_size);
- if (le32_to_cpu(a->length) < min_len)
- break;
-
- mp_offset = le16_to_cpu(a->data.non_resident.mapping_pairs_offset);
- if (mp_offset < min_len ||
- mp_offset > le32_to_cpu(a->length))
- break;
- }
+ if (!ntfs_attr_value_is_valid(vol, a, ctx->ntfs_ino->mft_no))
+ break;
/*
* The names match or @name not present and attribute is
@@ -843,11 +970,71 @@ char *ntfs_attr_name_get(const struct ntfs_volume *vol, const __le16 *uname,
return NULL;
}
+/*
+ * ntfs_attr_list_entry_is_valid - sanity check one $ATTRIBUTE_LIST entry
+ * @ale: the attribute-list entry to check
+ * @al_end: end of the attribute-list buffer @ale lives in
+ *
+ * Verify that @ale is a well-formed attr_list_entry wholly contained in
+ * [.., @al_end): its fixed header must lie in range before any field is
+ * dereferenced, its length must be a multiple of 8 that covers the fixed
+ * header plus the name, the name must lie within the buffer, the entry must
+ * be in use and carry a live MFT reference. Return true if valid.
+ */
+bool ntfs_attr_list_entry_is_valid(const struct attr_list_entry *ale,
+ const u8 *al_end)
+{
+ const u8 *al = (const u8 *)ale;
+ u16 ale_len;
+
+ /* The fixed header must be in bounds before it is parsed. */
+ if (al + offsetof(struct attr_list_entry, name) > al_end)
+ return false;
+ ale_len = le16_to_cpu(ale->length);
+ /* On-disk entries are 8-byte aligned (see struct attr_list_entry). */
+ if (ale_len & 7)
+ return false;
+ if (ale->name_offset != sizeof(struct attr_list_entry))
+ return false;
+ if ((u32)ale->name_offset +
+ (u32)ale->name_length * sizeof(__le16) > ale_len ||
+ al + ale_len > al_end)
+ return false;
+ if (ale->type == AT_UNUSED)
+ return false;
+ if (MSEQNO_LE(ale->mft_reference) == 0)
+ return false;
+ return true;
+}
+
+/*
+ * ntfs_attr_list_is_valid - sanity check an in-memory $ATTRIBUTE_LIST
+ * @al_start: start of the attribute list buffer
+ * @size: length of the attribute list in bytes
+ *
+ * Verify that [@al_start, @al_start + @size) is a sequence of valid
+ * attr_list_entry records (see ntfs_attr_list_entry_is_valid()) that tile the
+ * buffer exactly. Return true if valid, false otherwise.
+ */
+bool ntfs_attr_list_is_valid(const u8 *al_start, s64 size)
+{
+ const u8 *al = al_start;
+ const u8 *al_end = al_start + size;
+
+ while (al < al_end) {
+ const struct attr_list_entry *ale =
+ (const struct attr_list_entry *)al;
+
+ if (!ntfs_attr_list_entry_is_valid(ale, al_end))
+ return false;
+ al += le16_to_cpu(ale->length);
+ }
+ return al == al_end;
+}
+
int load_attribute_list(struct ntfs_inode *base_ni, u8 *al_start, const s64 size)
{
struct inode *attr_vi = NULL;
- u8 *al;
- struct attr_list_entry *ale;
if (!al_start || size <= 0)
return -EINVAL;
@@ -869,19 +1056,7 @@ int load_attribute_list(struct ntfs_inode *base_ni, u8 *al_start, const s64 size
}
iput(attr_vi);
- for (al = al_start; al < al_start + size; al += le16_to_cpu(ale->length)) {
- ale = (struct attr_list_entry *)al;
- if (ale->name_offset != sizeof(struct attr_list_entry))
- break;
- if (le16_to_cpu(ale->length) <= ale->name_offset + ale->name_length ||
- al + le16_to_cpu(ale->length) > al_start + size)
- break;
- if (ale->type == AT_UNUSED)
- break;
- if (MSEQNO_LE(ale->mft_reference) == 0)
- break;
- }
- if (al != al_start + size) {
+ if (!ntfs_attr_list_is_valid(al_start, size)) {
ntfs_error(base_ni->vol->sb, "Corrupt attribute list, mft = %llu",
base_ni->mft_no);
return -EIO;
@@ -1137,9 +1312,8 @@ find_attr_list_attr:
* we have reached the right one or the search has failed.
*/
if (lowest_vcn && (u8 *)next_al_entry >= al_start &&
- (u8 *)next_al_entry + 6 < al_end &&
- (u8 *)next_al_entry + le16_to_cpu(
- next_al_entry->length) <= al_end &&
+ ntfs_attr_list_entry_is_valid(next_al_entry,
+ al_end) &&
le64_to_cpu(next_al_entry->lowest_vcn) <=
lowest_vcn &&
next_al_entry->type == al_entry->type &&
@@ -1252,22 +1426,8 @@ do_next_attr_loop:
ctx->attr = a;
- if (a->non_resident) {
- u32 min_len;
- u16 mp_offset;
-
- min_len = offsetof(struct attr_record,
- data.non_resident.initialized_size) +
- sizeof(a->data.non_resident.initialized_size);
-
- if (le32_to_cpu(a->length) < min_len)
- break;
-
- mp_offset =
- le16_to_cpu(a->data.non_resident.mapping_pairs_offset);
- if (mp_offset < min_len || mp_offset > attr_len)
- break;
- }
+ if (!ntfs_attr_value_is_valid(vol, a, ctx->ntfs_ino->mft_no))
+ break;
/*
* If no @val specified or @val specified and it matches, we
@@ -1279,19 +1439,6 @@ do_next_attr_loop:
u32 value_length = le32_to_cpu(a->data.resident.value_length);
u16 value_offset = le16_to_cpu(a->data.resident.value_offset);
- if (attr_len < offsetof(struct attr_record, data.resident.reserved) +
- sizeof(a->data.resident.reserved))
- break;
- if (value_length > attr_len || value_offset > attr_len - value_length)
- break;
-
- value_length = ntfs_resident_attr_min_value_length(a->type);
- if (value_length && le32_to_cpu(a->data.resident.value_length) <
- value_length) {
- pr_err("Too small resident attribute value in MFT record %lld, type %#x\n",
- (long long)ctx->ntfs_ino->mft_no, a->type);
- break;
- }
if (value_length == val_len &&
!memcmp((u8 *)a + value_offset, val, val_len)) {
attr_found:
@@ -4536,10 +4683,12 @@ attr_resize_again:
while (!(err = ntfs_attr_lookup(AT_UNUSED, NULL, 0, 0, 0, NULL, 0, ctx))) {
struct inode *tvi;
struct attr_record *a;
+ u32 value_len;
a = ctx->attr;
if (a->non_resident || a->type == AT_ATTRIBUTE_LIST)
continue;
+ value_len = le32_to_cpu(a->data.resident.value_length);
if (ntfs_attr_can_be_non_resident(vol, a->type))
continue;
@@ -4551,6 +4700,8 @@ attr_resize_again:
if (le32_to_cpu(a->length) <= (sizeof(struct attr_record) - sizeof(s64)) +
((a->name_length * sizeof(__le16) + 7) & ~7) + 8)
continue;
+ if (a->type == AT_DATA && !value_len)
+ continue;
if (a->type == AT_DATA)
tvi = ntfs_iget(sb, base_ni->mft_no);
@@ -4563,8 +4714,7 @@ attr_resize_again:
continue;
}
- if (ntfs_attr_make_non_resident(NTFS_I(tvi),
- le32_to_cpu(ctx->attr->data.resident.value_length))) {
+ if (ntfs_attr_make_non_resident(NTFS_I(tvi), value_len)) {
iput(tvi);
continue;
}
@@ -5177,6 +5327,7 @@ int ntfs_non_resident_attr_insert_range(struct ntfs_inode *ni, s64 start_vcn, s6
ret = ntfs_attr_map_whole_runlist(ni);
if (ret) {
up_write(&ni->runlist.lock);
+ kfree(hole_rl);
return ret;
}
@@ -5388,6 +5539,7 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
s64 old_data_size;
s64 vcn_start, vcn_end, vcn_uninit, vcn, try_alloc_cnt;
s64 lcn, alloc_cnt;
+ s64 rl_lcn, rl_length, rl_vcn;
int err = 0;
struct runlist_element *rl;
bool balloc;
@@ -5467,19 +5619,23 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
while (vcn < vcn_uninit) {
down_read(&ni->runlist.lock);
rl = ntfs_attr_find_vcn_nolock(ni, vcn, NULL);
- up_read(&ni->runlist.lock);
if (IS_ERR(rl)) {
+ up_read(&ni->runlist.lock);
err = PTR_ERR(rl);
goto out;
}
+ rl_lcn = rl->lcn;
+ rl_length = rl->length;
+ rl_vcn = rl->vcn;
+ up_read(&ni->runlist.lock);
- if (rl->lcn > 0) {
- vcn += rl->length - (vcn - rl->vcn);
- } else if (rl->lcn == LCN_DELALLOC || rl->lcn == LCN_HOLE) {
- try_alloc_cnt = min(rl->length - (vcn - rl->vcn),
+ if (rl_lcn > 0) {
+ vcn += rl_length - (vcn - rl_vcn);
+ } else if (rl_lcn == LCN_DELALLOC || rl_lcn == LCN_HOLE) {
+ try_alloc_cnt = min(rl_length - (vcn - rl_vcn),
vcn_uninit - vcn);
- if (rl->lcn == LCN_DELALLOC) {
+ if (rl_lcn == LCN_DELALLOC) {
vcn += try_alloc_cnt;
continue;
}
@@ -5494,11 +5650,14 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
if (err)
goto out;
- err = ntfs_dio_zero_range(VFS_I(ni),
- lcn << vol->cluster_size_bits,
- alloc_cnt << vol->cluster_size_bits);
- if (err > 0)
- goto out;
+ if (balloc) {
+ err = ntfs_dio_zero_range(VFS_I(ni),
+ lcn << vol->cluster_size_bits,
+ alloc_cnt <<
+ vol->cluster_size_bits);
+ if (err > 0)
+ goto out;
+ }
if (signal_pending(current))
goto out;