ntfs: do not map an unmappable runlist fragment as a hole

When the extent mft record holding part of a file's runlist cannot be
read, ntfs_attr_vcn_to_rl() ignores the failed ntfs_map_runlist_nolock()
retry and returns with *lcn == LCN_RL_NOT_MAPPED.  The iomap read path
only rejects lcn < LCN_ENOENT, so it maps the range as a hole and read()
returns zeros with no error.  The first read already does this:
ntfs_attr_map_whole_runlist() keeps the fragments it could read,
readahead drops its error, and the next lookup finds the unmapped tail.

On a file whose runlist is split between its base record (vcn 0-214) and
one extent record (vcn 215-1499), breaking only the extent record's FILE
magic makes the kernel log "Failed to map extent mft record", yet read()
returns 1285 clusters of zeros for vcn 215-1499.  A damaged attribute
list entry, which makes the retry fail with -ENOENT, gives the same
zeros.

Return -ENOMEM if the retry ran out of memory and -EIO otherwise.  Both
callers already handle an ERR_PTR; other negative lcns, including
LCN_ENOENT, are returned as before, and so is LCN_RL_NOT_MAPPED at or
beyond allocated_size, where nothing is mapped: the runlist ends there
with LCN_RL_NOT_MAPPED when only the last extent is mapped, as after a
write into it, and with clusters smaller than a page every read of a
file's last folio looks up such vcns.

Reads of vcn 215-1499 now fail with -EIO.  An intact volume exercised
with buffered, mmap and O_DIRECT I/O, fallocate, truncate, sparse and
compressed files behaves as before.

Fixes: 495e90fa33 ("ntfs: update attrib operations")
Cc: stable@vger.kernel.org
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:30 +08:00 committed by Namjae Jeon
parent 87a548005f
commit 2b451a6565

View File

@ -317,7 +317,7 @@ int ntfs_map_runlist(struct ntfs_inode *ni, s64 vcn)
struct runlist_element *ntfs_attr_vcn_to_rl(struct ntfs_inode *ni, s64 vcn, s64 *lcn)
{
struct runlist_element *rl = ni->runlist.rl;
int err;
int err = 0;
bool is_retry = false;
if (!rl) {
@ -335,12 +335,32 @@ struct runlist_element *ntfs_attr_vcn_to_rl(struct ntfs_inode *ni, s64 vcn, s64
if (*lcn <= LCN_RL_NOT_MAPPED && is_retry == false) {
is_retry = true;
if (!ntfs_map_runlist_nolock(ni, vcn, NULL)) {
err = ntfs_map_runlist_nolock(ni, vcn, NULL);
if (!err) {
rl = ni->runlist.rl;
goto remap_rl;
}
}
/*
* The runlist fragment containing @vcn could not be mapped, e.g.
* because the extent mft record holding it is corrupt. Do not hand
* LCN_RL_NOT_MAPPED back to callers, which would treat it as a hole.
* At or beyond the allocated size nothing is mapped, and the runlist
* ends there with LCN_RL_NOT_MAPPED if only a later extent has been
* mapped, so return that end as it is.
*/
if (*lcn == LCN_RL_NOT_MAPPED) {
unsigned long flags;
s64 allocated_size;
read_lock_irqsave(&ni->size_lock, flags);
allocated_size = ni->allocated_size;
read_unlock_irqrestore(&ni->size_lock, flags);
if ((s64)ntfs_cluster_to_bytes(ni->vol, vcn) < allocated_size)
return ERR_PTR(err == -ENOMEM ? -ENOMEM : -EIO);
}
return rl;
}