[PATCH] ntfs: don't let a fatal signal interrupt metadata reads
From: xkeyC
Date: Sat Nov 07 2026 - 04:42:09 EST
read_mapping_folio() returns -EINTR when the caller has a fatal signal
pending while it waits for the read it submitted. ntfs reads its metadata
through it and treats the failure as a volume error, so killing a task
that waits on a metadata read turns the volume read-only with
errors=remount-ro. A read that is part of an update also stops the update
half way: killing an rm can leave clusters allocated in $Bitmap, an MFT
record in use without a directory entry, or a directory index with
entries that point to freed records.
Add ntfs_read_metadata_folio(), which waits for the read uninterruptibly,
and use it for metadata reads. Mount-time checks, compressed data and
named stream contents keep using read_mapping_folio(). A killed task now
waits for its metadata read to complete, as it does on filesystems that
use the buffer cache.
Closes: https://github.com/namjaejeon/linux-ntfs/issues/98
Fixes: 11ccc9107dc4 ("ntfs: update runlist handling and cluster allocator")
Fixes: 115380f9a2f9 ("ntfs: update mft operations")
Fixes: af0db57d4293 ("ntfs: update inode operations")
Fixes: 495e90fa3348 ("ntfs: update attrib operations")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: xkeyC <xkeyc@xxxxxx>
---
Reproducers and logs: https://github.com/xkeyC/linux-ntfs/tree/eintr-repro
In QEMU (KASAN, lockdep, reads throttled), killing rm, ls -l, fallocate
or dd partway through turned the volume read-only within a few runs
without this patch, and ntfsck found damage after rm. With it, 20 runs of
each stayed read-write and clean. Index inserts were not reached.
Developed with an AI coding assistant (Claude Code), which wrote the
change and the reproducers and ran the tests.
fs/ntfs/aops.c | 58 ++++++++++++++++++++++++++++++++++++++++++++++
fs/ntfs/attrib.c | 2 +-
fs/ntfs/bdev-io.c | 2 +-
fs/ntfs/bitmap.c | 4 ++--
fs/ntfs/dir.c | 4 ++--
fs/ntfs/inode.c | 12 +++++++---
fs/ntfs/lcnalloc.c | 2 +-
fs/ntfs/mft.c | 20 ++++++++--------
fs/ntfs/ntfs.h | 4 ++++
9 files changed, 88 insertions(+), 20 deletions(-)
diff --git a/fs/ntfs/aops.c b/fs/ntfs/aops.c
index ad781674f..767aecf42 100644
--- a/fs/ntfs/aops.c
+++ b/fs/ntfs/aops.c
@@ -7,6 +7,7 @@
* Copyright (c) 2025 LG Electronics Co., Ltd.
*/
+#include <linux/swap.h>
#include <linux/writeback.h>
#include "attrib.h"
@@ -304,6 +305,63 @@ static int ntfs_swap_activate(struct swap_info_struct *sis,
&ntfs_read_iomap_ops);
}
+/**
+ * ntfs_read_metadata_folio - read a metadata folio, ignoring fatal signals
+ * @mapping: address space of the metadata inode or block device
+ * @index: page cache index to read
+ *
+ * Like read_mapping_folio(), but the wait for the read cannot be interrupted,
+ * so a fatal signal cannot fail a metadata read in the middle of an update.
+ * The caller must not hold the lock of the folio at @index.
+ *
+ * Return: the uptodate folio with a reference held, or an ERR_PTR().
+ */
+struct folio *ntfs_read_metadata_folio(struct address_space *mapping,
+ pgoff_t index)
+{
+ struct folio *folio;
+ bool read_failed = false;
+ int err;
+
+ folio = filemap_get_folio(mapping, index);
+ if (!IS_ERR(folio)) {
+ if (folio_test_uptodate(folio))
+ goto out;
+ folio_put(folio);
+ }
+repeat:
+ folio = __filemap_get_folio(mapping, index, FGP_LOCK | FGP_CREAT,
+ mapping_gfp_mask(mapping));
+ if (IS_ERR(folio))
+ return folio;
+ /* $MFT updates clear uptodate under the folio lock, so check it locked. */
+ if (folio_test_uptodate(folio)) {
+ folio_unlock(folio);
+ goto out;
+ }
+ if (read_failed) {
+ folio_unlock(folio);
+ folio_put(folio);
+ return ERR_PTR(-EIO);
+ }
+
+ /* ->read_folio() unlocks the folio when the read completes. */
+ err = mapping->a_ops->read_folio(NULL, folio);
+ if (err) {
+ folio_put(folio);
+ if (err == AOP_TRUNCATED_PAGE)
+ goto repeat;
+ return ERR_PTR(err);
+ }
+ folio_wait_locked(folio);
+ read_failed = !folio_test_uptodate(folio);
+ folio_put(folio);
+ goto repeat;
+out:
+ folio_mark_accessed(folio);
+ return folio;
+}
+
const struct address_space_operations ntfs_aops = {
.read_folio = ntfs_read_folio,
.readahead = ntfs_readahead,
diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index 800e53df5..cf73fa4c3 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -2323,7 +2323,7 @@ int ntfs_attr_set(struct ntfs_inode *ni, s64 ofs, s64 cnt, const u8 val)
index = ofs >> PAGE_SHIFT;
while (cnt) {
- folio = read_mapping_folio(mapping, index, NULL);
+ folio = ntfs_read_metadata_folio(mapping, index);
if (IS_ERR(folio)) {
ret = PTR_ERR(folio);
ntfs_error(VFS_I(ni)->i_sb, "Failed to read a page %lu for attr %#x: %ld",
diff --git a/fs/ntfs/bdev-io.c b/fs/ntfs/bdev-io.c
index 4f27eed3b..afb1d1df3 100644
--- a/fs/ntfs/bdev-io.c
+++ b/fs/ntfs/bdev-io.c
@@ -99,7 +99,7 @@ int ntfs_bdev_write(struct super_block *sb, void *buf, loff_t start, size_t size
for (; idx < idx_end; idx++, from = 0) {
u32 len;
- folio = read_mapping_folio(sb->s_bdev->bd_mapping, idx, NULL);
+ folio = ntfs_read_metadata_folio(sb->s_bdev->bd_mapping, idx);
if (IS_ERR(folio)) {
ntfs_error(sb, "Unable to read %ld page", idx);
return PTR_ERR(folio);
diff --git a/fs/ntfs/bitmap.c b/fs/ntfs/bitmap.c
index af86fb1c1..1002d9c98 100644
--- a/fs/ntfs/bitmap.c
+++ b/fs/ntfs/bitmap.c
@@ -203,7 +203,7 @@ int __ntfs_bitmap_set_bits_in_run(struct inode *vi, const s64 start_bit,
/* Get the page containing the first bit (@start_bit). */
mapping = vi->i_mapping;
- folio = read_mapping_folio(mapping, index, NULL);
+ folio = ntfs_read_metadata_folio(mapping, index);
if (IS_ERR(folio)) {
if (!is_rollback)
ntfs_error(vi->i_sb,
@@ -267,7 +267,7 @@ int __ntfs_bitmap_set_bits_in_run(struct inode *vi, const s64 start_bit,
folio_unlock(folio);
kunmap_local(kaddr);
folio_put(folio);
- folio = read_mapping_folio(mapping, ++index, NULL);
+ folio = ntfs_read_metadata_folio(mapping, ++index);
if (IS_ERR(folio)) {
ntfs_error(vi->i_sb,
"Failed to map subsequent page (error %li), aborting.",
diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c
index 9fb5bb3d3..ce87799a2 100644
--- a/fs/ntfs/dir.c
+++ b/fs/ntfs/dir.c
@@ -327,8 +327,8 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
* of PAGE_SIZE and map the page cache page, reading it from
* disk if necessary.
*/
- folio = read_mapping_folio(ia_mapping, vcn <<
- dir_ni->itype.index.vcn_size_bits >> PAGE_SHIFT, NULL);
+ folio = ntfs_read_metadata_folio(ia_mapping, vcn <<
+ dir_ni->itype.index.vcn_size_bits >> PAGE_SHIFT);
if (IS_ERR(folio)) {
ntfs_error(sb, "Failed to map directory index page, error %ld.",
-PTR_ERR(folio));
diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 62dd5bd5b..9bf7957af 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -3773,8 +3773,14 @@ s64 ntfs_inode_attr_pread(struct inode *vi, s64 pos, s64 count, u8 *buf)
index = pos >> PAGE_SHIFT;
do {
- /* Update @index and get the next folio. */
- folio = read_mapping_folio(mapping, index, NULL);
+ /*
+ * Update @index and get the next folio. The contents of a named
+ * stream are user data, so a fatal signal may still stop that read.
+ */
+ if (ntfs_inode_is_named_stream(ni))
+ folio = read_mapping_folio(mapping, index, NULL);
+ else
+ folio = ntfs_read_metadata_folio(mapping, index);
if (IS_ERR(folio)) {
err = PTR_ERR(folio);
break;
@@ -3926,7 +3932,7 @@ static s64 __ntfs_inode_non_resident_attr_pwrite(struct inode *vi,
break;
}
} else {
- folio = read_mapping_folio(mapping, index, NULL);
+ folio = ntfs_read_metadata_folio(mapping, index);
if (IS_ERR(folio)) {
ret = PTR_ERR(folio);
ntfs_error(vi->i_sb, "Failed to read a page %lu for attr %#x: %ld",
diff --git a/fs/ntfs/lcnalloc.c b/fs/ntfs/lcnalloc.c
index 3ebcac4c2..7e00016f4 100644
--- a/fs/ntfs/lcnalloc.c
+++ b/fs/ntfs/lcnalloc.c
@@ -340,7 +340,7 @@ struct runlist_element *ntfs_cluster_alloc(struct ntfs_volume *vol, const s64 st
if (vol->lcn_empty_bits_per_page[index] == 0)
goto next_bmp_pos;
- folio = read_mapping_folio(mapping, index, NULL);
+ folio = ntfs_read_metadata_folio(mapping, index);
if (IS_ERR(folio)) {
err = PTR_ERR(folio);
ntfs_error(vol->sb, "Failed to map page.");
diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c
index 89288b794..3601f77ee 100644
--- a/fs/ntfs/mft.c
+++ b/fs/ntfs/mft.c
@@ -143,7 +143,7 @@ static inline struct mft_record *map_mft_record_folio(struct ntfs_inode *ni)
}
/* Read, map, and pin the folio. */
- folio = read_mapping_folio(mft_vi->i_mapping, index, NULL);
+ folio = ntfs_read_metadata_folio(mft_vi->i_mapping, index);
if (!IS_ERR(folio)) {
u8 *addr;
@@ -549,8 +549,8 @@ static int ntfs_sync_mft_mirror_unit(struct ntfs_volume *vol,
if (unit->folio_ofs + unit->len > folio_size(source))
return -EIO;
- mirror = read_mapping_folio(vol->mftmirr_ino->i_mapping,
- mirror_file_ofs >> PAGE_SHIFT, NULL);
+ mirror = ntfs_read_metadata_folio(vol->mftmirr_ino->i_mapping,
+ mirror_file_ofs >> PAGE_SHIFT);
if (IS_ERR(mirror))
return PTR_ERR(mirror);
@@ -1023,7 +1023,7 @@ static bool mft_reserved_is_free(struct ntfs_volume *vol,
if (!m)
return false;
- folio = read_mapping_folio(vol->mft_ino->i_mapping, index, NULL);
+ folio = ntfs_read_metadata_folio(vol->mft_ino->i_mapping, index);
if (IS_ERR(folio))
goto free_m;
@@ -1199,8 +1199,8 @@ static s64 mft_bitmap_alloc_free_rec(struct ntfs_volume *vol,
* for a zero bit.
*/
if (size) {
- folio = read_mapping_folio(mftbmp_mapping,
- ofs >> PAGE_SHIFT, NULL);
+ folio = ntfs_read_metadata_folio(mftbmp_mapping,
+ ofs >> PAGE_SHIFT);
if (IS_ERR(folio)) {
ntfs_error(vol->sb, "Failed to read mft bitmap, aborting.");
return PTR_ERR(folio);
@@ -1392,8 +1392,8 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
* to us.
*/
ll = lcn >> 3;
- folio = read_mapping_folio(vol->lcnbmp_ino->i_mapping,
- ll >> PAGE_SHIFT, NULL);
+ folio = ntfs_read_metadata_folio(vol->lcnbmp_ino->i_mapping,
+ ll >> PAGE_SHIFT);
if (IS_ERR(folio)) {
up_write(&mftbmp_ni->runlist.lock);
ntfs_error(vol->sb, "Failed to read from lcn bitmap.");
@@ -2207,7 +2207,7 @@ static int ntfs_mft_record_format(const struct ntfs_volume *vol, const s64 mft_n
}
/* Read, map, and pin the folio containing the mft record. */
- folio = read_mapping_folio(mft_vi->i_mapping, index, NULL);
+ folio = ntfs_read_metadata_folio(mft_vi->i_mapping, index);
if (IS_ERR(folio)) {
ntfs_error(vol->sb, "Failed to map page containing mft record to format 0x%llx.",
(long long)mft_no);
@@ -2725,7 +2725,7 @@ int ntfs_mft_record_alloc(struct ntfs_volume *vol, const int mode,
index = NTFS_MFT_NR_TO_PIDX(vol, bit);
ofs = NTFS_MFT_NR_TO_POFS(vol, bit);
/* Read, map, and pin the folio containing the mft record. */
- folio = read_mapping_folio(vol->mft_ino->i_mapping, index, NULL);
+ folio = ntfs_read_metadata_folio(vol->mft_ino->i_mapping, index);
if (IS_ERR(folio)) {
ntfs_error(vol->sb, "Failed to map page containing allocated mft record 0x%llx.",
bit);
diff --git a/fs/ntfs/ntfs.h b/fs/ntfs/ntfs.h
index fac72d40f..f33df986f 100644
--- a/fs/ntfs/ntfs.h
+++ b/fs/ntfs/ntfs.h
@@ -294,6 +294,10 @@ static inline int ntfs_ffs(int x)
return r;
}
+/* From fs/ntfs/aops.c */
+struct folio *ntfs_read_metadata_folio(struct address_space *mapping,
+ pgoff_t index);
+
/* From fs/ntfs/bdev-io.c */
int ntfs_bdev_read(struct block_device *bdev, char *data, loff_t start, size_t size);
int ntfs_bdev_write(struct super_block *sb, void *buf, loff_t start, size_t size);
--
2.56.0