ntfs: reject non-resident attributes whose sizes exceed their allocation

ntfs_read_locked_inode(), ntfs_read_locked_attr_inode() and
ntfs_read_locked_index_inode() take a non-resident attribute's data_size
and initialized_size from disk without checking them against its
allocated_size.  The runlist ends at allocated_size and the read path
maps what lies beyond it as a hole, so a data_size larger than the
allocation makes reads return zeros that are not on disk, with no error.

On a crafted volume with 4 KiB clusters where a 6144000-byte file claims
a data_size and initialized_size 64 KiB beyond its allocation, reading
the file returns 16 clusters of such zeros.  A sparse file behaves the
same.  A compressed file instead fails with -EIO after a burst of "Still
have pages left!" errors from ntfs_read_compressed_block().

Require 0 <= initialized_size <= data_size <= allocated_size, with
allocated_size a multiple of the cluster size, when the inode is read,
as fs/ntfs3 does in mi_enum_attr(), and fail with -EIO otherwise, which
marks the inode bad and the volume as having errors.  initialized_size
above data_size is rejected too because an extending write zeroes the
gap it opens only from initialized_size onwards.  An unaligned
allocated_size ends inside a cluster that the read path treats as past
the end: a crafted 66440-byte file with 4 KiB clusters reads its last
904 bytes as zeros.

Sparse and compressed attributes need no exception.  Every non-resident
attribute has a cluster-aligned allocated_size >= data_size, holes
included, on eight public test volumes (seven written by Windows, with
LZNT1-compressed files, a sparse $UsnJrnl:$J and a OneDrive placeholder
whose unnamed $DATA is one hole, and one by mkntfs), on a volume written
by ntfs-3g and on one written by this driver, and the patched driver
reads every file on them as before.  fs/ntfs3 has enforced the same
checks since v6.6, also without exceptions.  The layout.h comment saying
that data_size can exceed allocated_size for compressed and sparse
attributes is corrected.

This also covers a non-resident attribute list, which
load_attribute_list() reads through ntfs_attr_iget(), and $MFT, whose
inode goes through ntfs_read_locked_inode() after the previous patch's
check.  $MFT's own non-resident attribute list goes through neither:
ntfs_read_inode_mount() loads it with load_attribute_list_mount() and
ntfs_read_locked_inode() skips it for $MFT, so the check is added there
too.  The check was already missing in the classic driver; the Fixes
tag names the commit that brought that code back.

Fixes: 1e9ea7e044 ("Revert "fs: Remove NTFS classic"")
Signed-off-by: Matthias Goergens <matthias.goergens@gmail.com>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
This commit is contained in:
Matthias Goergens 2026-09-30 11:48:35 +08:00 committed by Namjae Jeon
parent 82a5e003ef
commit 8e91379152
2 changed files with 35 additions and 5 deletions

View File

@ -651,6 +651,25 @@ void ntfs_set_vfs_operations(struct inode *inode, mode_t mode, dev_t dev)
}
}
static bool ntfs_non_resident_sizes_inconsistent(struct inode *vi,
const struct attr_record *a)
{
s64 allocated_size = le64_to_cpu(a->data.non_resident.allocated_size);
s64 data_size = le64_to_cpu(a->data.non_resident.data_size);
s64 initialized_size = le64_to_cpu(a->data.non_resident.initialized_size);
if (initialized_size >= 0 && initialized_size <= data_size &&
data_size <= allocated_size &&
!ntfs_bytes_to_cluster_off(NTFS_I(vi)->vol, allocated_size))
return false;
ntfs_error(vi->i_sb,
"Attribute 0x%x of inode 0x%llx is corrupt (initialized size %lld, data size %lld, allocated size %lld).",
le32_to_cpu(a->type), NTFS_I(vi)->mft_no, initialized_size,
data_size, allocated_size);
return true;
}
/*
* ntfs_read_locked_inode - read an inode from its device
* @vi: inode to read
@ -1191,6 +1210,8 @@ static int ntfs_read_locked_inode(struct inode *vi)
"First extent of $DATA attribute has non zero lowest_vcn.");
goto unm_err_out;
}
if (ntfs_non_resident_sizes_inconsistent(vi, a))
goto unm_err_out;
vi->i_size = ni->data_size = le64_to_cpu(a->data.non_resident.data_size);
ni->initialized_size = le64_to_cpu(a->data.non_resident.initialized_size);
ni->allocated_size = le64_to_cpu(a->data.non_resident.allocated_size);
@ -1453,6 +1474,8 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi)
ntfs_error(vi->i_sb, "First extent of attribute has non-zero lowest_vcn.");
goto unm_err_out;
}
if (ntfs_non_resident_sizes_inconsistent(vi, a))
goto unm_err_out;
vi->i_size = ni->data_size = le64_to_cpu(a->data.non_resident.data_size);
ni->initialized_size = le64_to_cpu(a->data.non_resident.initialized_size);
ni->allocated_size = le64_to_cpu(a->data.non_resident.allocated_size);
@ -1682,6 +1705,8 @@ static int ntfs_read_locked_index_inode(struct inode *base_vi, struct inode *vi)
"First extent of $INDEX_ALLOCATION attribute has non zero lowest_vcn.");
goto unm_err_out;
}
if (ntfs_non_resident_sizes_inconsistent(vi, a))
goto unm_err_out;
vi->i_size = ni->data_size = le64_to_cpu(a->data.non_resident.data_size);
ni->initialized_size = le64_to_cpu(a->data.non_resident.initialized_size);
ni->allocated_size = le64_to_cpu(a->data.non_resident.allocated_size);
@ -2015,6 +2040,8 @@ int ntfs_read_inode_mount(struct inode *vi)
"Attribute list has non zero lowest_vcn. $MFT is corrupt. You should run chkdsk.");
goto put_err_out;
}
if (ntfs_non_resident_sizes_inconsistent(vi, a))
goto put_err_out;
rl = ntfs_mapping_pairs_decompress(vol, a, NULL, &new_rl_count);
if (IS_ERR(rl)) {

View File

@ -811,14 +811,17 @@ enum {
* on XP SP2+.
* @data.non_resident.reserved: 5 bytes for 8-byte alignment.
* @data.non_resident.allocated_size:
* Allocated disk space in bytes.
* For compressed: logical allocated size.
* Allocated size in bytes, a multiple of
* the cluster size. For compressed and
* sparse attributes holes count as
* allocated; the clusters actually in use
* are in compressed_size.
* @data.non_resident.data_size: Logical attribute value size in bytes.
* Can be larger than allocated_size if
* compressed/sparse.
* Never larger than allocated_size, also
* when compressed/sparse.
* @data.non_resident.initialized_size:
* Initialized portion size in bytes.
* Usually equals data_size.
* Usually equals data_size, never larger.
* @data.non_resident.compressed_size:
* Compressed on-disk size in bytes.
* Only present when compressed or sparse.