mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify
@ 2026-10-20  8:43 Baolin Liu
  2026-10-20  8:43 ` [PATCH v4 01/31] ntfs: propagate mapping-pairs update errors from inode writeback Baolin Liu
                   ` (31 more replies)
  0 siblings, 32 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:43 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

Report NTFS metadata corruption, failed recovery and shutdown through
FAN_FS_ERROR. File events identify the affected base file; volume events
cover errors without a usable file reference. Preserve the existing
volume error bit and shutdown policy.

Confirmed metadata corruption uses -EFSCORRUPTED (-EUCLEAN) in both
events and failing NTFS operations. This intentionally changes
userspace-visible corruption errors previously returned as EIO or
obscured as ENOMEM/EINVAL. Genuine I/O, allocation and unsupported-format
errors retain their own errnos. Ordinary EOF and missing names or streams
are not corruption. Partial-I/O and best-effort operations retain their
existing return semantics; an independent recovery failure may produce
an event with a different errno from the initiating operation.
The generic open_by_handle_at() wrapper still converts decode errors
other than ENOMEM to ESTALE.
NFS handle decoding also maps corruption errors to STALE.

Based on ntfs-next commit 19dc08f1f2c5 ("ntfs: support persistent
noatime attribute").

Changes since v3:
- Extend reporting to previously uncovered corruption and recovery
  failures in system metadata, attributes, runlists, directories/indexes,
  MFT records and writeback, LZNT1/WOF, EA/ACLs, named streams, reparse
  points and object IDs. This includes ntfs_ib_read(), nonresident iomap
  and ntfs_lookup_inode_by_name(), the lookup path in this base; the
  cited ntfs_lookup_inode_by_example() is absent.
- Reuse ntfs_inode_base() for attribute and extent inode attribution.
  Use volume reports during eviction; before SB_ACTIVE, record only
  the volume error state.
- Use test_and_set_bit(NV_Shutdown, &vol->flags) so concurrent shutdown
  callers initiate only one shutdown report.
- Preserve specific errors through attribute search contexts, index
  traversal, buffered reads and mount initialization. Keep recoverable
  resource failures distinct from failed recovery after metadata changes.
- Stabilize export parent reconstruction against rename/rmdir and avoid
  classifying unlocked named-stream revalidation races as corruption.
  Preserve directory resume behavior after deleted keys and partial
  reads, including retries after traversal errors.

Series organization:
- 1-4: fix lost writeback errors and cluster-free count handling;
  add reporting helpers and preserve search-context initialization errors
  through the API and all callers.
- 5-7: boot geometry/MFT bootstrap, required system files/volume
  metadata, and logfile validation/emptying in separate patches.
- 8-10: runlist/cluster allocation, iomap/buffered reads, and
  LZNT1/WOF compressed streams in separate patches.
- 11: handle source-record removal failure during attribute moves
  before updating the attribute-list reference.
- 12-14: attribute lookup/validation, record updates/cleanup, and
  attribute-list updates/recovery.
- 15-17: attribute conversion/mapping, resize/relocation recovery,
  and range updates.
- 18: inode metadata and eviction cleanup.
- 19-22: index validation/block I/O, traversal errors/directory
  retries, directory metadata, and index update/recovery failures.
- 23-24: namespace/file recovery and export parent reconstruction.
- 25-27: MFT record lifecycle, bitmap/data growth recovery, and
  MFT writeback in separate patches.
- 28-30: EA/ACLs, named streams, and reparse/ObjectID indexes.
- 31: add the atomic shutdown helper and convert all callers.

Validation:
- The final kernel passed 35 QEMU guest cases covering normal file and
  namespace operations, streams, metadata corruption, directory resume,
  compression/mapping gaps, AttrDef, mount, concurrent shutdown, export
  handles, fsync/remount/readback and filesystem ustat selftests.
- Compression-buffer allocation injection reproduced EIO before the
  fix and ENOMEM afterward. A complete retry succeeded after disabling
  injection, with no filesystem error event; the healthy control passed.
- Six additional ACL guest cases passed with CONFIG_MULTIUSER=y.
  On-disk invalid UID/GID values in access and default ACLs returned
  EUCLEAN and generated EUCLEAN events identifying the affected inode.
  Valid ACLs read back correctly; invalid userspace ACL submissions
  returned EINVAL without error events. These tests used no kernel hooks.
- Eleven additional NFSv3/TCP loopback guest cases passed using kernel
  nfsd, a mounted Linux client and targeted RPC tests. Tracing confirmed
  ntfs_get_parent() calls from nfsd for healthy and corrupt parents,
  with subtree_check enabled and disabled. The tests also covered
  stale handles and 320 rename/rmdir race iterations without false
  corruption reports. Corruption events identified the affected directory.

- Sparse C=2 checked all 31 NTFS translation units, including WOF,
  on both the final tree and its ntfs-next base, without diagnostics.
- Seventeen guest cases passed with KCSAN and lockdep enabled, covering
  normal I/O, namespace/streams, concurrent shutdown, ACLs and NFS races.
  KCSAN selftests passed and both monitors remained active throughout;
  no KCSAN or lockdep diagnostics were printed.
- Eighteen targeted injection cases covered the two resident free-space
  failure exits and six attribute/MFT errno branches, including moved
  versus unmoved records, source-record restoration, ENOMEM controls,
  event attribution and cluster cleanup.
- Twenty-six directory-retry, EA/ACL and iomap/MFT injection cases were
  rerun against the final source tree and passed.

Baolin Liu (11):
  ntfs: preserve cluster-free counts in attribute cleanup
  ntfs: add filesystem metadata error reporting helpers
  ntfs: report runlist and cluster allocation metadata errors
  ntfs: report attribute lookup and validation errors
  ntfs: report attribute list update and recovery failures
  ntfs: report attribute conversion and mapping update failures
  ntfs: report inode metadata and eviction cleanup errors
  ntfs: report MFT record validation and lifecycle errors
  ntfs: report MFT bitmap and data growth recovery failures
  ntfs: report MFT writeback errors to fsnotify
  ntfs: report each shutdown transition once

Dandan Wu (11):
  ntfs: report required system file and volume metadata errors
  ntfs: report logfile metadata and emptying errors
  ntfs: report iomap corruption and preserve buffered read errors
  ntfs: report LZNT1 and WOF compressed stream errors
  ntfs: report attribute resize and relocation recovery failures
  ntfs: report incomplete attribute range updates
  ntfs: report index validation and block I/O errors
  ntfs: preserve index traversal errors and directory retries
  ntfs: report directory lookup and metadata corruption
  ntfs: report index update and recovery failures
  ntfs: report namespace and file recovery errors

Xuewen Wang (9):
  ntfs: propagate mapping-pairs update errors from inode writeback
  ntfs: preserve attribute search context initialization errors
  ntfs: report boot geometry and MFT bootstrap errors
  ntfs: handle source record removal failure during attribute moves
  ntfs: report attribute record update and cleanup failures
  ntfs: serialize and report export parent reconstruction errors
  ntfs: report EA metadata and restoration errors
  ntfs: report named stream metadata and recovery errors
  ntfs: report reparse and object ID metadata errors

 fs/ntfs/aops.c         |  24 +-
 fs/ntfs/attrib.c       | 738 ++++++++++++++++++++++++++++++-----------
 fs/ntfs/attrib.h       |   8 +-
 fs/ntfs/attrlist.c     |  43 ++-
 fs/ntfs/bitmap.c       |   2 +-
 fs/ntfs/compress.c     |  30 +-
 fs/ntfs/dir.c          | 108 ++++--
 fs/ntfs/ea.c           |  84 +++--
 fs/ntfs/file.c         |  33 +-
 fs/ntfs/index.c        | 313 +++++++++++------
 fs/ntfs/inode.c        | 265 ++++++++++-----
 fs/ntfs/inode.h        |  10 +-
 fs/ntfs/iomap.c        |  53 ++-
 fs/ntfs/lcnalloc.c     |  17 +-
 fs/ntfs/logfile.c      |  51 +--
 fs/ntfs/mft.c          | 326 +++++++++++-------
 fs/ntfs/named_stream.c |  55 ++-
 fs/ntfs/namei.c        | 140 ++++++--
 fs/ntfs/object_id.c    |  40 ++-
 fs/ntfs/reparse.c      |  98 ++++--
 fs/ntfs/reparse.h      |   2 +-
 fs/ntfs/runlist.c      |  16 +-
 fs/ntfs/runlist.h      |   1 +
 fs/ntfs/stream.h       |   2 +-
 fs/ntfs/super.c        | 279 +++++++++++-----
 fs/ntfs/volume.h       |   4 +
 fs/ntfs/wof.c          |  41 ++-
 27 files changed, 1970 insertions(+), 813 deletions(-)


base-commit: 19dc08f1f2c5563ba4da6afd5ecace8eba163aef
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 01/31] ntfs: propagate mapping-pairs update errors from inode writeback
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
@ 2026-10-20  8:43 ` Baolin Liu
  2026-10-20  8:43 ` [PATCH v4 02/31] ntfs: preserve cluster-free counts in attribute cleanup Baolin Liu
                   ` (30 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:43 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Xuewen Wang <wangxuewen@kylinos.cn>

__ntfs_write_inode() overwrites a failed mapping-pairs update with the
result of syncing standard information. fsync() can then report success
while the mapping pairs remain dirty.

Check the update result after releasing the runlist lock and take the
existing error path. Preserve the errno and retry handling for ENOMEM.

Fixes: af0db57d4293 ("ntfs: update inode operations")
Signed-off-by: Xuewen Wang <wangxuewen@kylinos.cn>
---
Testing:
Fault injection reproduced successful fsync() returns for ENOMEM and
EIO before the fix. Both errors reach userspace afterward; disabling
injection allows fsync() to succeed, with size and data preserved across
remount.

 fs/ntfs/inode.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 206db4e99ef1..52e22022b4e7 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -2940,6 +2940,8 @@ int __ntfs_write_inode(struct inode *vi, int sync)
 		if (!err)
 			NInoClearRunlistDirty(ni);
 		up_write(&ni->runlist.lock);
+		if (err)
+			goto unm_err_out;
 	}
 
 	err = ntfs_inode_sync_standard_information(vi, m);
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 02/31] ntfs: preserve cluster-free counts in attribute cleanup
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
  2026-10-20  8:43 ` [PATCH v4 01/31] ntfs: propagate mapping-pairs update errors from inode writeback Baolin Liu
@ 2026-10-20  8:43 ` Baolin Liu
  2026-10-20  8:43 ` [PATCH v4 03/31] ntfs: add filesystem metadata error reporting helpers Baolin Liu
                   ` (29 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:43 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

ntfs_cluster_free() returns a signed 64-bit cluster count on success,
not an integer status. Several attribute cleanup paths truncate that
count or treat a positive count as an error. ntfs_attr_rm() can even
return the count to callers that expect zero on success.

Store the result in s64 variables, test negative results for failure,
and compare the full count when validating an allocation rollback.
Return zero from ntfs_attr_rm() after successful removal. This keeps
successful deallocation from being misclassified by later reporting.

Fixes: 495e90fa3348 ("ntfs: update attrib operations")
Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
 fs/ntfs/attrib.c | 27 +++++++++++++++------------
 1 file changed, 15 insertions(+), 12 deletions(-)

diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index fce7144d7bef..0be410973176 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -4164,7 +4164,7 @@ static int ntfs_attr_make_resident(struct ntfs_inode *ni, struct ntfs_attr_searc
 	struct super_block *sb = vol->sb;
 	struct attr_record *a = ctx->attr;
 	int name_ofs, val_ofs, err;
-	s64 arec_size;
+	s64 arec_size, freed;
 
 	ntfs_debug("Entering for inode 0x%llx, attr 0x%x.\n",
 			(unsigned long long)ni->mft_no, ni->type);
@@ -4252,8 +4252,8 @@ static int ntfs_attr_make_resident(struct ntfs_inode *ni, struct ntfs_attr_searc
 	 * the whole run list and thus it doesn't matter that the attribute
 	 * record is in a transiently corrupted state at this moment in time.
 	 */
-	err = ntfs_cluster_free(ni, 0, -1, ctx);
-	if (err) {
+	freed = ntfs_cluster_free(ni, 0, -1, ctx);
+	if (freed < 0) {
 		ntfs_error(sb, "Eeek! Failed to release allocated clusters");
 		ntfs_debug("Ignoring error and leaving behind wasted clusters.\n");
 	}
@@ -4496,6 +4496,7 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz
 	struct ntfs_attr_search_ctx *ctx = NULL;
 	struct runlist_element *rl, *rln;
 	s64 org_alloc_size, org_compressed_size;
+	s64 freed;
 	int err, err2;
 	struct ntfs_inode *base_ni;
 	struct super_block *sb = ni->vol->sb;
@@ -4760,9 +4761,9 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz
 		down_write(&ni->runlist.lock);
 rollback_locked:
 	/* Free allocated clusters. */
-	err2 = ntfs_cluster_free(ni, ntfs_bytes_to_cluster(vol, org_alloc_size),
-				-1, ctx);
-	if (err2)
+	freed = ntfs_cluster_free(ni, ntfs_bytes_to_cluster(vol, org_alloc_size),
+				  -1, ctx);
+	if (freed < 0)
 		ntfs_debug("Leaking clusters");
 
 	/*
@@ -5229,6 +5230,7 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 	struct runlist_element *rl, *rlc;
 	struct runlist_element *old_rl = NULL;
 	s64 vcn = vcn_start, lcn, clu_count;
+	s64 freed;
 	s64 lcn_seek_from = -1;
 	int err = 0;
 	size_t new_rl_count, old_rl_count;
@@ -5359,10 +5361,8 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 		ntfs_attr_reinit_search_ctx(ctx);
 		err = ntfs_attr_update_mapping_pairs_locked(ni, 0, ni);
 		if (err) {
-			int err2;
-
-			err2 = ntfs_cluster_free(ni, vcn, clu_count, ctx);
-			if (err2 < 0 || err2 != clu_count) {
+			freed = ntfs_cluster_free(ni, vcn, clu_count, ctx);
+			if (freed < 0 || freed != clu_count) {
 				ntfs_error(vol->sb,
 					   "Failed to free cluster allocation. Leaving inconsistent metadata.\n");
 				NVolSetErrors(vol);
@@ -5410,6 +5410,7 @@ int ntfs_attr_rm(struct ntfs_inode *ni)
 {
 	struct ntfs_attr_search_ctx *ctx;
 	int err = 0, ret = 0;
+	s64 freed = 0;
 	struct ntfs_inode *base_ni;
 	struct super_block *sb = ni->vol->sb;
 
@@ -5431,10 +5432,12 @@ int ntfs_attr_rm(struct ntfs_inode *ni)
 			return -ENOMEM;
 		}
 
-		ret = ntfs_cluster_free(ni, 0, -1, ctx);
-		if (ret < 0)
+		freed = ntfs_cluster_free(ni, 0, -1, ctx);
+		if (freed < 0) {
+			ret = freed;
 			ntfs_error(sb,
 				"Failed to free cluster allocation. Leaving inconstant metadata.\n");
+		}
 		ntfs_attr_put_search_ctx(ctx);
 	}
 
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 03/31] ntfs: add filesystem metadata error reporting helpers
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
  2026-10-20  8:43 ` [PATCH v4 01/31] ntfs: propagate mapping-pairs update errors from inode writeback Baolin Liu
  2026-10-20  8:43 ` [PATCH v4 02/31] ntfs: preserve cluster-free counts in attribute cleanup Baolin Liu
@ 2026-10-20  8:43 ` Baolin Liu
  2026-10-20  8:43 ` [PATCH v4 04/31] ntfs: preserve attribute search context initialization errors Baolin Liu
                   ` (28 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:43 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

Add file and volume helpers to report metadata errors through the generic
deferred fsnotify infrastructure while retaining the volume error bit.
Pass the caller's errno unchanged.

Move ntfs_inode_base() to inode.h and use it to identify the base file for
attribute and extent inodes. Fall back to a volume report for I_FREEING
or I_WILL_FREE, when retaining the inode for deferred reporting is unsafe.
When SB_ACTIVE is clear, record only the volume error state.

Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
 fs/ntfs/inode.c  |  7 -------
 fs/ntfs/inode.h  |  8 ++++++++
 fs/ntfs/super.c  | 29 +++++++++++++++++++++++++++++
 fs/ntfs/volume.h |  3 +++
 4 files changed, 40 insertions(+), 7 deletions(-)

diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 52e22022b4e7..c8560dd47d44 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -3532,13 +3532,6 @@ void ntfs_destroy_ext_inode(struct ntfs_inode *ni)
 	kmem_cache_free(ntfs_inode_cache, ni);
 }
 
-static struct ntfs_inode *ntfs_inode_base(struct ntfs_inode *ni)
-{
-	if (ni->nr_extents == -1)
-		return ni->ext.base_ntfs_ino;
-	return ni;
-}
-
 static int ntfs_attr_position(__le32 type, struct ntfs_attr_search_ctx *ctx)
 {
 	int err;
diff --git a/fs/ntfs/inode.h b/fs/ntfs/inode.h
index cf447fb51725..952398753ed1 100644
--- a/fs/ntfs/inode.h
+++ b/fs/ntfs/inode.h
@@ -298,6 +298,14 @@ static inline bool ntfs_inode_is_named_stream(struct ntfs_inode *ni)
 	return NInoAttr(ni) && ni->type == AT_DATA && ni->name_len;
 }
 
+/* Extent and attached attribute inodes refer to their base MFT inode. */
+static inline struct ntfs_inode *ntfs_inode_base(struct ntfs_inode *ni)
+{
+	if (ni->nr_extents == -1)
+		return ni->ext.base_ntfs_ino;
+	return ni;
+}
+
 /* Return the base MFT inode for an attribute inode, or @ni itself. */
 static inline struct ntfs_inode *ntfs_base_inode(struct ntfs_inode *ni)
 {
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index 71f0aea46bf8..df09c277ee54 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -14,6 +14,7 @@
 #include <linux/sched/mm.h>
 #include <linux/fs_context.h>
 #include <linux/fs_parser.h>
+#include <linux/fserror.h>
 
 #include "sysctl.h"
 #include "logfile.h"
@@ -426,6 +427,34 @@ const struct option_t on_errors_arr[] = {
 	{ 0,			NULL }
 };
 
+/*
+ * Report confirmed metadata corruption as -EFSCORRUPTED, matching the errno
+ * returned to the caller. Preserve the errno of I/O and recovery failures
+ * so listeners can distinguish their causes.
+ */
+void ntfs_report_metadata_error(struct ntfs_volume *vol, int error)
+{
+	NVolSetErrors(vol);
+	if (vol->sb->s_flags & SB_ACTIVE)
+		fserror_report_metadata(vol->sb, error, GFP_ATOMIC);
+}
+
+void ntfs_report_file_metadata_error(struct inode *inode, int error)
+{
+	struct ntfs_inode *ni = NTFS_I(inode);
+
+	/* Attribute inodes share the file handle of their base inode. */
+	inode = VFS_I(ntfs_inode_base(ni));
+	/* Eviction cannot retain an inode reference for the deferred report. */
+	if (inode_state_read_once(inode) & (I_FREEING | I_WILL_FREE)) {
+		ntfs_report_metadata_error(NTFS_SB(inode->i_sb), error);
+		return;
+	}
+	NVolSetErrors(NTFS_SB(inode->i_sb));
+	if (inode->i_sb->s_flags & SB_ACTIVE)
+		fserror_report_file_metadata(inode, error, GFP_ATOMIC);
+}
+
 void ntfs_handle_error(struct super_block *sb)
 {
 	struct ntfs_volume *vol = NTFS_SB(sb);
diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h
index ed5c0ccb9cd9..27f58beb4b2d 100644
--- a/fs/ntfs/volume.h
+++ b/fs/ntfs/volume.h
@@ -262,6 +262,9 @@ DEFINE_NVOL_BIT_OPS(SymlinkNative)
 DEFINE_NVOL_BIT_OPS(MftBootstrap)
 DEFINE_NVOL_BIT_OPS(StreamsWindows)
 
+void ntfs_report_metadata_error(struct ntfs_volume *vol, int error);
+void ntfs_report_file_metadata_error(struct inode *inode, int error);
+
 static inline void ntfs_inc_free_clusters(struct ntfs_volume *vol, s64 nr)
 {
 	if (!NVolFreeClusterKnown(vol))
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 04/31] ntfs: preserve attribute search context initialization errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (2 preceding siblings ...)
  2026-10-20  8:43 ` [PATCH v4 03/31] ntfs: add filesystem metadata error reporting helpers Baolin Liu
@ 2026-10-20  8:43 ` Baolin Liu
  2026-10-27  5:46   ` Hyunchul Lee
  2026-10-20  8:43 ` [PATCH v4 05/31] ntfs: report boot geometry and MFT bootstrap errors Baolin Liu
                   ` (27 subsequent siblings)
  31 siblings, 1 reply; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:43 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Xuewen Wang <wangxuewen@kylinos.cn>

ntfs_attr_get_search_ctx() returns NULL for both allocation and MFT
mapping failures, causing callers to misreport mapping errors as ENOMEM.

Return error pointers carrying the original errno. Convert all callers
and the documented usage, clearing pointers before shared cleanup where
needed. Preserve deliberate boolean and best-effort behavior while making
those callers recognize error pointers.

Signed-off-by: Xuewen Wang <wangxuewen@kylinos.cn>
---
 fs/ntfs/attrib.c       | 110 ++++++++++++++++++++++-------------------
 fs/ntfs/attrib.h       |   4 +-
 fs/ntfs/attrlist.c     |   5 +-
 fs/ntfs/dir.c          |  18 ++++---
 fs/ntfs/ea.c           |  10 ++--
 fs/ntfs/file.c         |   4 +-
 fs/ntfs/index.c        |   3 +-
 fs/ntfs/inode.c        |  51 ++++++++++---------
 fs/ntfs/iomap.c        |  10 ++--
 fs/ntfs/mft.c          |  23 +++++----
 fs/ntfs/named_stream.c |  18 ++++---
 fs/ntfs/namei.c        |  13 ++---
 fs/ntfs/super.c        |  14 ++++--
 fs/ntfs/wof.c          |  10 ++--
 14 files changed, 164 insertions(+), 129 deletions(-)

diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index 0be410973176..28099ed6ad99 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -120,8 +120,9 @@ int ntfs_map_runlist_nolock(struct ntfs_inode *ni, s64 vcn, struct ntfs_attr_sea
 		if (IS_ERR(m))
 			return PTR_ERR(m);
 		ctx = ntfs_attr_get_search_ctx(base_ni, m);
-		if (unlikely(!ctx)) {
-			err = -ENOMEM;
+		if (IS_ERR(ctx)) {
+			err = PTR_ERR(ctx);
+			ctx = NULL;
 			goto err_out;
 		}
 	} else {
@@ -1654,13 +1655,13 @@ int ntfs_attr_lookup(const __le32 type, const __le16 *name,
  *
  * Initialize the attribute search context @ctx with @ni and @mrec.
  */
-static bool ntfs_attr_init_search_ctx(struct ntfs_attr_search_ctx *ctx,
-		struct ntfs_inode *ni, struct mft_record *mrec)
+static int ntfs_attr_init_search_ctx(struct ntfs_attr_search_ctx *ctx,
+				     struct ntfs_inode *ni, struct mft_record *mrec)
 {
 	if (!mrec) {
 		mrec = map_mft_record(ni);
 		if (IS_ERR(mrec))
-			return false;
+			return PTR_ERR(mrec);
 		ctx->mapped_mrec = true;
 	} else {
 		ctx->mapped_mrec = false;
@@ -1676,7 +1677,7 @@ static bool ntfs_attr_init_search_ctx(struct ntfs_attr_search_ctx *ctx,
 	ctx->base_mrec = NULL;
 	ctx->base_attr = NULL;
 	ctx->mapped_base_mrec = false;
-	return true;
+	return 0;
 }
 
 /*
@@ -1720,23 +1721,22 @@ void ntfs_attr_reinit_search_ctx(struct ntfs_attr_search_ctx *ctx)
  * @mrec:	mft record with which to initialize the search context
  *
  * Allocate a new attribute search context, initialize it with @ni and @mrec,
- * and return it. Return NULL if allocation failed.
+ * and return it. Return ERR_PTR(-errno) on allocation or mapping failure.
  */
 struct ntfs_attr_search_ctx *ntfs_attr_get_search_ctx(struct ntfs_inode *ni,
 		struct mft_record *mrec)
 {
 	struct ntfs_attr_search_ctx *ctx;
-	bool init;
+	int err;
 
 	ctx = kmem_cache_alloc(ntfs_attr_ctx_cache, GFP_NOFS);
-	if (ctx) {
-		init = ntfs_attr_init_search_ctx(ctx, ni, mrec);
-		if (init == false) {
-			kmem_cache_free(ntfs_attr_ctx_cache, ctx);
-			ctx = NULL;
-		}
+	if (!ctx)
+		return ERR_PTR(-ENOMEM);
+	err = ntfs_attr_init_search_ctx(ctx, ni, mrec);
+	if (err) {
+		kmem_cache_free(ntfs_attr_ctx_cache, ctx);
+		return ERR_PTR(err);
 	}
-
 	return ctx;
 }
 
@@ -2019,8 +2019,9 @@ int ntfs_attr_make_non_resident(struct ntfs_inode *ni, const u32 data_size)
 		goto err_out;
 	}
 	ctx = ntfs_attr_get_search_ctx(base_ni, m);
-	if (unlikely(!ctx)) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto err_out;
 	}
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
@@ -2359,8 +2360,8 @@ int ntfs_attr_set_initialized_size(struct ntfs_inode *ni, loff_t new_size)
 		return -EINVAL;
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx)
-		return -ENOMEM;
+	if (IS_ERR(ctx))
+		return PTR_ERR(ctx);
 
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 			       CASE_SENSITIVE, 0, NULL, 0, ctx);
@@ -2460,10 +2461,10 @@ int ntfs_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 
 	/* Locate place where record should be. */
 	ctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(ni->vol->sb, "%s: Failed to get search context",
 				__func__);
-		return -ENOMEM;
+		return PTR_ERR(ctx);
 	}
 	/*
 	 * Use ntfs_attr_find instead of ntfs_attr_lookup to find place for
@@ -2584,9 +2585,9 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 
 	/* Locate place where record should be. */
 	ctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		pr_err("%s: Failed to get search context\n", __func__);
-		return -ENOMEM;
+		return PTR_ERR(ctx);
 	}
 	/*
 	 * Use ntfs_attr_find instead of ntfs_attr_lookup to find place for
@@ -3158,9 +3159,10 @@ int ntfs_attr_open(struct ntfs_inode *ni, const __le32 type,
 	}
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
 		pr_err("%s: Failed to get search context\n", __func__);
+		ctx = NULL;
 		goto err_out;
 	}
 
@@ -3334,9 +3336,9 @@ int ntfs_attr_map_whole_runlist(struct ntfs_inode *ni)
 		base_ni = ni;
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(sb, "%s: Failed to get search context", __func__);
-		return -ENOMEM;
+		return PTR_ERR(ctx);
 	}
 
 	/* Map all attribute extents one by one. */
@@ -3462,9 +3464,9 @@ int ntfs_attr_record_move_to(struct ntfs_attr_search_ctx *ctx, struct ntfs_inode
 	/* Find place in MFT record where attribute will be moved. */
 	a = ctx->attr;
 	nctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!nctx) {
+	if (IS_ERR(nctx)) {
 		ntfs_error(sb, "%s: Failed to get search context", __func__);
-		return -ENOMEM;
+		return PTR_ERR(nctx);
 	}
 
 	/*
@@ -3798,9 +3800,9 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 		base_ni = ni;
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(sb, "%s: Failed to get search context", __func__);
-		return -ENOMEM;
+		return PTR_ERR(ctx);
 	}
 
 	/* Fill attribute records with new mapping pairs. */
@@ -4356,9 +4358,10 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni,
 		}
 
 		ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-		if (!ctx) {
+		if (IS_ERR(ctx)) {
 			ntfs_error(vol->sb, "%s: Failed to get search context", __func__);
-			err = -ENOMEM;
+			err = PTR_ERR(ctx);
+			ctx = NULL;
 			goto unlock_runlist;
 		}
 
@@ -4409,9 +4412,9 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni,
 
 	/* Get the first attribute record. */
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(vol->sb, "%s: Failed to get search context", __func__);
-		return -ENOMEM;
+		return PTR_ERR(ctx);
 	}
 
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
@@ -4728,10 +4731,11 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz
 	}
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
 		if (ni->allocated_size == org_alloc_size)
 			return err;
+		ctx = NULL;
 		goto rollback;
 	}
 
@@ -4842,9 +4846,9 @@ static int ntfs_resident_attr_resize(struct ntfs_inode *attr_ni, const s64 newsi
 
 	/* Get the attribute record that needs modification. */
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(sb, "%s: Failed to get search context", __func__);
-		return -ENOMEM;
+		return PTR_ERR(ctx);
 	}
 
 	err = ntfs_attr_lookup(attr_ni->type, attr_ni->name, attr_ni->name_len,
@@ -5240,9 +5244,9 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 		return err;
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(vol->sb, "%s: Failed to get search context", __func__);
-		return -ENOMEM;
+		return PTR_ERR(ctx);
 	}
 
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
@@ -5427,9 +5431,9 @@ int ntfs_attr_rm(struct ntfs_inode *ni)
 		if (err)
 			return err;
 		ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-		if (!ctx) {
+		if (IS_ERR(ctx)) {
 			ntfs_error(sb, "%s: Failed to get search context", __func__);
-			return -ENOMEM;
+			return PTR_ERR(ctx);
 		}
 
 		freed = ntfs_cluster_free(ni, 0, -1, ctx);
@@ -5443,9 +5447,9 @@ int ntfs_attr_rm(struct ntfs_inode *ni)
 
 	/* Search for attribute extents and remove them all. */
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(sb, "%s: Failed to get search context", __func__);
-		return -ENOMEM;
+		return PTR_ERR(ctx);
 	}
 	while (!(err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 				CASE_SENSITIVE, 0, NULL, 0, ctx))) {
@@ -5476,7 +5480,7 @@ int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name,
 	ntfs_debug("Entering\n");
 
 	ctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(ni->vol->sb, "%s: Failed to get search context",
 				__func__);
 		return 0;
@@ -5661,8 +5665,8 @@ int ntfs_non_resident_attr_insert_range(struct ntfs_inode *ni, s64 start_vcn, s6
 		return ret;
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
-		ret = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		ret = PTR_ERR(ctx);
 		return ret;
 	}
 
@@ -5750,8 +5754,9 @@ int ntfs_non_resident_attr_collapse_range(struct ntfs_inode *ni, s64 start_vcn,
 	up_write(&ni->runlist.lock);
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
-		ret = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		ret = PTR_ERR(ctx);
+		ctx = NULL;
 		goto out_rl;
 	}
 
@@ -5869,8 +5874,9 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
 	}
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, mrec);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto out_unmap;
 	}
 
diff --git a/fs/ntfs/attrib.h b/fs/ntfs/attrib.h
index bee91c9f6f81..9d6bf0320219 100644
--- a/fs/ntfs/attrib.h
+++ b/fs/ntfs/attrib.h
@@ -158,8 +158,8 @@ struct runlist_element *ntfs_attr_vcn_to_rl(struct ntfs_inode *ni, s64 vcn, s64
  *
  *	int err;
  *	struct ntfs_attr_search_ctx *ctx = ntfs_attr_get_search_ctx(ni, NULL);
- *	if (!ctx)
- *		// Error code is in errno. Handle this case.
+ *	if (IS_ERR(ctx))
+ *		// PTR_ERR(ctx) is the error code. Handle this case.
  *	while (!(err = ntfs_attrs_walk(ctx))) {
  *		struct attr_record *attr = ctx->attr;
  *		// attr now contains the next attribute. Do whatever you want
diff --git a/fs/ntfs/attrlist.c b/fs/ntfs/attrlist.c
index 8291c4c95b37..7e7440c5f2ba 100644
--- a/fs/ntfs/attrlist.c
+++ b/fs/ntfs/attrlist.c
@@ -362,9 +362,10 @@ int ntfs_attrlist_entry_add(struct ntfs_inode *ni, struct attr_record *attr)
 
 	/* Find place for the new entry. */
 	ctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
 		ntfs_error(ni->vol->sb, "Failed to get search context");
+		ctx = NULL;
 		goto err_out;
 	}
 	if (attr->non_resident)
diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c
index fce10baa2b93..afee30f8b501 100644
--- a/fs/ntfs/dir.c
+++ b/fs/ntfs/dir.c
@@ -100,8 +100,9 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 		return ERR_MREF(PTR_ERR(m));
 	}
 	ctx = ntfs_attr_get_search_ctx(dir_ni, m);
-	if (unlikely(!ctx)) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto err_out;
 	}
 	/* Find the index root attribute in the mft record. */
@@ -832,8 +833,9 @@ static int ntfs_readdir(struct file *file, struct dir_context *actor)
 	}
 
 	ctx = ntfs_attr_get_search_ctx(ndir, NULL);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto out;
 	}
 
@@ -1034,9 +1036,9 @@ int ntfs_check_empty_dir(struct ntfs_inode *ni, struct mft_record *ni_mrec)
 		return 0;
 
 	ctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(ni->vol->sb, "Failed to get search context");
-		return -ENOMEM;
+		return PTR_ERR(ctx);
 	}
 
 	/* Find the index root attribute in the mft record. */
@@ -1135,8 +1137,8 @@ static int ntfs_dir_fsync(struct file *filp, loff_t start, loff_t end,
 		return -EIO;
 
 	ctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!ctx)
-		return -ENOMEM;
+	if (IS_ERR(ctx))
+		return PTR_ERR(ctx);
 
 	mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL_CHILD);
 	while (!(err = ntfs_attr_lookup(AT_FILE_NAME, NULL, 0, 0, 0, NULL, 0, ctx))) {
diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index 199a602348d4..1198f3059c5d 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -192,8 +192,9 @@ static int ntfs_ea_attr_lookup(struct ntfs_inode *ni, __le32 type)
 	if (IS_ERR(mrec))
 		return PTR_ERR(mrec);
 	ctx = ntfs_attr_get_search_ctx(ni, mrec);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto out;
 	}
 	err = ntfs_attr_lookup(type, AT_UNNAMED, 0, CASE_SENSITIVE,
@@ -684,8 +685,9 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr)
 		return PTR_ERR(m);
 
 	ctx = ntfs_attr_get_search_ctx(ni, m);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto err_out;
 	}
 
diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index d810ca5904bd..cd5ed8bf3164 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -331,8 +331,8 @@ int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end,
 		goto sync_volume;
 
 	ctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!ctx)
-		return -ENOMEM;
+	if (IS_ERR(ctx))
+		return PTR_ERR(ctx);
 
 	mutex_lock_nested(&ni->mrec_lock, NTFS_INODE_MUTEX_NORMAL_CHILD);
 	while (!(err = ntfs_attr_lookup(AT_UNUSED, NULL, 0, 0, 0, NULL, 0, ctx))) {
diff --git a/fs/ntfs/index.c b/fs/ntfs/index.c
index ac22d8407a77..afb08f0aa596 100644
--- a/fs/ntfs/index.c
+++ b/fs/ntfs/index.c
@@ -579,8 +579,9 @@ static struct index_root *ntfs_ir_lookup(struct ntfs_inode *ni, __le16 *name,
 
 	ntfs_debug("Entering\n");
 	*ctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!*ctx) {
+	if (IS_ERR(*ctx)) {
 		ntfs_error(ni->vol->sb, "%s, Failed to get search context", __func__);
+		*ctx = NULL;
 		return NULL;
 	}
 
diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index c8560dd47d44..5b3d1453d5e1 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -372,8 +372,8 @@ static int ntfs_non_resident_dealloc_clusters(struct ntfs_inode *ni)
 	int err = 0;
 
 	actx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!actx)
-		return -ENOMEM;
+	if (IS_ERR(actx))
+		return PTR_ERR(actx);
 	WARN_ON(actx->mrec->link_count != 0);
 
 	/**
@@ -779,8 +779,9 @@ static int ntfs_read_locked_inode(struct inode *vi)
 	}
 
 	ctx = ntfs_attr_get_search_ctx(ni, m);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto unm_err_out;
 	}
 
@@ -1390,8 +1391,9 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi)
 		goto err_out;
 	}
 	ctx = ntfs_attr_get_search_ctx(base_ni, m);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto unm_err_out;
 	}
 	/* Find the attribute. */
@@ -1655,8 +1657,9 @@ static int ntfs_read_locked_index_inode(struct inode *base_vi, struct inode *vi)
 		goto err_out;
 	}
 	ctx = ntfs_attr_get_search_ctx(base_ni, m);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto unm_err_out;
 	}
 	/* Find the index root attribute. */
@@ -2046,8 +2049,9 @@ int ntfs_read_inode_mount(struct inode *vi)
 	vi->i_mapping->a_ops = &ntfs_mft_aops;
 
 	ctx = ntfs_attr_get_search_ctx(ni, m);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto err_out;
 	}
 
@@ -2643,8 +2647,8 @@ static int ntfs_inode_sync_standard_information(struct inode *vi, struct mft_rec
 
 	/* Update the access times in the standard information attribute. */
 	ctx = ntfs_attr_get_search_ctx(ni, m);
-	if (unlikely(!ctx))
-		return -ENOMEM;
+	if (IS_ERR(ctx))
+		return PTR_ERR(ctx);
 	err = ntfs_attr_lookup(AT_STANDARD_INFORMATION, NULL, 0,
 			CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (unlikely(err)) {
@@ -2744,8 +2748,8 @@ int ntfs_inode_sync_filename(struct ntfs_inode *ni)
 	ntfs_debug("Entering for inode %llu\n", ni->mft_no);
 
 	ctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!ctx)
-		return -ENOMEM;
+	if (IS_ERR(ctx))
+		return PTR_ERR(ctx);
 
 	/* Collect the reparse tag, if any */
 	reparse_tag = cpu_to_le32(0);
@@ -3233,8 +3237,9 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
 
 	/* Form attribute list. */
 	ctx = ntfs_attr_get_search_ctx(ni, ni_mrec);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto err_out;
 	}
 
@@ -3598,9 +3603,9 @@ int ntfs_inode_free_space(struct ntfs_inode *ni, int size)
 		return 0;
 
 	ctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(sb, "%s, Failed to get search context", __func__);
-		return -ENOMEM;
+		return PTR_ERR(ctx);
 	}
 
 	/*
@@ -3722,10 +3727,11 @@ s64 ntfs_inode_attr_pread(struct inode *vi, s64 pos, s64 count, u8 *buf)
 		u8 *attr;
 
 		ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-		if (!ctx) {
+		if (IS_ERR(ctx)) {
 			ntfs_error(vi->i_sb, "Failed to get attr search ctx");
-			err = -ENOMEM;
+			err = PTR_ERR(ctx);
 			mutex_unlock(&mrec_ni->mrec_lock);
+			ctx = NULL;
 			goto out;
 		}
 
@@ -4047,9 +4053,10 @@ s64 ntfs_inode_attr_pwrite(struct inode *vi, s64 pos, s64 count, u8 *buf, bool s
 	}
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(vi->i_sb, "Failed to get attr search ctx");
-		ret = -ENOMEM;
+		ret = PTR_ERR(ctx);
+		ctx = NULL;
 		goto out_unlock;
 	}
 
diff --git a/fs/ntfs/iomap.c b/fs/ntfs/iomap.c
index 2940a5587add..c248c61854cc 100644
--- a/fs/ntfs/iomap.c
+++ b/fs/ntfs/iomap.c
@@ -68,8 +68,9 @@ static int ntfs_read_iomap_begin_resident(struct inode *inode, loff_t offset, lo
 	mutex_lock(&base_ni->mrec_lock);
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto out;
 	}
 
@@ -688,8 +689,9 @@ static int ntfs_write_iomap_begin_resident(struct inode *inode, loff_t offset,
 	char *kattr;
 
 	ctx = ntfs_attr_get_search_ctx(mrec_ni, NULL);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto out;
 	}
 
diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c
index 89288b794348..9b14e69c9c6f 100644
--- a/fs/ntfs/mft.c
+++ b/fs/ntfs/mft.c
@@ -1473,9 +1473,10 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
 		goto undo_alloc;
 	}
 	ctx = ntfs_attr_get_search_ctx(mft_ni, mrec);
-	if (unlikely(!ctx)) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(vol->sb, "Failed to get search context.");
-		ret = -ENOMEM;
+		ret = PTR_ERR(ctx);
+		ctx = NULL;
 		goto undo_alloc;
 	}
 	ret = ntfs_attr_lookup(mftbmp_ni->type, mftbmp_ni->name,
@@ -1673,9 +1674,10 @@ static int ntfs_mft_bitmap_extend_initialized_nolock(struct ntfs_volume *vol)
 		return PTR_ERR(mrec);
 	}
 	ctx = ntfs_attr_get_search_ctx(mft_ni, mrec);
-	if (unlikely(!ctx)) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(vol->sb, "Failed to get search context.");
-		ret = -ENOMEM;
+		ret = PTR_ERR(ctx);
+		ctx = NULL;
 		goto unm_err_out;
 	}
 	ret = ntfs_attr_lookup(mftbmp_ni->type, mftbmp_ni->name,
@@ -1724,9 +1726,10 @@ static int ntfs_mft_bitmap_extend_initialized_nolock(struct ntfs_volume *vol)
 		return ret;
 	}
 	ctx = ntfs_attr_get_search_ctx(mft_ni, mrec);
-	if (unlikely(!ctx)) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(vol->sb, "Failed to get search context.%s", es);
 		NVolSetErrors(vol);
+		ctx = NULL;
 		goto unm_err_out;
 	}
 	if (ntfs_attr_lookup(mftbmp_ni->type, mftbmp_ni->name,
@@ -1912,9 +1915,10 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 		goto undo_alloc;
 	}
 	ctx = ntfs_attr_get_search_ctx(mft_ni, mrec);
-	if (unlikely(!ctx)) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(vol->sb, "Failed to get search context.");
-		ret = -ENOMEM;
+		ret = PTR_ERR(ctx);
+		ctx = NULL;
 		goto undo_alloc;
 	}
 	ret = ntfs_attr_lookup(mft_ni->type, mft_ni->name, mft_ni->name_len,
@@ -2663,10 +2667,11 @@ int ntfs_mft_record_alloc(struct ntfs_volume *vol, const int mode,
 		goto undo_data_init;
 	}
 	ctx = ntfs_attr_get_search_ctx(mft_ni, m);
-	if (unlikely(!ctx)) {
+	if (IS_ERR(ctx)) {
 		ntfs_error(vol->sb, "Failed to get search context.");
-		err = -ENOMEM;
+		err = PTR_ERR(ctx);
 		unmap_mft_record(mft_ni);
+		ctx = NULL;
 		goto undo_data_init;
 	}
 	err = ntfs_attr_lookup(mft_ni->type, mft_ni->name, mft_ni->name_len,
diff --git a/fs/ntfs/named_stream.c b/fs/ntfs/named_stream.c
index e44dbf09057b..6130359f616a 100644
--- a/fs/ntfs/named_stream.c
+++ b/fs/ntfs/named_stream.c
@@ -129,8 +129,8 @@ int ntfs_stream_inode_validate(struct inode *vi)
 		return -ESTALE;
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx)
-		return -ENOMEM;
+	if (IS_ERR(ctx))
+		return PTR_ERR(ctx);
 	err = ntfs_attr_lookup(AT_DATA, ni->name, ni->name_len,
 			CASE_SENSITIVE, 0, NULL, 0, ctx);
 	ntfs_attr_put_search_ctx(ctx);
@@ -600,8 +600,9 @@ struct inode *ntfs_create_named_stream(struct mnt_idmap *idmap,
 		goto out_unlock;
 	}
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto out_unlock;
 	}
 
@@ -1255,8 +1256,9 @@ static int ntfs_stream_ioctl_get_inode(struct inode *base_vi, __le16 *sname,
 	}
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto out_unlock;
 	}
 
@@ -1633,9 +1635,9 @@ int ntfs_ioctl_list_streams(struct file *filp, unsigned long arg)
 
 	mutex_lock(&ni->mrec_lock);
 	actx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!actx) {
+	if (IS_ERR(actx)) {
 		mutex_unlock(&ni->mrec_lock);
-		return -ENOMEM;
+		return PTR_ERR(actx);
 	}
 
 	while ((err = ntfs_attrs_walk(actx)) == 0) {
diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index 0db7da017233..8918379140f1 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -290,8 +290,9 @@ static struct dentry *ntfs_lookup(struct inode *dir_ino, struct dentry *dent,
 				goto err_out;
 			}
 			ctx = ntfs_attr_get_search_ctx(ni, m);
-			if (unlikely(!ctx)) {
-				err = -ENOMEM;
+			if (IS_ERR(ctx)) {
+				err = PTR_ERR(ctx);
+				ctx = NULL;
 				goto err_out;
 			}
 			do {
@@ -941,13 +942,13 @@ static int ntfs_delete(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,
 	 * only then remove WIN32 name.
 	 */
 	actx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!actx) {
+	if (IS_ERR(actx)) {
 		ntfs_error(sb, "%s, Failed to get search context", __func__);
 		if (need_lock) {
 			mutex_unlock(&dir_ni->mrec_lock);
 			mutex_unlock(&ni->mrec_lock);
 		}
-		return -ENOMEM;
+		return PTR_ERR(actx);
 	}
 search:
 	while ((err = ntfs_attr_lookup(AT_FILE_NAME, AT_UNNAMED, 0, CASE_SENSITIVE,
@@ -1710,9 +1711,9 @@ static struct dentry *ntfs_get_parent(struct dentry *child_dent)
 		return ERR_CAST(mrec);
 	/* Find the first file name attribute in the mft record. */
 	ctx = ntfs_attr_get_search_ctx(ni, mrec);
-	if (unlikely(!ctx)) {
+	if (IS_ERR(ctx)) {
 		unmap_mft_record(ni);
-		return ERR_PTR(-ENOMEM);
+		return ERR_PTR(PTR_ERR(ctx));
 	}
 try_next:
 	err = ntfs_attr_lookup(AT_FILE_NAME, NULL, 0, CASE_SENSITIVE, 0, NULL,
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index df09c277ee54..9bf5a626abbc 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -527,8 +527,9 @@ static int ntfs_write_volume_flags(struct ntfs_volume *vol,
 		goto done;
 
 	ctx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto put_unm_err_out;
 	}
 
@@ -650,8 +651,9 @@ int ntfs_write_volume_label(struct ntfs_volume *vol, char *label)
 
 	mutex_lock(&vol_ni->mrec_lock);
 	ctx = ntfs_attr_get_search_ctx(vol_ni, NULL);
-	if (!ctx) {
-		ret = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		ret = PTR_ERR(ctx);
+		ctx = NULL;
 		goto out;
 	}
 
@@ -1834,8 +1836,10 @@ static int load_system_files(struct ntfs_volume *vol)
 	}
 
 	ctx = ntfs_attr_get_search_ctx(NTFS_I(vol->vol_ino), m);
-	if (!ctx) {
+	if (IS_ERR(ctx)) {
+		ret = PTR_ERR(ctx);
 		ntfs_error(sb, "Failed to get attribute search context.");
+		ctx = NULL;
 		goto get_ctx_vol_failed;
 	}
 
diff --git a/fs/ntfs/wof.c b/fs/ntfs/wof.c
index 294dbe5d711f..e64f40835957 100644
--- a/fs/ntfs/wof.c
+++ b/fs/ntfs/wof.c
@@ -306,8 +306,9 @@ static int parse_wof_chunk_table(struct ntfs_inode *base_ni,
 
 		mutex_lock(&base_ni->mrec_lock);
 		ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-		if (!ctx) {
-			ret = -ENOMEM;
+		if (IS_ERR(ctx)) {
+			ret = PTR_ERR(ctx);
+			ctx = NULL;
 			goto out_unlock_mrec;
 		}
 		ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
@@ -394,8 +395,9 @@ static int ntfs_read_wof_chunk(struct ntfs_volume *vol,
 
 	mutex_lock(&base_ni->mrec_lock);
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
-	if (!ctx) {
-		err = -ENOMEM;
+	if (IS_ERR(ctx)) {
+		err = PTR_ERR(ctx);
+		ctx = NULL;
 		goto out_unlock_mrec;
 	}
 
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 05/31] ntfs: report boot geometry and MFT bootstrap errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (3 preceding siblings ...)
  2026-10-20  8:43 ` [PATCH v4 04/31] ntfs: preserve attribute search context initialization errors Baolin Liu
@ 2026-10-20  8:43 ` Baolin Liu
  2026-10-27  5:48   ` Hyunchul Lee
  2026-10-20  8:43 ` [PATCH v4 06/31] ntfs: report required system file and volume metadata errors Baolin Liu
                   ` (26 subsequent siblings)
  31 siblings, 1 reply; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:43 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Xuewen Wang <wangxuewen@kylinos.cn>

Mount initialization detects invalid MFT locations, mirror mismatches
and corrupt bootstrap metadata without consistently recording them.
Report these failures and use EFSCORRUPTED for confirmed corruption.

Preserve specific errors through boot-sector parsing and MFT loading.
Keep allocation failures and unsupported geometry distinct, and retain
the existing read-only fallback. Before SB_ACTIVE, reporting records
the volume error state without sending events.

Signed-off-by: Xuewen Wang <wangxuewen@kylinos.cn>
---
 fs/ntfs/inode.c | 27 ++++++++++++++++++++----
 fs/ntfs/super.c | 55 +++++++++++++++++++++++++++++++++++--------------
 2 files changed, 62 insertions(+), 20 deletions(-)

diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 5b3d1453d5e1..6ed995b09a3f 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -1880,7 +1880,7 @@ static int load_attribute_list_mount(struct ntfs_volume *vol,
 	ntfs_debug("Entering.");
 	if (!vol || !rl || !al || size <= 0 || initialized_size < 0 ||
 			initialized_size > size)
-		return -EINVAL;
+		return -EFSCORRUPTED;
 	if (!initialized_size) {
 		memset(al, 0, size);
 		return 0;
@@ -1896,7 +1896,7 @@ static int load_attribute_list_mount(struct ntfs_volume *vol,
 		/* The attribute list cannot be sparse. */
 		if (lcn < 0) {
 			ntfs_error(sb, "ntfs_rl_vcn_to_lcn() failed. Cannot read attribute list.");
-			return -EIO;
+			return -EFSCORRUPTED;
 		}
 
 		rl_byte_off = ntfs_cluster_to_bytes(vol, lcn);
@@ -1974,7 +1974,7 @@ int ntfs_read_inode_mount(struct inode *vi)
 	struct attr_record *a;
 	struct ntfs_attr_search_ctx *ctx;
 	unsigned int i;
-	int err;
+	int err = -EIO;
 	size_t new_rl_count;
 
 	ntfs_debug("Entering.");
@@ -2002,6 +2002,7 @@ int ntfs_read_inode_mount(struct inode *vi)
 
 	/* Allocate enough memory to read the first mft record. */
 	if (vol->mft_record_size > 64 * 1024) {
+		err = -EOPNOTSUPP;
 		ntfs_error(sb, "Unsupported mft record size %i (max 64kiB).",
 				vol->mft_record_size);
 		goto err_out;
@@ -2013,6 +2014,7 @@ int ntfs_read_inode_mount(struct inode *vi)
 
 	m = kzalloc(i, GFP_NOFS);
 	if (!m) {
+		err = -ENOMEM;
 		ntfs_error(sb, "Failed to allocate buffer for $MFT record 0.");
 		goto err_out;
 	}
@@ -2101,6 +2103,7 @@ int ntfs_read_inode_mount(struct inode *vi)
 		ni->attr_list = kvzalloc(round_up(ni->attr_list_size, SECTOR_SIZE),
 					 GFP_NOFS);
 		if (!ni->attr_list) {
+			err = -ENOMEM;
 			ntfs_error(sb, "Not enough memory to allocate buffer for attribute list.");
 			goto put_err_out;
 		}
@@ -2120,6 +2123,8 @@ int ntfs_read_inode_mount(struct inode *vi)
 			rl = ntfs_mapping_pairs_decompress(vol, a, NULL, &new_rl_count);
 			if (IS_ERR(rl)) {
 				err = PTR_ERR(rl);
+				if (err != -ENOMEM)
+					err = -EFSCORRUPTED;
 				ntfs_error(sb,
 					   "Mapping pairs decompression failed with error code %i.",
 					   -err);
@@ -2222,6 +2227,9 @@ int ntfs_read_inode_mount(struct inode *vi)
 		nrl = ntfs_mapping_pairs_decompress(vol, a, &ni->runlist,
 						    &new_rl_count);
 		if (IS_ERR(nrl)) {
+			err = PTR_ERR(nrl);
+			if (err != -ENOMEM)
+				err = -EFSCORRUPTED;
 			ntfs_error(sb,
 				"ntfs_mapping_pairs_decompress() failed with error code %ld.",
 				PTR_ERR(nrl));
@@ -2260,6 +2268,7 @@ int ntfs_read_inode_mount(struct inode *vi)
 			 */
 			if ((vi->i_size >> vol->mft_record_size_bits) >=
 					(1ULL << 32)) {
+				err = -EOPNOTSUPP;
 				ntfs_error(sb, "$MFT is too big! Aborting.");
 				goto put_err_out;
 			}
@@ -2285,6 +2294,11 @@ int ntfs_read_inode_mount(struct inode *vi)
 			err = ntfs_read_locked_inode(vi);
 			if (err) {
 				ntfs_error(sb, "ntfs_read_inode() of $MFT failed.\n");
+				if (err == -ENOENT) {
+					err = -EFSCORRUPTED;
+					ntfs_report_metadata_error(vol,
+								   -EFSCORRUPTED);
+				}
 				NVolClearMftBootstrap(vol);
 				ntfs_attr_put_search_ctx(ctx);
 				/* Revert to the safe super operations. */
@@ -2356,9 +2370,14 @@ int ntfs_read_inode_mount(struct inode *vi)
 err_out:
 	/* Also reached from inside the $DATA loop. */
 	NVolClearMftBootstrap(vol);
+	if (err == -ENOENT || !err)
+		err = -EFSCORRUPTED;
+	if (err != -ENOMEM && err != -EOPNOTSUPP &&
+	    err != -EINTR && err != -ERESTARTSYS)
+		ntfs_report_metadata_error(vol, err);
 	ntfs_error(sb, "Failed. Marking inode as bad.");
 	kfree(m);
-	return -1;
+	return err;
 }
 
 static void __ntfs_clear_inode(struct ntfs_inode *ni)
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index 9bf5a626abbc..afee1b262a12 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -869,10 +869,10 @@ static bool ntfs_validate_mft_io_geometry(struct ntfs_volume *vol,
  * @b:		boot sector to parse
  *
  * Parse the ntfs boot sector @b and store all imporant information therein in
- * the ntfs super block @vol.  Return 'true' on success and 'false' on error.
+ * the ntfs super block @vol. Return 0 on success and -errno on error.
  */
-static bool parse_ntfs_boot_sector(struct ntfs_volume *vol,
-		const struct ntfs_boot_sector *b)
+static int parse_ntfs_boot_sector(struct ntfs_volume *vol,
+				  const struct ntfs_boot_sector *b)
 {
 	unsigned int sectors_per_cluster, sectors_per_cluster_bits, nr_hidden_sects;
 	unsigned int logical_block_size;
@@ -890,7 +890,7 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol,
 		ntfs_error(vol->sb,
 			"Sector size (%i) is smaller than the device block size (%lu).  This is not supported.",
 			vol->sector_size, vol->sb->s_blocksize);
-		return false;
+		return -EINVAL;
 	}
 
 	if (b->bpb.sectors_per_cluster >= 0xf4)
@@ -914,7 +914,7 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol,
 		ntfs_error(vol->sb,
 			"Cluster size (%i) is smaller than the sector size (%i).  This is not supported.",
 			vol->cluster_size, vol->sector_size);
-		return false;
+		return -EINVAL;
 	}
 	clusters_per_mft_record = b->clusters_per_mft_record;
 	ntfs_debug("clusters_per_mft_record = %i (0x%x)",
@@ -945,7 +945,7 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol,
 		ntfs_error(vol->sb,
 			"Mft record size (%i) exceeds the PAGE_SIZE on your system (%lu).  This is not supported.",
 			vol->mft_record_size, PAGE_SIZE);
-		return false;
+		return -EINVAL;
 	}
 	/* We cannot support mft record sizes below the sector size. */
 	if (vol->mft_record_size < vol->sector_size) {
@@ -953,7 +953,7 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol,
 				vol->mft_record_size, vol->sector_size);
 	}
 	if (!ntfs_validate_mft_io_geometry(vol, logical_block_size))
-		return false;
+		return -EINVAL;
 
 	clusters_per_index_record = b->clusters_per_index_record;
 	ntfs_debug("clusters_per_index_record = %i (0x%x)",
@@ -983,7 +983,7 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol,
 		ntfs_error(vol->sb,
 			   "Index record size (%i) is smaller than the sector size (%i).  This is not supported.",
 			   vol->index_record_size, vol->sector_size);
-		return false;
+		return -EINVAL;
 	}
 	/*
 	 * Get the size of the volume in clusters and check for 64-bit-ness.
@@ -993,23 +993,25 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol,
 	ll = le64_to_cpu(b->number_of_sectors) >> sectors_per_cluster_bits;
 	if (ll >= 1ULL << 32) {
 		ntfs_error(vol->sb, "Cannot handle 64-bit clusters.");
-		return false;
+		return -EINVAL;
 	}
 	vol->nr_clusters = ll;
 	ntfs_debug("vol->nr_clusters = 0x%llx", vol->nr_clusters);
 	ll = le64_to_cpu(b->mft_lcn);
 	if (ll >= (u64)vol->nr_clusters) {
+		ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		ntfs_error(vol->sb, "MFT LCN (%llu, 0x%llx) is beyond end of volume.  Weird.",
 				ll, ll);
-		return false;
+		return -EFSCORRUPTED;
 	}
 	vol->mft_lcn = ll;
 	ntfs_debug("vol->mft_lcn = 0x%llx", vol->mft_lcn);
 	ll = le64_to_cpu(b->mftmirr_lcn);
 	if (ll >= (u64)vol->nr_clusters) {
+		ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		ntfs_error(vol->sb, "MFTMirr LCN (%llu, 0x%llx) is beyond end of volume.  Weird.",
 				ll, ll);
-		return false;
+		return -EFSCORRUPTED;
 	}
 	vol->mftmirr_lcn = ll;
 	ntfs_debug("vol->mftmirr_lcn = 0x%llx", vol->mftmirr_lcn);
@@ -1050,7 +1052,7 @@ static bool parse_ntfs_boot_sector(struct ntfs_volume *vol,
 		}
 	}
 
-	return true;
+	return 0;
 }
 
 /*
@@ -1127,6 +1129,18 @@ static void ntfs_setup_allocators(struct ntfs_volume *vol)
 	ntfs_debug("vol->mft_data_pos = 0x%llx", vol->mft_data_pos);
 }
 
+/* An unused record is corruption when a required system file is loaded. */
+static struct inode *ntfs_system_iget(struct ntfs_volume *vol, unsigned long mft_no)
+{
+	struct inode *inode = ntfs_iget(vol->sb, mft_no);
+
+	if (IS_ERR(inode) && PTR_ERR(inode) == -ENOENT) {
+		ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+		inode = ERR_PTR(-EFSCORRUPTED);
+	}
+	return inode;
+}
+
 static struct lock_class_key mftmirr_runlist_lock_key,
 			     mftmirr_mrec_lock_key;
 /*
@@ -1142,7 +1156,7 @@ static bool load_and_init_mft_mirror(struct ntfs_volume *vol)
 
 	ntfs_debug("Entering.");
 	/* Get mft mirror inode. */
-	tmp_ino = ntfs_iget(vol->sb, FILE_MFTMirr);
+	tmp_ino = ntfs_system_iget(vol, FILE_MFTMirr);
 	if (IS_ERR(tmp_ino)) {
 		if (!IS_ERR(tmp_ino))
 			iput(tmp_ino);
@@ -1242,6 +1256,7 @@ static bool check_mft_mirror(struct ntfs_volume *vol)
 		if (((struct mft_record *)kmft)->flags & MFT_RECORD_IN_USE) {
 			/* Make sure the record is ok. */
 			if (ntfs_is_baad_recordp((__le32 *)kmft)) {
+				ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 				ntfs_error(sb,
 					"Incomplete multi sector transfer detected in mft record %i.",
 					i);
@@ -1257,6 +1272,7 @@ static bool check_mft_mirror(struct ntfs_volume *vol)
 		/* Do not check the mirror record if it is not in use. */
 		if (((struct mft_record *)kmirr)->flags & MFT_RECORD_IN_USE) {
 			if (ntfs_is_baad_recordp((__le32 *)kmirr)) {
+				ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 				ntfs_error(sb,
 					"Incomplete multi sector transfer detected in mft mirror record %i.",
 					i);
@@ -1276,6 +1292,7 @@ static bool check_mft_mirror(struct ntfs_volume *vol)
 		}
 		/* Compare the two records. */
 		if (memcmp(kmft, kmirr, bytes)) {
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 			ntfs_error(sb,
 				   "$MFT and $MFTMirr record %i do not match.  Run chkdsk.",
 				   i);
@@ -1310,6 +1327,7 @@ static bool check_mft_mirror(struct ntfs_volume *vol)
 	do {
 		if (rl2[i].vcn != rl[i].vcn || rl2[i].lcn != rl[i].lcn ||
 				rl2[i].length != rl[i].length) {
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 			ntfs_error(sb, "$MFTMirr location mismatch.  Run chkdsk.");
 			up_read(&mirr_ni->runlist.lock);
 			return false;
@@ -2741,9 +2759,10 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc)
 	 */
 	result = parse_ntfs_boot_sector(vol, (struct ntfs_boot_sector *)boot);
 	kfree(boot);
-	if (!result) {
+	if (result) {
+		err = result;
 		if (!silent)
-			ntfs_error(sb, "Unsupported NTFS filesystem.");
+			ntfs_error(sb, "Failed to parse NTFS boot sector.");
 		goto err_out_now;
 	}
 	if (!vol->mft_write_supported && !sb_rdonly(sb)) {
@@ -2795,6 +2814,7 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc)
 		ntfs_set_default_dentry_ops(sb);
 	tmp_ino = new_inode(sb);
 	if (!tmp_ino) {
+		err = -ENOMEM;
 		if (!silent)
 			ntfs_error(sb, "Failed to load essential metadata.");
 		goto err_out_now;
@@ -2802,7 +2822,8 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc)
 
 	tmp_ino->i_ino = FILE_MFT;
 	insert_inode_hash(tmp_ino);
-	if (ntfs_read_inode_mount(tmp_ino) < 0) {
+	err = ntfs_read_inode_mount(tmp_ino);
+	if (err) {
 		if (!silent)
 			ntfs_error(sb, "Failed to load essential metadata.");
 		goto iput_tmp_ino_err_out_now;
@@ -2826,6 +2847,7 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc)
 	vol->lcn_empty_bits_per_page = kvmalloc_array(lcn_bit_pages, sizeof(unsigned int),
 						      GFP_KERNEL);
 	if (!vol->lcn_empty_bits_per_page) {
+		err = -ENOMEM;
 		ntfs_error(sb,
 			   "Unable to allocate pages for storing LCN empty bit counts\n");
 		goto unl_upcase_iput_tmp_ino_err_out_now;
@@ -2876,6 +2898,7 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc)
 		queue_work(ntfs_wq, &vol->precalc_work);
 		return 0;
 	}
+	err = -ENOMEM;
 	ntfs_error(sb, "Failed to allocate root directory.");
 	/* Clean up after the successful load_system_files() call from above. */
 	iput(vol->vol_ino);
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 06/31] ntfs: report required system file and volume metadata errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (4 preceding siblings ...)
  2026-10-20  8:43 ` [PATCH v4 05/31] ntfs: report boot geometry and MFT bootstrap errors Baolin Liu
@ 2026-10-20  8:43 ` Baolin Liu
  2026-10-27  5:50   ` Hyunchul Lee
  2026-10-20  8:43 ` [PATCH v4 07/31] ntfs: report logfile metadata and emptying errors Baolin Liu
                   ` (25 subsequent siblings)
  31 siblings, 1 reply; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:43 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Dandan Wu <wudandan@kylinos.cn>

Required system-file loading can obscure the original errno or fail
without recording corrupt metadata. Report missing system records,
invalid tables and allocation metadata, and a stale hiberfil.sys
reference. An absent hiberfil.sys remains valid.

Preserve errors from the AttrDef and UpCase loaders and other system-file
initialization paths, retaining the default-upcase fallback. Report a
missing AT_VOLUME_INFORMATION during volume-flag updates and preserve
the errno when synchronizing the volume dirty state.

Signed-off-by: Dandan Wu <wudandan@kylinos.cn>
---
 fs/ntfs/super.c | 167 +++++++++++++++++++++++++++++++++---------------
 1 file changed, 114 insertions(+), 53 deletions(-)

diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index afee1b262a12..f5c9cf368f8a 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -535,8 +535,13 @@ static int ntfs_write_volume_flags(struct ntfs_volume *vol,
 
 	err = ntfs_attr_lookup(AT_VOLUME_INFORMATION, NULL, 0, 0, 0, NULL, 0,
 			ctx);
-	if (err)
+	if (err) {
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+		}
 		goto put_unm_err_out;
+	}
 
 	vi = (struct volume_information *)((u8 *)ctx->attr +
 			le16_to_cpu(ctx->attr->data.resident.value_offset));
@@ -1433,6 +1438,10 @@ static int check_windows_hibernation_status(struct ntfs_volume *vol)
 	/* Get the inode. */
 	vi = ntfs_iget(vol->sb, MREF(mref));
 	if (IS_ERR(vi)) {
+		if (PTR_ERR(vi) == -ENOENT) {
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+			vi = ERR_PTR(-EFSCORRUPTED);
+		}
 		if (!IS_ERR(vi))
 			iput(vi);
 		ntfs_error(vol->sb, "Failed to load hiberfil.sys.");
@@ -1478,9 +1487,9 @@ static int check_windows_hibernation_status(struct ntfs_volume *vol)
  * load_and_init_attrdef - load the attribute definitions table for a volume
  * @vol:	ntfs super block describing device whose attrdef to load
  *
- * Return 'true' on success or 'false' on error.
+ * Return 0 on success or -errno on error.
  */
-static bool load_and_init_attrdef(struct ntfs_volume *vol)
+static int load_and_init_attrdef(struct ntfs_volume *vol)
 {
 	loff_t i_size;
 	struct super_block *sb = vol->sb;
@@ -1489,20 +1498,32 @@ static bool load_and_init_attrdef(struct ntfs_volume *vol)
 	u8 *addr;
 	pgoff_t index, max_index;
 	unsigned int size;
+	int err;
 
 	ntfs_debug("Entering.");
 	/* Read attrdef table and setup vol->attrdef and vol->attrdef_size. */
-	ino = ntfs_iget(sb, FILE_AttrDef);
-	if (IS_ERR(ino))
+	ino = ntfs_system_iget(vol, FILE_AttrDef);
+	if (IS_ERR(ino)) {
+		err = PTR_ERR(ino);
 		goto failed;
+	}
 	NInoSetSparseDisabled(NTFS_I(ino));
 	/* FILE_AttrDef must hold at least one entry and fit inside 31 bits. */
 	i_size = i_size_read(ino);
-	if (i_size < (s64)sizeof(struct attr_def) || i_size > 0x7fffffff)
+	if (i_size < (s64)sizeof(struct attr_def)) {
+		err = -EFSCORRUPTED;
+		ntfs_report_metadata_error(vol, err);
+		goto iput_failed;
+	}
+	if (i_size > 0x7fffffff) {
+		err = -EFBIG;
 		goto iput_failed;
+	}
 	vol->attrdef = kvzalloc(i_size, GFP_NOFS);
-	if (!vol->attrdef)
+	if (!vol->attrdef) {
+		err = -ENOMEM;
 		goto iput_failed;
+	}
 	index = 0;
 	max_index = i_size >> PAGE_SHIFT;
 	size = PAGE_SIZE;
@@ -1510,8 +1531,10 @@ static bool load_and_init_attrdef(struct ntfs_volume *vol)
 		/* Read the attrdef table and copy it into the linear buffer. */
 read_partial_attrdef_page:
 		folio = read_mapping_folio(ino->i_mapping, index, NULL);
-		if (IS_ERR(folio))
+		if (IS_ERR(folio)) {
+			err = PTR_ERR(folio);
 			goto free_iput_failed;
+		}
 		addr = kmap_local_folio(folio, 0);
 		memcpy((u8 *)vol->attrdef + (index++ << PAGE_SHIFT),
 				addr, size);
@@ -1526,7 +1549,7 @@ static bool load_and_init_attrdef(struct ntfs_volume *vol)
 	vol->attrdef_size = i_size;
 	ntfs_debug("Read %llu bytes from $AttrDef.", i_size);
 	iput(ino);
-	return true;
+	return 0;
 free_iput_failed:
 	kvfree(vol->attrdef);
 	vol->attrdef = NULL;
@@ -1534,16 +1557,16 @@ static bool load_and_init_attrdef(struct ntfs_volume *vol)
 	iput(ino);
 failed:
 	ntfs_error(sb, "Failed to initialize attribute definition table.");
-	return false;
+	return err;
 }
 
 /*
  * load_and_init_upcase - load the upcase table for an ntfs volume
  * @vol:	ntfs super block describing device whose upcase to load
  *
- * Return 'true' on success or 'false' on error.
+ * Return 0 on success or -errno on error.
  */
-static bool load_and_init_upcase(struct ntfs_volume *vol)
+static int load_and_init_upcase(struct ntfs_volume *vol)
 {
 	loff_t i_size;
 	struct super_block *sb = vol->sb;
@@ -1552,23 +1575,31 @@ static bool load_and_init_upcase(struct ntfs_volume *vol)
 	u8 *addr;
 	pgoff_t index, max_index;
 	unsigned int size;
+	int err;
 
 	ntfs_debug("Entering.");
 	/* Read upcase table and setup vol->upcase and vol->upcase_len. */
-	ino = ntfs_iget(sb, FILE_UpCase);
-	if (IS_ERR(ino))
+	ino = ntfs_system_iget(vol, FILE_UpCase);
+	if (IS_ERR(ino)) {
+		err = PTR_ERR(ino);
 		goto upcase_failed;
+	}
 	/*
 	 * The upcase size must not be above 64k Unicode characters, must not
 	 * be zero and must be a multiple of sizeof(__le16).
 	 */
 	i_size = i_size_read(ino);
 	if (!i_size || i_size & (sizeof(__le16) - 1) ||
-			i_size > 64ULL * 1024 * sizeof(__le16))
+			i_size > 64ULL * 1024 * sizeof(__le16)) {
+		err = -EFSCORRUPTED;
+		ntfs_report_metadata_error(vol, err);
 		goto iput_upcase_failed;
+	}
 	vol->upcase = kvzalloc(i_size, GFP_NOFS);
-	if (!vol->upcase)
+	if (!vol->upcase) {
+		err = -ENOMEM;
 		goto iput_upcase_failed;
+	}
 	index = 0;
 	max_index = i_size >> PAGE_SHIFT;
 	size = PAGE_SIZE;
@@ -1576,8 +1607,10 @@ static bool load_and_init_upcase(struct ntfs_volume *vol)
 		/* Read the upcase table and copy it into the linear buffer. */
 read_partial_upcase_page:
 		folio = read_mapping_folio(ino->i_mapping, index, NULL);
-		if (IS_ERR(folio))
+		if (IS_ERR(folio)) {
+			err = PTR_ERR(folio);
 			goto iput_upcase_failed;
+		}
 		addr = kmap_local_folio(folio, 0);
 		memcpy((char *)vol->upcase + (index++ << PAGE_SHIFT),
 				addr, size);
@@ -1597,7 +1630,7 @@ static bool load_and_init_upcase(struct ntfs_volume *vol)
 	if (!default_upcase) {
 		ntfs_debug("Using volume specified $UpCase since default is not present.");
 		mutex_unlock(&ntfs_lock);
-		return true;
+		return 0;
 	}
 	if (default_upcase_len == vol->upcase_len &&
 	    !memcmp(vol->upcase, default_upcase,
@@ -1607,11 +1640,11 @@ static bool load_and_init_upcase(struct ntfs_volume *vol)
 		ntfs_nr_upcase_users++;
 		mutex_unlock(&ntfs_lock);
 		ntfs_debug("Volume specified $UpCase matches default. Using default.");
-		return true;
+		return 0;
 	}
 	mutex_unlock(&ntfs_lock);
 	ntfs_debug("Using volume specified $UpCase since it does not match the default.");
-	return true;
+	return 0;
 iput_upcase_failed:
 	iput(ino);
 	kvfree(vol->upcase);
@@ -1624,11 +1657,11 @@ static bool load_and_init_upcase(struct ntfs_volume *vol)
 		ntfs_nr_upcase_users++;
 		mutex_unlock(&ntfs_lock);
 		ntfs_error(sb, "Failed to load $UpCase from the volume. Using default.");
-		return true;
+		return 0;
 	}
 	mutex_unlock(&ntfs_lock);
 	ntfs_error(sb, "Failed to initialize upcase table.");
-	return false;
+	return err;
 }
 
 static int ntfs_validate_badclus(struct ntfs_volume *vol,
@@ -1639,7 +1672,7 @@ static int ntfs_validate_badclus(struct ntfs_volume *vol,
 	size_t i;
 
 	if (!rl)
-		return -EIO;
+		return -EFSCORRUPTED;
 
 	for (i = 0; i < runlist->count; i++) {
 		if (!rl[i].length) {
@@ -1647,22 +1680,22 @@ static int ntfs_validate_badclus(struct ntfs_volume *vol,
 			    rl[i].vcn != vol->nr_clusters ||
 			    rl[i].lcn != LCN_ENOENT ||
 			    next_vcn != vol->nr_clusters)
-				return -EIO;
+				return -EFSCORRUPTED;
 			*bad_clusters = bad;
 			return 0;
 		}
 		if (rl[i].vcn != next_vcn || rl[i].length < 0 ||
 		    rl[i].length > vol->nr_clusters - next_vcn)
-			return -EIO;
+			return -EFSCORRUPTED;
 		if (rl[i].lcn != LCN_HOLE) {
 			/* Each bad cluster is mapped to its own volume offset. */
 			if (rl[i].lcn != rl[i].vcn)
-				return -EIO;
+				return -EFSCORRUPTED;
 			bad += rl[i].length;
 		}
 		next_vcn += rl[i].length;
 	}
-	return -EIO;
+	return -EFSCORRUPTED;
 }
 
 /*
@@ -1685,23 +1718,29 @@ static int load_and_check_badclus(struct ntfs_volume *vol)
 	size_t i;
 	int err;
 
-	base_ino = ntfs_iget(sb, FILE_BadClus);
+	base_ino = ntfs_system_iget(vol, FILE_BadClus);
 	if (IS_ERR(base_ino))
 		return PTR_ERR(base_ino);
 	if (!S_ISREG(base_ino->i_mode)) {
-		err = -EIO;
+		err = -EFSCORRUPTED;
+		ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		goto iput_base;
 	}
 	bad_ino = ntfs_attr_iget(base_ino, AT_DATA, bad_name,
 				 ARRAY_SIZE(bad_name));
 	if (IS_ERR(bad_ino)) {
 		err = PTR_ERR(bad_ino);
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+		}
 		goto iput_base;
 	}
 	ni = NTFS_I(bad_ino);
 	if (!NInoNonResident(ni) || NInoCompressed(ni) || NInoEncrypted(ni) ||
 	    ni->data_size != size || ni->allocated_size != size) {
-		err = -EIO;
+		err = -EFSCORRUPTED;
+		ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		goto iput_bad;
 	}
 
@@ -1710,8 +1749,10 @@ static int load_and_check_badclus(struct ntfs_volume *vol)
 	if (err)
 		goto unlock;
 	err = ntfs_validate_badclus(vol, &ni->runlist, &vol->bad_clusters);
-	if (err)
+	if (err) {
+		ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		goto unlock;
+	}
 	for (i = 0; i < ni->runlist.count - 1; i++) {
 		struct runlist_element *rl = &ni->runlist.rl[i];
 
@@ -1732,8 +1773,10 @@ static int load_and_check_badclus(struct ntfs_volume *vol)
 		return err;
 	if (free_bits) {
 		/* A read-only mount must not later remount RW without repair. */
-		if (sb_rdonly(sb))
-			return -EIO;
+		if (sb_rdonly(sb)) {
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+			return -EFSCORRUPTED;
+		}
 		err = filemap_write_and_wait(vol->lcnbmp_ino->i_mapping);
 		if (err)
 			return err;
@@ -1795,6 +1838,11 @@ static int load_system_files(struct ntfs_volume *vol)
 	/* Get mft bitmap attribute inode. */
 	vol->mftbmp_ino = ntfs_attr_iget(vol->mft_ino, AT_BITMAP, NULL, 0);
 	if (IS_ERR(vol->mftbmp_ino)) {
+		if (PTR_ERR(vol->mftbmp_ino) == -ENOENT) {
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+			vol->mftbmp_ino = ERR_PTR(-EFSCORRUPTED);
+		}
+		ret = PTR_ERR(vol->mftbmp_ino);
 		ntfs_error(sb, "Failed to load $MFT/$BITMAP attribute.");
 		goto iput_mirr_err_out;
 	}
@@ -1803,23 +1851,24 @@ static int load_system_files(struct ntfs_volume *vol)
 	lockdep_set_class(&NTFS_I(vol->mftbmp_ino)->mrec_lock,
 			   &mftbmp_mrec_lock_key);
 	/* Read upcase table and setup @vol->upcase and @vol->upcase_len. */
-	if (!load_and_init_upcase(vol))
+	ret = load_and_init_upcase(vol);
+	if (ret)
 		goto iput_mftbmp_err_out;
 	/*
 	 * Read attribute definitions table and setup @vol->attrdef and
 	 * @vol->attrdef_size.
 	 */
-	if (!load_and_init_attrdef(vol))
+	ret = load_and_init_attrdef(vol);
+	if (ret)
 		goto iput_upcase_err_out;
 	/*
 	 * Get the cluster allocation bitmap inode and verify the size, no
 	 * need for any locking at this stage as we are already running
 	 * exclusively as we are mount in progress task.
 	 */
-	vol->lcnbmp_ino = ntfs_iget(sb, FILE_Bitmap);
+	vol->lcnbmp_ino = ntfs_system_iget(vol, FILE_Bitmap);
 	if (IS_ERR(vol->lcnbmp_ino)) {
-		if (!IS_ERR(vol->lcnbmp_ino))
-			iput(vol->lcnbmp_ino);
+		ret = PTR_ERR(vol->lcnbmp_ino);
 		goto bitmap_failed;
 	}
 	lockdep_set_class(&NTFS_I(vol->lcnbmp_ino)->runlist.lock,
@@ -1829,6 +1878,8 @@ static int load_system_files(struct ntfs_volume *vol)
 
 	NInoSetSparseDisabled(NTFS_I(vol->lcnbmp_ino));
 	if ((vol->nr_clusters + 7) >> 3 > i_size_read(vol->lcnbmp_ino)) {
+		ret = -EFSCORRUPTED;
+		ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		iput(vol->lcnbmp_ino);
 bitmap_failed:
 		ntfs_error(sb, "Failed to load $Bitmap.");
@@ -1838,8 +1889,9 @@ static int load_system_files(struct ntfs_volume *vol)
 	 * Get the volume inode and setup our cache of the volume flags and
 	 * version.
 	 */
-	vol->vol_ino = ntfs_iget(sb, FILE_Volume);
+	vol->vol_ino = ntfs_system_iget(vol, FILE_Volume);
 	if (IS_ERR(vol->vol_ino)) {
+		ret = PTR_ERR(vol->vol_ino);
 		vol->vol_ino = NULL;
 volume_failed:
 		ntfs_error(sb, "Failed to load $Volume.");
@@ -1847,6 +1899,7 @@ static int load_system_files(struct ntfs_volume *vol)
 	}
 	m = map_mft_record(NTFS_I(vol->vol_ino));
 	if (IS_ERR(m)) {
+		ret = PTR_ERR(m);
 iput_volume_failed:
 		iput(vol->vol_ino);
 		vol->vol_ino = NULL;
@@ -1874,8 +1927,14 @@ static int load_system_files(struct ntfs_volume *vol)
 	}
 
 	ntfs_attr_reinit_search_ctx(ctx);
-	if (ntfs_attr_lookup(AT_VOLUME_INFORMATION, NULL, 0, 0, 0, NULL, 0,
-			ctx) || ctx->attr->non_resident || ctx->attr->flags) {
+	err = ntfs_attr_lookup(AT_VOLUME_INFORMATION, NULL, 0, 0, 0,
+			       NULL, 0, ctx);
+	if (err || ctx->attr->non_resident || ctx->attr->flags) {
+		ret = err;
+		if (!err || err == -ENOENT) {
+			ret = -EFSCORRUPTED;
+			ntfs_report_metadata_error(vol, ret);
+		}
 		ntfs_attr_put_search_ctx(ctx);
 get_ctx_vol_failed:
 		unmap_mft_record(NTFS_I(vol->vol_ino));
@@ -1939,10 +1998,9 @@ static int load_system_files(struct ntfs_volume *vol)
 
 	kvfree(rp);
 	/* Get the root directory inode so we can do path lookups. */
-	vol->root_ino = ntfs_iget(sb, FILE_root);
+	vol->root_ino = ntfs_system_iget(vol, FILE_root);
 	if (IS_ERR(vol->root_ino)) {
-		if (!IS_ERR(vol->root_ino))
-			iput(vol->root_ino);
+		ret = PTR_ERR(vol->root_ino);
 		ntfs_error(sb, "Failed to load root directory.");
 		goto iput_logfile_err_out;
 	}
@@ -2025,19 +2083,22 @@ static int load_system_files(struct ntfs_volume *vol)
 		return 0;
 	/* NTFS 3.0+ specific initialization. */
 	/* Get the security descriptors inode. */
-	vol->secure_ino = ntfs_iget(sb, FILE_Secure);
+	vol->secure_ino = ntfs_system_iget(vol, FILE_Secure);
 	if (IS_ERR(vol->secure_ino)) {
-		if (!IS_ERR(vol->secure_ino))
-			iput(vol->secure_ino);
+		ret = PTR_ERR(vol->secure_ino);
 		ntfs_error(sb, "Failed to load $Secure.");
 		goto iput_root_err_out;
 	}
 	/* Get the extended system files' directory inode. */
-	vol->extend_ino = ntfs_iget(sb, FILE_Extend);
+	vol->extend_ino = ntfs_system_iget(vol, FILE_Extend);
 	if (IS_ERR(vol->extend_ino) ||
 	    !S_ISDIR(vol->extend_ino->i_mode)) {
-		if (!IS_ERR(vol->extend_ino))
+		ret = IS_ERR(vol->extend_ino) ?
+			PTR_ERR(vol->extend_ino) : -EFSCORRUPTED;
+		if (!IS_ERR(vol->extend_ino)) {
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 			iput(vol->extend_ino);
+		}
 		ntfs_error(sb, "Failed to load $Extend.");
 		goto iput_sec_err_out;
 	}
@@ -2331,10 +2392,10 @@ static int ntfs_sync_fs(struct super_block *sb, int wait)
 	 * seen no modification; with NVolErrors() set this can only set
 	 * the bit, never clear it.
 	 */
-	if (NVolErrors(vol) &&
-	    ntfs_sync_volume_dirty_state(vol)) {
-		ntfs_warning(sb, "Failed to sync dirty bit in volume information flags.  Run chkdsk.");
-		err = -EIO;
+	if (NVolErrors(vol)) {
+		err = ntfs_sync_volume_dirty_state(vol);
+		if (err)
+			ntfs_warning(sb, "Failed to sync dirty bit in volume information flags.  Run chkdsk.");
 	}
 	sync_inodes_sb(sb);
 	ret = sync_blockdev(sb->s_bdev);
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 07/31] ntfs: report logfile metadata and emptying errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (5 preceding siblings ...)
  2026-10-20  8:43 ` [PATCH v4 06/31] ntfs: report required system file and volume metadata errors Baolin Liu
@ 2026-10-20  8:43 ` Baolin Liu
  2026-10-20  8:43 ` [PATCH v4 08/31] ntfs: report runlist and cluster allocation metadata errors Baolin Liu
                   ` (24 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:43 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Dandan Wu <wudandan@kylinos.cn>

Logfile validation and emptying can detect corruption or I/O failures
without recording them. Report an unusable nonempty log after trying
alternate restart pages, keeping unsupported versions distinct from
corrupt restart metadata.

Report a missing required logfile, read failures, and write failures or
corrupt runlists when emptying it, including on remount. Retain the
existing read-only fallback.

Signed-off-by: Dandan Wu <wudandan@kylinos.cn>
---
 fs/ntfs/logfile.c | 51 +++++++++++++++++++++++++++--------------------
 fs/ntfs/super.c   |  2 +-
 2 files changed, 30 insertions(+), 23 deletions(-)

diff --git a/fs/ntfs/logfile.c b/fs/ntfs/logfile.c
index ce38ce87f8fc..1b70a5126845 100644
--- a/fs/ntfs/logfile.c
+++ b/fs/ntfs/logfile.c
@@ -17,14 +17,14 @@
  * @rp:		restart page header to check
  * @pos:	position in @vi at which the restart page header resides
  *
- * Check the restart page header @rp for consistency and return 'true' if it is
- * consistent and 'false' otherwise.
+ * Check the restart page header @rp for consistency. Return 0 on success,
+ * -EOPNOTSUPP for an unsupported version, or -EINVAL for corruption.
  *
  * This function only needs NTFS_BLOCK_SIZE bytes in @rp, i.e. it does not
  * require the full restart page.
  */
-static bool ntfs_check_restart_page_header(struct inode *vi,
-		struct restart_page_header *rp, s64 pos)
+static int ntfs_check_restart_page_header(struct inode *vi,
+					  struct restart_page_header *rp, s64 pos)
 {
 	u32 logfile_system_page_size, logfile_log_page_size;
 	u16 ra_ofs, usa_count, usa_ofs, usa_end = 0;
@@ -43,7 +43,7 @@ static bool ntfs_check_restart_page_header(struct inode *vi,
 			(logfile_system_page_size - 1) ||
 			!is_power_of_2(logfile_log_page_size)) {
 		ntfs_error(vi->i_sb, "LogFile uses unsupported page size.");
-		return false;
+		return -EINVAL;
 	}
 	/*
 	 * We must be either at !pos (1st restart page) or at pos = system page
@@ -51,7 +51,7 @@ static bool ntfs_check_restart_page_header(struct inode *vi,
 	 */
 	if (pos && pos != logfile_system_page_size) {
 		ntfs_error(vi->i_sb, "Found restart area in incorrect position in LogFile.");
-		return false;
+		return -EINVAL;
 	}
 	/* We only know how to handle version 1.1. */
 	if (le16_to_cpu(rp->major_ver) != 1 ||
@@ -60,7 +60,7 @@ static bool ntfs_check_restart_page_header(struct inode *vi,
 			"LogFile version %i.%i is not supported.  (This driver supports version 1.1 only.)",
 			(int)le16_to_cpu(rp->major_ver),
 			(int)le16_to_cpu(rp->minor_ver));
-		return false;
+		return -EOPNOTSUPP;
 	}
 	/*
 	 * If chkdsk has been run the restart page may not be protected by an
@@ -75,7 +75,7 @@ static bool ntfs_check_restart_page_header(struct inode *vi,
 	if (usa_count != le16_to_cpu(rp->usa_count)) {
 		ntfs_error(vi->i_sb,
 			"LogFile restart page specifies inconsistent update sequence array count.");
-		return false;
+		return -EINVAL;
 	}
 	/* Verify the position of the update sequence array. */
 	usa_ofs = le16_to_cpu(rp->usa_ofs);
@@ -84,7 +84,7 @@ static bool ntfs_check_restart_page_header(struct inode *vi,
 			usa_end > NTFS_BLOCK_SIZE - sizeof(u16)) {
 		ntfs_error(vi->i_sb,
 			"LogFile restart page specifies inconsistent update sequence array offset.");
-		return false;
+		return -EINVAL;
 	}
 skip_usa_checks:
 	/*
@@ -99,7 +99,7 @@ static bool ntfs_check_restart_page_header(struct inode *vi,
 			ra_ofs > logfile_system_page_size) {
 		ntfs_error(vi->i_sb,
 			"LogFile restart page specifies inconsistent restart area offset.");
-		return false;
+		return -EINVAL;
 	}
 	/*
 	 * Only restart pages modified by chkdsk are allowed to have chkdsk_lsn
@@ -108,10 +108,10 @@ static bool ntfs_check_restart_page_header(struct inode *vi,
 	if (!ntfs_is_chkd_record(rp->magic) && le64_to_cpu(rp->chkdsk_lsn)) {
 		ntfs_error(vi->i_sb,
 			"LogFile restart page is not modified by chkdsk but a chkdsk LSN is specified.");
-		return false;
+		return -EINVAL;
 	}
 	ntfs_debug("Done.");
-	return true;
+	return 0;
 }
 
 /*
@@ -315,6 +315,7 @@ static bool ntfs_check_log_client_array(struct inode *vi,
  *
  * The following error codes are defined:
  *	-EINVAL	- The restart page is inconsistent.
+ *	-EOPNOTSUPP - The logfile version is unsupported.
  *	-ENOMEM	- Not enough memory to load the restart page.
  *	-EIO	- Failed to reading from LogFile.
  */
@@ -328,10 +329,9 @@ static int ntfs_check_and_load_restart_page(struct inode *vi,
 
 	ntfs_debug("Entering.");
 	/* Check the restart page header for consistency. */
-	if (!ntfs_check_restart_page_header(vi, rp, pos)) {
-		/* Error output already done inside the function. */
-		return -EINVAL;
-	}
+	err = ntfs_check_restart_page_header(vi, rp, pos);
+	if (err)
+		return err;
 	/* Check the restart area for consistency. */
 	if (!ntfs_check_restart_area(vi, rp)) {
 		/* Error output already done inside the function. */
@@ -372,8 +372,6 @@ static int ntfs_check_and_load_restart_page(struct inode *vi,
 				ntfs_error(vi->i_sb, "Error mapping LogFile page (index %lu).",
 						idx);
 				err = PTR_ERR(folio);
-				if (err != -EIO && err != -ENOMEM)
-					err = -EIO;
 				goto err_out;
 			}
 			size = min_t(int, to_read, PAGE_SIZE);
@@ -461,7 +459,7 @@ bool ntfs_check_logfile(struct inode *log_vi, struct restart_page_header **rp)
 	struct restart_page_header *rstr1_ph = NULL;
 	struct restart_page_header *rstr2_ph = NULL;
 	int log_page_size, err;
-	bool logfile_is_empty = true;
+	bool logfile_is_empty = true, unsupported_version = false;
 	u8 log_page_bits;
 
 	ntfs_debug("Entering.");
@@ -495,6 +493,7 @@ bool ntfs_check_logfile(struct inode *log_vi, struct restart_page_header **rp)
 	 */
 	if (size < log_page_size * 2 || (size - log_page_size * 2) >>
 			log_page_bits < MinLogRecordPages) {
+		ntfs_report_file_metadata_error(log_vi, -EFSCORRUPTED);
 		ntfs_error(vol->sb, "LogFile is too small.");
 		return false;
 	}
@@ -516,6 +515,8 @@ bool ntfs_check_logfile(struct inode *log_vi, struct restart_page_header **rp)
 			}
 			folio = read_mapping_folio(mapping, idx, NULL);
 			if (IS_ERR(folio)) {
+				if (PTR_ERR(folio) == -EIO)
+					ntfs_report_file_metadata_error(log_vi, -EIO);
 				ntfs_error(vol->sb, "Error mapping LogFile page (index %lu).",
 						idx);
 				goto err_out;
@@ -574,7 +575,11 @@ bool ntfs_check_logfile(struct inode *log_vi, struct restart_page_header **rp)
 		 * not abort if the restart page was invalid as we might still
 		 * find a valid one further in the file.
 		 */
-		if (err != -EINVAL) {
+		if (err == -EOPNOTSUPP)
+			unsupported_version = true;
+		else if (err != -EINVAL) {
+			if (err == -EIO)
+				ntfs_report_file_metadata_error(log_vi, err);
 			kunmap_local(kbase);
 			folio_put(folio);
 			goto err_out;
@@ -594,6 +599,8 @@ bool ntfs_check_logfile(struct inode *log_vi, struct restart_page_header **rp)
 		return true;
 	}
 	if (!rstr1_ph) {
+		if (!unsupported_version)
+			ntfs_report_file_metadata_error(log_vi, -EFSCORRUPTED);
 		ntfs_error(vol->sb,
 			"Did not find any restart pages in LogFile and it was not empty.");
 		return false;
@@ -767,10 +774,10 @@ bool ntfs_empty_logfile(struct inode *log_vi)
 	ntfs_error(sb, "Failed to write buffer.  Unmount and run chkdsk.");
 	goto dirty_err;
 rl_err:
+	err = -EFSCORRUPTED;
 	ntfs_error(sb, "Runlist is corrupt.  Unmount and run chkdsk.");
 dirty_err:
-	NVolSetErrors(vol);
-	err = -EIO;
+	ntfs_report_file_metadata_error(log_vi, err);
 err:
 	kvfree(empty_buf);
 	kfree(ra);
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index f5c9cf368f8a..7c10a1b24b14 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -1357,7 +1357,7 @@ static int load_and_check_logfile(struct ntfs_volume *vol,
 	int err = 0;
 
 	ntfs_debug("Entering.");
-	tmp_ino = ntfs_iget(vol->sb, FILE_LogFile);
+	tmp_ino = ntfs_system_iget(vol, FILE_LogFile);
 	if (IS_ERR(tmp_ino)) {
 		if (!IS_ERR(tmp_ino))
 			iput(tmp_ino);
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 08/31] ntfs: report runlist and cluster allocation metadata errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (6 preceding siblings ...)
  2026-10-20  8:43 ` [PATCH v4 07/31] ntfs: report logfile metadata and emptying errors Baolin Liu
@ 2026-10-20  8:43 ` Baolin Liu
  2026-10-20  8:43 ` [PATCH v4 09/31] ntfs: report iomap corruption and preserve buffered read errors Baolin Liu
                   ` (23 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:43 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

Runlist mapping and cluster cleanup can detect invalid mappings or
failed recovery without reporting them. Report malformed mapping pairs,
missing extents within allocated_size, and failed bitmap or allocation
recovery. Preserve valid sparse holes and EOF.

Use EFSCORRUPTED for corruption and propagate it through mapping callers
and an LCN_EFSCORRUPTED sentinel. Keep allocation and underlying I/O
errors distinct, including when freeing clusters.

Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
 fs/ntfs/aops.c     |   2 +-
 fs/ntfs/attrib.c   | 121 +++++++++++++++++++++++++++++++++++----------
 fs/ntfs/bitmap.c   |   2 +-
 fs/ntfs/lcnalloc.c |  17 +++++--
 fs/ntfs/runlist.c  |  16 +++---
 fs/ntfs/runlist.h  |   1 +
 6 files changed, 119 insertions(+), 40 deletions(-)

diff --git a/fs/ntfs/aops.c b/fs/ntfs/aops.c
index ad781674fef1..7884737657d5 100644
--- a/fs/ntfs/aops.c
+++ b/fs/ntfs/aops.c
@@ -175,7 +175,7 @@ static sector_t ntfs_bmap(struct address_space *mapping, sector_t block)
 		/*
 		 * Step down to an integer to avoid gcc doing a long long
 		 * comparision in the switch when we know @lcn is between
-		 * LCN_HOLE and LCN_EIO (i.e. -1 to -5).
+		 * LCN_HOLE and LCN_EFSCORRUPTED (i.e. -2 to -8).
 		 *
 		 * Otherwise older gcc (at least on some architectures) will
 		 * try to use __cmpdi2() which is of course not available in
diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index 28099ed6ad99..c59643fb766a 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -112,7 +112,7 @@ int ntfs_map_runlist_nolock(struct ntfs_inode *ni, s64 vcn, struct ntfs_attr_sea
 		     base_ni == NTFS_I(ni->vol->mft_ino))) {
 		ntfs_error(ni->vol->sb,
 			   "$MFT needs its own extent records to describe itself; cannot mount.");
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 	if (!ctx) {
 		ctx_is_temporary = ctx_needs_reset = true;
@@ -131,7 +131,9 @@ int ntfs_map_runlist_nolock(struct ntfs_inode *ni, s64 vcn, struct ntfs_attr_sea
 		WARN_ON(IS_ERR(ctx->mrec));
 		a = ctx->attr;
 		if (!a->non_resident) {
-			err = -EIO;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+			err = -EFSCORRUPTED;
 			goto err_out;
 		}
 		end_vcn = le64_to_cpu(a->data.non_resident.highest_vcn);
@@ -183,12 +185,17 @@ int ntfs_map_runlist_nolock(struct ntfs_inode *ni, s64 vcn, struct ntfs_attr_sea
 		err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 				CASE_SENSITIVE, vcn, NULL, 0, ctx);
 		if (unlikely(err)) {
-			if (err == -ENOENT)
-				err = -EIO;
+			if (err == -ENOENT) {
+				err = -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
+			}
 			goto err_out;
 		}
 		if (unlikely(!ctx->attr->non_resident)) {
-			err = -EIO;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+			err = -EFSCORRUPTED;
 			goto err_out;
 		}
 	}
@@ -201,13 +208,25 @@ int ntfs_map_runlist_nolock(struct ntfs_inode *ni, s64 vcn, struct ntfs_attr_sea
 	 */
 	end_vcn = le64_to_cpu(a->data.non_resident.highest_vcn) + 1;
 	if (unlikely(vcn && vcn >= end_vcn)) {
+		/* Report missing extents inside the allocation to direct callers. */
 		err = -ENOENT;
+		read_lock_irqsave(&ni->size_lock, flags);
+		if (vcn >= 0 && vcn < ntfs_bytes_to_cluster(ni->vol,
+							    ni->allocated_size))
+			err = -EFSCORRUPTED;
+		read_unlock_irqrestore(&ni->size_lock, flags);
+		if (err == -EFSCORRUPTED)
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
 		goto err_out;
 	}
 	rl = ntfs_mapping_pairs_decompress(ni->vol, a, &ni->runlist, &new_rl_count);
-	if (IS_ERR(rl))
+	if (IS_ERR(rl)) {
 		err = PTR_ERR(rl);
-	else {
+		if (err != -ENOMEM)
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+	} else {
 		ni->runlist.rl = rl;
 		ni->runlist.count = new_rl_count;
 	}
@@ -335,7 +354,7 @@ struct runlist_element *ntfs_attr_vcn_to_rl(struct ntfs_inode *ni, s64 vcn, s64
 	if (!rl) {
 		err = ntfs_attr_map_whole_runlist(ni);
 		if (err)
-			return ERR_PTR(-ENOENT);
+			return ERR_PTR(err);
 		rl = ni->runlist.rl;
 	}
 
@@ -357,12 +376,11 @@ struct runlist_element *ntfs_attr_vcn_to_rl(struct ntfs_inode *ni, s64 vcn, s64
 	/*
 	 * Neither the runlist nor the retry mapped @vcn, e.g. because the
 	 * extent mft record holding it is corrupt or because the mapping
-	 * pairs end too soon.  ntfs_map_runlist_nolock() reports the latter
-	 * as -ENOENT, as @vcn lies past the extent it found.  Below the
-	 * allocated size, callers would treat LCN_RL_NOT_MAPPED or LCN_ENOENT
-	 * as a hole, so fail instead.  At or beyond it nothing is mapped: the
-	 * runlist ends there with LCN_ENOENT, or with LCN_RL_NOT_MAPPED if
-	 * only a later extent has been mapped, so return that end as it is.
+	 * pairs end too soon.  Below the allocated size, callers would treat
+	 * LCN_RL_NOT_MAPPED or LCN_ENOENT as a hole, so fail instead.  At or
+	 * beyond it nothing is mapped: the runlist ends there with LCN_ENOENT,
+	 * or 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;
@@ -371,8 +389,12 @@ struct runlist_element *ntfs_attr_vcn_to_rl(struct ntfs_inode *ni, s64 vcn, s64
 		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);
+		if ((s64)ntfs_cluster_to_bytes(ni->vol, vcn) < allocated_size) {
+			if (!err || err == -ENOENT)
+				ntfs_report_file_metadata_error(VFS_I(ni),
+								-EFSCORRUPTED);
+			return ERR_PTR(!err || err == -ENOENT ? -EFSCORRUPTED : err);
+		}
 	}
 
 	return rl;
@@ -401,7 +423,8 @@ struct runlist_element *ntfs_attr_vcn_to_rl(struct ntfs_inode *ni, s64 vcn, s64
  *  LCN_HOLE	Hole / not allocated on disk.
  *  LCN_ENOENT	There is no such vcn in the runlist, i.e. @vcn is out of bounds.
  *  LCN_ENOMEM	Not enough memory to map runlist.
- *  LCN_EIO	Critical error (runlist/file is corrupt, i/o error, etc).
+ *  LCN_EIO	I/O or other mapping failure.
+ *  LCN_EFSCORRUPTED	Corrupt runlist or file metadata.
  *
  * Locking: - The runlist must be locked on entry and is left locked on return.
  *	    - If @write_locked is 'false', i.e. the runlist is locked for reading,
@@ -434,8 +457,11 @@ s64 ntfs_attr_vcn_to_lcn_nolock(struct ntfs_inode *ni, const s64 vcn,
 		return lcn;
 	}
 	if (lcn != LCN_RL_NOT_MAPPED) {
-		if (lcn != LCN_ENOENT)
-			lcn = LCN_EIO;
+		if (lcn != LCN_ENOENT) {
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+			lcn = LCN_EFSCORRUPTED;
+		}
 	} else if (!is_retry) {
 		int err;
 
@@ -462,8 +488,23 @@ s64 ntfs_attr_vcn_to_lcn_nolock(struct ntfs_inode *ni, const s64 vcn,
 			lcn = LCN_ENOENT;
 		else if (err == -ENOMEM)
 			lcn = LCN_ENOMEM;
+		else if (err == -EFSCORRUPTED)
+			lcn = LCN_EFSCORRUPTED;
 		else
 			lcn = LCN_EIO;
+	} else {
+		lcn = LCN_EFSCORRUPTED;
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
+	}
+	if (lcn == LCN_ENOENT) {
+		read_lock_irqsave(&ni->size_lock, flags);
+		if (vcn >= 0 && vcn < ntfs_bytes_to_cluster(ni->vol,
+							    ni->allocated_size))
+			lcn = LCN_EFSCORRUPTED;
+		read_unlock_irqrestore(&ni->size_lock, flags);
+		if (lcn == LCN_EFSCORRUPTED)
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
 	}
 	if (lcn != LCN_ENOENT)
 		ntfs_error(ni->vol->sb, "Failed with error code %lli.",
@@ -585,8 +626,11 @@ struct runlist_element *ntfs_attr_find_vcn_nolock(struct ntfs_inode *ni, const s
 			err = PTR_ERR(rl);
 		else if (rl->lcn >= LCN_HOLE)
 			return rl;
-		else if (rl->lcn <= LCN_ENOENT)
-			err = -EIO;
+		else if (rl->lcn <= LCN_ENOENT) {
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+			err = -EFSCORRUPTED;
+		}
 	}
 	if (!err && !is_retry) {
 		/*
@@ -608,8 +652,20 @@ struct runlist_element *ntfs_attr_find_vcn_nolock(struct ntfs_inode *ni, const s
 		}
 		if (err == -EINVAL)
 			err = -EIO;
-	} else if (!err)
-		err = -EIO;
+	} else if (!err) {
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
+		err = -EFSCORRUPTED;
+	}
+	if (err == -ENOENT) {
+		read_lock_irqsave(&ni->size_lock, flags);
+		if (vcn >= 0 && vcn < ntfs_bytes_to_cluster(ni->vol,
+							    ni->allocated_size))
+			err = -EFSCORRUPTED;
+		read_unlock_irqrestore(&ni->size_lock, flags);
+		if (err == -EFSCORRUPTED)
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+	}
 	if (err != -ENOENT)
 		ntfs_error(ni->vol->sb, "Failed with error code %i.", err);
 	return ERR_PTR(err);
@@ -3365,6 +3421,9 @@ int ntfs_attr_map_whole_runlist(struct ntfs_inode *ni)
 							   &new_rl_count);
 			if (IS_ERR(rl)) {
 				err = PTR_ERR(rl);
+				if (err != -ENOMEM)
+					ntfs_report_file_metadata_error(VFS_I(base_ni),
+									-EFSCORRUPTED);
 				goto err_out;
 			}
 			ni->runlist.rl = rl;
@@ -3374,7 +3433,9 @@ int ntfs_attr_map_whole_runlist(struct ntfs_inode *ni)
 		/* Are we in the first extent? */
 		if (!next_vcn) {
 			if (a->data.non_resident.lowest_vcn) {
-				err = -EIO;
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
+				err = -EFSCORRUPTED;
 				ntfs_error(sb,
 					"First extent of inode %llu attribute has non-zero lowest_vcn",
 					(unsigned long long)ni->mft_no);
@@ -3397,18 +3458,26 @@ int ntfs_attr_map_whole_runlist(struct ntfs_inode *ni)
 
 		/* Avoid endless loops due to corruption. */
 		if (next_vcn < le64_to_cpu(a->data.non_resident.lowest_vcn)) {
-			err = -EIO;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+			err = -EFSCORRUPTED;
 			ntfs_error(sb, "Inode %llu has corrupt attribute list",
 					(unsigned long long)ni->mft_no);
 			goto err_out;
 		}
 	}
 	if (!a) {
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		}
 		ntfs_error(sb, "Couldn't find attribute for runlist mapping");
 		goto err_out;
 	}
 	if (not_mapped && highest_vcn && highest_vcn != last_vcn - 1) {
-		err = -EIO;
+		ntfs_report_file_metadata_error(VFS_I(base_ni), -EFSCORRUPTED);
+		err = -EFSCORRUPTED;
 		ntfs_error(sb,
 			"Failed to load full runlist: inode: %llu highest_vcn: 0x%llx last_vcn: 0x%llx",
 			(unsigned long long)ni->mft_no,
diff --git a/fs/ntfs/bitmap.c b/fs/ntfs/bitmap.c
index af86fb1c199b..fabf03cf6cef 100644
--- a/fs/ntfs/bitmap.c
+++ b/fs/ntfs/bitmap.c
@@ -340,7 +340,7 @@ int __ntfs_bitmap_set_bits_in_run(struct inode *vi, const s64 start_bit,
 		ntfs_error(vi->i_sb,
 			"Failed to map subsequent page (error %i) and rollback failed (error %i). Aborting and leaving inconsistent metadata. Unmount and run chkdsk.",
 			err, pos);
-		NVolSetErrors(NTFS_SB(vi->i_sb));
+		ntfs_report_file_metadata_error(vi, pos);
 	}
 	return err;
 }
diff --git a/fs/ntfs/lcnalloc.c b/fs/ntfs/lcnalloc.c
index 3ebcac4c2864..d368207d1773 100644
--- a/fs/ntfs/lcnalloc.c
+++ b/fs/ntfs/lcnalloc.c
@@ -792,7 +792,7 @@ switch_to_data1_zone:		search_zone = 2;
 			ntfs_error(vol->sb,
 				"Failed to rollback (error %i). Leaving inconsistent metadata! Unmount and run chkdsk.",
 				err2);
-			NVolSetErrors(vol);
+			ntfs_report_metadata_error(vol, err2);
 		}
 		/* Free the runlist. */
 		kvfree(rl);
@@ -921,9 +921,11 @@ s64 __ntfs_cluster_free(struct ntfs_inode *ni, const s64 start_vcn, s64 count,
 		goto err_out;
 	}
 	if (unlikely(rl->lcn < LCN_HOLE)) {
+		ntfs_report_file_metadata_error(VFS_I(ntfs_inode_base(ni)),
+						-EFSCORRUPTED);
 		if (!is_rollback)
 			ntfs_error(vol->sb, "First runlist element has invalid lcn, aborting.");
-		err = -EIO;
+		err = -EFSCORRUPTED;
 		goto err_out;
 	}
 	/* Find the starting cluster inside the run that needs freeing. */
@@ -968,17 +970,24 @@ s64 __ntfs_cluster_free(struct ntfs_inode *ni, const s64 start_vcn, s64 count,
 			rl = ntfs_attr_find_vcn_nolock(ni, vcn, ctx);
 			if (IS_ERR(rl)) {
 				err = PTR_ERR(rl);
+				if (err == -ENOENT) {
+					err = -EFSCORRUPTED;
+					ntfs_report_file_metadata_error(VFS_I(ni),
+									-EFSCORRUPTED);
+				}
 				if (!is_rollback)
 					ntfs_error(vol->sb,
 						"Failed to map runlist fragment or failed to find subsequent runlist element.");
 				goto err_out;
 			}
 			if (unlikely(rl->lcn < LCN_HOLE)) {
+				ntfs_report_file_metadata_error(VFS_I(ntfs_inode_base(ni)),
+								-EFSCORRUPTED);
 				if (!is_rollback)
 					ntfs_error(vol->sb,
 						"Runlist element has invalid lcn (0x%llx).",
 						rl->lcn);
-				err = -EIO;
+				err = -EFSCORRUPTED;
 				goto err_out;
 			}
 		}
@@ -1073,7 +1082,7 @@ s64 __ntfs_cluster_free(struct ntfs_inode *ni, const s64 start_vcn, s64 count,
 		ntfs_error(vol->sb,
 			"Failed to rollback (error %i).  Leaving inconsistent metadata!  Unmount and run chkdsk.",
 			(int)delta);
-		NVolSetErrors(vol);
+		ntfs_report_metadata_error(vol, delta);
 	} else {
 		ntfs_dec_free_clusters(vol, delta);
 	}
diff --git a/fs/ntfs/runlist.c b/fs/ntfs/runlist.c
index 76ba6ab72b8a..606a30ab9fa7 100644
--- a/fs/ntfs/runlist.c
+++ b/fs/ntfs/runlist.c
@@ -817,7 +817,7 @@ struct runlist_element *ntfs_mapping_pairs_decompress(const struct ntfs_volume *
 	/* Validate lowest_vcn from on-disk metadata to ensure it is sane. */
 	if (overflows_type(lowest_vcn, vcn)) {
 		ntfs_error(vol->sb, "Invalid lowest_vcn in mapping pairs.");
-		return ERR_PTR(-EIO);
+		return ERR_PTR(-EFSCORRUPTED);
 	}
 	/* Start at vcn = lowest_vcn and lcn 0. */
 	vcn = lowest_vcn;
@@ -828,7 +828,7 @@ struct runlist_element *ntfs_mapping_pairs_decompress(const struct ntfs_volume *
 	attr_end = (u8 *)attr + le32_to_cpu(attr->length);
 	if (unlikely(buf < (u8 *)attr || buf >= attr_end)) {
 		ntfs_error(vol->sb, "Corrupt attribute.");
-		return ERR_PTR(-EIO);
+		return ERR_PTR(-EFSCORRUPTED);
 	}
 
 	/*
@@ -843,7 +843,7 @@ struct runlist_element *ntfs_mapping_pairs_decompress(const struct ntfs_volume *
 	     le64_to_cpu(attr->data.non_resident.data_size) ||
 	     le64_to_cpu(attr->data.non_resident.initialized_size))) {
 		ntfs_error(vol->sb, "Invalid empty mapping pairs array.");
-		return ERR_PTR(-EIO);
+		return ERR_PTR(-EFSCORRUPTED);
 	}
 	if (!vcn && !*buf && old_runlist && old_runlist->rl) {
 		*new_rl_count = old_runlist->count;
@@ -1079,12 +1079,12 @@ struct runlist_element *ntfs_mapping_pairs_decompress(const struct ntfs_volume *
 		return new_rl;
 	kvfree(rl);
 	ntfs_error(vol->sb, "Failed to merge runlists.");
-	return new_rl;
+	return PTR_ERR(new_rl) == -ENOMEM ? new_rl : ERR_PTR(-EFSCORRUPTED);
 io_error:
 	ntfs_error(vol->sb, "Corrupt attribute.");
 err_out:
 	kvfree(rl);
-	return ERR_PTR(-EIO);
+	return ERR_PTR(-EFSCORRUPTED);
 }
 
 /*
@@ -1337,7 +1337,7 @@ int ntfs_get_size_for_mapping_pairs(const struct ntfs_volume *vol,
 	if (rl->lcn == LCN_RL_NOT_MAPPED)
 		rls = -EINVAL;
 	else
-		rls = -EIO;
+		rls = -EFSCORRUPTED;
 	return rls;
 }
 
@@ -1426,7 +1426,7 @@ static inline int ntfs_write_significant_bytes(s8 *dst, const s8 *dst_max,
  * defined:
  *	-EINVAL	- Run list contains unmapped elements.  Make sure to only pass
  *		  fully mapped runlists to this function.
- *	-EIO	- The runlist is corrupt.
+ *	-EFSCORRUPTED - The runlist is corrupt.
  *	-ENOSPC	- The destination buffer is too small.
  *
  * Locking: @rl must be locked on entry (either for reading or writing), it
@@ -1585,7 +1585,7 @@ int ntfs_mapping_pairs_build(const struct ntfs_volume *vol, s8 *dst,
 	if (rl->lcn == LCN_RL_NOT_MAPPED)
 		err = -EINVAL;
 	else
-		err = -EIO;
+		err = -EFSCORRUPTED;
 	return err;
 }
 
diff --git a/fs/ntfs/runlist.h b/fs/ntfs/runlist.h
index 0d4c32b1a338..85073f9fb6ec 100644
--- a/fs/ntfs/runlist.h
+++ b/fs/ntfs/runlist.h
@@ -63,6 +63,7 @@ enum {
 	LCN_ENOMEM		= -5,
 	LCN_EIO			= -6,
 	LCN_EINVAL		= -7,
+	LCN_EFSCORRUPTED	= -8,
 };
 
 struct runlist_element *ntfs_runlists_merge(struct runlist *d_runlist,
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 09/31] ntfs: report iomap corruption and preserve buffered read errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (7 preceding siblings ...)
  2026-10-20  8:43 ` [PATCH v4 08/31] ntfs: report runlist and cluster allocation metadata errors Baolin Liu
@ 2026-10-20  8:43 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 10/31] ntfs: report LZNT1 and WOF compressed stream errors Baolin Liu
                   ` (22 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:43 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Dandan Wu <wudandan@kylinos.cn>

The iomap paths detect missing required attributes and invalid runs
without reporting them. Report these conditions as EFSCORRUPTED while
preserving allocation and underlying mapping errors.

Capture synchronous iomap errors in read_folio so buffered reads retain
their errno instead of turning corruption and allocation failures into
EIO. Keep the existing partial-read behavior.

Signed-off-by: Dandan Wu <wudandan@kylinos.cn>
---
 fs/ntfs/aops.c  | 22 ++++++++++++++++++++--
 fs/ntfs/iomap.c | 43 ++++++++++++++++++++++++++++++++++---------
 2 files changed, 54 insertions(+), 11 deletions(-)

diff --git a/fs/ntfs/aops.c b/fs/ntfs/aops.c
index 7884737657d5..474e99aa123d 100644
--- a/fs/ntfs/aops.c
+++ b/fs/ntfs/aops.c
@@ -48,6 +48,22 @@ static const struct iomap_read_ops ntfs_iomap_bio_read_ops = {
 	.submit_read		= ntfs_iomap_bio_submit_read,
 };
 
+static int ntfs_read_folio_iomap_next(const struct iomap_iter *iter,
+				      struct iomap *iomap, struct iomap *srcmap)
+{
+	int *error = iter->private;
+	int ret;
+
+	ret = ntfs_read_iomap_ops.iomap_next(iter, iomap, srcmap);
+	if (ret < 0)
+		*error = ret;
+	return ret;
+}
+
+static const struct iomap_ops ntfs_read_folio_iomap_ops = {
+	.iomap_next = ntfs_read_folio_iomap_next,
+};
+
 /*
  * ntfs_read_folio - Read data for a folio from the device
  * @file:	open file to which the folio @folio belongs or NULL
@@ -72,6 +88,7 @@ static int ntfs_read_folio(struct file *file, struct folio *folio)
 		.cur_folio = folio,
 		.ops = &ntfs_iomap_bio_read_ops,
 	};
+	int err = 0;
 
 	/*
 	 * Only $DATA attributes can be encrypted and only unnamed $DATA
@@ -103,8 +120,9 @@ static int ntfs_read_folio(struct file *file, struct folio *folio)
 			return ntfs_read_compressed_block(folio);
 	}
 
-	iomap_read_folio(&ntfs_read_iomap_ops, &ctx, NULL);
-	return 0;
+	/* iomap_read_folio() otherwise discards synchronous mapping errors. */
+	iomap_read_folio(&ntfs_read_folio_iomap_ops, &ctx, &err);
+	return err;
 }
 
 /*
diff --git a/fs/ntfs/iomap.c b/fs/ntfs/iomap.c
index c248c61854cc..e0ac03dd5138 100644
--- a/fs/ntfs/iomap.c
+++ b/fs/ntfs/iomap.c
@@ -76,8 +76,13 @@ static int ntfs_read_iomap_begin_resident(struct inode *inode, loff_t offset, lo
 
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 			CASE_SENSITIVE, 0, NULL, 0, ctx);
-	if (unlikely(err))
+	if (unlikely(err)) {
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(inode, -EFSCORRUPTED);
+		}
 		goto out;
+	}
 
 	attr_len = le32_to_cpu(ctx->attr->data.resident.value_length);
 	if (unlikely(attr_len > ni->initialized_size))
@@ -215,11 +220,12 @@ static int ntfs_read_iomap_begin_non_resident(struct inode *inode, loff_t offset
 	rl_length = ntfs_cluster_to_bytes(vol, rl->length - (vcn - rl->vcn));
 
 	if (rl_length == 0 && rl->lcn > LCN_DELALLOC) {
+		ntfs_report_file_metadata_error(inode, -EFSCORRUPTED);
 		ntfs_error(inode->i_sb,
 				"runlist(vcn : %lld, length : %lld, lcn : %lld) is corrupted\n",
 				rl->vcn, rl->length, rl->lcn);
 		up_write(&ni->runlist.lock);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	if (rl_length && length > rl_length - vcn_ofs)
@@ -418,7 +424,7 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 		err = ntfs_map_runlist(ni, vcn);
 		if (err) {
 			mutex_unlock(&mrec_ni->mrec_lock);
-			return -ENOENT;
+			return err;
 		}
 		down_read(&ni->runlist.lock);
 		rl = ni->runlist.rl;
@@ -430,9 +436,17 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 	/* Seek to element containing target vcn. */
 	rl = __ntfs_attr_find_vcn_nolock(&ni->runlist, vcn);
 	if (IS_ERR(rl)) {
+		unsigned long irqflags;
+		s64 allocated_size;
+
+		read_lock_irqsave(&ni->size_lock, irqflags);
+		allocated_size = ni->allocated_size;
+		read_unlock_irqrestore(&ni->size_lock, irqflags);
+		if (offset < allocated_size)
+			ntfs_report_file_metadata_error(inode, -EFSCORRUPTED);
 		up_write(&ni->runlist.lock);
 		mutex_unlock(&mrec_ni->mrec_lock);
-		return -EIO;
+		return offset < allocated_size ? -EFSCORRUPTED : -EIO;
 	}
 	lcn = ntfs_rl_vcn_to_lcn(rl, vcn);
 
@@ -451,19 +465,22 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 	 * extent over clusters that are allocated on disk but unknown to us.
 	 */
 	if (lcn == LCN_RL_NOT_MAPPED) {
+		if (!map_err || map_err == -ENOENT)
+			ntfs_report_file_metadata_error(inode, -EFSCORRUPTED);
 		up_write(&ni->runlist.lock);
 		mutex_unlock(&mrec_ni->mrec_lock);
-		return map_err == -ENOMEM ? -ENOMEM : -EIO;
+		return !map_err || map_err == -ENOENT ? -EFSCORRUPTED : map_err;
 	}
 
 	max_clu_count = min(max_clu_count, rl->length - (vcn - rl->vcn));
 	if (max_clu_count == 0) {
+		ntfs_report_file_metadata_error(inode, -EFSCORRUPTED);
 		ntfs_error(inode->i_sb,
 				"runlist(vcn : %lld, length : %lld) is corrupted\n",
 				rl->vcn, rl->length);
 		up_write(&ni->runlist.lock);
 		mutex_unlock(&mrec_ni->mrec_lock);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	iomap->bdev = inode->i_sb->s_bdev;
@@ -506,11 +523,17 @@ static int ntfs_write_simple_iomap_begin_non_resident(struct inode *inode, loff_
 			rl = ntfs_runlists_merge(&ni->runlist, rlc, 0,
 					&new_rl_count);
 			if (IS_ERR(rl)) {
+				err = PTR_ERR(rl);
+				if (err != -ENOMEM) {
+					err = -EFSCORRUPTED;
+					ntfs_report_file_metadata_error(inode,
+									err);
+				}
 				ntfs_error(vol->sb, "Failed to merge runlists");
 				up_write(&ni->runlist.lock);
 				mutex_unlock(&mrec_ni->mrec_lock);
 				kvfree(rlc);
-				return PTR_ERR(rl);
+				return err;
 			}
 
 			ni->runlist.rl = rl;
@@ -698,8 +721,10 @@ static int ntfs_write_iomap_begin_resident(struct inode *inode, loff_t offset,
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 			CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (err) {
-		if (err == -ENOENT)
-			err = -EIO;
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(inode, -EFSCORRUPTED);
+		}
 		goto out;
 	}
 
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 10/31] ntfs: report LZNT1 and WOF compressed stream errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (8 preceding siblings ...)
  2026-10-20  8:43 ` [PATCH v4 09/31] ntfs: report iomap corruption and preserve buffered read errors Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 11/31] ntfs: handle source record removal failure during attribute moves Baolin Liu
                   ` (21 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Dandan Wu <wudandan@kylinos.cn>

Compressed I/O can detect malformed LZNT1 data or WOF metadata and
compressed data without reporting it. Report corruption as EFSCORRUPTED
and retain underlying read, mapping and allocation errors.

Save compression-buffer allocation errors before cleanup so ENOMEM does
not become EIO. Report failed mapping updates after compressed writes
and failed cluster release separately from the initiating error.

Signed-off-by: Dandan Wu <wudandan@kylinos.cn>
---
Testing:
Fault injection of compression-buffer allocation failure confirms that
it emits no error event and a subsequent read can succeed.

 fs/ntfs/compress.c | 30 +++++++++++++++++++++++-------
 fs/ntfs/wof.c      | 31 ++++++++++++++++++++++++++-----
 2 files changed, 49 insertions(+), 12 deletions(-)

diff --git a/fs/ntfs/compress.c b/fs/ntfs/compress.c
index 82c7c7d29073..a82c05bd7625 100644
--- a/fs/ntfs/compress.c
+++ b/fs/ntfs/compress.c
@@ -585,11 +585,13 @@ int ntfs_read_compressed_block(struct folio *folio)
 	nr_cbs--;
 
 	mutex_lock(&ntfs_cb_lock);
-	if (!ntfs_compression_buffer)
-		if (allocate_compression_buffers()) {
+	if (!ntfs_compression_buffer) {
+		err = allocate_compression_buffers();
+		if (err) {
 			mutex_unlock(&ntfs_cb_lock);
 			goto err_out;
 		}
+	}
 
 
 	cb = ntfs_compression_buffer;
@@ -633,7 +635,8 @@ int ntfs_read_compressed_block(struct folio *folio)
 			 * duration.
 			 */
 			up_read(&ni->runlist.lock);
-			if (!ntfs_map_runlist(ni, vcn))
+			err = ntfs_map_runlist(ni, vcn);
+			if (!err)
 				goto lock_retry_remap;
 			mutex_unlock(&ntfs_cb_lock);
 			goto map_rl_err;
@@ -787,6 +790,8 @@ int ntfs_read_compressed_block(struct folio *folio)
 			ntfs_error(vol->sb,
 				"ntfs_decompress() failed in inode 0x%llx with error code %i. Skipping this compression block.",
 				ni->mft_no, -err);
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(ni), err);
 			/* Release the unfinished pages. */
 			for (; prev_cur_page < cur_page; prev_cur_page++) {
 				page = pages[prev_cur_page];
@@ -839,7 +844,9 @@ int ntfs_read_compressed_block(struct folio *folio)
 	goto err_out;
 
 rl_err:
+	err = -EFSCORRUPTED;
 	up_read(&ni->runlist.lock);
+	ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
 	ntfs_error(vol->sb, "ntfs_rl_vcn_to_lcn() failed. Cannot read compression block.");
 	goto err_out;
 
@@ -859,7 +866,7 @@ int ntfs_read_compressed_block(struct folio *folio)
 	}
 	kfree(pages);
 	kfree(completed_pages);
-	return -EIO;
+	return err;
 }
 
 /*
@@ -1443,6 +1450,9 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages,
 		up_write(&ni->runlist.lock);
 		ntfs_error(vol->sb, "Failed to merge runlists");
 		err = PTR_ERR(rl);
+		if (err != -ENOMEM)
+			err = -EFSCORRUPTED;
+		ntfs_report_file_metadata_error(VFS_I(ni), err);
 		goto free_rlc;
 	}
 
@@ -1453,12 +1463,18 @@ static int ntfs_write_cb(struct ntfs_inode *ni, loff_t pos, struct page **pages,
 	err = ntfs_attr_update_mapping_pairs_locked(ni, 0, ni);
 	up_write(&ni->runlist.lock);
 	if (err)
-		err = -EIO;
+		ntfs_report_file_metadata_error(VFS_I(ni), err);
 	goto out;
 
 free_rlc:
-	if (ntfs_cluster_free_from_rl(vol, rlc))
-		ntfs_error(vol->sb, "Failed to free hot clusters.");
+	{
+		int free_err = ntfs_cluster_free_from_rl(vol, rlc);
+
+		if (free_err) {
+			ntfs_report_file_metadata_error(VFS_I(ni), free_err);
+			ntfs_error(vol->sb, "Failed to free hot clusters.");
+		}
+	}
 	kvfree(rlc);
 out:
 	NInoSetFileNameDirty(ni);
diff --git a/fs/ntfs/wof.c b/fs/ntfs/wof.c
index e64f40835957..30b048bcac83 100644
--- a/fs/ntfs/wof.c
+++ b/fs/ntfs/wof.c
@@ -294,7 +294,7 @@ static int parse_wof_chunk_table(struct ntfs_inode *base_ni,
 		ret = ntfs_bdev_read_from_rl(ni->vol, &ni->runlist,
 					     start_sector, sectors, buf);
 		if (ret)
-			return -EIO;
+			return ret;
 		buf += sector_off;
 	} else {
 		struct ntfs_attr_search_ctx *ctx;
@@ -685,15 +685,21 @@ int ntfs_read_wof_compressed_block(struct folio *folio)
 				   WOF_NAME_LEN);
 	if (IS_ERR(wof_inode)) {
 		err = PTR_ERR(wof_inode);
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+		}
 		goto out;
 	}
 
 	wof_ni = NTFS_I(wof_inode);
 	if (wof_ni->initialized_size != wof_ni->data_size) {
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
 		ntfs_error(vol->sb,
 			   "WOF compressed stream is not fully initialized (init %lld, data %lld).",
 			   wof_ni->initialized_size, wof_ni->data_size);
-		err = -EIO;
+		err = -EFSCORRUPTED;
 		goto out_iput;
 	}
 	if (NInoNonResident(wof_ni) && !NInoFullyMapped(wof_ni)) {
@@ -725,21 +731,36 @@ int ntfs_read_wof_compressed_block(struct folio *folio)
 		err = parse_wof_chunk_table(ni, wof_ni, chunk_idx, chunk_count,
 					    decomp_size, &chunk_offset,
 					    &chunk_size, input, input_size);
-		if (err)
+		if (err) {
+			if (err == -EINVAL || err == -EOVERFLOW || err == -ENOENT)
+				err = -EFSCORRUPTED;
+			if (err == -EIO || err == -EFSCORRUPTED)
+				ntfs_report_file_metadata_error(VFS_I(ni), err);
 			goto out_free_input;
+		}
 
 		err = ntfs_read_wof_chunk(vol, wof_ni, chunk_offset, chunk_size,
 					  input, input_size, &chunk_mem);
-		if (err)
+		if (err) {
+			if (err == -EINVAL || err == -EOVERFLOW || err == -ENOENT) {
+				err = -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(VFS_I(ni), err);
+			}
 			goto out_free_input;
+		}
 
 		err = ntfs_wof_decompress_chunk(ws, vol, mapping, folio,
 						folio_start, folio_end,
 						chunk_idx * ws->comp_unit,
 						chunk_mem, chunk_size,
 						decomp_size);
-		if (err)
+		if (err) {
+			if (err != -ENOMEM) {
+				err = -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(VFS_I(ni), err);
+			}
 			goto out_free_input;
+		}
 	}
 
 	if (folio_end > i_size)
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 11/31] ntfs: handle source record removal failure during attribute moves
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (9 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 10/31] ntfs: report LZNT1 and WOF compressed stream errors Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 12/31] ntfs: report attribute lookup and validation errors Baolin Liu
                   ` (20 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Xuewen Wang <wangxuewen@kylinos.cn>

ntfs_attr_record_move_to() ignores failure to remove the source attribute
after copying it. It can update the attribute-list reference and report
success with both copies present.

Check removal before changing the list entry. On failure, attempt to
remove the destination copy, mark its record dirty and unmap it, leaving
the list pointing to the source. Report destination cleanup failure
separately.

Shrinking a record to zero needs no extra space, so failure indicates
inconsistent record accounting. Return and report EFSCORRUPTED even if
cleanup also fails.

Fixes: 495e90fa3348 ("ntfs: update attrib operations")
Signed-off-by: Xuewen Wang <wangxuewen@kylinos.cn>
---
 fs/ntfs/attrib.c | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index c59643fb766a..07fb1c77c7af 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -3576,7 +3576,17 @@ int ntfs_attr_record_move_to(struct ntfs_attr_search_ctx *ctx, struct ntfs_inode
 	nctx->attr->instance = nctx->mrec->next_attr_instance;
 	nctx->mrec->next_attr_instance =
 		cpu_to_le16((le16_to_cpu(nctx->mrec->next_attr_instance) + 1) & 0xffff);
-	ntfs_attr_record_resize(ctx->mrec, a, 0);
+	err = ntfs_attr_record_resize(ctx->mrec, a, 0);
+	if (err) {
+		err = -EFSCORRUPTED;
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
+		if (ntfs_attr_record_resize(ni_mrec, nctx->attr, 0))
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+		mark_mft_record_dirty(ni);
+		unmap_mft_record(ni);
+		goto put_err_out;
+	}
 	mark_mft_record_dirty(ctx->ntfs_ino);
 	mark_mft_record_dirty(ni);
 
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 12/31] ntfs: report attribute lookup and validation errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (10 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 11/31] ntfs: handle source record removal failure during attribute moves Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 13/31] ntfs: report attribute record update and cleanup failures Baolin Liu
                   ` (19 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

Attribute lookup and reading can detect malformed records and lists,
invalid stored flags or sizes, and short metadata reads without reporting
them. Report corruption against the base file and use EFSCORRUPTED.

Report missing required attributes and missing AttrDef definitions while
preserving underlying errors, ordinary lookup misses and size-limit
results.

Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
 fs/ntfs/attrib.c | 86 +++++++++++++++++++++++++++++++++---------------
 fs/ntfs/attrib.h |  4 +--
 2 files changed, 62 insertions(+), 28 deletions(-)

diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index 07fb1c77c7af..9eafa87dce7b 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -894,7 +894,7 @@ static bool ntfs_attr_value_is_valid(struct ntfs_volume *vol,
  * @ctx->attr will point to the attribute before which the attribute being
  * searched for would need to be inserted if such an action were to be desired.
  *
- * On actual error, ntfs_attr_find() returns -EIO.  In this case @ctx->attr is
+ * On actual error, ntfs_attr_find() returns -EFSCORRUPTED.  In this case @ctx->attr is
  * undefined and in particular do not rely on it not changing.
  *
  * If @ctx->is_first is 'true', the search begins with @ctx->attr itself.  If it
@@ -932,6 +932,7 @@ static int ntfs_attr_find(const __le32 type, const __le16 *name,
 		const u8 *val, const u32 val_len, struct ntfs_attr_search_ctx *ctx)
 {
 	struct attr_record *a;
+	struct ntfs_inode *base_ni;
 	struct ntfs_volume *vol = ctx->ntfs_ino->vol;
 	__le16 *upcase = vol->upcase;
 	u32 upcase_len = vol->upcase_len;
@@ -1058,8 +1059,9 @@ static int ntfs_attr_find(const __le32 type, const __le16 *name,
 	}
 	ntfs_error(vol->sb, "mft %#llx, type %#x is corrupt. Run chkdsk.",
 		   (long long)ctx->ntfs_ino->mft_no, le32_to_cpu(type));
-	NVolSetErrors(vol);
-	return -EIO;
+	base_ni = ntfs_inode_base(ctx->ntfs_ino);
+	ntfs_report_file_metadata_error(VFS_I(base_ni), -EFSCORRUPTED);
+	return -EFSCORRUPTED;
 }
 
 void ntfs_attr_name_free(unsigned char **name)
@@ -1157,31 +1159,42 @@ bool ntfs_attr_list_is_valid(const u8 *al_start, s64 size)
 int load_attribute_list(struct ntfs_inode *base_ni, u8 *al_start, const s64 size)
 {
 	struct inode *attr_vi = NULL;
+	s64 read;
 
 	if (!al_start || size <= 0)
 		return -EINVAL;
 
 	attr_vi = ntfs_attr_iget(VFS_I(base_ni), AT_ATTRIBUTE_LIST, AT_UNNAMED, 0);
 	if (IS_ERR(attr_vi)) {
+		if (PTR_ERR(attr_vi) == -ENOENT) {
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+			attr_vi = ERR_PTR(-EFSCORRUPTED);
+		}
 		ntfs_error(base_ni->vol->sb,
 			   "Failed to open an inode for Attribute list, mft = %llu",
 			   base_ni->mft_no);
 		return PTR_ERR(attr_vi);
 	}
 
-	if (ntfs_inode_attr_pread(attr_vi, 0, size, al_start) != size) {
+	read = ntfs_inode_attr_pread(attr_vi, 0, size, al_start);
+	if (read != size) {
+		if (read >= 0)
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
 		iput(attr_vi);
 		ntfs_error(base_ni->vol->sb,
 			   "Failed to read attribute list, mft = %llu",
 			   base_ni->mft_no);
-		return -EIO;
+		return read < 0 ? read : -EFSCORRUPTED;
 	}
 	iput(attr_vi);
 
 	if (!ntfs_attr_list_is_valid(al_start, size)) {
+		ntfs_report_file_metadata_error(VFS_I(base_ni), -EFSCORRUPTED);
 		ntfs_error(base_ni->vol->sb, "Corrupt attribute list, mft = %llu",
 			   base_ni->mft_no);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 	return 0;
 }
@@ -1233,7 +1246,7 @@ int load_attribute_list(struct ntfs_inode *base_ni, u8 *al_start, const s64 size
  * attribute list entry for the inserted attribute should be inserted in the
  * attribute list attribute at @ctx->al_entry.
  *
- * On actual error, ntfs_external_attr_find() returns -EIO.  In this case
+ * On actual error, ntfs_external_attr_find() returns -EFSCORRUPTED.  In this case
  * @ctx->attr is undefined and in particular do not rely on it not changing.
  */
 static int ntfs_external_attr_find(const __le32 type,
@@ -1355,7 +1368,9 @@ static int ntfs_external_attr_find(const __le32 type,
 
 			/* Not found?!? Absurd! */
 			ntfs_error(ctx->ntfs_ino->vol->sb, "Attribute list wasn't found");
-			return -EIO;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+			return -EFSCORRUPTED;
 		}
 	}
 	for (;; al_entry = next_al_entry) {
@@ -1454,7 +1469,8 @@ static int ntfs_external_attr_find(const __le32 type,
 				ntfs_error(vol->sb,
 					"Found stale mft reference in attribute list of base inode 0x%llx.%s",
 					base_ni->mft_no, es);
-				err = -EIO;
+
+				err = -EFSCORRUPTED;
 				break;
 			}
 		} else { /* Mft references do not match. */
@@ -1479,7 +1495,7 @@ static int ntfs_external_attr_find(const __le32 type,
 							base_ni->mft_no, es);
 					err = PTR_ERR(ctx->mrec);
 					if (err == -ENOENT)
-						err = -EIO;
+						err = -EFSCORRUPTED;
 					/* Cause @ctx to be sanitized below. */
 					ni = NULL;
 					break;
@@ -1533,7 +1549,7 @@ static int ntfs_external_attr_find(const __le32 type,
 		/*
 		 * If the type and/or the name are mismatched between the
 		 * attribute list entry and the attribute record, there is
-		 * corruption so we break and return error EIO.
+		 * corruption so we break and return error EFSCORRUPTED.
 		 */
 		if (al_entry->type != a->type)
 			break;
@@ -1592,11 +1608,17 @@ static int ntfs_external_attr_find(const __le32 type,
 			"Base inode 0x%llx contains corrupt attribute, mft %#llx, type %#x. %s",
 			(long long)base_ni->mft_no, (long long)mft_no, type,
 			"Unmount and run chkdsk.");
-		err = -EIO;
+
+		err = -EFSCORRUPTED;
 	}
 
-	if (err != -ENOMEM)
-		NVolSetErrors(vol);
+	if (err != -ENOMEM) {
+		if (err != -EINTR && err != -ERESTARTSYS)
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							err);
+		else
+			NVolSetErrors(vol);
+	}
 	return err;
 not_found:
 	/*
@@ -1824,8 +1846,8 @@ void ntfs_attr_put_search_ctx(struct ntfs_attr_search_ctx *ctx)
  *
  * Return the attribute type definition record if found and NULL if not found.
  */
-static struct attr_def *ntfs_attr_find_in_attrdef(const struct ntfs_volume *vol,
-		const __le32 type)
+static struct attr_def *ntfs_attr_find_in_attrdef(struct ntfs_volume *vol,
+						  const __le32 type)
 {
 	struct attr_def *ad;
 
@@ -1841,7 +1863,8 @@ static struct attr_def *ntfs_attr_find_in_attrdef(const struct ntfs_volume *vol,
 		/* We have gone too far already.  No point in continuing. */
 		break;
 	}
-	/* Attribute not found. */
+	/* The requested attribute type must have a definition on this volume. */
+	ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 	ntfs_debug("Attribute type 0x%x not found in $AttrDef.",
 			le32_to_cpu(type));
 	return NULL;
@@ -1856,8 +1879,8 @@ static struct attr_def *ntfs_attr_find_in_attrdef(const struct ntfs_volume *vol,
  * Check whether the @size in bytes is valid for an attribute of @type on the
  * ntfs volume @vol.  This information is obtained from $AttrDef system file.
  */
-int ntfs_attr_size_bounds_check(const struct ntfs_volume *vol, const __le32 type,
-		const s64 size)
+int ntfs_attr_size_bounds_check(struct ntfs_volume *vol, const __le32 type,
+				const s64 size)
 {
 	struct attr_def *ad;
 
@@ -1873,7 +1896,7 @@ int ntfs_attr_size_bounds_check(const struct ntfs_volume *vol, const __le32 type
 	/* Get the $AttrDef entry for the attribute @type. */
 	ad = ntfs_attr_find_in_attrdef(vol, type);
 	if (unlikely(!ad))
-		return -ENOENT;
+		return -EFSCORRUPTED;
 	/* Do the bounds check. */
 	if (((le64_to_cpu(ad->min_size) > 0) &&
 			size < le64_to_cpu(ad->min_size)) ||
@@ -1891,15 +1914,15 @@ int ntfs_attr_size_bounds_check(const struct ntfs_volume *vol, const __le32 type
  * Check whether the attribute of @type on the ntfs volume @vol is allowed to
  * be non-resident.  This information is obtained from $AttrDef system file.
  */
-static int ntfs_attr_can_be_non_resident(const struct ntfs_volume *vol,
-		const __le32 type)
+static int ntfs_attr_can_be_non_resident(struct ntfs_volume *vol,
+					 const __le32 type)
 {
 	struct attr_def *ad;
 
 	/* Find the attribute definition record in $AttrDef. */
 	ad = ntfs_attr_find_in_attrdef(vol, type);
 	if (unlikely(!ad))
-		return -ENOENT;
+		return -EFSCORRUPTED;
 	/* Check the flags and return the result. */
 	if (ad->flags & ATTR_DEF_RESIDENT)
 		return -EPERM;
@@ -3281,10 +3304,11 @@ int ntfs_attr_open(struct ntfs_inode *ni, const __le32 type,
 	    ((!(a->flags & ATTR_IS_COMPRESSED) != !NInoCompressed(ni)) ||
 	     (!(a->flags & ATTR_IS_SPARSE)     != !NInoSparse(ni)) ||
 	     (!(a->flags & ATTR_IS_ENCRYPTED)  != !NInoEncrypted(ni)))) {
-		err = -EIO;
+		err = -EFSCORRUPTED;
 		pr_err("Inode %lld has corrupt attribute flags (0x%x <> 0x%x)\n",
 				(unsigned long long)ni->mft_no,
 				a->flags, ni->flags);
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
 		goto put_err_out;
 	}
 
@@ -3300,7 +3324,9 @@ int ntfs_attr_open(struct ntfs_inode *ni, const __le32 type,
 		}
 
 		if ((a->flags & ATTR_IS_COMPRESSED) && !a->data.non_resident.compression_unit) {
-			err = -EIO;
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
 			pr_err("Compressed inode %lld attr 0x%x has no compression unit\n",
 					(unsigned long long)ni->mft_no, type);
 			goto put_err_out;
@@ -5653,6 +5679,11 @@ void *ntfs_attr_readall(struct ntfs_inode *ni, const __le32 type,
 	}
 	bmp_ni = NTFS_I(bmp_vi);
 
+	if (bmp_ni->data_size < 0) {
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
+		ret = ERR_PTR(-EFSCORRUPTED);
+		goto out;
+	}
 	if (bmp_ni->data_size > NTFS_ATTR_READALL_MAX_SIZE &&
 		(bmp_ni->type != AT_BITMAP ||
 		bmp_ni->data_size > ((ni->vol->nr_clusters + 7) >> 3))) {
@@ -5670,8 +5701,11 @@ void *ntfs_attr_readall(struct ntfs_inode *ni, const __le32 type,
 	size = ntfs_inode_attr_pread(VFS_I(bmp_ni), 0, bmp_ni->data_size,
 			(u8 *)data);
 	if (size != bmp_ni->data_size) {
+		if (size >= 0 || size == -EIO)
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							size < 0 ? (int)size : -EFSCORRUPTED);
 		ntfs_error(sb, "ntfs_attr_pread failed");
-		ret = size < 0 ? ERR_PTR((int)size) : ERR_PTR(-EIO);
+		ret = size < 0 ? ERR_PTR((int)size) : ERR_PTR(-EFSCORRUPTED);
 		kvfree(data);
 		goto out;
 	}
diff --git a/fs/ntfs/attrib.h b/fs/ntfs/attrib.h
index 9d6bf0320219..5a133ba741a6 100644
--- a/fs/ntfs/attrib.h
+++ b/fs/ntfs/attrib.h
@@ -89,8 +89,8 @@ void ntfs_attr_reinit_search_ctx(struct ntfs_attr_search_ctx *ctx);
 struct ntfs_attr_search_ctx *ntfs_attr_get_search_ctx(struct ntfs_inode *ni,
 		struct mft_record *mrec);
 void ntfs_attr_put_search_ctx(struct ntfs_attr_search_ctx *ctx);
-int ntfs_attr_size_bounds_check(const struct ntfs_volume *vol,
-		const __le32 type, const s64 size);
+int ntfs_attr_size_bounds_check(struct ntfs_volume *vol,
+				const __le32 type, const s64 size);
 int ntfs_attr_can_be_resident(const struct ntfs_volume *vol,
 		const __le32 type);
 int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 13/31] ntfs: report attribute record update and cleanup failures
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (11 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 12/31] ntfs: report attribute lookup and validation errors Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 14/31] ntfs: report attribute list update and recovery failures Baolin Liu
                   ` (18 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Xuewen Wang <wangxuewen@kylinos.cn>

Attribute creation, removal and relocation can fail after modifying
metadata without reporting the incomplete update. Report failed record
restoration, cluster release and cleanup, preserving the initiating
error separately from recovery failures.

Include interrupted removal after clusters or records have been freed.
Retain best-effort removal behavior and ordinary early allocation errors;
the earlier cluster-count fix keeps successful deallocation from being
reported as a failure.

Signed-off-by: Xuewen Wang <wangxuewen@kylinos.cn>
---
 fs/ntfs/attrib.c | 113 ++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 87 insertions(+), 26 deletions(-)

diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index 9eafa87dce7b..66845c78ab8d 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -2558,7 +2558,6 @@ int ntfs_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 		goto put_err_out;
 	}
 	if (err != -ENOENT) {
-		err = -EIO;
 		goto put_err_out;
 	}
 	a = ctx->attr;
@@ -2609,7 +2608,9 @@ int ntfs_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 	if (type != AT_ATTRIBUTE_LIST && NInoAttrList(base_ni)) {
 		err = ntfs_attrlist_entry_add(ni, a);
 		if (err) {
-			ntfs_attr_record_resize(m, a, 0);
+			if (ntfs_attr_record_resize(m, a, 0))
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
 			mark_mft_record_dirty(ctx->ntfs_ino);
 			ntfs_debug("Failed add attribute entry to ATTRIBUTE_LIST.\n");
 			goto put_err_out;
@@ -2682,7 +2683,6 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 	}
 	if (err != -ENOENT) {
 		pr_err("ntfs_attr_find failed\n");
-		err = -EIO;
 		goto put_err_out;
 	}
 	a = ctx->attr;
@@ -2737,7 +2737,9 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 		err = ntfs_attrlist_entry_add(ni, a);
 		if (err) {
 			pr_err("Failed add attr entry to attrlist\n");
-			ntfs_attr_record_resize(m, a, 0);
+			if (ntfs_attr_record_resize(m, a, 0))
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
 			goto put_err_out;
 		}
 	}
@@ -2751,6 +2753,11 @@ static int ntfs_non_resident_attr_record_add(struct ntfs_inode *ni, __le32 type,
 	err = ntfs_attr_lookup(type, name, name_len, ic,
 				lowest_vcn, NULL, 0, ctx);
 	if (err) {
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		}
 		pr_err("%s: attribute lookup failed\n", __func__);
 		ntfs_attr_put_search_ctx(ctx);
 		return err;
@@ -2792,8 +2799,9 @@ int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx)
 
 	/* Remove attribute itself. */
 	if (ntfs_attr_record_resize(ctx->mrec, ctx->attr, 0)) {
+		ntfs_report_file_metadata_error(VFS_I(base_ni), -EFSCORRUPTED);
 		ntfs_debug("Couldn't remove attribute record. Bug or damaged MFT record.\n");
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 	mark_mft_record_dirty(ni);
 
@@ -2804,6 +2812,7 @@ int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx)
 	if (NInoAttrList(base_ni) && type != AT_ATTRIBUTE_LIST) {
 		err = ntfs_attrlist_entry_rm(ctx);
 		if (err) {
+			ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 			ntfs_debug("Couldn't delete record from $ATTRIBUTE_LIST.\n");
 			return err;
 		}
@@ -2820,9 +2829,11 @@ int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx)
 	/* Free MFT record, if it doesn't contain attributes. */
 	if (le32_to_cpu(ctx->mrec->bytes_in_use) -
 			le16_to_cpu(ctx->mrec->attrs_offset) == 8) {
-		if (ntfs_mft_record_free(ni->vol, ni)) {
+		err = ntfs_mft_record_free(ni->vol, ni);
+		if (err) {
+			ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 			ntfs_debug("Couldn't free MFT record.\n");
-			return -EIO;
+			return err;
 		}
 		/* Remove done if we freed base inode. */
 		if (ni == base_ni)
@@ -2840,8 +2851,14 @@ int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx)
 		struct inode *attr_vi;
 
 		ntfs_attr_reinit_search_ctx(ctx);
-		if (ntfs_attr_lookup(AT_ATTRIBUTE_LIST, NULL, 0, CASE_SENSITIVE,
-					0, NULL, 0, ctx)) {
+		err = ntfs_attr_lookup(AT_ATTRIBUTE_LIST, NULL, 0, CASE_SENSITIVE,
+				       0, NULL, 0, ctx);
+		if (err) {
+			if (err == -ENOENT) {
+				err = -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
+			}
 			ntfs_debug("Couldn't find attribute list. Succeed anyway.\n");
 			return 0;
 		}
@@ -2853,11 +2870,17 @@ int ntfs_attr_record_rm(struct ntfs_attr_search_ctx *ctx)
 			al_rl = ntfs_mapping_pairs_decompress(base_ni->vol,
 					ctx->attr, NULL, &new_rl_count);
 			if (IS_ERR(al_rl)) {
+				if (PTR_ERR(al_rl) != -ENOMEM)
+					ntfs_report_file_metadata_error(VFS_I(base_ni),
+									-EFSCORRUPTED);
 				ntfs_debug("Couldn't decompress attribute list runlist. Succeed anyway.\n");
 				return 0;
 			}
-			if (ntfs_cluster_free_from_rl(base_ni->vol, al_rl))
+			err = ntfs_cluster_free_from_rl(base_ni->vol, al_rl);
+			if (err) {
+				ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 				ntfs_debug("Leaking clusters! Run chkdsk. Couldn't free clusters from attribute list runlist.\n");
+			}
 			kvfree(al_rl);
 		}
 		/* Remove attribute record itself. */
@@ -2913,7 +2936,7 @@ int ntfs_attr_add(struct ntfs_inode *ni, __le32 type,
 {
 	struct super_block *sb;
 	u32 attr_rec_size;
-	int err, i, offset;
+	int err, i, offset, rollback_err;
 	bool is_resident;
 	bool can_be_non_resident = false;
 	struct ntfs_inode *attr_ni;
@@ -2990,7 +3013,7 @@ int ntfs_attr_add(struct ntfs_inode *ni, __le32 type,
 retry:
 	ni_mrec = map_mft_record(ni);
 	if (IS_ERR(ni_mrec)) {
-		err = -EIO;
+		err = PTR_ERR(ni_mrec);
 		goto err_out;
 	}
 
@@ -3013,7 +3036,7 @@ int ntfs_attr_add(struct ntfs_inode *ni, __le32 type,
 		attr_ni = ni->ext.extent_ntfs_inos[i];
 		ni_mrec = map_mft_record(attr_ni);
 		if (IS_ERR(ni_mrec)) {
-			err = -EIO;
+			err = PTR_ERR(ni_mrec);
 			goto err_out;
 		}
 
@@ -3079,18 +3102,31 @@ int ntfs_attr_add(struct ntfs_inode *ni, __le32 type,
 	attr_vi = ntfs_attr_iget(VFS_I(ni), type, name, name_len);
 	if (IS_ERR(attr_vi)) {
 		err = PTR_ERR(attr_vi);
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+		}
 		ntfs_error(sb, "Failed to open just added attribute");
 		goto rm_attr_err_out;
 	}
 	attr_ni = NTFS_I(attr_vi);
 
 	/* Resize and set attribute value. */
-	if (ntfs_attr_truncate(attr_ni, size) ||
-		(val && (ntfs_inode_attr_pwrite(attr_vi, 0, size, val, false) != size))) {
-		err = -EIO;
+	err = ntfs_attr_truncate(attr_ni, size);
+	if (!err && val) {
+		s64 written = ntfs_inode_attr_pwrite(attr_vi, 0, size, val, false);
+
+		if (written != size)
+			err = written < 0 ? written : -EIO;
+	}
+	if (err) {
 		ntfs_error(sb, "Failed to initialize just added attribute");
-		if (ntfs_attr_rm(attr_ni))
+		rollback_err = ntfs_attr_rm(attr_ni);
+		if (rollback_err) {
+			ntfs_report_file_metadata_error(VFS_I(ni), rollback_err);
 			ntfs_error(sb, "Failed to remove just added attribute");
+		}
 		iput(attr_vi);
 		goto err_out;
 	}
@@ -3101,12 +3137,18 @@ int ntfs_attr_add(struct ntfs_inode *ni, __le32 type,
 	/* Remove just added attribute. */
 	ni_mrec = map_mft_record(attr_ni);
 	if (!IS_ERR(ni_mrec)) {
-		if (ntfs_attr_record_resize(ni_mrec,
-					(struct attr_record *)((u8 *)ni_mrec + offset), 0))
+		struct attr_record *a = (struct attr_record *)((u8 *)ni_mrec + offset);
+
+		rollback_err = ntfs_attr_record_resize(ni_mrec, a, 0);
+		if (rollback_err) {
+			ntfs_report_file_metadata_error(VFS_I(ni), rollback_err);
 			ntfs_error(sb, "Failed to remove just added attribute #2");
+		}
 		unmap_mft_record(attr_ni);
-	} else
+	} else {
+		ntfs_report_file_metadata_error(VFS_I(ni), PTR_ERR(ni_mrec));
 		pr_err("EIO when try to remove new added attr\n");
+	}
 
 free_err_out:
 	/* Free MFT record, if it doesn't contain attributes. */
@@ -3118,13 +3160,18 @@ int ntfs_attr_add(struct ntfs_inode *ni, __le32 type,
 			le16_to_cpu(ni_mrec->attrs_offset);
 		unmap_mft_record(attr_ni);
 		if (attr_size == 8) {
-			if (ntfs_mft_record_free(attr_ni->vol, attr_ni))
+			rollback_err = ntfs_mft_record_free(attr_ni->vol, attr_ni);
+			if (rollback_err) {
+				ntfs_report_file_metadata_error(VFS_I(ni), rollback_err);
 				ntfs_error(sb, "Failed to free MFT record");
+			}
 			if (attr_ni->nr_extents < 0)
 				ntfs_inode_close(attr_ni);
 		}
-	} else
+	} else {
+		ntfs_report_file_metadata_error(VFS_I(ni), PTR_ERR(ni_mrec));
 		pr_err("EIO when testing mft record is free-able\n");
+	}
 
 err_out:
 	return err;
@@ -3587,7 +3634,7 @@ int ntfs_attr_record_move_to(struct ntfs_attr_search_ctx *ctx, struct ntfs_inode
 	/* Make space and move attribute. */
 	ni_mrec = map_mft_record(ni);
 	if (IS_ERR(ni_mrec)) {
-		err = -EIO;
+		err = PTR_ERR(ni_mrec);
 		goto put_err_out;
 	}
 
@@ -3676,7 +3723,7 @@ int ntfs_attr_record_move_away(struct ntfs_attr_search_ctx *ctx, int extra)
 			ntfs_error(sb, "Can not map mft record for mft_no %lld",
 					(unsigned long long)ni->mft_no);
 			mutex_unlock(&base_ni->extent_lock);
-			return -EIO;
+			return PTR_ERR(m);
 		}
 		if (le32_to_cpu(m->bytes_allocated) -
 		    le32_to_cpu(m->bytes_in_use) < le32_to_cpu(ctx->attr->length) + extra) {
@@ -3711,11 +3758,16 @@ int ntfs_attr_record_move_away(struct ntfs_attr_search_ctx *ctx, int extra)
 
 	err = ntfs_attr_record_move_to(ctx, ni);
 	if (err) {
+		int free_err;
+
 		ntfs_error(sb, "Couldn't move attribute to MFT record");
-		if (ntfs_mft_record_free(base_ni->vol, ni))
+		free_err = ntfs_mft_record_free(base_ni->vol, ni);
+		if (free_err) {
+			ntfs_report_file_metadata_error(VFS_I(base_ni), free_err);
 			ntfs_error(sb, "Couldn't free empty MFT record");
-		else
+		} else {
 			ntfs_inode_close(ni);
+		}
 	}
 
 	return err;
@@ -5520,6 +5572,7 @@ int ntfs_attr_rm(struct ntfs_inode *ni)
 	struct ntfs_attr_search_ctx *ctx;
 	int err = 0, ret = 0;
 	s64 freed = 0;
+	bool removed = false;
 	struct ntfs_inode *base_ni;
 	struct super_block *sb = ni->vol->sb;
 
@@ -5544,6 +5597,7 @@ int ntfs_attr_rm(struct ntfs_inode *ni)
 		freed = ntfs_cluster_free(ni, 0, -1, ctx);
 		if (freed < 0) {
 			ret = freed;
+			ntfs_report_file_metadata_error(VFS_I(base_ni), ret);
 			ntfs_error(sb,
 				"Failed to free cluster allocation. Leaving inconstant metadata.\n");
 		}
@@ -5553,6 +5607,8 @@ int ntfs_attr_rm(struct ntfs_inode *ni)
 	/* Search for attribute extents and remove them all. */
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
 	if (IS_ERR(ctx)) {
+		if (freed > 0)
+			ntfs_report_file_metadata_error(VFS_I(base_ni), PTR_ERR(ctx));
 		ntfs_error(sb, "%s: Failed to get search context", __func__);
 		return PTR_ERR(ctx);
 	}
@@ -5560,14 +5616,19 @@ int ntfs_attr_rm(struct ntfs_inode *ni)
 				CASE_SENSITIVE, 0, NULL, 0, ctx))) {
 		err = ntfs_attr_record_rm(ctx);
 		if (err) {
+			ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 			ntfs_error(sb,
 				"Failed to remove attribute extent. Leaving inconstant metadata.\n");
 			ret = err;
+		} else {
+			removed = true;
 		}
 		ntfs_attr_reinit_search_ctx(ctx);
 	}
 	ntfs_attr_put_search_ctx(ctx);
 	if (err != -ENOENT) {
+		if (freed > 0 || removed)
+			ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 		ntfs_error(sb, "Attribute lookup failed. Probably leaving inconstant metadata.\n");
 		ret = err;
 	}
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 14/31] ntfs: report attribute list update and recovery failures
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (12 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 13/31] ntfs: report attribute record update and cleanup failures Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 15/31] ntfs: report attribute conversion and mapping update failures Baolin Liu
                   ` (17 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

Attribute-list building, repacking and updating can leave inconsistent
metadata without reporting it. Report missing required list attributes,
invalid list state, failed mapping restoration and cluster cleanup.

Preserve the original update error and report independent recovery
failures against the base file. Use EFSCORRUPTED for confirmed corruption
while retaining allocation failures as such.

Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
 fs/ntfs/attrlist.c | 38 ++++++++++++++++++++++++++++----------
 fs/ntfs/inode.c    | 43 +++++++++++++++++++++++++++++--------------
 2 files changed, 57 insertions(+), 24 deletions(-)

diff --git a/fs/ntfs/attrlist.c b/fs/ntfs/attrlist.c
index 7e7440c5f2ba..de642af3bc30 100644
--- a/fs/ntfs/attrlist.c
+++ b/fs/ntfs/attrlist.c
@@ -39,8 +39,9 @@ int ntfs_attrlist_need(struct ntfs_inode *ni)
 	}
 
 	if (!ni->attr_list) {
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
 		ntfs_debug("Corrupt in-memory struct.\n");
-		return -EINVAL;
+		return -EFSCORRUPTED;
 	}
 
 	ale = (struct attr_list_entry *)ni->attr_list;
@@ -76,7 +77,7 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,
 	s64 old_alloc_size;
 	size_t old_rl_count, new_rl_count;
 	unsigned long flags;
-	int err, restore_err;
+	int err, free_err, restore_err;
 	if (attr_ni->mft_no != FILE_MFT || !NInoNonResident(attr_ni) ||
 		min_alloc_size < 0)
 		return -EINVAL;
@@ -89,8 +90,10 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,
 		return err;
 
 	data_size = attr_ni->data_size;
-	if (data_size < 0)
-		return -EIO;
+	if (data_size < 0) {
+		ntfs_report_file_metadata_error(attr_vi, -EFSCORRUPTED);
+		return -EFSCORRUPTED;
+	}
 
 	if (data_size) {
 		data = kvmalloc(data_size, GFP_NOFS);
@@ -99,7 +102,11 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,
 
 		written = ntfs_inode_attr_pread(attr_vi, 0, data_size, data);
 		if (written != data_size) {
-			err = written < 0 ? (int)written : -EIO;
+			err = written < 0 ? (int)written : -EFSCORRUPTED;
+			if (written >= 0 || written == -EIO)
+				ntfs_report_file_metadata_error(attr_vi,
+								written < 0 ?
+								(int)written : -EFSCORRUPTED);
 			goto out_free_data;
 		}
 	}
@@ -127,7 +134,9 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,
 		new_rl_count = 2;
 
 	if (new_rl_count != 2) {
-		ntfs_cluster_free_from_rl(vol, new_rl);
+		free_err = ntfs_cluster_free_from_rl(vol, new_rl);
+		if (free_err)
+			ntfs_report_file_metadata_error(attr_vi, free_err);
 		kvfree(new_rl);
 		err = -ENOSPC;
 		goto out_free_data;
@@ -157,11 +166,12 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,
 		goto restore_old_runlist;
 
 	/* The new mapping is now authoritative; release the old data runs. */
-	if (ntfs_cluster_free_from_rl(vol, old_rl)) {
+	free_err = ntfs_cluster_free_from_rl(vol, old_rl);
+	if (free_err) {
 		ntfs_error(vol->sb,
 			   "Failed to free old ATTRIBUTE_LIST extent: inode %#llx",
 			   (long long)attr_ni->mft_no);
-		NVolSetErrors(vol);
+		ntfs_report_file_metadata_error(attr_vi, free_err);
 	}
 	kvfree(old_rl);
 	kvfree(data);
@@ -186,10 +196,12 @@ static int ntfs_attrlist_repack(struct inode *attr_vi,
 	if (restore_err) {
 		ntfs_error(vol->sb, "Failed to restore ATTRIBUTE_LIST mapping pairs (%d)",
 			   restore_err);
-		NVolSetErrors(vol);
+		ntfs_report_file_metadata_error(attr_vi, restore_err);
 	}
 
-	ntfs_cluster_free_from_rl(vol, new_rl);
+	free_err = ntfs_cluster_free_from_rl(vol, new_rl);
+	if (free_err)
+		ntfs_report_file_metadata_error(attr_vi, free_err);
 	kvfree(new_rl);
 	err = err ? err : restore_err;
 
@@ -218,6 +230,11 @@ int ntfs_attrlist_update_locked(struct ntfs_inode *base_ni,
 	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);
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		}
 		return err;
 	}
 	attr_ni = NTFS_I(attr_vi);
@@ -293,6 +310,7 @@ int ntfs_attrlist_update_locked(struct ntfs_inode *base_ni,
 					 base_ni->attr_list, false);
 	if (written != base_ni->attr_list_size) {
 		err = written < 0 ? (int)written : -EIO;
+		ntfs_report_file_metadata_error(attr_vi, err);
 		iput(attr_vi);
 		ntfs_error(base_ni->vol->sb,
 			   "Failed to write attribute list of inode %#llx",
diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 6ed995b09a3f..564682f59203 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -3231,7 +3231,7 @@ int ntfs_inode_attach_all_extents(struct ntfs_inode *ni)
  */
 int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
 {
-	int err;
+	int err, rollback_err;
 	struct ntfs_attr_search_ctx *ctx;
 	u8 *al = NULL, *aln;
 	int al_len = 0;
@@ -3252,7 +3252,7 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
 
 	ni_mrec = map_mft_record(ni);
 	if (IS_ERR(ni_mrec))
-		return -EIO;
+		return PTR_ERR(ni_mrec);
 
 	/* Form attribute list. */
 	ctx = ntfs_attr_get_search_ctx(ni, ni_mrec);
@@ -3267,7 +3267,9 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
 		int ale_size;
 
 		if (ctx->attr->type == AT_ATTRIBUTE_LIST) {
-			err = -EIO;
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
 			ntfs_error(ni->vol->sb, "Attribute list already present");
 			goto put_err_out;
 		}
@@ -3355,15 +3357,19 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
 	NInoClearAttrList(ni);
 	/* Remove $ATTRIBUTE_LIST record. */
 	ntfs_attr_reinit_search_ctx(ctx);
-	if (!ntfs_attr_lookup(AT_ATTRIBUTE_LIST, NULL, 0,
-				CASE_SENSITIVE, 0, NULL, 0, ctx)) {
-		if (ntfs_attr_record_rm(ctx))
+	rollback_err = ntfs_attr_lookup(AT_ATTRIBUTE_LIST, NULL, 0,
+					CASE_SENSITIVE, 0, NULL, 0, ctx);
+	if (!rollback_err) {
+		rollback_err = ntfs_attr_record_rm(ctx);
+		if (rollback_err)
 			ntfs_error(ni->vol->sb, "Rollback failed to remove attrlist");
 		else
 			free_empty_extents = true;
 	} else {
 		ntfs_error(ni->vol->sb, "Rollback failed to find attrlist");
 	}
+	if (rollback_err)
+		ntfs_report_file_metadata_error(VFS_I(ni), rollback_err);
 
 	/* Setup back in-memory runlist. */
 	ni->attr_list = al;
@@ -3378,17 +3384,21 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
 	ale = (struct attr_list_entry *)al;
 	while ((u8 *)ale < al + al_len) {
 		if (MREF_LE(ale->mft_reference) != ni->mft_no) {
-			if (!ntfs_attr_lookup(ale->type, ale->name,
-						ale->name_length,
-						CASE_SENSITIVE,
-						le64_to_cpu(ale->lowest_vcn),
-						NULL, 0, ctx)) {
-				if (ntfs_attr_record_move_to(ctx, ni))
+			rollback_err = ntfs_attr_lookup(ale->type, ale->name,
+							ale->name_length,
+							CASE_SENSITIVE,
+							le64_to_cpu(ale->lowest_vcn),
+							NULL, 0, ctx);
+			if (!rollback_err) {
+				rollback_err = ntfs_attr_record_move_to(ctx, ni);
+				if (rollback_err)
 					ntfs_error(ni->vol->sb,
 							"Rollback failed to move attribute");
 			} else {
 				ntfs_error(ni->vol->sb, "Rollback failed to find attr");
 			}
+			if (rollback_err)
+				ntfs_report_file_metadata_error(VFS_I(ni), rollback_err);
 			ntfs_attr_reinit_search_ctx(ctx);
 		}
 		ale = (struct attr_list_entry *)((u8 *)ale + le16_to_cpu(ale->length));
@@ -3401,8 +3411,13 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
 	NInoClearAttrListDirty(ni);
 	ntfs_attr_put_search_ctx(ctx);
 	ctx = NULL;
-	if (free_empty_extents && ntfs_inode_free_empty_extents(ni))
-		ntfs_error(ni->vol->sb, "Rollback failed to free empty extent");
+	if (free_empty_extents) {
+		rollback_err = ntfs_inode_free_empty_extents(ni);
+		if (rollback_err) {
+			ntfs_report_file_metadata_error(VFS_I(ni), rollback_err);
+			ntfs_error(ni->vol->sb, "Rollback failed to free empty extent");
+		}
+	}
 	goto err_out;
 put_err_out:
 	ntfs_attr_put_search_ctx(ctx);
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 15/31] ntfs: report attribute conversion and mapping update failures
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (13 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 14/31] ntfs: report attribute list update and recovery failures Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 16/31] ntfs: report attribute resize and relocation recovery failures Baolin Liu
                   ` (16 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

Attribute conversion and mapping updates can detect corruption or fail
to recover modified metadata without reporting it. Cover missing
required attributes, malformed mapping pairs and failed extent cleanup.

Preserve ENOMEM from runlist merges and report invalid merges as
EFSCORRUPTED. Report failed cluster release, mapping restoration and
record cleanup separately from the initiating error.

Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
 fs/ntfs/attrib.c | 136 +++++++++++++++++++++++++++++++++++++----------
 1 file changed, 107 insertions(+), 29 deletions(-)

diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index 66845c78ab8d..73ccc438b319 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -2106,8 +2106,11 @@ int ntfs_attr_make_non_resident(struct ntfs_inode *ni, const u32 data_size)
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 			CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (unlikely(err)) {
-		if (err == -ENOENT)
-			err = -EIO;
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		}
 		goto err_out;
 	}
 	m = ctx->mrec;
@@ -2161,12 +2164,16 @@ int ntfs_attr_make_non_resident(struct ntfs_inode *ni, const u32 data_size)
 	mp_size = ntfs_get_size_for_mapping_pairs(vol, rl, 0, -1, -1);
 	if (unlikely(mp_size < 0)) {
 		err = mp_size;
+		if (err == -EFSCORRUPTED)
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
 		ntfs_debug("Failed to get size for mapping pairs array, error code %i.\n", err);
 		goto rl_err_out;
 	}
 
 	if (NInoNonResident(ni) || a->non_resident) {
-		err = -EIO;
+		err = -EFSCORRUPTED;
+		ntfs_report_file_metadata_error(VFS_I(base_ni), -EFSCORRUPTED);
 		goto rl_err_out;
 	}
 
@@ -2240,6 +2247,9 @@ int ntfs_attr_make_non_resident(struct ntfs_inode *ni, const u32 data_size)
 	err = ntfs_mapping_pairs_build(vol, (u8 *)a + mp_ofs,
 			arec_size - mp_ofs, rl, 0, -1, NULL, NULL, NULL);
 	if (unlikely(err)) {
+		if (err == -EFSCORRUPTED)
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
 		ntfs_error(vol->sb, "Failed to build mapping pairs, error code %i.",
 				err);
 		goto undo_err_out;
@@ -2350,10 +2360,11 @@ int ntfs_attr_make_non_resident(struct ntfs_inode *ni, const u32 data_size)
 rl_err_out:
 	up_write(&ni->runlist.lock);
 	if (rl) {
-		if (ntfs_cluster_free_from_rl(vol, rl) < 0) {
+		err2 = ntfs_cluster_free_from_rl(vol, rl);
+		if (err2 < 0) {
 			ntfs_error(vol->sb,
 				"Failed to release allocated cluster(s) in error code path.  Run chkdsk to recover the lost cluster(s).");
-			NVolSetErrors(vol);
+			ntfs_report_file_metadata_error(vi, err2);
 		}
 		kvfree(rl);
 folio_err_out:
@@ -2444,8 +2455,14 @@ int ntfs_attr_set_initialized_size(struct ntfs_inode *ni, loff_t new_size)
 
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 			       CASE_SENSITIVE, 0, NULL, 0, ctx);
-	if (err)
+	if (err) {
+		if (err == -ENOENT && !ntfs_inode_is_named_stream(ni)) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		}
 		goto out_ctx;
+	}
 
 	ctx->attr->data.non_resident.initialized_size = cpu_to_le64(new_size);
 	ni->initialized_size = new_size;
@@ -3800,7 +3817,8 @@ static int ntfs_attr_update_meta(struct attr_record *a, struct ntfs_inode *ni,
 
 	sparse = ntfs_rl_sparse(ni->runlist.rl);
 	if (sparse < 0) {
-		err = -EIO;
+		err = -EFSCORRUPTED;
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
 		goto out;
 	}
 
@@ -3838,8 +3856,10 @@ static int ntfs_attr_update_meta(struct attr_record *a, struct ntfs_inode *ni,
 		}
 		if (!(le32_to_cpu(a->length) -
 		    le16_to_cpu(a->data.non_resident.mapping_pairs_offset))) {
-			err = -EIO;
+			err = -EFSCORRUPTED;
 			ntfs_error(sb, "Mapping pairs space is 0");
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
 			goto out;
 		}
 
@@ -3883,7 +3903,9 @@ static int ntfs_attr_update_meta(struct attr_record *a, struct ntfs_inode *ni,
 
 		new_compr_size = ntfs_rl_get_compressed_size(ni->vol, ni->runlist.rl);
 		if (new_compr_size < 0) {
-			err = new_compr_size;
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+			err = -EFSCORRUPTED;
 			goto out;
 		}
 
@@ -3932,6 +3954,7 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 	struct attr_record *a;
 	s64 stop_vcn;
 	int err = 0, mp_size, cur_max_mp_size, exp_max_mp_size;
+	int rollback_err;
 	bool finished_build;
 	bool first_updated = false;
 	struct super_block *sb;
@@ -3994,8 +4017,10 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 			 */
 			first_lcn = ntfs_rl_vcn_to_lcn(ni->runlist.rl, stop_vcn);
 			if (first_lcn == LCN_EINVAL) {
-				err = -EIO;
+				err = -EFSCORRUPTED;
 				ntfs_error(sb, "Bad runlist");
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
 				goto put_err_out;
 			}
 			if (first_lcn == LCN_ENOENT ||
@@ -4046,6 +4071,9 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 				stop_vcn, -1, exp_max_mp_size);
 		if (mp_size <= 0) {
 			err = mp_size;
+			if (err == -EFSCORRUPTED)
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
 			ntfs_error(sb, "%s: get MP size failed", __func__);
 			goto put_err_out;
 		}
@@ -4081,9 +4109,10 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 					goto put_err_out;
 				}
 				ntfs_attr_put_search_ctx(ctx);
-				if (ntfs_inode_add_attrlist(base_ni)) {
+				err = ntfs_inode_add_attrlist(base_ni);
+				if (err) {
 					ntfs_error(sb, "Can not add attrlist");
-					return -EIO;
+					return err;
 				}
 				goto retry;
 			}
@@ -4100,8 +4129,9 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 		if (((mp_size + 7) & ~7) != cur_max_mp_size) {
 			if (ntfs_attr_record_resize(m, a,
 					le16_to_cpu(a->data.non_resident.mapping_pairs_offset) +
-						mp_size)) {
+					mp_size)) {
 				err = -EIO;
+				ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 				ntfs_error(sb, "Failed to resize attribute");
 				goto put_err_out;
 			}
@@ -4136,6 +4166,9 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 		if (!err)
 			finished_build = true;
 		if (!finished_build && err != -ENOSPC) {
+			if (err == -EFSCORRUPTED)
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
 			ntfs_error(sb, "Failed to build mapping pairs");
 			goto put_err_out;
 		}
@@ -4169,6 +4202,11 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 				NInoSetFileNameDirty(ni);
 			mark_mft_record_dirty(ctx->ntfs_ino);
 		} else {
+			if (err == -ENOENT && !ntfs_inode_is_named_stream(ni)) {
+				err = -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
+			}
 			ntfs_error(sb, "Failed to update sizes in base extent\n");
 			goto put_err_out;
 		}
@@ -4222,10 +4260,14 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 		m = map_mft_record(ext_ni);
 		if (IS_ERR(m)) {
 			ntfs_error(sb, "Could not map new MFT record");
-			if (ntfs_mft_record_free(ni->vol, ext_ni))
+			rollback_err = ntfs_mft_record_free(ni->vol, ext_ni);
+			if (rollback_err) {
 				ntfs_error(sb, "Could not free MFT record");
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								rollback_err);
+			}
 			ntfs_inode_close(ext_ni);
-			err = -ENOMEM;
+			err = PTR_ERR(m);
 			ext_ni = NULL;
 			goto put_err_out;
 		}
@@ -4247,6 +4289,9 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 			unmap_mft_record(ext_ni);
 			ntfs_inode_close(ext_ni);
 			err = mp_size;
+			if (err == -EFSCORRUPTED)
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
 			ntfs_error(sb, "%s: get mp size failed", __func__);
 			goto put_err_out;
 		}
@@ -4259,8 +4304,12 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 		if (err < 0) {
 			ntfs_error(sb, "Could not add attribute extent");
 			unmap_mft_record(ext_ni);
-			if (ntfs_mft_record_free(ni->vol, ext_ni))
+			rollback_err = ntfs_mft_record_free(ni->vol, ext_ni);
+			if (rollback_err) {
 				ntfs_error(sb, "Could not free MFT record");
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								rollback_err);
+			}
 			ntfs_inode_close(ext_ni);
 			goto put_err_out;
 		}
@@ -4271,10 +4320,17 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 				mp_size, start_rl, stop_vcn, -1, &stop_vcn, &start_rl,
 				&de_cnt);
 		if (err < 0 && err != -ENOSPC) {
+			if (err == -EFSCORRUPTED)
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
 			ntfs_error(sb, "Failed to build MP");
 			unmap_mft_record(ext_ni);
-			if (ntfs_mft_record_free(ni->vol, ext_ni))
+			rollback_err = ntfs_mft_record_free(ni->vol, ext_ni);
+			if (rollback_err) {
 				ntfs_error(sb, "Couldn't free MFT record");
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								rollback_err);
+			}
 			goto put_err_out;
 		}
 		a->data.non_resident.highest_vcn = cpu_to_le64(stop_vcn - 1);
@@ -4385,7 +4441,8 @@ static int ntfs_attr_make_resident(struct ntfs_inode *ni, struct ntfs_attr_searc
 		 * already checked that attribute fits MFT record).
 		 */
 		ntfs_error(ctx->ntfs_ino->vol->sb, "BUG! Failed to resize attribute record. ");
-		return -EIO;
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
+		return -EFSCORRUPTED;
 	}
 
 	/* Convert the attribute record to describe a resident attribute. */
@@ -4413,6 +4470,7 @@ static int ntfs_attr_make_resident(struct ntfs_inode *ni, struct ntfs_attr_searc
 	 */
 	freed = ntfs_cluster_free(ni, 0, -1, ctx);
 	if (freed < 0) {
+		ntfs_report_file_metadata_error(VFS_I(ni), freed);
 		ntfs_error(sb, "Eeek! Failed to release allocated clusters");
 		ntfs_debug("Ignoring error and leaving behind wasted clusters.\n");
 	}
@@ -5393,7 +5451,7 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 	s64 vcn = vcn_start, lcn, clu_count;
 	s64 freed;
 	s64 lcn_seek_from = -1;
-	int err = 0;
+	int err = 0, err2;
 	size_t new_rl_count, old_rl_count;
 
 	err = ntfs_attr_map_whole_runlist(ni);
@@ -5409,6 +5467,11 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 			CASE_SENSITIVE, vcn, NULL, 0, ctx);
 	if (err) {
+		if (err == -ENOENT && !ntfs_inode_is_named_stream(ni)) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		}
 		ntfs_error(vol->sb,
 			   "ntfs_attr_lookup failed, ntfs inode(mft_no : %llu) type : 0x%x, err : %d",
 			   ni->mft_no, ni->type, err);
@@ -5434,10 +5497,13 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 		}
 	} else {
 		WARN_ON(lcn == LCN_RL_NOT_MAPPED);
-		if (lcn == LCN_ENOENT)
+		if (lcn == LCN_ENOENT) {
 			err = -ENOENT;
-		else
-			err = -EIO;
+		} else {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		}
 		goto out;
 	}
 
@@ -5489,10 +5555,11 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 			 old_rl_count * sizeof(*old_rl), GFP_NOFS);
 	if (!old_rl) {
 		err = -ENOMEM;
-		if (ntfs_cluster_free_from_rl(vol, rlc)) {
+		err2 = ntfs_cluster_free_from_rl(vol, rlc);
+		if (err2) {
 			ntfs_error(vol->sb,
 				   "Failed to free cluster allocation after runlist backup failure.");
-			NVolSetErrors(vol);
+			ntfs_report_file_metadata_error(VFS_I(ni), err2);
 		}
 		kvfree(rlc);
 		goto out;
@@ -5502,8 +5569,16 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 	if (IS_ERR(rl)) {
 		ntfs_error(vol->sb, "Failed to merge runlists");
 		err = PTR_ERR(rl);
-		if (ntfs_cluster_free_from_rl(vol, rlc))
+		if (err != -ENOMEM) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+		}
+		err2 = ntfs_cluster_free_from_rl(vol, rlc);
+		if (err2) {
 			ntfs_error(vol->sb, "Failed to free hot clusters.");
+			ntfs_report_file_metadata_error(VFS_I(ni), err2);
+		}
 		kvfree(rlc);
 		goto out;
 	}
@@ -5526,7 +5601,10 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 			if (freed < 0 || freed != clu_count) {
 				ntfs_error(vol->sb,
 					   "Failed to free cluster allocation. Leaving inconsistent metadata.\n");
-				NVolSetErrors(vol);
+				err2 = freed < 0 ? freed : -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(VFS_I(ni),
+								freed < 0 ?
+								(int)freed : -EFSCORRUPTED);
 				goto out;
 			}
 
@@ -5538,11 +5616,11 @@ int ntfs_attr_map_cluster(struct ntfs_inode *ni, s64 vcn_start, s64 *lcn_start,
 			ni->runlist.rl = old_rl;
 			ni->runlist.count = old_rl_count;
 			old_rl = NULL;
-			if (ntfs_attr_update_mapping_pairs_locked(
-					ni, 0, ni)) {
+			err2 = ntfs_attr_update_mapping_pairs_locked(ni, 0, ni);
+			if (err2) {
 				ntfs_error(vol->sb,
 					   "Failed to restore mapping pairs after allocation rollback.\n");
-				NVolSetErrors(vol);
+				ntfs_report_file_metadata_error(VFS_I(ni), err2);
 			}
 		}
 	} else {
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 16/31] ntfs: report attribute resize and relocation recovery failures
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (14 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 15/31] ntfs: report attribute conversion and mapping update failures Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 17/31] ntfs: report incomplete attribute range updates Baolin Liu
                   ` (15 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Dandan Wu <wudandan@kylinos.cn>

Resize can fail after changing allocation or record placement without
reporting the resulting state. Report failed recovery and missing
required attributes, preserving allocation errors and using
EFSCORRUPTED for confirmed corruption.

After a nonresident shrink, report context or lookup failures when the
old data size exceeds the shortened allocation. Track whether resident
resize moved records and report interrupted moves or failed list updates
afterward. Keep early allocation failures distinct.

Signed-off-by: Dandan Wu <wudandan@kylinos.cn>
---
 fs/ntfs/attrib.c | 88 ++++++++++++++++++++++++++++++++++++++++--------
 fs/ntfs/inode.c  | 11 ++++--
 fs/ntfs/inode.h  |  2 +-
 3 files changed, 83 insertions(+), 18 deletions(-)

diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index 73ccc438b319..62985be67a09 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -4092,7 +4092,7 @@ static int __ntfs_attr_update_mapping_pairs(struct ntfs_inode *ni,
 					goto put_err_out;
 				}
 				err = ntfs_inode_free_space(base_ni, mp_size -
-							cur_max_mp_size);
+							cur_max_mp_size, NULL);
 				if (err)
 					return err;
 				err = ntfs_attrlist_update_locked(
@@ -4514,6 +4514,7 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni,
 	int err;
 	struct ntfs_inode *base_ni;
 	bool runlist_locked = locked_ni == ni;
+	bool sizes_inconsistent = false;
 
 	ntfs_debug("Inode 0x%llx attr 0x%x new size %lld\n",
 		(unsigned long long)ni->mft_no, ni->type, (long long)newsize);
@@ -4591,7 +4592,9 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni,
 		ntfs_attr_put_search_ctx(ctx);
 
 		/* Truncate the runlist itself. */
-		if (ntfs_rl_truncate_nolock(vol, &ni->runlist, first_free_vcn)) {
+		err = ntfs_rl_truncate_nolock(vol, &ni->runlist, first_free_vcn);
+		if (err) {
+			ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 			/*
 			 * Failed to truncate the runlist, so just throw it
 			 * away, it will be mapped afresh on next use.
@@ -4619,15 +4622,20 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni,
 		err = ntfs_attr_update_mapping_pairs_locked(ni, 0, ni);
 		if (err) {
 			ntfs_debug("Eeek! Mapping pairs update failed. Leaving inconstant metadata. Run chkdsk.\n");
+			ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 			goto unlock_runlist;
 		}
 		if (!runlist_locked)
 			up_write(&ni->runlist.lock);
+		sizes_inconsistent = ni->allocated_size < ni->data_size;
 	}
 
 	/* Get the first attribute record. */
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
 	if (IS_ERR(ctx)) {
+		/* The old sizes no longer describe the shortened allocation. */
+		if (sizes_inconsistent)
+			ntfs_report_file_metadata_error(VFS_I(base_ni), PTR_ERR(ctx));
 		ntfs_error(vol->sb, "%s: Failed to get search context", __func__);
 		return PTR_ERR(ctx);
 	}
@@ -4635,8 +4643,15 @@ static int ntfs_non_resident_attr_shrink(struct ntfs_inode *ni,
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 				CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (err) {
-		if (err == -ENOENT)
-			err = -EIO;
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		} else if (sizes_inconsistent &&
+			   (err == -ENOMEM || err == -EINTR || err == -ERESTARTSYS)) {
+			/* Lookup does not report these otherwise harmless errors. */
+			ntfs_report_file_metadata_error(VFS_I(base_ni), err);
+		}
 		ntfs_debug("Eeek! Lookup of first attribute extent failed. Leaving inconstant metadata.\n");
 		goto put_err_out;
 	}
@@ -4886,7 +4901,9 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz
 				 rl->length != first_free_vcn -
 					ntfs_bytes_to_cluster(vol, ni->allocated_size) ||
 				 rl[1].length)) {
-				ntfs_cluster_free_from_rl(vol, rl);
+				err2 = ntfs_cluster_free_from_rl(vol, rl);
+				if (err2)
+					ntfs_report_file_metadata_error(VFS_I(base_ni), err2);
 				kvfree(rl);
 				rl = ntfs_cluster_alloc(vol,
 						ntfs_bytes_to_cluster(vol,
@@ -4907,11 +4924,20 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz
 			/* Append new clusters to attribute runlist. */
 			rln = ntfs_runlists_merge(&ni->runlist, rl, 0, &new_rl_count);
 			if (IS_ERR(rln)) {
+				int free_err;
+
+				err = PTR_ERR(rln);
+				if (err != -ENOMEM) {
+					err = -EFSCORRUPTED;
+					ntfs_report_file_metadata_error(VFS_I(base_ni),
+									-EFSCORRUPTED);
+				}
 				/* Failed, free just allocated clusters. */
 				ntfs_error(sb, "Run list merge failed");
-				ntfs_cluster_free_from_rl(vol, rl);
+				free_err = ntfs_cluster_free_from_rl(vol, rl);
+				if (free_err)
+					ntfs_report_file_metadata_error(VFS_I(base_ni), free_err);
 				kvfree(rl);
-				err = -EIO;
 				goto unlock_runlist;
 			}
 			ni->runlist.rl = rln;
@@ -4957,8 +4983,11 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 			       CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (err) {
-		if (err == -ENOENT)
-			err = -EIO;
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		}
 		if (ni->allocated_size != org_alloc_size)
 			goto rollback;
 		goto put_err_out;
@@ -4982,8 +5011,10 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz
 	/* Free allocated clusters. */
 	freed = ntfs_cluster_free(ni, ntfs_bytes_to_cluster(vol, org_alloc_size),
 				  -1, ctx);
-	if (freed < 0)
+	if (freed < 0) {
 		ntfs_debug("Leaking clusters");
+		ntfs_report_file_metadata_error(VFS_I(base_ni), freed);
+	}
 
 	/*
 	 * Now, truncate the runlist itself.  Restore allocated_size before
@@ -5005,10 +5036,14 @@ static int ntfs_non_resident_attr_expand(struct ntfs_inode *ni, const s64 newsiz
 		kvfree(ni->runlist.rl);
 		ni->runlist.rl = NULL;
 		ntfs_error(sb, "Couldn't truncate runlist. Rollback failed");
+		ntfs_report_file_metadata_error(VFS_I(base_ni), err2);
 	} else {
 		/* Restore mapping pairs. */
-		if (__ntfs_attr_update_mapping_pairs(ni, 0, ni, true))
+		err2 = __ntfs_attr_update_mapping_pairs(ni, 0, ni, true);
+		if (err2) {
 			ntfs_error(sb, "Failed to restore old mapping pairs");
+			ntfs_report_file_metadata_error(VFS_I(base_ni), err2);
+		}
 
 		if (NInoSparse(ni) || NInoCompressed(ni)) {
 			ni->itype.compressed.size =  org_compressed_size;
@@ -5048,6 +5083,7 @@ static int ntfs_resident_attr_resize(struct ntfs_inode *attr_ni, const s64 newsi
 	struct super_block *sb = vol->sb;
 	int err = -EIO;
 	struct ntfs_inode *base_ni, *ext_ni = NULL;
+	bool moved;
 
 attr_resize_again:
 	ntfs_debug("Inode 0x%llx attr 0x%x new size %lld\n",
@@ -5069,6 +5105,11 @@ static int ntfs_resident_attr_resize(struct ntfs_inode *attr_ni, const s64 newsi
 	err = ntfs_attr_lookup(attr_ni->type, attr_ni->name, attr_ni->name_len,
 			CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (err) {
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		}
 		ntfs_error(sb, "ntfs_attr_lookup failed");
 		goto put_err_out;
 	}
@@ -5157,6 +5198,10 @@ static int ntfs_resident_attr_resize(struct ntfs_inode *attr_ni, const s64 newsi
 				(__le16 *)((u8 *)a + le16_to_cpu(a->name_offset)),
 				a->name_length);
 		if (IS_ERR(tvi)) {
+			if (PTR_ERR(tvi) == -ENOENT) {
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
+			}
 			ntfs_error(sb, "Couldn't open attribute");
 			continue;
 		}
@@ -5192,16 +5237,23 @@ static int ntfs_resident_attr_resize(struct ntfs_inode *attr_ni, const s64 newsi
 				return err;
 		}
 
-		err = ntfs_inode_free_space(base_ni, sizeof(struct attr_record));
+		err = ntfs_inode_free_space(base_ni, sizeof(struct attr_record),
+					    &moved);
 		if (err) {
+			/* Previously moved records are not restored on this exit. */
+			if (moved)
+				ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 			err = -ENOSPC;
 			ntfs_error(sb,
 				"Couldn't free space in the MFT record to make attribute list non resident");
 			return err;
 		}
 		err = ntfs_attrlist_update(base_ni);
-		if (err)
+		if (err) {
+			if (moved)
+				ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 			return err;
+		}
 		goto attr_resize_again;
 	}
 
@@ -5215,6 +5267,11 @@ static int ntfs_resident_attr_resize(struct ntfs_inode *attr_ni, const s64 newsi
 	err = ntfs_attr_lookup(attr_ni->type, attr_ni->name, attr_ni->name_len,
 			CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (err) {
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		}
 		ntfs_error(sb, "%s: Attribute lookup failed 2", __func__);
 		goto put_err_out;
 	}
@@ -5256,8 +5313,11 @@ static int ntfs_resident_attr_resize(struct ntfs_inode *attr_ni, const s64 newsi
 	}
 
 	err = ntfs_attrlist_update(base_ni);
-	if (err < 0)
+	if (err < 0) {
+		/* The record moved, but its on-disk list entry is still stale. */
+		ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 		goto put_err_out;
+	}
 
 	ntfs_attr_put_search_ctx(ctx);
 	/* Try to perform resize once again. */
diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 564682f59203..83d45383a5d6 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -3326,7 +3326,7 @@ int ntfs_inode_add_attrlist(struct ntfs_inode *ni)
 	/* Free space if there is not enough it for $ATTRIBUTE_LIST. */
 	if (le32_to_cpu(ni_mrec->bytes_allocated) -
 			le32_to_cpu(ni_mrec->bytes_in_use) < attr_al_len) {
-		if (ntfs_inode_free_space(ni, (int)attr_al_len)) {
+		if (ntfs_inode_free_space(ni, (int)attr_al_len, NULL)) {
 			/* Failed to free space. */
 			err = -ENOSPC;
 			ntfs_error(ni->vol->sb, "Failed to free space for attrlist");
@@ -3609,16 +3609,19 @@ static int ntfs_attr_position(__le32 type, struct ntfs_attr_search_ctx *ctx)
  * ntfs_inode_free_space - free space in the MFT record of inode
  * @ni:		ntfs inode in which MFT record free space
  * @size:	amount of space needed to free
+ * @moved:	optional output, set if any attribute record was moved
  *
  * Return 0 on success or error.
  */
-int ntfs_inode_free_space(struct ntfs_inode *ni, int size)
+int ntfs_inode_free_space(struct ntfs_inode *ni, int size, bool *moved)
 {
 	struct ntfs_attr_search_ctx *ctx;
 	int freed, err;
 	struct mft_record *ni_mrec;
 	struct super_block *sb;
 
+	if (moved)
+		*moved = false;
 	if (!ni || size < 0)
 		return -EINVAL;
 	ntfs_debug("Entering for inode %llu, size %d\n", ni->mft_no, size);
@@ -3626,7 +3629,7 @@ int ntfs_inode_free_space(struct ntfs_inode *ni, int size)
 	sb = ni->vol->sb;
 	ni_mrec = map_mft_record(ni);
 	if (IS_ERR(ni_mrec))
-		return -EIO;
+		return PTR_ERR(ni_mrec);
 
 	freed = (le32_to_cpu(ni_mrec->bytes_allocated) -
 			le32_to_cpu(ni_mrec->bytes_in_use));
@@ -3699,6 +3702,8 @@ int ntfs_inode_free_space(struct ntfs_inode *ni, int size)
 			break;
 		}
 		freed += record_size;
+		if (moved)
+			*moved = true;
 
 		/* Check whether we done. */
 		if (size <= freed) {
diff --git a/fs/ntfs/inode.h b/fs/ntfs/inode.h
index 952398753ed1..45eee1e8d462 100644
--- a/fs/ntfs/inode.h
+++ b/fs/ntfs/inode.h
@@ -371,7 +371,7 @@ int ntfs_inode_attach_all_extents(struct ntfs_inode *ni);
 int ntfs_inode_add_attrlist(struct ntfs_inode *ni);
 int ntfs_inode_free_empty_extents(struct ntfs_inode *ni);
 void ntfs_destroy_ext_inode(struct ntfs_inode *ni);
-int ntfs_inode_free_space(struct ntfs_inode *ni, int size);
+int ntfs_inode_free_space(struct ntfs_inode *ni, int size, bool *moved);
 s64 ntfs_inode_attr_pread(struct inode *vi, s64 pos, s64 count, u8 *buf);
 s64 ntfs_inode_attr_pwrite(struct inode *vi, s64 pos, s64 count, u8 *buf,
 		bool sync);
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 17/31] ntfs: report incomplete attribute range updates
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (15 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 16/31] ntfs: report attribute resize and relocation recovery failures Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 18/31] ntfs: report inode metadata and eviction cleanup errors Baolin Liu
                   ` (14 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Dandan Wu <wudandan@kylinos.cn>

Range insertion, collapse, hole punching and preallocation can fail
after changing runlists or allocation without reporting the unfinished
metadata update. Report mapping-update and cluster-release failures
against the affected file.

Use EFSCORRUPTED for missing required attributes and invalid runlist
state. Report context and lookup failures after a range update, including
ENOMEM, while keeping ordinary early allocation failures unreported.

Signed-off-by: Dandan Wu <wudandan@kylinos.cn>
---
 fs/ntfs/attrib.c | 47 ++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 38 insertions(+), 9 deletions(-)

diff --git a/fs/ntfs/attrib.c b/fs/ntfs/attrib.c
index 62985be67a09..910c69da9e24 100644
--- a/fs/ntfs/attrib.c
+++ b/fs/ntfs/attrib.c
@@ -5952,9 +5952,10 @@ int ntfs_non_resident_attr_insert_range(struct ntfs_inode *ni, s64 start_vcn, s6
 
 	rl = ntfs_rl_find_vcn_nolock(ni->runlist.rl, start_vcn);
 	if (!rl) {
+		ntfs_report_file_metadata_error(VFS_I(base_ni), -EFSCORRUPTED);
 		up_write(&ni->runlist.lock);
 		kfree(hole_rl);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	rl = ntfs_rl_insert_range(ni->runlist.rl, (int)ni->runlist.count,
@@ -5974,19 +5975,20 @@ int ntfs_non_resident_attr_insert_range(struct ntfs_inode *ni, s64 start_vcn, s6
 	ret = ntfs_attr_update_mapping_pairs_locked(ni, 0, ni);
 	up_write(&ni->runlist.lock);
 	if (ret)
-		return ret;
+		goto report_error;
 
 	ctx = ntfs_attr_get_search_ctx(base_ni, NULL);
 	if (IS_ERR(ctx)) {
 		ret = PTR_ERR(ctx);
-		return ret;
+		ctx = NULL;
+		goto report_error;
 	}
 
 	ret = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 			       CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (ret) {
 		ntfs_attr_put_search_ctx(ctx);
-		return ret;
+		goto report_error;
 	}
 
 	ctx->attr->data.non_resident.data_size = cpu_to_le64(ni->data_size);
@@ -5996,6 +5998,13 @@ int ntfs_non_resident_attr_insert_range(struct ntfs_inode *ni, s64 start_vcn, s6
 	mark_mft_record_dirty(ctx->ntfs_ino);
 	ntfs_attr_put_search_ctx(ctx);
 	return ret;
+
+report_error:
+	if (ret == -ENOENT)
+		ret = -EFSCORRUPTED;
+	/* The runlist and sizes have already changed without rollback. */
+	ntfs_report_file_metadata_error(VFS_I(base_ni), ret);
+	return ret;
 }
 
 int ntfs_non_resident_attr_collapse_range(struct ntfs_inode *ni, s64 start_vcn, s64 len)
@@ -6028,8 +6037,9 @@ int ntfs_non_resident_attr_collapse_range(struct ntfs_inode *ni, s64 start_vcn,
 		dst_cnt++;
 	rl = ntfs_rl_find_vcn_nolock(ni->runlist.rl, start_vcn);
 	if (!rl) {
+		ntfs_report_file_metadata_error(VFS_I(base_ni), -EFSCORRUPTED);
 		up_write(&ni->runlist.lock);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	rl = ntfs_rl_collapse_range(ni->runlist.rl, dst_cnt + 1,
@@ -6090,6 +6100,11 @@ int ntfs_non_resident_attr_collapse_range(struct ntfs_inode *ni, s64 start_vcn,
 	if (ctx)
 		ntfs_attr_put_search_ctx(ctx);
 out_rl:
+	if (ret == -ENOENT)
+		ret = -EFSCORRUPTED;
+	/* The range was removed from the runlist and cannot be restored. */
+	if (ret)
+		ntfs_report_file_metadata_error(VFS_I(base_ni), ret);
 	kvfree(punch_rl);
 	mark_mft_record_dirty(base_ni);
 	return ret;
@@ -6124,8 +6139,9 @@ int ntfs_non_resident_attr_punch_hole(struct ntfs_inode *ni, s64 start_vcn, s64
 		dst_cnt++;
 	rl = ntfs_rl_find_vcn_nolock(ni->runlist.rl, start_vcn);
 	if (!rl) {
+		ntfs_report_file_metadata_error(VFS_I(base_ni), -EFSCORRUPTED);
 		up_write(&ni->runlist.lock);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	rl = ntfs_rl_punch_hole(ni->runlist.rl, dst_cnt + 1,
@@ -6140,13 +6156,16 @@ int ntfs_non_resident_attr_punch_hole(struct ntfs_inode *ni, s64 start_vcn, s64
 	ret = ntfs_attr_update_mapping_pairs_locked(ni, 0, ni);
 	up_write(&ni->runlist.lock);
 	if (ret) {
+		ntfs_report_file_metadata_error(VFS_I(base_ni), ret);
 		kvfree(punch_rl);
 		return ret;
 	}
 
 	ret = ntfs_cluster_free_from_rl(vol, punch_rl);
-	if (ret)
+	if (ret) {
+		ntfs_report_file_metadata_error(VFS_I(base_ni), ret);
 		ntfs_error(vol->sb, "Freeing of clusters failed");
+	}
 
 	kvfree(punch_rl);
 	mark_mft_record_dirty(base_ni);
@@ -6195,7 +6214,11 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 			CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (err) {
-		err = -EIO;
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+		}
 		goto out_unmap;
 	}
 
@@ -6210,7 +6233,9 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
 					CASE_SENSITIVE, 0, NULL, 0,
 					ctx);
 			if (err) {
-				err = -EIO;
+				if (err == -ENOENT)
+					err = -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(VFS_I(base_ni), err);
 				goto out_unmap;
 			}
 			ni->data_size = old_data_size;
@@ -6296,6 +6321,10 @@ int ntfs_attr_fallocate(struct ntfs_inode *ni, loff_t start, loff_t byte_len, bo
 			}
 		} else {
 			err = -EIO;
+			if (rl_lcn < LCN_HOLE) {
+				err = -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(VFS_I(base_ni), err);
+			}
 			goto out;
 		}
 	}
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 18/31] ntfs: report inode metadata and eviction cleanup errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (16 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 17/31] ntfs: report incomplete attribute range updates Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 19/31] ntfs: report index validation and block I/O errors Baolin Liu
                   ` (13 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

Report corrupt inode metadata, stale extent references and missing
required attributes as EFSCORRUPTED. During FILE_NAME synchronization,
report missing index entries and unused parents unless the child is
being deleted. Retain best-effort parent lookup handling.

Use volume reports for deletion and eviction cleanup. Report failed
attribute walks even on ENOMEM or interruption: deletion still frees the
MFT records and may leave unvisited allocations behind.

At eviction, report dirty base records that the final write failed to
save, using its errno or EIO if none was returned. Also report discarded
dirty runlists and extent records. Normal writeback keeps its ENOMEM
retry behavior; after SB_ACTIVE is cleared, only volume state is updated.

Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
 fs/ntfs/inode.c | 126 ++++++++++++++++++++++++++++++++++++------------
 1 file changed, 96 insertions(+), 30 deletions(-)

diff --git a/fs/ntfs/inode.c b/fs/ntfs/inode.c
index 83d45383a5d6..0e67078552cb 100644
--- a/fs/ntfs/inode.c
+++ b/fs/ntfs/inode.c
@@ -372,8 +372,10 @@ static int ntfs_non_resident_dealloc_clusters(struct ntfs_inode *ni)
 	int err = 0;
 
 	actx = ntfs_attr_get_search_ctx(ni, NULL);
-	if (IS_ERR(actx))
+	if (IS_ERR(actx)) {
+		ntfs_report_metadata_error(ni->vol, PTR_ERR(actx));
 		return PTR_ERR(actx);
+	}
 	WARN_ON(actx->mrec->link_count != 0);
 
 	/**
@@ -383,9 +385,11 @@ static int ntfs_non_resident_dealloc_clusters(struct ntfs_inode *ni)
 	 */
 	if (NInoRunlistDirty(ni)) {
 		err = ntfs_cluster_free_from_rl(ni->vol, ni->runlist.rl);
-		if (err)
+		if (err) {
+			ntfs_report_metadata_error(ni->vol, err);
 			ntfs_error(sb,
 					"Failed to free clusters. Leaving inconsistent metadata.\n");
+		}
 	}
 
 	while ((err = ntfs_attrs_walk(actx)) == 0) {
@@ -398,19 +402,27 @@ static int ntfs_non_resident_dealloc_clusters(struct ntfs_inode *ni)
 					&new_rl_count);
 			if (IS_ERR(rl)) {
 				err = PTR_ERR(rl);
+				ntfs_report_metadata_error(ni->vol,
+							   err == -ENOMEM ? err : -EFSCORRUPTED);
 				ntfs_error(sb,
 					   "Failed to decompress runlist. Leaving inconsistent metadata.\n");
 				continue;
 			}
 
 			err = ntfs_cluster_free_from_rl(ni->vol, rl);
-			if (err)
+			if (err) {
+				ntfs_report_metadata_error(ni->vol, err);
 				ntfs_error(sb,
 					   "Failed to free attribute clusters. Leaving inconsistent metadata.\n");
+			}
 			kvfree(rl);
 		}
 	}
 
+	/* The caller frees the MFT records even if enumeration was interrupted. */
+	if (err == -ENOMEM || err == -EINTR || err == -ERESTARTSYS)
+		ntfs_report_metadata_error(ni->vol, err);
+
 	ntfs_release_dirty_clusters(ni->vol, ni->i_dealloc_clusters);
 	ntfs_attr_put_search_ctx(actx);
 	return err;
@@ -608,22 +620,22 @@ static int ntfs_is_extended_system_file(struct ntfs_attr_search_ctx *ctx)
 err_corrupt_attr:
 			ntfs_error(ctx->ntfs_ino->vol->sb,
 					"Corrupt file name attribute. You should run chkdsk.");
-			return -EIO;
+			return -EFSCORRUPTED;
 		}
 		if (attr->non_resident) {
 			ntfs_error(ctx->ntfs_ino->vol->sb,
 					"Non-resident file name. You should run chkdsk.");
-			return -EIO;
+			return -EFSCORRUPTED;
 		}
 		if (attr->flags) {
 			ntfs_error(ctx->ntfs_ino->vol->sb,
 					"File name with invalid flags. You should run chkdsk.");
-			return -EIO;
+			return -EFSCORRUPTED;
 		}
 		if (!(attr->data.resident.flags & RESIDENT_ATTR_IS_INDEXED)) {
 			ntfs_error(ctx->ntfs_ino->vol->sb,
 					"Unindexed file name. You should run chkdsk.");
-			return -EIO;
+			return -EFSCORRUPTED;
 		}
 		file_name_attr = (struct file_name_attr *)((u8 *)attr +
 				le16_to_cpu(attr->data.resident.value_offset));
@@ -656,7 +668,7 @@ static int ntfs_is_extended_system_file(struct ntfs_attr_search_ctx *ctx)
 	if (unlikely(nr_links)) {
 		ntfs_error(ctx->ntfs_ino->vol->sb,
 			"Inode hard link count doesn't match number of name attributes. You should run chkdsk.");
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 	return 0;	/* NO, it is not an extended system file. */
 }
@@ -1317,8 +1329,8 @@ static int ntfs_read_locked_inode(struct inode *vi)
 	ntfs_debug("Done.");
 	return 0;
 unm_err_out:
-	if (!err)
-		err = -EIO;
+	if (!err || (err == -ENOENT && vol_err))
+		err = -EFSCORRUPTED;
 	if (ctx)
 		ntfs_attr_put_search_ctx(ctx);
 	if (m)
@@ -1329,7 +1341,7 @@ static int ntfs_read_locked_inode(struct inode *vi)
 		ntfs_error(vol->sb,
 			"Failed with error code %i.  Marking corrupt inode 0x%llx as bad.  Run chkdsk.",
 			err, ni->mft_no);
-		NVolSetErrors(vol);
+		ntfs_report_file_metadata_error(vi, err);
 	}
 	return err;
 }
@@ -1576,7 +1588,7 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi)
 
 unm_err_out:
 	if (!err)
-		err = -EIO;
+		err = -EFSCORRUPTED;
 	if (ctx)
 		ntfs_attr_put_search_ctx(ctx);
 	unmap_mft_record(base_ni);
@@ -1586,9 +1598,9 @@ static int ntfs_read_locked_attr_inode(struct inode *base_vi, struct inode *vi)
 			"Failed with error code %i while reading attribute inode (mft_no 0x%llx, type 0x%x, name_len %i).  Marking corrupt inode and base inode 0x%llx as bad.  Run chkdsk.",
 			err, ni->mft_no, ni->type, ni->name_len,
 			base_ni->mft_no);
-	if (err != -ENOENT && err != -ENOMEM &&
+	if (err != -ENOENT && err != -ENOMEM && err != -EOPNOTSUPP &&
 	    err != -EINTR && err != -ERESTARTSYS)
-		NVolSetErrors(vol);
+		ntfs_report_file_metadata_error(base_vi, err);
 	return err;
 }
 
@@ -1823,8 +1835,10 @@ static int ntfs_read_locked_index_inode(struct inode *base_vi, struct inode *vi)
 	 * Make sure the base inode doesn't go away and attach it to the
 	 * index inode.
 	 */
-	if (!igrab(base_vi))
+	if (!igrab(base_vi)) {
+		err = -EIO;
 		goto unm_err_out;
+	}
 	ni->ext.base_ntfs_ino = base_ni;
 	ni->nr_extents = -1;
 
@@ -1833,8 +1847,8 @@ static int ntfs_read_locked_index_inode(struct inode *base_vi, struct inode *vi)
 iput_unm_err_out:
 	iput(bvi);
 unm_err_out:
-	if (!err)
-		err = -EIO;
+	if (!err || err == -ENOENT)
+		err = -EFSCORRUPTED;
 	if (ctx)
 		ntfs_attr_put_search_ctx(ctx);
 	if (m)
@@ -1846,7 +1860,7 @@ static int ntfs_read_locked_index_inode(struct inode *base_vi, struct inode *vi)
 			err, ni->mft_no, ni->name_len);
 	if (err != -EOPNOTSUPP && err != -ENOMEM &&
 	    err != -EINTR && err != -ERESTARTSYS)
-		NVolSetErrors(vol);
+		ntfs_report_file_metadata_error(base_vi, err);
 	return err;
 }
 
@@ -2428,16 +2442,20 @@ static int ntfs_delete_base_inode(struct ntfs_inode *ni)
 	 */
 	while (ni->nr_extents) {
 		err = ntfs_mft_record_free(ni->vol, *(ni->ext.extent_ntfs_inos));
-		if (err)
+		if (err) {
+			ntfs_report_metadata_error(ni->vol, err);
 			ntfs_error(sb,
 				"Failed to free extent MFT record. Leaving inconsistent metadata.\n");
+		}
 		ntfs_inode_close(*(ni->ext.extent_ntfs_inos));
 	}
 
 	/* Deallocate base mft record */
 	err = ntfs_mft_record_free(ni->vol, ni);
-	if (err)
+	if (err) {
+		ntfs_report_metadata_error(ni->vol, err);
 		ntfs_error(sb, "Failed to free base MFT record. Leaving inconsistent metadata.\n");
+	}
 	return err;
 }
 
@@ -2468,9 +2486,11 @@ void ntfs_evict_big_inode(struct inode *vi)
 
 	if (NInoDirty(ni)) {
 		/* Committing the inode also commits all extent inodes. */
-		ntfs_commit_inode(vi);
+		int err = __ntfs_write_inode(vi, 1);
 
 		if (NInoDirty(ni)) {
+			/* Eviction discards the dirty record, so it cannot be retried. */
+			ntfs_report_metadata_error(ni->vol, err ? err : -EIO);
 			ntfs_debug("Failed to commit dirty inode 0x%llx.  Losing data!",
 				   ni->mft_no);
 			NInoClearAttrListDirty(ni);
@@ -2478,13 +2498,20 @@ void ntfs_evict_big_inode(struct inode *vi)
 		}
 	}
 
+	/* A deferred mapping update can outlive the base record's dirty bit. */
+	if (NInoRunlistDirty(ni))
+		ntfs_report_metadata_error(ni->vol, -EIO);
+
 	/* No need to lock at this stage as no one else has a reference. */
 	if (ni->nr_extents > 0) {
 		int i;
 
 		for (i = 0; i < ni->nr_extents; i++) {
-			if (ni->ext.extent_ntfs_inos[i])
+			if (ni->ext.extent_ntfs_inos[i]) {
+				if (NInoDirty(ni->ext.extent_ntfs_inos[i]))
+					ntfs_report_metadata_error(ni->vol, -EIO);
 				ntfs_clear_extent_inode(ni->ext.extent_ntfs_inos[i]);
+			}
 		}
 		ni->nr_extents = 0;
 		kvfree(ni->ext.extent_ntfs_inos);
@@ -2773,11 +2800,15 @@ int ntfs_inode_sync_filename(struct ntfs_inode *ni)
 	/* Collect the reparse tag, if any */
 	reparse_tag = cpu_to_le32(0);
 	if (ni->flags & FILE_ATTR_REPARSE_POINT) {
-		if (!ntfs_attr_lookup(AT_REPARSE_POINT, NULL,
-					0, CASE_SENSITIVE, 0, NULL, 0, ctx)) {
+		err = ntfs_attr_lookup(AT_REPARSE_POINT, NULL, 0,
+				       CASE_SENSITIVE, 0, NULL, 0, ctx);
+		if (!err) {
 			rpp = (struct reparse_point *)((u8 *)ctx->attr +
 					le16_to_cpu(ctx->attr->data.resident.value_offset));
 			reparse_tag = rpp->reparse_tag;
+		} else if (err == -ENOENT) {
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
 		}
 		ntfs_attr_reinit_search_ctx(ctx);
 	}
@@ -2791,6 +2822,9 @@ int ntfs_inode_sync_filename(struct ntfs_inode *ni)
 
 		index_vi = ntfs_iget(sb, MREF_LE(fn->parent_directory));
 		if (IS_ERR(index_vi)) {
+			if (PTR_ERR(index_vi) == -ENOENT && !NInoBeingDeleted(ni))
+				ntfs_report_file_metadata_error(VFS_I(ni),
+								-EFSCORRUPTED);
 			ntfs_error(sb, "Failed to open inode %lld with index",
 					(long long)MREF_LE(fn->parent_directory));
 			continue;
@@ -2816,6 +2850,11 @@ int ntfs_inode_sync_filename(struct ntfs_inode *ni)
 
 		err = ntfs_index_lookup(fn, sizeof(struct file_name_attr), ictx);
 		if (err) {
+			if (err == -ENOENT) {
+				err = -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(index_vi,
+								-EFSCORRUPTED);
+			}
 			ntfs_debug("Index lookup failed, inode %llu",
 					index_ni->mft_no);
 			ntfs_index_ctx_put(ictx);
@@ -2886,7 +2925,8 @@ int ntfs_get_block_mft_record(struct ntfs_inode *mft_ni, struct ntfs_inode *ni)
 	rl = mft_ni->runlist.rl;
 	if (!rl) {
 		ntfs_error(mft_ni->vol->sb, "$MFT runlist is not present");
-		return -EIO;
+		ntfs_report_metadata_error(mft_ni->vol, -EFSCORRUPTED);
+		return -EFSCORRUPTED;
 	}
 
 	/* Seek to element containing target vcn. */
@@ -2968,8 +3008,11 @@ int __ntfs_write_inode(struct inode *vi, int sync)
 	}
 
 	err = ntfs_inode_sync_standard_information(vi, m);
-	if (err)
+	if (err) {
+		if (err == -ENOENT)
+			err = -EFSCORRUPTED;
 		goto unm_err_out;
+	}
 
 	/*
 	 * when being umounted and inodes are evicted, write_inode()
@@ -2987,8 +3030,10 @@ int __ntfs_write_inode(struct inode *vi, int sync)
 		down_read(&mft_ni->runlist.lock);
 		err = ntfs_get_block_mft_record(mft_ni, ni);
 		up_read(&mft_ni->runlist.lock);
-		if (err)
+		if (err) {
+			err = -EFSCORRUPTED;
 			goto unm_err_out;
+		}
 
 		err = write_mft_record(ni, m, sync);
 		if (err)
@@ -3006,6 +3051,7 @@ int __ntfs_write_inode(struct inode *vi, int sync)
 			err = ntfs_get_block_mft_record(mft_ni,
 						   ni->ext.extent_ntfs_inos[i]);
 			if (err) {
+				err = -EFSCORRUPTED;
 				mutex_unlock(&ni->extent_lock);
 				up_read(&mft_ni->runlist.lock);
 				mutex_unlock(&ni->mrec_lock);
@@ -3067,7 +3113,10 @@ int __ntfs_write_inode(struct inode *vi, int sync)
 		mark_inode_dirty(vi);
 	else {
 		ntfs_error(vi->i_sb, "Failed (error %i):  Run chkdsk.", -err);
-		NVolSetErrors(ni->vol);
+		if (err != -EINTR && err != -ERESTARTSYS)
+			ntfs_report_file_metadata_error(vi, err);
+		else
+			NVolSetErrors(ni->vol);
 	}
 	if (need_iput)
 		iput(vi);
@@ -3136,8 +3185,10 @@ static struct ntfs_inode *ntfs_extent_inode_open(struct ntfs_inode *base_ni,
 			    seq_no != le16_to_cpu(ni_mrec->sequence_number)) {
 				ntfs_error(sb, "Found stale extent mft reference mft=%llu",
 						ni->mft_no);
+				ntfs_report_file_metadata_error(VFS_I(base_ni),
+								-EFSCORRUPTED);
 				unmap_mft_record(ni);
-				return ERR_PTR(-EIO);
+				return ERR_PTR(-EFSCORRUPTED);
 			}
 			unmap_mft_record(ni);
 			goto out;
@@ -3200,8 +3251,9 @@ int ntfs_inode_attach_all_extents(struct ntfs_inode *ni)
 		return 0;
 
 	if (!ni->attr_list) {
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
 		ntfs_debug("Corrupt in-memory struct.\n");
-		return -EINVAL;
+		return -EFSCORRUPTED;
 	}
 
 	/* Walk through attribute list and attach all extents. */
@@ -3778,6 +3830,11 @@ s64 ntfs_inode_attr_pread(struct inode *vi, s64 pos, s64 count, u8 *buf)
 				       CASE_SENSITIVE,
 				       0, NULL, 0, ctx);
 		if (err) {
+			if (err == -ENOENT && !ntfs_inode_is_named_stream(ni)) {
+				err = -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(vi,
+								-EFSCORRUPTED);
+			}
 			ntfs_error(vi->i_sb, "Failed to look up attr %#x", ni->type);
 			ntfs_attr_put_search_ctx(ctx);
 			mutex_unlock(&mrec_ni->mrec_lock);
@@ -3853,6 +3910,11 @@ static inline int ntfs_enlarge_attribute(struct inode *vi, s64 pos, s64 count,
 				       CASE_SENSITIVE,
 				       0, NULL, 0, ctx);
 		if (ret) {
+			if (ret == -ENOENT) {
+				ret = -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(vi,
+								-EFSCORRUPTED);
+			}
 			ntfs_error(sb, "Failed to look up attr %#x", ni->type);
 			return ret;
 		}
@@ -4103,6 +4165,10 @@ s64 ntfs_inode_attr_pwrite(struct inode *vi, s64 pos, s64 count, u8 *buf, bool s
 			       CASE_SENSITIVE,
 			       0, NULL, 0, ctx);
 	if (ret) {
+		if (ret == -ENOENT && !ntfs_inode_is_named_stream(ni)) {
+			ret = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(vi, -EFSCORRUPTED);
+		}
 		ntfs_attr_put_search_ctx(ctx);
 		ntfs_error(vi->i_sb, "Failed to look up attr %#x", ni->type);
 		goto out_unlock;
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 19/31] ntfs: report index validation and block I/O errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (17 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 18/31] ntfs: report inode metadata and eviction cleanup errors Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 20/31] ntfs: preserve index traversal errors and directory retries Baolin Liu
                   ` (12 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Dandan Wu <wudandan@kylinos.cn>

Index validation and block I/O can detect corrupt entries, MST
failures and short reads without reporting them. Use EFSCORRUPTED for
corruption and report failures against the index owner.

Preserve block-read errors through index lookup and update its handling
of corrupt search results. Keep underlying I/O errors distinct and
normalize short index-block writes to EIO.

Signed-off-by: Dandan Wu <wudandan@kylinos.cn>
---
 fs/ntfs/index.c | 99 ++++++++++++++++++++++++++++++-------------------
 1 file changed, 60 insertions(+), 39 deletions(-)

diff --git a/fs/ntfs/index.c b/fs/ntfs/index.c
index afb08f0aa596..1adb722f0e7c 100644
--- a/fs/ntfs/index.c
+++ b/fs/ntfs/index.c
@@ -37,7 +37,7 @@ static int ntfs_index_entry_inconsistent(const struct ntfs_volume *vol,
 	     le16_to_cpu(ie->length))) {
 		ntfs_error(vol->sb, "Overflow from index entry in inode %lld\n",
 				(long long)inum);
-		return -EIO;
+		return -EFSCORRUPTED;
 
 	} else {
 		if (collation_rule == COLLATION_FILE_NAME) {
@@ -47,7 +47,7 @@ static int ntfs_index_entry_inconsistent(const struct ntfs_volume *vol,
 				ntfs_error(vol->sb,
 					"File name overflow from index entry in inode %lld\n",
 					(long long)inum);
-				return -EIO;
+				return -EFSCORRUPTED;
 			}
 		} else {
 			if (ie->data.vi.data_length &&
@@ -57,7 +57,7 @@ static int ntfs_index_entry_inconsistent(const struct ntfs_volume *vol,
 				ntfs_error(vol->sb,
 					"Data overflow from index entry in inode %lld\n",
 					(long long)inum);
-				return -EIO;
+				return -EFSCORRUPTED;
 			}
 		}
 	}
@@ -104,8 +104,11 @@ static int ntfs_ib_write(struct ntfs_index_context *icx, struct index_block *ib)
 	ntfs_debug("vcn: %lld\n", vcn);
 
 	ret = pre_write_mst_fixup((struct ntfs_record *)ib, icx->block_size);
-	if (ret)
-		return -EIO;
+	if (ret) {
+		ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+						-EFSCORRUPTED);
+		return -EFSCORRUPTED;
+	}
 
 	ret = ntfs_inode_attr_pwrite(VFS_I(icx->ia_ni),
 			ntfs_ib_vcn_to_pos(icx, vcn), icx->block_size,
@@ -115,6 +118,9 @@ static int ntfs_ib_write(struct ntfs_index_context *icx, struct index_block *ib)
 	post_write_mst_fixup((struct ntfs_record *)ib);
 
 	if (ret != icx->block_size) {
+		if (ret >= 0)
+			ret = -EIO;
+		ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), ret);
 		ntfs_debug("Failed to write index block %lld, inode %llu",
 				vcn, (unsigned long long)icx->idx_ni->mft_no);
 		return ret;
@@ -285,7 +291,7 @@ static int ntfs_index_header_inconsistent(struct ntfs_volume *vol,
 		ntfs_error(vol->sb,
 			   "index block in inode %llu is smaller than an index header.",
 			   (unsigned long long)inum);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	entries_offset = le32_to_cpu(ih->entries_offset);
@@ -297,28 +303,28 @@ static int ntfs_index_header_inconsistent(struct ntfs_volume *vol,
 		ntfs_error(vol->sb,
 			   "Invalid index entry offset in inode %llu.",
 			   (unsigned long long)inum);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	if (index_length <= entries_offset) {
 		ntfs_error(vol->sb,
 			   "No space for index entries in inode %llu.",
 			   (unsigned long long)inum);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	if (allocated_size < index_length) {
 		ntfs_error(vol->sb,
 			   "Index entries overflow in inode %llu.",
 			   (unsigned long long)inum);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	if (allocated_size > bytes_available || index_length > bytes_available) {
 		ntfs_error(vol->sb,
 			   "Index entries in inode %llu exceed the available buffer.",
 			   (unsigned long long)inum);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	return 0;
@@ -338,25 +344,25 @@ int ntfs_index_entries_inconsistent(const struct ntfs_volume *vol,
 			ntfs_error(vol->sb,
 				   "Index entry out of bounds in inode %llu.",
 				   (unsigned long long)inum);
-			return -EIO;
+			return -EFSCORRUPTED;
 		}
 
 		if (le16_to_cpu(ie->length) < sizeof(struct index_entry_header)) {
 			ntfs_error(vol->sb,
 				   "Index entry too small in inode %llu.",
 				   inum);
-			return -EIO;
+			return -EFSCORRUPTED;
 		}
 
 		if (ntfs_ie_end(ie))
 			break;
 
 		if (!ie->key_length)
-			return -EIO;
+			return -EFSCORRUPTED;
 
 		if (ntfs_index_entry_inconsistent(vol, ie,
 						  collation_rule, inum))
-			return -EIO;
+			return -EFSCORRUPTED;
 	}
 
 	return 0;
@@ -496,7 +502,7 @@ static struct index_entry *ntfs_ie_dup_novcn(struct index_entry *ie)
  * The size of block is assumed to have been checked to be what is
  * defined in the index root.
  *
- * Returns 0 if no error was found, -EIO otherwise
+ * Returns 0 if no error was found, -EFSCORRUPTED otherwise
  *
  * |<--->|  offsetof(struct index_block, index)
  * |     |<--->|  sizeof(struct index_header)
@@ -525,7 +531,7 @@ int ntfs_index_block_inconsistent(struct ntfs_volume *vol,
 	if (!ntfs_is_indx_record(ib->magic)) {
 		ntfs_error(sb, "Corrupt index block signature: vcn %lld inode %llu\n",
 			   vcn, (unsigned long long)inum);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	if (le64_to_cpu(ib->index_block_vcn) != vcn) {
@@ -533,23 +539,23 @@ int ntfs_index_block_inconsistent(struct ntfs_volume *vol,
 			"Corrupt index block: s64 (%lld) is different from expected s64 (%lld) in inode %llu\n",
 			(long long)le64_to_cpu(ib->index_block_vcn),
 			vcn, inum);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	if (ib_size != block_size) {
 		ntfs_error(sb,
 			   "Corrupt index block : s64 (%lld) of inode %llu has a size (%u) differing from the index specified size (%u)\n",
 			   vcn, inum, ib_size, block_size);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	if (ntfs_index_header_inconsistent(vol, &ib->index,
 					   block_size -
 					   offsetof(struct index_block, index),
 					   inum))
-		return -EIO;
+		return -EFSCORRUPTED;
 	if (ntfs_index_entries_inconsistent(vol, &ib->index, cr, inum))
-		return -EIO;
+		return -EFSCORRUPTED;
 	return 0;
 }
 
@@ -562,7 +568,7 @@ int ntfs_index_root_inconsistent(struct ntfs_volume *vol,
 	if (value_length < offsetof(struct index_root, index)) {
 		ntfs_error(vol->sb, "$INDEX_ROOT in inode %llu is too small.",
 			   (unsigned long long)inum);
-		return -EIO;
+		return -EFSCORRUPTED;
 	}
 
 	return ntfs_index_header_inconsistent(vol, &ir->index,
@@ -668,7 +674,9 @@ static int ntfs_ie_lookup(const void *key, const u32 key_len,
 			ntfs_error(icx->idx_ni->vol->sb,
 					"Index entry out of bounds in inode %llu.\n",
 					(unsigned long long)icx->idx_ni->mft_no);
-			return -ERANGE;
+			ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+							-EFSCORRUPTED);
+			return -EFSCORRUPTED;
 		}
 
 		/*
@@ -687,7 +695,9 @@ static int ntfs_ie_lookup(const void *key, const u32 key_len,
 		if (rc == -EINVAL) {
 			ntfs_error(icx->idx_ni->vol->sb,
 				"Collation error. Perhaps a filename contains invalid characters?\n");
-			return -ERANGE;
+			ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+							-EFSCORRUPTED);
+			return -EFSCORRUPTED;
 		}
 		/*
 		 * If @key collates before the key of the current entry, there
@@ -719,9 +729,11 @@ static int ntfs_ie_lookup(const void *key, const u32 key_len,
 	/* Get the starting vcn of the index_block holding the child node. */
 	*vcn = ntfs_ie_get_vcn(ie);
 	if (*vcn < 0) {
+		ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+						-EFSCORRUPTED);
 		ntfs_error(icx->idx_ni->vol->sb, "Negative vcn in inode %llu\n",
 				(unsigned long long)icx->idx_ni->mft_no);
-		return -EINVAL;
+		return -EFSCORRUPTED;
 	}
 
 	ntfs_debug("Parent entry number %d\n", item);
@@ -755,19 +767,26 @@ static int ntfs_ib_read(struct ntfs_index_context *icx, s64 vcn, struct index_bl
 
 	ret = ntfs_inode_attr_pread(VFS_I(icx->ia_ni), pos, icx->block_size, (u8 *)dst);
 	if (ret != icx->block_size) {
+		if (ret >= 0 || ret == -EINVAL)
+			ret = -EFSCORRUPTED;
+		if (ret == -EIO || ret == -EFSCORRUPTED)
+			ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), ret);
 		if (ret == -1)
 			ntfs_error(icx->idx_ni->vol->sb, "Failed to read index block");
 		else
 			ntfs_error(icx->idx_ni->vol->sb,
 				"Failed to read full index block at %lld\n", pos);
-		return -EIO;
+		return ret;
 	}
 
-	post_read_mst_fixup((struct ntfs_record *)((u8 *)dst), icx->block_size);
-	if (ntfs_index_block_inconsistent(icx->idx_ni->vol, dst,
-					  icx->block_size, vcn, icx->cr,
-					  icx->idx_ni->mft_no))
-		return -EIO;
+	ret = post_read_mst_fixup((struct ntfs_record *)dst, icx->block_size);
+	if (ret || ntfs_index_block_inconsistent(icx->idx_ni->vol, dst,
+						 icx->block_size, vcn, icx->cr,
+						 icx->idx_ni->mft_no)) {
+		ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+						-EFSCORRUPTED);
+		return -EFSCORRUPTED;
+	}
 	return 0;
 }
 
@@ -785,8 +804,10 @@ static int ntfs_icx_parent_dec(struct ntfs_index_context *icx)
 {
 	icx->pindex--;
 	if (icx->pindex < 0) {
+		ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+						-EFSCORRUPTED);
 		ntfs_error(icx->idx_ni->vol->sb, "Corrupt index pointer (%d)", icx->pindex);
-		return -EINVAL;
+		return -EFSCORRUPTED;
 	}
 	return 0;
 }
@@ -868,7 +889,7 @@ int ntfs_index_lookup(const void *key, const u32 key_len, struct ntfs_index_cont
 
 	old_vcn = VCN_INDEX_ROOT_PARENT;
 	err = ntfs_ie_lookup(key, key_len, icx, &ir->index, &vcn, &ie);
-	if (err == -ERANGE || err == -EINVAL)
+	if (err == -EFSCORRUPTED)
 		goto err_out;
 
 	icx->ir = ir;
@@ -893,21 +914,19 @@ int ntfs_index_lookup(const void *key, const u32 key_len, struct ntfs_index_cont
 
 descend_into_child_node:
 	icx->parent_vcn[icx->pindex] = old_vcn;
-	if (ntfs_icx_parent_inc(icx)) {
-		err = -EIO;
+	err = ntfs_icx_parent_inc(icx);
+	if (err)
 		goto err_out;
-	}
 	old_vcn = vcn;
 
 	ntfs_debug("Descend into node with s64 %lld.\n", vcn);
 
-	if (ntfs_ib_read(icx, vcn, ib)) {
-		err = -EIO;
+	err = ntfs_ib_read(icx, vcn, ib);
+	if (err)
 		goto err_out;
-	}
 	err = ntfs_ie_lookup(key, key_len, icx, &ib->index, &vcn, &ie);
 	if (err != -EAGAIN) {
-		if (err == -EINVAL || err == -ERANGE)
+		if (err == -EFSCORRUPTED)
 			goto err_out;
 
 		icx->is_in_root = false;
@@ -917,6 +936,8 @@ int ntfs_index_lookup(const void *key, const u32 key_len, struct ntfs_index_cont
 	}
 
 	if ((ib->index.flags & NODE_MASK) == LEAF_NODE) {
+		err = -EFSCORRUPTED;
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
 		ntfs_error(icx->idx_ni->vol->sb,
 			"Index entry with child node found in a leaf node in inode 0x%llx.\n",
 			(unsigned long long)ni->mft_no);
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 20/31] ntfs: preserve index traversal errors and directory retries
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (18 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 19/31] ntfs: report index validation and block I/O errors Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 21/31] ntfs: report directory lookup and metadata corruption Baolin Liu
                   ` (11 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Dandan Wu <wudandan@kylinos.cn>

Index-root and allocation lookups collapse errors into NULL, and
tree traversal can turn failures into EIO or EOF. Return error pointers
and update all callers together, including both tree-walk directions.
Report missing or malformed required index metadata as EFSCORRUPTED.

Let readdir return traversal errors without marking the directory as
exhausted. Resume from the insertion position after a saved key is
deleted, advance past consumed entries before a partial result, and
retain the saved key when lookup fails before consuming it.

Signed-off-by: Dandan Wu <wudandan@kylinos.cn>
---
 fs/ntfs/dir.c   |  19 +++++--
 fs/ntfs/index.c | 134 ++++++++++++++++++++++++++++++------------------
 2 files changed, 97 insertions(+), 56 deletions(-)

diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c
index afee30f8b501..7854a06a83b5 100644
--- a/fs/ntfs/dir.c
+++ b/fs/ntfs/dir.c
@@ -805,7 +805,9 @@ static int ntfs_readdir(struct file *file, struct dir_context *actor)
 		err = ntfs_index_lookup(private->key,
 					le16_to_cpu(private->key_length),
 					ictx);
-		if (!err) {
+		if (!err || err == -ENOENT) {
+			/* A deleted resume key leaves the next entry in the context. */
+			err = 0;
 			next = ictx->entry;
 			/*
 			 * Update ie_pos with private->curr_pos
@@ -819,6 +821,8 @@ static int ntfs_readdir(struct file *file, struct dir_context *actor)
 					goto out;
 			}
 
+			if (next->flags & INDEX_ENTRY_END)
+				goto next_entry;
 			goto nextdir;
 		} else {
 			goto out;
@@ -879,8 +883,9 @@ static int ntfs_readdir(struct file *file, struct dir_context *actor)
 
 	if (next->flags & INDEX_ENTRY_NODE) {
 		ictx->ia_ni = ntfs_ia_open(ictx, ictx->idx_ni);
-		if (!ictx->ia_ni) {
-			err = -EINVAL;
+		if (IS_ERR(ictx->ia_ni)) {
+			err = PTR_ERR(ictx->ia_ni);
+			ictx->ia_ni = NULL;
 			goto out;
 		}
 
@@ -901,6 +906,7 @@ static int ntfs_readdir(struct file *file, struct dir_context *actor)
 		goto nextdir;
 
 	while (1) {
+next_entry:
 		next = ntfs_index_next(next, ictx);
 		if (IS_ERR(next)) {
 			err = PTR_ERR(next);
@@ -989,7 +995,11 @@ static int ntfs_readdir(struct file *file, struct dir_context *actor)
 			private->key_length = next->key_length;
 			break;
 		}
+		/* The saved key is consumed; retry from the next offset. */
+		kfree(private->key);
+		private->key = NULL;
 		ie_pos += le16_to_cpu(next->length);
+		actor->pos = ie_pos;
 	}
 
 	if (!err)
@@ -1015,9 +1025,8 @@ static int ntfs_readdir(struct file *file, struct dir_context *actor)
 	if (err) {
 		if (private) {
 			private->curr_pos = actor->pos;
-			private->end_in_iterate = true;
+			private->end_in_iterate = false;
 		}
-		err = 0;
 	}
 	ntfs_index_ctx_put(ictx);
 	kfree(name);
diff --git a/fs/ntfs/index.c b/fs/ntfs/index.c
index 1adb722f0e7c..ab678998afc8 100644
--- a/fs/ntfs/index.c
+++ b/fs/ntfs/index.c
@@ -581,35 +581,44 @@ static struct index_root *ntfs_ir_lookup(struct ntfs_inode *ni, __le16 *name,
 		u32 name_len, struct ntfs_attr_search_ctx **ctx)
 {
 	struct attr_record *a;
-	struct index_root *ir = NULL;
+	struct index_root *ir;
+	int err;
 
 	ntfs_debug("Entering\n");
 	*ctx = ntfs_attr_get_search_ctx(ni, NULL);
 	if (IS_ERR(*ctx)) {
 		ntfs_error(ni->vol->sb, "%s, Failed to get search context", __func__);
+		err = PTR_ERR(*ctx);
 		*ctx = NULL;
-		return NULL;
+		return ERR_PTR(err);
 	}
 
-	if (ntfs_attr_lookup(AT_INDEX_ROOT, name, name_len, CASE_SENSITIVE,
-				0, NULL, 0, *ctx)) {
+	err = ntfs_attr_lookup(AT_INDEX_ROOT, name, name_len, CASE_SENSITIVE,
+			       0, NULL, 0, *ctx);
+	if (err) {
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+		}
 		ntfs_error(ni->vol->sb, "Failed to lookup $INDEX_ROOT");
 		goto err_out;
 	}
 
 	a = (*ctx)->attr;
 	if (a->non_resident) {
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
 		ntfs_error(ni->vol->sb, "Non-resident $INDEX_ROOT detected");
+		err = -EFSCORRUPTED;
 		goto err_out;
 	}
 
 	ir = (struct index_root *)((char *)a + le16_to_cpu(a->data.resident.value_offset));
-err_out:
-	if (!ir) {
-		ntfs_attr_put_search_ctx(*ctx);
-		*ctx = NULL;
-	}
 	return ir;
+err_out:
+	ntfs_attr_put_search_ctx(*ctx);
+	*ctx = NULL;
+	return ERR_PTR(err);
 }
 
 static struct index_root *ntfs_ir_lookup2(struct ntfs_inode *ni, __le16 *name, u32 len)
@@ -618,7 +627,7 @@ static struct index_root *ntfs_ir_lookup2(struct ntfs_inode *ni, __le16 *name, u
 	struct index_root *ir;
 
 	ir = ntfs_ir_lookup(ni, name, len, &ctx);
-	if (ir)
+	if (!IS_ERR(ir))
 		ntfs_attr_put_search_ctx(ctx);
 	return ir;
 }
@@ -631,8 +640,8 @@ static int ntfs_ir_move_to_base(struct ntfs_index_context *icx)
 	int ret = 0;
 
 	ir = ntfs_ir_lookup(icx->idx_ni, icx->name, icx->name_len, &ctx);
-	if (!ir)
-		return -ENOENT;
+	if (IS_ERR(ir))
+		return PTR_ERR(ir);
 
 	if (ctx->ntfs_ino->mft_no != icx->idx_ni->mft_no) {
 		ret = ntfs_attr_record_move_to(ctx, icx->idx_ni);
@@ -751,7 +760,7 @@ struct ntfs_inode *ntfs_ia_open(struct ntfs_index_context *icx, struct ntfs_inod
 		ntfs_error(icx->idx_ni->vol->sb,
 				"Failed to open index allocation of inode %llu",
 				(unsigned long long)ni->mft_no);
-		return NULL;
+		return ERR_CAST(ia_vi);
 	}
 
 	return NTFS_I(ia_vi);
@@ -862,12 +871,13 @@ int ntfs_index_lookup(const void *key, const u32 key_len, struct ntfs_index_cont
 	}
 
 	ir = ntfs_ir_lookup(ni, icx->name, icx->name_len, &icx->actx);
-	if (!ir)
-		return -EIO;
+	if (IS_ERR(ir))
+		return PTR_ERR(ir);
 
 	icx->block_size = le32_to_cpu(ir->index_block_size);
 	if (icx->block_size < NTFS_BLOCK_SIZE) {
-		err = -EINVAL;
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
+		err = -EFSCORRUPTED;
 		ntfs_error(sb,
 			"Index block size (%d) is smaller than the sector size (%d)",
 			icx->block_size, NTFS_BLOCK_SIZE);
@@ -901,8 +911,9 @@ int ntfs_index_lookup(const void *key, const u32 key_len, struct ntfs_index_cont
 
 	/* Child node present, descend into it. */
 	icx->ia_ni = ntfs_ia_open(icx, ni);
-	if (!icx->ia_ni) {
-		err = -EIO;
+	if (IS_ERR(icx->ia_ni)) {
+		err = PTR_ERR(icx->ia_ni);
+		icx->ia_ni = NULL;
 		goto err_out;
 	}
 
@@ -1161,7 +1172,8 @@ static s64 ntfs_ibm_get_free(struct ntfs_index_context *icx)
 	return vcn;
 }
 
-static struct index_block *ntfs_ir_to_ib(struct index_root *ir, s64 ib_vcn)
+static struct index_block *ntfs_ir_to_ib(struct ntfs_index_context *icx,
+					 struct index_root *ir, s64 ib_vcn)
 {
 	struct index_block *ib;
 	struct index_entry *ie_last;
@@ -1173,7 +1185,7 @@ static struct index_block *ntfs_ir_to_ib(struct index_root *ir, s64 ib_vcn)
 
 	ib = ntfs_ib_alloc(ib_vcn, le32_to_cpu(ir->index_block_size), LEAF_NODE);
 	if (!ib)
-		return NULL;
+		return ERR_PTR(-ENOMEM);
 
 	ies_start = (char *)ntfs_ie_get_first(&ir->index);
 	ies_end   = (char *)ntfs_ie_get_end(&ir->index);
@@ -1188,9 +1200,11 @@ static struct index_block *ntfs_ir_to_ib(struct index_root *ir, s64 ib_vcn)
 	ib_cap = le32_to_cpu(ib->index.allocated_size) -
 			le32_to_cpu(ib->index.entries_offset);
 	if ((u32)i > ib_cap) {
+		ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+						-EFSCORRUPTED);
 		ntfs_error(NULL, "Entries (%d B) exceed IB capacity", i);
 		kvfree(ib);
-		return NULL;
+		return ERR_PTR(-EFSCORRUPTED);
 	}
 
 	memcpy(ntfs_ie_get_first(&ib->index), ies_start, i);
@@ -1296,8 +1310,11 @@ static int ntfs_ia_add(struct ntfs_index_context *icx)
 	}
 
 	icx->ia_ni = ntfs_ia_open(icx, icx->idx_ni);
-	if (!icx->ia_ni)
-		return -ENOENT;
+	if (IS_ERR(icx->ia_ni)) {
+		ret = PTR_ERR(icx->ia_ni);
+		icx->ia_ni = NULL;
+		return ret;
+	}
 
 	return 0;
 }
@@ -1317,8 +1334,8 @@ static int ntfs_ir_reparent(struct ntfs_index_context *icx)
 	ntfs_debug("Entering\n");
 
 	ir = ntfs_ir_lookup2(icx->idx_ni, icx->name, icx->name_len);
-	if (!ir) {
-		ret = -ENOENT;
+	if (IS_ERR(ir)) {
+		ret = PTR_ERR(ir);
 		goto out;
 	}
 
@@ -1335,14 +1352,15 @@ static int ntfs_ir_reparent(struct ntfs_index_context *icx)
 	}
 
 	ir = ntfs_ir_lookup2(icx->idx_ni, icx->name, icx->name_len);
-	if (!ir) {
-		ret = -ENOENT;
+	if (IS_ERR(ir)) {
+		ret = PTR_ERR(ir);
 		goto clear_bmp;
 	}
 
-	ib = ntfs_ir_to_ib(ir, new_ib_vcn);
-	if (ib == NULL) {
-		ret = -EIO;
+	ib = ntfs_ir_to_ib(icx, ir, new_ib_vcn);
+	if (IS_ERR(ib)) {
+		ret = PTR_ERR(ib);
+		ib = NULL;
 		ntfs_error(icx->idx_ni->vol->sb, "Failed to move index root to index block");
 		goto clear_bmp;
 	}
@@ -1353,8 +1371,8 @@ static int ntfs_ir_reparent(struct ntfs_index_context *icx)
 
 retry:
 	ir = ntfs_ir_lookup(icx->idx_ni, icx->name, icx->name_len, &ctx);
-	if (!ir) {
-		ret = -ENOENT;
+	if (IS_ERR(ir)) {
+		ret = PTR_ERR(ir);
 		goto clear_bmp;
 	}
 
@@ -1475,8 +1493,8 @@ static int ntfs_ir_truncate(struct ntfs_index_context *icx, int data_size)
 	if (!ret) {
 		i_size_write(VFS_I(icx->idx_ni), icx->idx_ni->initialized_size);
 		icx->ir = ntfs_ir_lookup2(icx->idx_ni, icx->name, icx->name_len);
-		if (!icx->ir)
-			return -ENOENT;
+		if (IS_ERR(icx->ir))
+			return PTR_ERR(icx->ir);
 
 		if (!shrink)
 			icx->ir->index.allocated_size = cpu_to_le32(data_size);
@@ -1578,8 +1596,8 @@ static int ntfs_ir_insert_median(struct ntfs_index_context *icx, struct index_en
 	ntfs_debug("Entering\n");
 
 	icx->ir = ntfs_ir_lookup2(icx->idx_ni, icx->name, icx->name_len);
-	if (!icx->ir)
-		return -ENOENT;
+	if (IS_ERR(icx->ir))
+		return PTR_ERR(icx->ir);
 
 	new_size = le32_to_cpu(icx->ir->index.index_length) +
 		le16_to_cpu(median->length);
@@ -1591,8 +1609,8 @@ static int ntfs_ir_insert_median(struct ntfs_index_context *icx, struct index_en
 		return ret;
 
 	icx->ir = ntfs_ir_lookup2(icx->idx_ni, icx->name, icx->name_len);
-	if (!icx->ir)
-		return -ENOENT;
+	if (IS_ERR(icx->ir))
+		return PTR_ERR(icx->ir);
 
 	return ntfs_ih_insert(&icx->ir->index, median, new_vcn,
 			ntfs_icx_parent_pos(icx));
@@ -1972,8 +1990,11 @@ static int ntfs_index_rm_node(struct ntfs_index_context *icx)
 
 	if (!icx->ia_ni) {
 		icx->ia_ni = ntfs_ia_open(icx, icx->idx_ni);
-		if (!icx->ia_ni)
-			return -EINVAL;
+		if (IS_ERR(icx->ia_ni)) {
+			ret = PTR_ERR(icx->ia_ni);
+			icx->ia_ni = NULL;
+			return ret;
+		}
 	}
 
 	ib = kvzalloc(icx->block_size, GFP_NOFS);
@@ -2187,11 +2208,13 @@ struct index_entry *ntfs_index_walk_down(struct index_entry *ie, struct ntfs_ind
 
 		ictx->parent_pos[ictx->pindex] = 0;
 		ictx->parent_vcn[ictx->pindex] = vcn;
-		if (!ntfs_ib_read(ictx, vcn, ictx->ib)) {
+		err = ntfs_ib_read(ictx, vcn, ictx->ib);
+		if (!err) {
 			ictx->entry = ntfs_ie_get_first(&ictx->ib->index);
 			entry = ictx->entry;
-		} else
-			entry = ERR_PTR(-EIO);
+		} else {
+			entry = ERR_PTR(err);
+		}
 	} while (!IS_ERR(entry) && (entry->flags & INDEX_ENTRY_NODE));
 
 	return entry;
@@ -2209,6 +2232,7 @@ static struct index_entry *ntfs_index_walk_up(struct index_entry *ie,
 {
 	struct index_entry *entry = ie;
 	s64 vcn;
+	int err;
 
 	if (ictx->pindex <= 0)
 		return NULL;
@@ -2225,21 +2249,23 @@ static struct index_entry *ntfs_index_walk_up(struct index_entry *ie,
 				ntfs_attr_put_search_ctx(ictx->actx);
 			ictx->ir = ntfs_ir_lookup(ictx->idx_ni, ictx->name,
 						  ictx->name_len, &ictx->actx);
-			if (ictx->ir)
+			if (!IS_ERR(ictx->ir))
 				entry = ntfs_ie_get_by_pos(
 					&ictx->ir->index,
 					ictx->parent_pos[ictx->pindex]);
 			else
-				entry = NULL;
+				return ERR_CAST(ictx->ir);
 		} else {
 			/* up into non-root node */
 			vcn = ictx->parent_vcn[ictx->pindex];
-			if (!ntfs_ib_read(ictx, vcn, ictx->ib)) {
+			err = ntfs_ib_read(ictx, vcn, ictx->ib);
+			if (!err) {
 				entry = ntfs_ie_get_by_pos(
 					&ictx->ib->index,
 					ictx->parent_pos[ictx->pindex]);
-			} else
-				entry = NULL;
+			} else {
+				return ERR_PTR(err);
+			}
 		}
 		ictx->entry = entry;
 	} while (entry && (ictx->pindex > 0) &&
@@ -2249,7 +2275,7 @@ static struct index_entry *ntfs_index_walk_up(struct index_entry *ie,
 
 /*
  * ntfs_index_next - get next entry in an index according to collating sequence.
- * Returns next entry or NULL if none.
+ * Returns the next entry, NULL if none, or an error pointer.
  *
  * Sample layout :
  *
@@ -2299,8 +2325,11 @@ struct index_entry *ntfs_index_next(struct index_entry *ie, struct ntfs_index_co
 		if (flags & INDEX_ENTRY_NODE) {
 			if (!ictx->ia_ni) {
 				ictx->ia_ni = ntfs_ia_open(ictx, ictx->idx_ni);
-				if (!ictx->ia_ni)
-					return ERR_PTR(-EIO);
+				if (IS_ERR(ictx->ia_ni)) {
+					next = ERR_CAST(ictx->ia_ni);
+					ictx->ia_ni = NULL;
+					return next;
+				}
 			}
 
 			next = ntfs_index_walk_down(next, ictx);
@@ -2314,6 +2343,9 @@ struct index_entry *ntfs_index_next(struct index_entry *ie, struct ntfs_index_co
 		}
 	}
 
+	if (IS_ERR(next))
+		return next;
+
 	/* return NULL if stuck at end of a block */
 	if (next && (next->flags & INDEX_ENTRY_END))
 		next = NULL;
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 21/31] ntfs: report directory lookup and metadata corruption
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (19 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 20/31] ntfs: preserve index traversal errors and directory retries Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 22/31] ntfs: report index update and recovery failures Baolin Liu
                   ` (10 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Dandan Wu <wudandan@kylinos.cn>

Directory lookup and enumeration detect malformed indexes, missing
required attributes and stale references without consistently reporting
them. Report corruption against the directory and use EFSCORRUPTED while
preserving underlying errors and ordinary lookup misses.

Attribute dangling reparse entries to the directory. Report unused
parents during directory fsync except for deleting children, retaining
its existing return behavior.

Signed-off-by: Dandan Wu <wudandan@kylinos.cn>
---
 fs/ntfs/dir.c     | 71 ++++++++++++++++++++++++++++++++---------------
 fs/ntfs/reparse.c |  9 ++++--
 fs/ntfs/reparse.h |  2 +-
 3 files changed, 56 insertions(+), 26 deletions(-)

diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c
index 7854a06a83b5..b54b7eee0c08 100644
--- a/fs/ntfs/dir.c
+++ b/fs/ntfs/dir.c
@@ -24,10 +24,12 @@
 __le16 I30[5] = { cpu_to_le16('$'), cpu_to_le16('I'),
 		cpu_to_le16('3'),	cpu_to_le16('0'), 0 };
 
-static inline u64 ntfs_check_mref(u64 mref)
+static inline u64 ntfs_check_mref(struct ntfs_inode *dir_ni, u64 mref)
 {
-	if (IS_ERR_MREF(mref))
-		return ERR_MREF(-EIO);
+	if (IS_ERR_MREF(mref)) {
+		ntfs_report_file_metadata_error(VFS_I(dir_ni), -EFSCORRUPTED);
+		return ERR_MREF(-EFSCORRUPTED);
+	}
 	return mref;
 }
 
@@ -113,7 +115,9 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 			ntfs_error(sb,
 				"Index root attribute missing in directory inode 0x%llx.",
 				dir_ni->mft_no);
-			err = -EIO;
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(dir_ni),
+							-EFSCORRUPTED);
 		}
 		goto err_out;
 	}
@@ -187,7 +191,7 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 			mref = le64_to_cpu(ie->data.dir.indexed_file);
 			ntfs_attr_put_search_ctx(ctx);
 			unmap_mft_record(dir_ni);
-			return ntfs_check_mref(mref);
+			return ntfs_check_mref(dir_ni, mref);
 		}
 		/*
 		 * For a case insensitive mount, we also perform a case
@@ -282,7 +286,7 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 		if (name) {
 			ntfs_attr_put_search_ctx(ctx);
 			unmap_mft_record(dir_ni);
-			return ntfs_check_mref(name->mref);
+			return ntfs_check_mref(dir_ni, name->mref);
 		}
 		ntfs_debug("Entry not found.");
 		err = -ENOENT;
@@ -320,6 +324,8 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 		ntfs_error(sb, "Failed to map directory index page, error %ld.",
 				-PTR_ERR(folio));
 		err = PTR_ERR(folio);
+		if (err == -EIO)
+			ntfs_report_file_metadata_error(VFS_I(dir_ni), err);
 		goto err_out;
 	}
 
@@ -344,14 +350,14 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 		ntfs_error(sb,
 			"Out of bounds check failed. Corrupt directory inode 0x%llx or driver bug.",
 			dir_ni->mft_no);
-		goto unm_err_out;
+		goto unm_corrupt;
 	}
 	index_end = (u8 *)ia + dir_ni->itype.index.block_size;
 	if (index_end > kaddr + PAGE_SIZE) {
 		ntfs_error(sb,
 			   "Index buffer (VCN 0x%llx) of directory inode 0x%llx crosses page boundary. Impossible! Cannot access! This is probably a bug in the driver.",
 			   vcn, dir_ni->mft_no);
-		goto unm_err_out;
+		goto unm_corrupt;
 	}
 	err = post_read_mst_fixup((struct ntfs_record *)ia,
 				  dir_ni->itype.index.block_size);
@@ -359,14 +365,14 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 		ntfs_error(sb,
 			   "MST fixup failed for index block vcn %lld in directory inode 0x%llx.",
 			   vcn, dir_ni->mft_no);
-		goto unm_err_out;
+		goto unm_corrupt;
 	}
 	err = ntfs_index_block_inconsistent(vol, ia,
 					    dir_ni->itype.index.block_size,
 					    vcn, COLLATION_FILE_NAME,
 					    dir_ni->mft_no);
 	if (err)
-		goto unm_err_out;
+		goto unm_corrupt;
 	index_end = (u8 *)&ia->index + le32_to_cpu(ia->index.index_length);
 	/* The first index entry. */
 	ie = (struct index_entry *)((u8 *)&ia->index +
@@ -385,7 +391,7 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 			break;
 		/* Key length should not be zero if it is not last entry. */
 		if (!ie->key_length)
-			goto unm_err_out;
+			goto unm_corrupt;
 		/*
 		 * We perform a case sensitive comparison and if that matches
 		 * we are done and return the mft reference of the inode (i.e.
@@ -429,7 +435,7 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 			mref = le64_to_cpu(ie->data.dir.indexed_file);
 			kfree(kaddr);
 			iput(ia_vi);
-			return ntfs_check_mref(mref);
+			return ntfs_check_mref(dir_ni, mref);
 		}
 		/*
 		 * For a case insensitive mount, we also perform a case
@@ -524,7 +530,7 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 			ntfs_error(sb,
 				"Index entry with child node found in a leaf node in directory inode 0x%llx.",
 				dir_ni->mft_no);
-			goto unm_err_out;
+			goto unm_corrupt;
 		}
 		/* Child node present, descend into it. */
 		old_vcn = vcn;
@@ -544,7 +550,7 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 		}
 		ntfs_error(sb, "Negative child node vcn in directory inode 0x%llx.",
 				dir_ni->mft_no);
-		goto unm_err_out;
+		goto unm_corrupt;
 	}
 	/*
 	 * No child node present, return -ENOENT, unless we have got a matching
@@ -554,15 +560,19 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 	if (name) {
 		kfree(kaddr);
 		iput(ia_vi);
-		return ntfs_check_mref(name->mref);
+		return ntfs_check_mref(dir_ni, name->mref);
 	}
 	ntfs_debug("Entry not found.");
 	err = -ENOENT;
+	goto unm_err_out;
+unm_corrupt:
+	err = -EFSCORRUPTED;
+	ntfs_report_file_metadata_error(VFS_I(dir_ni), -EFSCORRUPTED);
 unm_err_out:
 	kfree(kaddr);
 err_out:
 	if (!err)
-		err = -EIO;
+		err = -EFSCORRUPTED;
 	if (ctx)
 		ntfs_attr_put_search_ctx(ctx);
 	if (m)
@@ -573,6 +583,7 @@ u64 ntfs_lookup_inode_by_name(struct ntfs_inode *dir_ni, const __le16 *uname,
 		iput(ia_vi);
 	return ERR_MREF(err);
 dir_err_out:
+	ntfs_report_file_metadata_error(VFS_I(dir_ni), -EFSCORRUPTED);
 	ntfs_error(sb, "Corrupt directory.  Aborting lookup.");
 	goto err_out;
 }
@@ -640,7 +651,7 @@ static inline int ntfs_filldir(struct ntfs_volume *vol,
 	}
 	mref = MREF_LE(ie->data.dir.indexed_file);
 	if (ie->key.file_name.file_attributes & FILE_ATTR_REPARSE_POINT)
-		dt_type = ntfs_reparse_tag_dt_types(vol, mref);
+		dt_type = ntfs_reparse_tag_dt_types(VFS_I(ndir), mref);
 	else if (ie->key.file_name.file_attributes &
 			FILE_ATTR_DUP_FILE_NAME_INDEX_PRESENT)
 		dt_type = DT_DIR;
@@ -844,12 +855,17 @@ static int ntfs_readdir(struct file *file, struct dir_context *actor)
 	}
 
 	/* Find the index root attribute in the mft record. */
-	if (ntfs_attr_lookup(AT_INDEX_ROOT, I30, 4, CASE_SENSITIVE, 0, NULL, 0,
-				ctx)) {
+	err = ntfs_attr_lookup(AT_INDEX_ROOT, I30, 4, CASE_SENSITIVE, 0,
+			       NULL, 0, ctx);
+	if (err) {
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(ndir),
+							-EFSCORRUPTED);
+		}
 		ntfs_error(sb, "Index root attribute missing in directory inode %llu",
 				ndir->mft_no);
 		ntfs_attr_put_search_ctx(ctx);
-		err = -ENOMEM;
 		goto out;
 	}
 
@@ -867,7 +883,8 @@ static int ntfs_readdir(struct file *file, struct dir_context *actor)
 	if (ictx->block_size < NTFS_BLOCK_SIZE) {
 		ntfs_error(sb, "Index block size (%d) is smaller than the sector size (%d)",
 				ictx->block_size, NTFS_BLOCK_SIZE);
-		err = -EIO;
+		ntfs_report_file_metadata_error(VFS_I(ndir), -EFSCORRUPTED);
+		err = -EFSCORRUPTED;
 		goto out;
 	}
 
@@ -1054,6 +1071,11 @@ int ntfs_check_empty_dir(struct ntfs_inode *ni, struct mft_record *ni_mrec)
 	ret = ntfs_attr_lookup(AT_INDEX_ROOT, I30, 4, CASE_SENSITIVE, 0, NULL,
 				0, ctx);
 	if (ret) {
+		if (ret == -ENOENT) {
+			ret = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+		}
 		ntfs_error(ni->vol->sb, "Index root attribute missing in directory inode %llu",
 				ni->mft_no);
 		ntfs_attr_put_search_ctx(ctx);
@@ -1158,8 +1180,13 @@ static int ntfs_dir_fsync(struct file *filp, loff_t start, loff_t end,
 			continue;
 
 		parent_vi = ntfs_iget(vi->i_sb, MREF_LE(fn->parent_directory));
-		if (IS_ERR(parent_vi))
+		if (IS_ERR(parent_vi)) {
+			if (PTR_ERR(parent_vi) == -ENOENT && !NInoBeingDeleted(ni)) {
+				ntfs_report_file_metadata_error(vi,
+								-EFSCORRUPTED);
+			}
 			continue;
+		}
 		mutex_lock_nested(&NTFS_I(parent_vi)->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
 		ia_vi = ntfs_index_iget(parent_vi, I30, 4);
 		mutex_unlock(&NTFS_I(parent_vi)->mrec_lock);
diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c
index 2a6480211fe3..c05768a49440 100644
--- a/fs/ntfs/reparse.c
+++ b/fs/ntfs/reparse.c
@@ -396,16 +396,19 @@ int ntfs_parse_reparse(struct ntfs_inode *ni, unsigned int *mode)
 	return err;
 }
 
-unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mref)
+unsigned int ntfs_reparse_tag_dt_types(struct inode *dir, unsigned long mref)
 {
 	s64 attr_size = 0;
 	struct reparse_point *reparse_attr;
 	unsigned int dt_type = DT_UNKNOWN;
 	struct inode *vi;
 
-	vi = ntfs_iget(vol->sb, mref);
-	if (IS_ERR(vi))
+	vi = ntfs_iget(dir->i_sb, mref);
+	if (IS_ERR(vi)) {
+		if (PTR_ERR(vi) == -ENOENT)
+			ntfs_report_file_metadata_error(dir, -EFSCORRUPTED);
 		return DT_UNKNOWN;
+	}
 
 	reparse_attr = (struct reparse_point *)ntfs_attr_readall(NTFS_I(vi),
 			AT_REPARSE_POINT, NULL, 0, &attr_size);
diff --git a/fs/ntfs/reparse.h b/fs/ntfs/reparse.h
index b6360b0452e6..b81baf196d6c 100644
--- a/fs/ntfs/reparse.h
+++ b/fs/ntfs/reparse.h
@@ -10,7 +10,7 @@
 extern __le16 reparse_index_name[];
 
 int ntfs_parse_reparse(struct ntfs_inode *ni, unsigned int *mode);
-unsigned int ntfs_reparse_tag_dt_types(struct ntfs_volume *vol, unsigned long mref);
+unsigned int ntfs_reparse_tag_dt_types(struct inode *dir, unsigned long mref);
 int ntfs_translate_symlink_path(struct dentry *dentry, const char *target,
 				char **translated);
 int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni,
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 22/31] ntfs: report index update and recovery failures
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (20 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 21/31] ntfs: report directory lookup and metadata corruption Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 23/31] ntfs: report namespace and file recovery errors Baolin Liu
                   ` (9 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Dandan Wu <wudandan@kylinos.cn>

Index bitmap updates, root relocation and entry deletion can fail
after metadata has changed without reporting the incomplete update.
Report failed cleanup and restoration against the index owner,
including ENOMEM that leaves an unfinished deletion.

Report malformed bitmap and tree state as EFSCORRUPTED. Preserve
independent recovery errors and keep early allocation failures distinct.

Signed-off-by: Dandan Wu <wudandan@kylinos.cn>
---
 fs/ntfs/index.c | 77 +++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 61 insertions(+), 16 deletions(-)

diff --git a/fs/ntfs/index.c b/fs/ntfs/index.c
index ab678998afc8..4887dece73e3 100644
--- a/fs/ntfs/index.c
+++ b/fs/ntfs/index.c
@@ -1084,6 +1084,11 @@ static int ntfs_ibm_modify(struct ntfs_index_context *icx, s64 vcn, int set)
 
 	bmp_vi = ntfs_attr_iget(VFS_I(icx->idx_ni), AT_BITMAP, icx->name, icx->name_len);
 	if (IS_ERR(bmp_vi)) {
+		if (PTR_ERR(bmp_vi) == -ENOENT) {
+			ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+							-EFSCORRUPTED);
+			bmp_vi = ERR_PTR(-EFSCORRUPTED);
+		}
 		ntfs_error(icx->idx_ni->vol->sb, "Failed to open $BITMAP attribute");
 		return PTR_ERR(bmp_vi);
 	}
@@ -1101,8 +1106,12 @@ static int ntfs_ibm_modify(struct ntfs_index_context *icx, s64 vcn, int set)
 		}
 	}
 
-	if (ntfs_inode_attr_pread(bmp_vi, bpos, 1, &byte) != 1) {
-		ret = -EIO;
+	ret = ntfs_inode_attr_pread(bmp_vi, bpos, 1, &byte);
+	if (ret != 1) {
+		if (ret >= 0 || ret == -EINVAL)
+			ret = -EFSCORRUPTED;
+		if (ret == -EIO || ret == -EFSCORRUPTED)
+			ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), ret);
 		ntfs_error(icx->idx_ni->vol->sb, "Failed to read $BITMAP");
 		goto err;
 	}
@@ -1112,11 +1121,15 @@ static int ntfs_ibm_modify(struct ntfs_index_context *icx, s64 vcn, int set)
 	else
 		byte &= ~bit;
 
-	if (ntfs_inode_attr_pwrite(bmp_vi, bpos, 1, &byte, false) != 1) {
-		ret = -EIO;
+	ret = ntfs_inode_attr_pwrite(bmp_vi, bpos, 1, &byte, false);
+	if (ret != 1) {
+		if (ret >= 0)
+			ret = -EIO;
+		ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), ret);
 		ntfs_error(icx->idx_ni->vol->sb, "Failed to write $Bitmap");
 		goto err;
 	}
+	ret = 0;
 
 err:
 	iput(bmp_vi);
@@ -1130,7 +1143,12 @@ static int ntfs_ibm_set(struct ntfs_index_context *icx, s64 vcn)
 
 static int ntfs_ibm_clear(struct ntfs_index_context *icx, s64 vcn)
 {
-	return ntfs_ibm_modify(icx, vcn, 0);
+	int err = ntfs_ibm_modify(icx, vcn, 0);
+
+	/* Failure leaves an index block allocated, including during rollback. */
+	if (err)
+		ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), err);
+	return err;
 }
 
 static s64 ntfs_ibm_get_free(struct ntfs_index_context *icx)
@@ -1144,8 +1162,14 @@ static s64 ntfs_ibm_get_free(struct ntfs_index_context *icx)
 
 	bm = ntfs_attr_readall(icx->idx_ni, AT_BITMAP,  icx->name, icx->name_len,
 			&size);
-	if (IS_ERR(bm))
+	if (IS_ERR(bm)) {
+		if (PTR_ERR(bm) == -ENOENT) {
+			ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+							-EFSCORRUPTED);
+			bm = ERR_PTR(-EFSCORRUPTED);
+		}
 		return PTR_ERR(bm);
+	}
 
 	for (byte = 0; byte < size; byte++) {
 		if (bm[byte] == 255)
@@ -1445,10 +1469,13 @@ static int ntfs_ir_reparent(struct ntfs_index_context *icx)
 			ntfs_attr_put_search_ctx(ctx);
 			ctx = NULL;
 			rollback_ret = ntfs_ir_move_to_base(icx);
-			if (rollback_ret)
+			if (rollback_ret) {
+				ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+								rollback_ret);
 				ntfs_error(icx->idx_ni->vol->sb,
 					   "Failed to roll back INDEX_ROOT relocation: %d",
 					   rollback_ret);
+			}
 			goto clear_bmp;
 		}
 
@@ -1916,8 +1943,11 @@ static int ntfs_ih_reparent_end(struct ntfs_index_context *icx, struct index_hea
 
 	ie = ntfs_ie_get_by_pos(ih, ntfs_icx_parent_pos(icx));
 	ie_prev = ntfs_ie_prev(ih, ie);
-	if (!ie_prev)
-		return -EIO;
+	if (!ie_prev) {
+		ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+						-EFSCORRUPTED);
+		return -EFSCORRUPTED;
+	}
 	ntfs_ie_set_vcn(ie, ntfs_ie_get_vcn(ie_prev));
 
 	return ntfs_ih_takeout(icx, ih, ie_prev, ib);
@@ -1944,8 +1974,10 @@ static int ntfs_index_rm_leaf(struct ntfs_index_context *icx)
 		parent_ih = &icx->ir->index;
 	else {
 		ib = kvzalloc(icx->block_size, GFP_NOFS);
-		if (!ib)
-			return -ENOMEM;
+		if (!ib) {
+			ret = -ENOMEM;
+			goto out;
+		}
 
 		ret = ntfs_ib_read(icx, ntfs_icx_parent_vcn(icx), ib);
 		if (ret)
@@ -1972,6 +2004,9 @@ static int ntfs_index_rm_leaf(struct ntfs_index_context *icx)
 
 	ret = ntfs_ih_reparent_end(icx, parent_ih, ib);
 out:
+	/* The allocation bit is clear, but the parent update did not finish. */
+	if (ret)
+		ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), ret);
 	kvfree(ib);
 	return ret;
 }
@@ -2023,7 +2058,9 @@ static int ntfs_index_rm_node(struct ntfs_index_context *icx)
 		goto descend;
 
 	if (ntfs_ih_zero_entry(&ib->index)) {
-		ret = -EIO;
+		ntfs_report_file_metadata_error(VFS_I(icx->idx_ni),
+						-EFSCORRUPTED);
+		ret = -EFSCORRUPTED;
 		ntfs_error(icx->idx_ni->vol->sb, "Empty index block");
 		goto out;
 	}
@@ -2068,10 +2105,14 @@ static int ntfs_index_rm_node(struct ntfs_index_context *icx)
 	ntfs_ie_delete(ih, entry);
 	ntfs_ie_insert(ih, ie, entry);
 
-	if (icx->is_in_root)
+	if (icx->is_in_root) {
 		ret = ntfs_ir_truncate(icx, new_size);
-	else
+		/* The old key has been replaced without removing its successor. */
+		if (ret)
+			ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), ret);
+	} else {
 		ret = ntfs_icx_ib_write(icx);
+	}
 	if (ret)
 		goto out2;
 
@@ -2120,10 +2161,14 @@ int ntfs_index_rm(struct ntfs_index_context *icx)
 	} else if (icx->is_in_root || !ntfs_ih_one_entry(ih)) {
 		ntfs_ie_delete(ih, icx->entry);
 
-		if (icx->is_in_root)
+		if (icx->is_in_root) {
 			ret = ntfs_ir_truncate(icx, le32_to_cpu(ih->index_length));
-		else
+			/* The entry is already removed and will not be restored. */
+			if (ret)
+				ntfs_report_file_metadata_error(VFS_I(icx->idx_ni), ret);
+		} else {
 			ret = ntfs_icx_ib_write(icx);
+		}
 		if (ret)
 			goto err_out;
 	} else {
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 23/31] ntfs: report namespace and file recovery errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (21 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 22/31] ntfs: report index update and recovery failures Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 24/31] ntfs: serialize and report export parent reconstruction errors Baolin Liu
                   ` (8 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Dandan Wu <wudandan@kylinos.cn>

Report stale directory references, missing FILE_NAME counterparts and
missing index entries during deletion. Return EFSCORRUPTED from the
affected failing lookups; ordinary missing names remain valid results.

Report independent cleanup failures during create, link, rename, file
size restoration and preallocation trimming. Use directory attribution
where appropriate and volume reports during failed creation.

Report missing required parents and attributes during file fsync while
retaining its best-effort lookup handling. Preserve the writeback errno
from the buffered fallback of direct writes instead of replacing it
with EIO.

Signed-off-by: Dandan Wu <wudandan@kylinos.cn>
---
 fs/ntfs/file.c  | 29 ++++++++++++++---
 fs/ntfs/namei.c | 86 ++++++++++++++++++++++++++++++++++++-------------
 2 files changed, 88 insertions(+), 27 deletions(-)

diff --git a/fs/ntfs/file.c b/fs/ntfs/file.c
index cd5ed8bf3164..9c4d9dbab19e 100644
--- a/fs/ntfs/file.c
+++ b/fs/ntfs/file.c
@@ -117,14 +117,17 @@ static int ntfs_trim_prealloc(struct inode *vi)
 		if (err) {
 			kvfree(ni->runlist.rl);
 			ni->runlist.rl = NULL;
+			ntfs_report_file_metadata_error(vi, err);
 			ntfs_error(vol->sb, "Preallocated block rollback failed");
 		} else {
 			ni->allocated_size = ntfs_cluster_to_bytes(vol, vcn_tr);
 			err = ntfs_attr_update_mapping_pairs_locked(
 					ni, 0, ni);
-			if (err)
+			if (err) {
+				ntfs_report_file_metadata_error(vi, err);
 				ntfs_error(vol->sb,
 					   "Failed to rollback mapping pairs for prealloc");
+			}
 		}
 	}
 
@@ -341,8 +344,13 @@ int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end,
 					le16_to_cpu(ctx->attr->data.resident.value_offset));
 
 			parent_vi = ntfs_iget(vi->i_sb, MREF_LE(fn->parent_directory));
-			if (IS_ERR(parent_vi))
+			if (IS_ERR(parent_vi)) {
+				if (PTR_ERR(parent_vi) == -ENOENT &&
+				    !NInoBeingDeleted(ni))
+					ntfs_report_file_metadata_error(vi,
+									-EFSCORRUPTED);
 				continue;
+			}
 			mutex_lock_nested(&NTFS_I(parent_vi)->mrec_lock, NTFS_INODE_MUTEX_NORMAL);
 			ia_vi = ntfs_index_iget(parent_vi, I30, 4);
 			mutex_unlock(&NTFS_I(parent_vi)->mrec_lock);
@@ -373,6 +381,11 @@ int ntfs_file_fsync(struct file *filp, loff_t start, loff_t end,
 			attr_vi = ntfs_attr_iget(vi, ctx->attr->type,
 						 name, ctx->attr->name_length);
 			if (IS_ERR(attr_vi)) {
+				if (PTR_ERR(attr_vi) == -ENOENT) {
+					ntfs_report_file_metadata_error(vi,
+									-EFSCORRUPTED);
+					attr_vi = ERR_PTR(-EFSCORRUPTED);
+				}
 				kfree(attr);
 				continue;
 			}
@@ -810,7 +823,7 @@ static ssize_t ntfs_dio_write_iter(struct kiocb *iocb, struct iov_iter *from)
 		ret2 = filemap_write_and_wait_range(iocb->ki_filp->f_mapping,
 				offset, end);
 		if (ret2) {
-			ret = -EIO;
+			ret = ret2;
 			goto out;
 		}
 		invalidate_mapping_pages(iocb->ki_filp->f_mapping,
@@ -934,12 +947,16 @@ ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
 				&ntfs_iomap_folio_ops, NULL);
 out:
 	if (ret < 0 && ret != -EIOCBQUEUED) {
+		int rollback_err;
+
 		if (ni->initialized_size != old_init_size) {
 			struct ntfs_inode *mrec_ni =
 				ntfs_base_inode(ni);
 
 			mutex_lock(&mrec_ni->mrec_lock);
-			ntfs_attr_set_initialized_size(ni, old_init_size);
+			rollback_err = ntfs_attr_set_initialized_size(ni, old_init_size);
+			if (rollback_err)
+				ntfs_report_file_metadata_error(vi, rollback_err);
 			mutex_unlock(&mrec_ni->mrec_lock);
 		}
 		if (ni->data_size != old_data_size) {
@@ -948,7 +965,9 @@ ssize_t ntfs_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
 
 			truncate_setsize(vi, old_data_size);
 			mutex_lock(&mrec_ni->mrec_lock);
-			ntfs_attr_truncate(ni, old_data_size);
+			rollback_err = ntfs_attr_truncate(ni, old_data_size);
+			if (rollback_err)
+				ntfs_report_file_metadata_error(vi, rollback_err);
 			mutex_unlock(&mrec_ni->mrec_lock);
 		}
 	}
diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index 8918379140f1..47e52270e8ee 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -236,14 +236,21 @@ static struct dentry *ntfs_lookup(struct inode *dir_ino, struct dentry *dent,
 				goto handle_name;
 			}
 			ntfs_error(vol->sb,
-				"Found stale reference to inode 0x%lx (reference sequence number = 0x%x, inode sequence number = 0x%x), returning -EIO. Run chkdsk.",
+				"Found stale reference to inode 0x%lx (reference sequence number = 0x%x, inode sequence number = 0x%x), returning -EFSCORRUPTED. Run chkdsk.",
 				dent_ino, MSEQNO(mref),
 				NTFS_I(dent_inode)->seq_no);
+			ntfs_report_file_metadata_error(dir_ino, -EFSCORRUPTED);
 			iput(dent_inode);
-			dent_inode = ERR_PTR(-EIO);
-		} else
+			dent_inode = ERR_PTR(-EFSCORRUPTED);
+		} else {
+			if (PTR_ERR(dent_inode) == -ENOENT) {
+				ntfs_report_file_metadata_error(dir_ino,
+								-EFSCORRUPTED);
+				dent_inode = ERR_PTR(-EFSCORRUPTED);
+			}
 			ntfs_error(vol->sb, "ntfs_iget(0x%lx) failed with error code %li.",
 					dent_ino, PTR_ERR(dent_inode));
+		}
 		kfree(name);
 		/* Return the error code. */
 		return ERR_CAST(dent_inode);
@@ -303,8 +310,11 @@ static struct dentry *ntfs_lookup(struct inode *dir_ino, struct dentry *dent,
 				if (unlikely(err)) {
 					ntfs_error(vol->sb,
 						"Inode corrupt: No WIN32 namespace counterpart to DOS file name. Run chkdsk.");
-					if (err == -ENOENT)
-						err = -EIO;
+					if (err == -ENOENT) {
+						err = -EFSCORRUPTED;
+						ntfs_report_file_metadata_error(dent_inode,
+										-EFSCORRUPTED);
+					}
 					goto err_out;
 				}
 				/* Consistency checks. */
@@ -339,7 +349,8 @@ static struct dentry *ntfs_lookup(struct inode *dir_ino, struct dentry *dent,
 
 eio_err_out:
 		ntfs_error(vol->sb, "Illegal file name attribute. Run chkdsk.");
-		err = -EIO;
+		err = -EFSCORRUPTED;
+		ntfs_report_file_metadata_error(dent_inode, -EFSCORRUPTED);
 err_out:
 		if (ctx)
 			ntfs_attr_put_search_ctx(ctx);
@@ -534,7 +545,7 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
 	if (IS_ERR(dni_mrec)) {
 		ntfs_error(dir_ni->vol->sb, "failed to map mft record for file 0x%llx.\n",
 			   dir_ni->mft_no);
-		err = -EIO;
+		err = PTR_ERR(dni_mrec);
 		goto err_out;
 	}
 	parent_mft_ref = MK_LE_MREF(dir_ni->mft_no,
@@ -725,9 +736,11 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
 		rollback_err = ntfs_attr_remove(ni,
 				AT_SECURITY_DESCRIPTOR, AT_UNNAMED, 0,
 				&rollback_sd_vi);
-		if (rollback_err)
+		if (rollback_err) {
+			ntfs_report_metadata_error(ni->vol, rollback_err);
 			ntfs_error(sb,
 				"Failed to roll back security descriptor.\n");
+		}
 	}
 
 	if (rollback_data) {
@@ -735,12 +748,18 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
 
 		rollback_err = ntfs_attr_remove(ni, AT_DATA, AT_UNNAMED,
 				0, &rollback_data_vi);
-		if (rollback_err)
+		if (rollback_err) {
+			ntfs_report_metadata_error(ni->vol, rollback_err);
 			ntfs_error(sb, "Failed to roll back DATA attribute.\n");
+		}
 	}
 
-	if (rollback_reparse)
-		ntfs_delete_reparse_index(ni);
+	if (rollback_reparse) {
+		int rollback_err = ntfs_delete_reparse_index(ni);
+
+		if (rollback_err)
+			ntfs_report_metadata_error(ni->vol, rollback_err);
+	}
 	/*
 	 * Free extent MFT records (should not exist any with current
 	 * ntfs_create implementation, but for any case if something will be
@@ -750,14 +769,22 @@ static struct ntfs_inode *__ntfs_create(struct mnt_idmap *idmap, struct inode *d
 		int err2;
 
 		err2 = ntfs_mft_record_free(ni->vol, *(ni->ext.extent_ntfs_inos));
-		if (err2)
+		if (err2) {
+			ntfs_report_metadata_error(ni->vol, err2);
 			ntfs_error(sb,
 				"Failed to free extent MFT record. Leaving inconsistent metadata.\n");
+		}
 		ntfs_inode_close(*(ni->ext.extent_ntfs_inos));
 	}
-	if (ntfs_mft_record_free(ni->vol, ni))
-		ntfs_error(sb,
-			"Failed to free MFT record. Leaving inconsistent metadata. Run chkdsk.\n");
+	{
+		int free_err = ntfs_mft_record_free(ni->vol, ni);
+
+		if (free_err) {
+			ntfs_report_metadata_error(ni->vol, free_err);
+			ntfs_error(sb,
+				   "Failed to free MFT record. Leaving inconsistent metadata. Run chkdsk.\n");
+		}
+	}
 	unmap_mft_record(ni);
 	kfree(fn);
 	kfree(si);
@@ -1013,6 +1040,11 @@ static int ntfs_delete(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,
 			ntfs_attr_reinit_search_ctx(actx);
 			goto search;
 		}
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+		}
 		goto err_out;
 	}
 
@@ -1021,8 +1053,14 @@ static int ntfs_delete(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,
 		goto err_out;
 
 	err = ntfs_index_remove(dir_ni, fn, le32_to_cpu(actx->attr->data.resident.value_length));
-	if (err)
+	if (err) {
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(VFS_I(dir_ni),
+							-EFSCORRUPTED);
+		}
 		goto err_out;
+	}
 
 	err = ntfs_attr_record_rm(actx);
 	if (err)
@@ -1234,7 +1272,7 @@ static int __ntfs_link(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,
 	struct super_block *sb;
 	struct inode *vi = VFS_I(ni);
 	struct file_name_attr *fn = NULL;
-	int fn_len, err = 0;
+	int fn_len, rollback_err, err = 0;
 	struct mft_record *dir_mrec = NULL, *ni_mrec = NULL;
 
 	ntfs_debug("Entering.\n");
@@ -1245,7 +1283,7 @@ static int __ntfs_link(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,
 
 	ni_mrec = map_mft_record(ni);
 	if (IS_ERR(ni_mrec)) {
-		err = -EIO;
+		err = PTR_ERR(ni_mrec);
 		goto err_out;
 	}
 
@@ -1265,7 +1303,7 @@ static int __ntfs_link(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,
 
 	dir_mrec = map_mft_record(dir_ni);
 	if (IS_ERR(dir_mrec)) {
-		err = -EIO;
+		err = PTR_ERR(dir_mrec);
 		goto err_out;
 	}
 
@@ -1307,7 +1345,8 @@ static int __ntfs_link(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,
 	if (err) {
 		ntfs_error(sb, "Failed to add FILE_NAME attribute.\n");
 		/* Try to remove just added attribute from index. */
-		if (ntfs_index_remove(dir_ni, fn, fn_len))
+		rollback_err = ntfs_index_remove(dir_ni, fn, fn_len);
+		if (rollback_err)
 			goto rollback_failed;
 		goto err_out;
 	}
@@ -1325,6 +1364,7 @@ static int __ntfs_link(struct ntfs_inode *ni, struct ntfs_inode *dir_ni,
 
 	return 0;
 rollback_failed:
+	ntfs_report_file_metadata_error(VFS_I(dir_ni), rollback_err);
 	ntfs_error(sb, "Rollback failed. Leaving inconsistent metadata.\n");
 err_out:
 	kfree(fn);
@@ -1428,7 +1468,7 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir,
 
 			ni_mrec = map_mft_record(NTFS_I(new_inode));
 			if (IS_ERR(ni_mrec)) {
-				err = -EIO;
+				err = PTR_ERR(ni_mrec);
 				goto err_out;
 			}
 			err = ntfs_check_empty_dir(NTFS_I(new_inode), ni_mrec);
@@ -1453,9 +1493,11 @@ static int ntfs_rename(struct mnt_idmap *idmap, struct inode *old_dir,
 		ntfs_error(sb, "Failed to delete old ntfs inode(%llu) in old dir, err : %d\n",
 				old_ni->mft_no, err);
 		err2 = ntfs_delete(old_ni, new_dir_ni, uname_new, new_name_len, false);
-		if (err2)
+		if (err2) {
+			ntfs_report_file_metadata_error(VFS_I(new_dir_ni), err2);
 			ntfs_error(sb, "Failed to delete old ntfs inode in new dir, err : %d\n",
 					err2);
+		}
 		goto err_out;
 	}
 
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 24/31] ntfs: serialize and report export parent reconstruction errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (22 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 23/31] ntfs: report namespace and file recovery errors Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 25/31] ntfs: report MFT record validation and lifecycle errors Baolin Liu
                   ` (7 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Xuewen Wang <wangxuewen@kylinos.cn>

Exportfs calls ntfs_get_parent() without the child inode lock. Hold the
child's mrec_lock across FILE_NAME lookup and parent loading to stabilize
corruption checks against rename and rmdir.

Report an unused parent as EFSCORRUPTED only for a linked, non-deleting
child. Report missing required or malformed FILE_NAME attributes, while
continuing past malformed candidates as before. Release the lock before
d_obtain_alias().

Preserve ordinary stale-handle behavior. NTFS returns EFSCORRUPTED for a
confirmed corrupt parent reference; generic open_by_handle_at() decoding
still converts that error to ESTALE.

Signed-off-by: Xuewen Wang <wangxuewen@kylinos.cn>
---
 fs/ntfs/namei.c | 43 ++++++++++++++++++++++++++++++++++---------
 1 file changed, 34 insertions(+), 9 deletions(-)

diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c
index 47e52270e8ee..a64ab7881052 100644
--- a/fs/ntfs/namei.c
+++ b/fs/ntfs/namei.c
@@ -1730,7 +1730,8 @@ const struct inode_operations ntfs_dir_inode_ops = {
  * fs/exportfs/expfs.c::find_exported_dentry() which in turn is called from the
  * default ->decode_fh() which is export_decode_fh() in the same file.
  *
- * Note: ntfs_get_parent() is called with @d_inode(child_dent)->i_mutex down.
+ * Serialize the file name lookup and parent load against rename and rmdir
+ * with the child's mrec_lock; exportfs does not hold the child's inode lock.
  *
  * Return the dentry of the parent directory on success or the error code on
  * error (IS_ERR() is true).
@@ -1738,6 +1739,7 @@ const struct inode_operations ntfs_dir_inode_ops = {
 static struct dentry *ntfs_get_parent(struct dentry *child_dent)
 {
 	struct inode *vi = d_inode(child_dent);
+	struct inode *parent;
 	struct ntfs_inode *ni = NTFS_I(vi);
 	struct mft_record *mrec;
 	struct ntfs_attr_search_ctx *ctx;
@@ -1747,15 +1749,20 @@ static struct dentry *ntfs_get_parent(struct dentry *child_dent)
 	int err;
 
 	ntfs_debug("Entering for inode 0x%llx.", ni->mft_no);
+	mutex_lock(&ni->mrec_lock);
 	/* Get the mft record of the inode belonging to the child dentry. */
 	mrec = map_mft_record(ni);
-	if (IS_ERR(mrec))
-		return ERR_CAST(mrec);
+	if (IS_ERR(mrec)) {
+		err = PTR_ERR(mrec);
+		goto unlock_err;
+	}
 	/* Find the first file name attribute in the mft record. */
 	ctx = ntfs_attr_get_search_ctx(ni, mrec);
 	if (IS_ERR(ctx)) {
 		unmap_mft_record(ni);
-		return ERR_PTR(PTR_ERR(ctx));
+		err = PTR_ERR(ctx);
+		ctx = NULL;
+		goto unlock_err;
 	}
 try_next:
 	err = ntfs_attr_lookup(AT_FILE_NAME, NULL, 0, CASE_SENSITIVE, 0, NULL,
@@ -1763,27 +1770,45 @@ static struct dentry *ntfs_get_parent(struct dentry *child_dent)
 	if (unlikely(err)) {
 		ntfs_attr_put_search_ctx(ctx);
 		unmap_mft_record(ni);
-		if (err == -ENOENT)
+		if (err == -ENOENT && vi->i_nlink) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(vi, -EFSCORRUPTED);
 			ntfs_error(vi->i_sb,
 				   "Inode 0x%llx does not have a file name attribute.  Run chkdsk.",
 				   ni->mft_no);
-		return ERR_PTR(err);
+		}
+		goto unlock_err;
 	}
 	attr = ctx->attr;
-	if (unlikely(attr->non_resident))
+	if (unlikely(attr->non_resident)) {
+		ntfs_report_file_metadata_error(vi, -EFSCORRUPTED);
 		goto try_next;
+	}
 	fn = (struct file_name_attr *)((u8 *)attr +
 			le16_to_cpu(attr->data.resident.value_offset));
 	if (unlikely((u8 *)fn + le32_to_cpu(attr->data.resident.value_length) >
-	    (u8 *)attr + le32_to_cpu(attr->length)))
+	    (u8 *)attr + le32_to_cpu(attr->length))) {
+		ntfs_report_file_metadata_error(vi, -EFSCORRUPTED);
 		goto try_next;
+	}
 	/* Get the inode number of the parent directory. */
 	parent_ino = MREF_LE(fn->parent_directory);
 	/* Release the search context and the mft record of the child. */
 	ntfs_attr_put_search_ctx(ctx);
 	unmap_mft_record(ni);
 
-	return d_obtain_alias(ntfs_iget(vi->i_sb, parent_ino));
+	parent = ntfs_iget(vi->i_sb, parent_ino);
+	if (IS_ERR(parent) && PTR_ERR(parent) == -ENOENT &&
+	    vi->i_nlink && !NInoBeingDeleted(ni)) {
+		ntfs_report_file_metadata_error(vi, -EFSCORRUPTED);
+		parent = ERR_PTR(-EFSCORRUPTED);
+	}
+	mutex_unlock(&ni->mrec_lock);
+	return d_obtain_alias(parent);
+
+unlock_err:
+	mutex_unlock(&ni->mrec_lock);
+	return ERR_PTR(err);
 }
 
 static struct inode *ntfs_nfs_get_inode(struct super_block *sb,
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 25/31] ntfs: report MFT record validation and lifecycle errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (23 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 24/31] ntfs: serialize and report export parent reconstruction errors Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 26/31] ntfs: report MFT bitmap and data growth recovery failures Baolin Liu
                   ` (6 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

MFT mapping, allocation and freeing can detect invalid records or
failed cleanup without reporting them. Cover stale extent references,
malformed reserved records and failures to restore allocation bits or
record state.

Use EFSCORRUPTED for corruption while retaining normal reserved-record
ineligibility. Use ntfs_inode_base() for file attribution and volume
reports when no usable file reference exists.

Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
 fs/ntfs/mft.c | 70 +++++++++++++++++++++++++++++++--------------------
 1 file changed, 43 insertions(+), 27 deletions(-)

diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c
index 9b14e69c9c6f..f494f283408b 100644
--- a/fs/ntfs/mft.c
+++ b/fs/ntfs/mft.c
@@ -26,7 +26,7 @@
  * Make sure its general fields are safe, then examine all its
  * attributes and apply generic checks to them.
  *
- * Returns 0 if the checks are successful. If not, return -EIO.
+ * Returns 0 if the checks are successful. If not, return -EFSCORRUPTED.
  */
 int ntfs_mft_record_check(const struct ntfs_volume *vol, struct mft_record *m,
 		u64 mft_no)
@@ -88,7 +88,7 @@ int ntfs_mft_record_check(const struct ntfs_volume *vol, struct mft_record *m,
 	return 0;
 
 err_out:
-	return -EIO;
+	return -EFSCORRUPTED;
 }
 
 /*
@@ -112,6 +112,7 @@ int ntfs_mft_record_check(const struct ntfs_volume *vol, struct mft_record *m,
 static inline struct mft_record *map_mft_record_folio(struct ntfs_inode *ni)
 {
 	loff_t i_size;
+	struct ntfs_inode *base_ni;
 	struct ntfs_volume *vol = ni->vol;
 	struct inode *mft_vi = vol->mft_ino;
 	struct folio *folio;
@@ -169,8 +170,9 @@ static inline struct mft_record *map_mft_record_folio(struct ntfs_inode *ni)
 		folio_put(folio);
 		kfree(ni->mrec);
 		ni->mrec = NULL;
-		folio = ERR_PTR(-EIO);
-		NVolSetErrors(vol);
+		folio = ERR_PTR(-EFSCORRUPTED);
+		base_ni = ntfs_inode_base(ni);
+		ntfs_report_file_metadata_error(VFS_I(base_ni), -EFSCORRUPTED);
 	}
 err_out:
 	ni->folio = NULL;
@@ -313,7 +315,9 @@ struct mft_record *map_extent_mft_record(struct ntfs_inode *base_ni, u64 mref,
 			unmap_mft_record(ni);
 			ntfs_error(base_ni->vol->sb,
 					"Found stale extent mft reference! Corrupt filesystem. Run chkdsk.");
-			return ERR_PTR(-EIO);
+			ntfs_report_file_metadata_error(VFS_I(base_ni),
+							-EFSCORRUPTED);
+			return ERR_PTR(-EFSCORRUPTED);
 		}
 map_err_out:
 		ntfs_error(base_ni->vol->sb,
@@ -344,8 +348,9 @@ struct mft_record *map_extent_mft_record(struct ntfs_inode *base_ni, u64 mref,
 	if (seq_no && (le16_to_cpu(m->sequence_number) != seq_no)) {
 		ntfs_error(base_ni->vol->sb,
 				"Found stale extent mft reference! Corrupt filesystem. Run chkdsk.");
+		ntfs_report_file_metadata_error(VFS_I(base_ni), -EFSCORRUPTED);
 		destroy_ni = true;
-		m = ERR_PTR(-EIO);
+		m = ERR_PTR(-EFSCORRUPTED);
 		goto unm_nolock_err_out;
 	}
 
@@ -1034,7 +1039,7 @@ static bool mft_reserved_is_free(struct ntfs_volume *vol,
 	folio_unlock(folio);
 	folio_put(folio);
 	if (post_read_mst_fixup((struct ntfs_record *)m, vol->mft_record_size))
-		goto free_m;
+		goto corrupt;
 
 	if (!ntfs_is_mft_record(m->magic) ||
 	    !(m->flags & MFT_RECORD_IN_USE) || m->base_mft_record ||
@@ -1045,7 +1050,7 @@ static bool mft_reserved_is_free(struct ntfs_volume *vol,
 	bytes_in_use = le32_to_cpu(m->bytes_in_use);
 	if (attrs_offset > bytes_in_use || bytes_in_use > vol->mft_record_size ||
 	    bytes_in_use - attrs_offset < sizeof(a->type))
-		goto out;
+		goto corrupt;
 
 	for (a = (struct attr_record *)((u8 *)m + attrs_offset);
 	     (u8 *)a + sizeof(a->type) <= (u8 *)m + bytes_in_use;) {
@@ -1054,36 +1059,40 @@ static bool mft_reserved_is_free(struct ntfs_volume *vol,
 		if (a->type == AT_END) {
 			if ((u8 *)a + sizeof(a->type) + sizeof(a->length) >
 			    (u8 *)m + bytes_in_use)
-				break;
+				goto corrupt;
 			/* Also accept a record emptied by an earlier bootstrap. */
 			available = have_std ||
 				    (u8 *)a == (u8 *)m + attrs_offset;
-			break;
+			goto out;
 		}
 		if (a->type == AT_FILE_NAME)
-			break;
+			goto out;
 		len = le32_to_cpu(a->length);
 		if (len < offsetof(struct attr_record, data) ||
 		    (u8 *)a + len > (u8 *)m + bytes_in_use)
-			break;
+			goto corrupt;
 		if (a->type == AT_STANDARD_INFORMATION) {
 			u32 value_len, value_ofs;
 
 			if (have_std || a->non_resident ||
 			    len < offsetof(struct attr_record,
 					   data.resident.reserved) + 1)
-				break;
+				goto corrupt;
 			value_len = le32_to_cpu(a->data.resident.value_length);
 			value_ofs = le16_to_cpu(a->data.resident.value_offset);
 			if (value_ofs > len || value_len > len - value_ofs)
-				break;
+				goto corrupt;
 			have_std = true;
 		}
 		a = (struct attr_record *)((u8 *)a + len);
 	}
+	/* No attribute terminator within the used part of the record. */
+	goto corrupt;
 out:
 	kfree(m);
 	return available;
+corrupt:
+	ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 free_m:
 	kfree(m);
 	return false;
@@ -2317,8 +2326,8 @@ static int ntfs_mft_record_format(const struct ntfs_volume *vol, const s64 mft_n
  *
  * On error, the volume will be left in a consistent state and no record will
  * be allocated.  If rolling back a partial operation fails, we may leave some
- * inconsistent metadata in which case we set NVolErrors() so the volume is
- * left dirty when unmounted.
+ * inconsistent metadata in which case we report the error so the volume is
+ * left dirty when unmounted and userspace is notified.
  *
  * Note, this function cannot make use of most of the normal functions, like
  * for example for attribute resizing, etc, because when the run list overflows
@@ -2349,7 +2358,7 @@ int ntfs_mft_record_alloc(struct ntfs_volume *vol, const int mode,
 	struct attr_record *a;
 	pgoff_t index;
 	unsigned int ofs;
-	int err;
+	int err, rollback_err;
 	__le16 seq_no, usn;
 	bool record_formatted = false, from_reserve = false, tail_alloc = false;
 	bool reserve_created = false;
@@ -2678,6 +2687,10 @@ int ntfs_mft_record_alloc(struct ntfs_volume *vol, const int mode,
 			CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (unlikely(err)) {
 		ntfs_error(vol->sb, "Failed to find first attribute extent of mft data attribute.");
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+		}
 		ntfs_attr_put_search_ctx(ctx);
 		unmap_mft_record(mft_ni);
 		goto undo_data_init;
@@ -2754,7 +2767,7 @@ int ntfs_mft_record_alloc(struct ntfs_volume *vol, const int mode,
 			folio_unlock(folio);
 			kunmap_local(m);
 			folio_put(folio);
-			NVolSetErrors(vol);
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 			goto search_free_rec;
 		}
 		/*
@@ -2910,9 +2923,12 @@ int ntfs_mft_record_alloc(struct ntfs_volume *vol, const int mode,
 	if (!base_ni || base_ni->mft_no != FILE_MFT)
 		down_write(&vol->mftbmp_lock);
 undo_mftbmp_alloc_nolock:
-	if (!forced_reserved_record && ntfs_bitmap_clear_bit(vol->mftbmp_ino, bit)) {
+	rollback_err = 0;
+	if (!forced_reserved_record)
+		rollback_err = ntfs_bitmap_clear_bit(vol->mftbmp_ino, bit);
+	if (rollback_err) {
 		ntfs_error(vol->sb, "Failed to clear bit in mft bitmap.%s", es);
-		NVolSetErrors(vol);
+		ntfs_report_metadata_error(vol, rollback_err);
 	}
 	if ((from_reserve || reserve_created) &&
 	    vol->mft_record_reserve_pos == bit + 1)
@@ -2949,7 +2965,7 @@ int ntfs_mft_record_alloc(struct ntfs_volume *vol, const int mode,
 int ntfs_mft_record_free(struct ntfs_volume *vol, struct ntfs_inode *ni)
 {
 	u64 mft_no;
-	int err;
+	int err, rollback_err;
 	u16 seq_no;
 	__le16 old_seq_no;
 	__le64 old_base_mft_record;
@@ -2965,14 +2981,11 @@ int ntfs_mft_record_free(struct ntfs_volume *vol, struct ntfs_inode *ni)
 
 	ni_mrec = map_mft_record(ni);
 	if (IS_ERR(ni_mrec))
-		return -EIO;
+		return PTR_ERR(ni_mrec);
 
 	/* Cache the mft reference for later. */
 	mft_no = ni->mft_no;
-	if (likely(ni->nr_extents >= 0))
-		base_ni = ni;
-	else
-		base_ni = ni->ext.base_ntfs_ino;
+	base_ni = ntfs_inode_base(ni);
 	keep_reserved = mft_no >= FILE_reserved12 &&
 			mft_no <= FILE_reserved15 &&
 			base_ni->mft_no == FILE_MFT;
@@ -3043,8 +3056,11 @@ int ntfs_mft_record_free(struct ntfs_volume *vol, struct ntfs_inode *ni)
 	memalloc_flags = memalloc_nofs_save();
 	if (base_ni->mft_no != FILE_MFT)
 		down_write(&vol->mftbmp_lock);
-	if (ntfs_bitmap_set_bit(vol->mftbmp_ino, mft_no))
+	rollback_err = ntfs_bitmap_set_bit(vol->mftbmp_ino, mft_no);
+	if (rollback_err) {
 		ntfs_error(vol->sb, "ntfs_bitmap_set_bit failed in bitmap_rollback\n");
+		ntfs_report_metadata_error(vol, rollback_err);
+	}
 	if (base_ni->mft_no != FILE_MFT)
 		up_write(&vol->mftbmp_lock);
 	memalloc_nofs_restore(memalloc_flags);
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 26/31] ntfs: report MFT bitmap and data growth recovery failures
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (24 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 25/31] ntfs: report MFT record validation and lifecycle errors Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 27/31] ntfs: report MFT writeback errors to fsnotify Baolin Liu
                   ` (5 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

Growing the MFT bitmap or data allocation can detect missing or
inconsistent metadata and leave failed recovery unreported. Record
corruption and failures to restore mappings, allocation, or search state
through volume metadata reports.

Use EFSCORRUPTED for corrupt mappings and preserve ENOMEM from runlist
merges. After a failed merge, report cluster-release failure separately
without replacing the merge error.

Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
 fs/ntfs/mft.c | 179 ++++++++++++++++++++++++++++++++------------------
 1 file changed, 116 insertions(+), 63 deletions(-)

diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c
index f494f283408b..cc264a0fc073 100644
--- a/fs/ntfs/mft.c
+++ b/fs/ntfs/mft.c
@@ -1349,8 +1349,9 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
 	struct ntfs_attr_search_ctx *ctx = NULL;
 	struct mft_record *mrec;
 	struct attr_record *a = NULL;
-	int ret, mp_size;
+	int err, ret, mp_size;
 	u32 old_alen = 0;
+	u16 mp_ofs;
 	u8 *b, tb;
 	struct {
 		u8 added_cluster:1;
@@ -1381,10 +1382,12 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
 		up_write(&mftbmp_ni->runlist.lock);
 		ntfs_error(vol->sb,
 			"Failed to determine last allocated cluster of mft bitmap attribute.");
-		if (!IS_ERR(rl))
-			ret = -EIO;
-		else
+		if (!IS_ERR(rl)) {
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+			ret = -EFSCORRUPTED;
+		} else {
 			ret = PTR_ERR(rl);
+		}
 		return ret;
 	}
 	lcn = rl->lcn + rl->length;
@@ -1453,15 +1456,20 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
 		status.added_cluster = rl2->lcn == lcn;
 		rl = ntfs_runlists_merge(&mftbmp_ni->runlist, rl2, 0, &new_rl_count);
 		if (IS_ERR(rl)) {
+			int merge_err = PTR_ERR(rl);
+
+			if (merge_err != -ENOMEM)
+				ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 			up_write(&mftbmp_ni->runlist.lock);
 			ntfs_error(vol->sb, "Failed to merge runlists for mft bitmap.");
-			if (ntfs_cluster_free_from_rl(vol, rl2)) {
+			err = ntfs_cluster_free_from_rl(vol, rl2);
+			if (err) {
 				ntfs_error(vol->sb, "Failed to deallocate allocated cluster.%s",
 						es);
-				NVolSetErrors(vol);
+				ntfs_report_metadata_error(vol, err);
 			}
 			kvfree(rl2);
-			return PTR_ERR(rl);
+			return merge_err == -ENOMEM ? merge_err : -EFSCORRUPTED;
 		}
 		mftbmp_ni->runlist.rl = rl;
 		mftbmp_ni->runlist.count = new_rl_count;
@@ -1494,8 +1502,10 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
 	if (unlikely(ret)) {
 		ntfs_error(vol->sb,
 			"Failed to find last attribute extent of mft bitmap attribute.");
-		if (ret == -ENOENT)
-			ret = -EIO;
+		if (ret == -ENOENT) {
+			ret = -EFSCORRUPTED;
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+		}
 		goto undo_alloc;
 	}
 	a = ctx->attr;
@@ -1514,7 +1524,9 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
 			"Get size for mapping pairs failed for mft bitmap attribute extent.");
 		ret = mp_size;
 		if (!ret)
-			ret = -EIO;
+			ret = -EFSCORRUPTED;
+		if (ret == -EFSCORRUPTED)
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		goto undo_alloc;
 	}
 	/* Expand the attribute record if necessary. */
@@ -1537,6 +1549,8 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
 	if (unlikely(ret)) {
 		ntfs_error(vol->sb,
 			"Failed to build mapping pairs array for mft bitmap attribute.");
+		if (ret == -EFSCORRUPTED)
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		goto undo_alloc;
 	}
 	/* Update the highest_vcn. */
@@ -1559,6 +1573,10 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
 		if (unlikely(ret)) {
 			ntfs_error(vol->sb,
 				"Failed to find first attribute extent of mft bitmap attribute.");
+			if (ret == -ENOENT) {
+				ret = -EFSCORRUPTED;
+				ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+			}
 			goto restore_undo_alloc;
 		}
 		a = ctx->attr;
@@ -1579,9 +1597,10 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
 
 restore_undo_alloc:
 	ntfs_attr_reinit_search_ctx(ctx);
-	if (ntfs_attr_lookup(mftbmp_ni->type, mftbmp_ni->name,
-			mftbmp_ni->name_len, CASE_SENSITIVE, rl[1].vcn, NULL,
-			0, ctx)) {
+	err = ntfs_attr_lookup(mftbmp_ni->type, mftbmp_ni->name,
+			       mftbmp_ni->name_len, CASE_SENSITIVE, rl[1].vcn,
+			       NULL, 0, ctx);
+	if (err) {
 		ntfs_error(vol->sb,
 			"Failed to find last attribute extent of mft bitmap attribute.%s", es);
 		write_lock_irqsave(&mftbmp_ni->size_lock, flags);
@@ -1594,7 +1613,7 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
 		 * The only thing that is now wrong is ->allocated_size of the
 		 * base attribute extent which chkdsk should be able to fix.
 		 */
-		NVolSetErrors(vol);
+		ntfs_report_metadata_error(vol, err);
 		return ret;
 	}
 	a = ctx->attr;
@@ -1613,31 +1632,36 @@ static int ntfs_mft_bitmap_extend_allocation_nolock(struct ntfs_volume *vol)
 	}
 	/* Deallocate the cluster. */
 	down_write(&vol->lcnbmp_lock);
-	if (ntfs_bitmap_clear_bit(vol->lcnbmp_ino, lcn)) {
+	err = ntfs_bitmap_clear_bit(vol->lcnbmp_ino, lcn);
+	if (err) {
 		ntfs_error(vol->sb, "Failed to free allocated cluster.%s", es);
-		NVolSetErrors(vol);
-	} else
+		ntfs_report_metadata_error(vol, err);
+	} else {
 		ntfs_inc_free_clusters(vol, 1);
+	}
 	up_write(&vol->lcnbmp_lock);
 	if (status.mp_rebuilt) {
-		if (ntfs_mapping_pairs_build(vol, (u8 *)a + le16_to_cpu(
-				a->data.non_resident.mapping_pairs_offset),
-				old_alen - le16_to_cpu(
-				a->data.non_resident.mapping_pairs_offset),
-				rl2, ll, -1, NULL, NULL, NULL)) {
+		mp_ofs = le16_to_cpu(a->data.non_resident.mapping_pairs_offset);
+		err = ntfs_mapping_pairs_build(vol, (u8 *)a + mp_ofs,
+					       old_alen - mp_ofs, rl2, ll, -1,
+					       NULL, NULL, NULL);
+		if (err) {
 			ntfs_error(vol->sb, "Failed to restore mapping pairs array.%s", es);
-			NVolSetErrors(vol);
+			ntfs_report_metadata_error(vol, err);
 		}
-		if (ntfs_attr_record_resize(ctx->mrec, a, old_alen)) {
+		err = ntfs_attr_record_resize(ctx->mrec, a, old_alen);
+		if (err) {
 			ntfs_error(vol->sb, "Failed to restore attribute record.%s", es);
-			NVolSetErrors(vol);
+			ntfs_report_metadata_error(vol, err);
 		}
 		mark_mft_record_dirty(ctx->ntfs_ino);
-	} else if (status.mp_extended &&
-		   ntfs_attr_update_mapping_pairs_locked(mftbmp_ni, 0,
-							  mftbmp_ni)) {
-		ntfs_error(vol->sb, "Failed to restore mapping pairs.%s", es);
-		NVolSetErrors(vol);
+	} else if (status.mp_extended) {
+		err = ntfs_attr_update_mapping_pairs_locked(mftbmp_ni, 0,
+							    mftbmp_ni);
+		if (err) {
+			ntfs_error(vol->sb, "Failed to restore mapping pairs.%s", es);
+			ntfs_report_metadata_error(vol, err);
+		}
 	}
 	if (ctx)
 		ntfs_attr_put_search_ctx(ctx);
@@ -1670,7 +1694,7 @@ static int ntfs_mft_bitmap_extend_initialized_nolock(struct ntfs_volume *vol)
 	struct ntfs_attr_search_ctx *ctx;
 	struct mft_record *mrec;
 	struct attr_record *a;
-	int ret;
+	int err, ret;
 
 	ntfs_debug("Extending mft bitmap initialized (and data) size.");
 	mft_ni = NTFS_I(vol->mft_ino);
@@ -1694,8 +1718,10 @@ static int ntfs_mft_bitmap_extend_initialized_nolock(struct ntfs_volume *vol)
 	if (unlikely(ret)) {
 		ntfs_error(vol->sb,
 			"Failed to find first attribute extent of mft bitmap attribute.");
-		if (ret == -ENOENT)
-			ret = -EIO;
+		if (ret == -ENOENT) {
+			ret = -EFSCORRUPTED;
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+		}
 		goto put_err_out;
 	}
 	a = ctx->attr;
@@ -1731,21 +1757,24 @@ static int ntfs_mft_bitmap_extend_initialized_nolock(struct ntfs_volume *vol)
 	mrec = map_mft_record(mft_ni);
 	if (IS_ERR(mrec)) {
 		ntfs_error(vol->sb, "Failed to map mft record.%s", es);
-		NVolSetErrors(vol);
+		err = PTR_ERR(mrec);
+		ntfs_report_metadata_error(vol, err);
 		return ret;
 	}
 	ctx = ntfs_attr_get_search_ctx(mft_ni, mrec);
 	if (IS_ERR(ctx)) {
 		ntfs_error(vol->sb, "Failed to get search context.%s", es);
-		NVolSetErrors(vol);
+		ntfs_report_metadata_error(vol, PTR_ERR(ctx));
 		ctx = NULL;
 		goto unm_err_out;
 	}
-	if (ntfs_attr_lookup(mftbmp_ni->type, mftbmp_ni->name,
-			mftbmp_ni->name_len, CASE_SENSITIVE, 0, NULL, 0, ctx)) {
+	err = ntfs_attr_lookup(mftbmp_ni->type, mftbmp_ni->name,
+			       mftbmp_ni->name_len, CASE_SENSITIVE, 0, NULL, 0,
+			       ctx);
+	if (err) {
 		ntfs_error(vol->sb,
 			"Failed to find first attribute extent of mft bitmap attribute.%s", es);
-		NVolSetErrors(vol);
+		ntfs_report_metadata_error(vol, err);
 put_err_out:
 		ntfs_attr_put_search_ctx(ctx);
 unm_err_out:
@@ -1807,7 +1836,7 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 	struct ntfs_attr_search_ctx *ctx = NULL;
 	struct mft_record *mrec;
 	struct attr_record *a = NULL;
-	int ret, mp_size;
+	int err, ret, mp_size;
 	u32 old_alen = 0;
 	bool mp_rebuilt = false, mp_extended = false;
 	size_t new_rl_count;
@@ -1829,10 +1858,12 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 		up_write(&mft_ni->runlist.lock);
 		ntfs_error(vol->sb,
 			"Failed to determine last allocated cluster of mft data attribute.");
-		if (!IS_ERR(rl))
-			ret = -EIO;
-		else
+		if (!IS_ERR(rl)) {
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+			ret = -EFSCORRUPTED;
+		} else {
 			ret = PTR_ERR(rl);
+		}
 		return ret;
 	}
 	lcn = rl->lcn + rl->length;
@@ -1898,15 +1929,20 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 	rl = ntfs_runlists_merge_keep_src(&mft_ni->runlist, alloc_rl, 0,
 			&new_rl_count);
 	if (IS_ERR(rl)) {
+		int merge_err = PTR_ERR(rl);
+
+		if (merge_err != -ENOMEM)
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		up_write(&mft_ni->runlist.lock);
 		ntfs_error(vol->sb, "Failed to merge runlists for mft data attribute.");
-		if (ntfs_cluster_free_from_rl(vol, alloc_rl)) {
+		err = ntfs_cluster_free_from_rl(vol, alloc_rl);
+		if (err) {
 			ntfs_error(vol->sb,
 				"Failed to deallocate clusters from the mft data attribute.%s", es);
-			NVolSetErrors(vol);
+			ntfs_report_metadata_error(vol, err);
 		}
 		kvfree(alloc_rl);
-		return PTR_ERR(rl);
+		return merge_err == -ENOMEM ? merge_err : -EFSCORRUPTED;
 	}
 	mft_ni->runlist.rl = rl;
 	mft_ni->runlist.count = new_rl_count;
@@ -1934,8 +1970,10 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 			CASE_SENSITIVE, rl[1].vcn, NULL, 0, ctx);
 	if (unlikely(ret)) {
 		ntfs_error(vol->sb, "Failed to find last attribute extent of mft data attribute.");
-		if (ret == -ENOENT)
-			ret = -EIO;
+		if (ret == -ENOENT) {
+			ret = -EFSCORRUPTED;
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+		}
 		goto undo_alloc;
 	}
 	a = ctx->attr;
@@ -1956,7 +1994,9 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 			"Get size for mapping pairs failed for mft data attribute extent.");
 		ret = mp_size;
 		if (!ret)
-			ret = -EIO;
+			ret = -EFSCORRUPTED;
+		if (ret == -EFSCORRUPTED)
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		up_write(&mft_ni->runlist.lock);
 		goto undo_alloc;
 	}
@@ -1981,6 +2021,8 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 			mp_size, rl2, ll, -1, NULL, NULL, NULL);
 	if (unlikely(ret)) {
 		ntfs_error(vol->sb, "Failed to build mapping pairs array of mft data attribute.");
+		if (ret == -EFSCORRUPTED)
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		goto undo_alloc;
 	}
 	/* Update the highest_vcn. */
@@ -2005,6 +2047,10 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 		if (unlikely(ret)) {
 			ntfs_error(vol->sb,
 				"Failed to find first attribute extent of mft data attribute.");
+			if (ret == -ENOENT) {
+				ret = -EFSCORRUPTED;
+				ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+			}
 			goto restore_undo_alloc;
 		}
 		a = ctx->attr;
@@ -2024,8 +2070,9 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 	return 0;
 restore_undo_alloc:
 	ntfs_attr_reinit_search_ctx(ctx);
-	if (ntfs_attr_lookup(mft_ni->type, mft_ni->name, mft_ni->name_len,
-			CASE_SENSITIVE, rl[1].vcn, NULL, 0, ctx)) {
+	err = ntfs_attr_lookup(mft_ni->type, mft_ni->name, mft_ni->name_len,
+			       CASE_SENSITIVE, rl[1].vcn, NULL, 0, ctx);
+	if (err) {
 		ntfs_error(vol->sb,
 			"Failed to find last attribute extent of mft data attribute.%s", es);
 		write_lock_irqsave(&mft_ni->size_lock, flags);
@@ -2038,7 +2085,7 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 		 * The only thing that is now wrong is ->allocated_size of the
 		 * base attribute extent which chkdsk should be able to fix.
 		 */
-		NVolSetErrors(vol);
+		ntfs_report_metadata_error(vol, err);
 		return ret;
 	}
 	ctx->attr->data.non_resident.highest_vcn =
@@ -2050,9 +2097,10 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 	 * is dropped, as lcnbmp_lock nests outside it (see above).
 	 */
 	down_write(&mft_ni->runlist.lock);
-	if (ntfs_rl_truncate_nolock(vol, &mft_ni->runlist, old_last_vcn)) {
+	err = ntfs_rl_truncate_nolock(vol, &mft_ni->runlist, old_last_vcn);
+	if (err) {
 		ntfs_error(vol->sb, "Failed to truncate mft data attribute runlist.%s", es);
-		NVolSetErrors(vol);
+		ntfs_report_metadata_error(vol, err);
 		/*
 		 * The runlist may still reference the clusters, so leave them
 		 * allocated until chkdsk.
@@ -2061,15 +2109,19 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 		alloc_rl = NULL;
 	}
 	up_write(&mft_ni->runlist.lock);
-	if (ntfs_cluster_free_from_rl(vol, alloc_rl)) {
+	err = ntfs_cluster_free_from_rl(vol, alloc_rl);
+	if (err) {
 		ntfs_error(vol->sb, "Failed to free clusters from mft data attribute.%s", es);
-		NVolSetErrors(vol);
+		ntfs_report_metadata_error(vol, err);
 	}
 	kvfree(alloc_rl);
-	if (mp_extended && ntfs_attr_update_mapping_pairs(mft_ni, 0)) {
-		ntfs_error(vol->sb, "Failed to restore mapping pairs.%s",
-			   es);
-		NVolSetErrors(vol);
+	if (mp_extended) {
+		err = ntfs_attr_update_mapping_pairs(mft_ni, 0);
+		if (err) {
+			ntfs_error(vol->sb, "Failed to restore mapping pairs.%s",
+				   es);
+			ntfs_report_metadata_error(vol, err);
+		}
 	}
 	if (ctx) {
 		a = ctx->attr;
@@ -2085,16 +2137,17 @@ static int ntfs_mft_data_extend_allocation_nolock(struct ntfs_volume *vol)
 			up_read(&mft_ni->runlist.lock);
 			if (err) {
 				ntfs_error(vol->sb, "Failed to restore mapping pairs array.%s", es);
-				NVolSetErrors(vol);
+				ntfs_report_metadata_error(vol, err);
 			}
-			if (ntfs_attr_record_resize(ctx->mrec, a, old_alen)) {
+			err = ntfs_attr_record_resize(ctx->mrec, a, old_alen);
+			if (err) {
 				ntfs_error(vol->sb, "Failed to restore attribute record.%s", es);
-				NVolSetErrors(vol);
+				ntfs_report_metadata_error(vol, err);
 			}
 			mark_mft_record_dirty(ctx->ntfs_ino);
 		} else if (IS_ERR(ctx->mrec)) {
 			ntfs_error(vol->sb, "Failed to restore attribute search context.%s", es);
-			NVolSetErrors(vol);
+			ntfs_report_metadata_error(vol, PTR_ERR(ctx->mrec));
 		}
 		ntfs_attr_put_search_ctx(ctx);
 	}
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 27/31] ntfs: report MFT writeback errors to fsnotify
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (25 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 26/31] ntfs: report MFT bitmap and data growth recovery failures Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 28/31] ntfs: report EA metadata and restoration errors Baolin Liu
                   ` (4 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

MFT write failures mark the volume without notifying error listeners.
Report device failures from the existing BIO completion callback for
both synchronous and asynchronous writes. Use blk_status_to_errno(),
matching the error recorded in the address_space.

Also report mirror and write-preparation failures. Use EFSCORRUPTED for
invalid record geometry, mappings and MST preparation, preserving the
errno through the affected error exits and mapping_set_error(). Keep
ENOMEM redirty and retry behavior.

Use the generic deferred reporter for completion-context notifications.

Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
 fs/ntfs/mft.c | 54 ++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 36 insertions(+), 18 deletions(-)

diff --git a/fs/ntfs/mft.c b/fs/ntfs/mft.c
index cc264a0fc073..6f84a4e78aa4 100644
--- a/fs/ntfs/mft.c
+++ b/fs/ntfs/mft.c
@@ -479,9 +479,11 @@ static void ntfs_mft_end_io(struct bio *bio)
 		err = blk_status_to_errno(bio->bi_status);
 	else
 		err = ctx->error;
+	/* Report device errors from both synchronous and asynchronous writes. */
+	if (bio->bi_status)
+		ntfs_report_file_metadata_error(ctx->mapping->host, err);
 	if (err) {
 		mapping_set_error(ctx->mapping, err);
-		NVolSetErrors(ctx->vol);
 		ntfs_error(ctx->vol->sb, "I/O error while writing MFT: %d",
 			   err);
 	}
@@ -542,29 +544,31 @@ static int ntfs_sync_mft_mirror_unit(struct ntfs_volume *vol,
 	u64 mirror_size;
 	unsigned int mirror_ofs;
 	u8 *src, *dst;
-	int err;
+	int err = -EFSCORRUPTED;
 
 	if (unlikely(!vol->mftmirr_ino))
-		return -EIO;
+		goto out_report;
 
 	mirror_size = (u64)vol->mftmirr_size * vol->mft_record_size;
 	if (mirror_file_ofs >= mirror_size ||
 	    unit->len > mirror_size - mirror_file_ofs)
-		return -EIO;
+		goto out_report;
 	if (unit->folio_ofs + unit->len > folio_size(source))
-		return -EIO;
+		goto out_report;
 
 	mirror = read_mapping_folio(vol->mftmirr_ino->i_mapping,
 				    mirror_file_ofs >> PAGE_SHIFT, NULL);
-	if (IS_ERR(mirror))
-		return PTR_ERR(mirror);
+	if (IS_ERR(mirror)) {
+		err = PTR_ERR(mirror);
+		goto out_report;
+	}
 
 	folio_lock(mirror);
 	if (folio_test_writeback(mirror))
 		folio_wait_writeback(mirror);
 	mirror_ofs = mirror_file_ofs - folio_pos(mirror);
 	if (mirror_ofs + unit->len > folio_size(mirror)) {
-		err = -EIO;
+		err = -EFSCORRUPTED;
 		goto out_unlock;
 	}
 
@@ -596,6 +600,9 @@ static int ntfs_sync_mft_mirror_unit(struct ntfs_volume *vol,
 out_unlock:
 	folio_unlock(mirror);
 	folio_put(mirror);
+out_report:
+	if (err)
+		ntfs_report_metadata_error(vol, err);
 	return err;
 }
 
@@ -627,10 +634,10 @@ static int ntfs_prepare_mft_record_io_units(struct ntfs_inode *ni,
 	unsigned int i;
 
 	if (!nr_units || nr_units > ARRAY_SIZE(ni->mft_lcn))
-		return -EIO;
+		return -EFSCORRUPTED;
 	for (i = 0; i < nr_units; i++)
 		if (ni->mft_lcn[i] < 0)
-			return -EIO;
+			return -EFSCORRUPTED;
 
 	cluster_ofs = ntfs_bytes_to_cluster_off(vol, record_byte);
 	if (vol->mft_io_unit_size > vol->mft_record_size) {
@@ -715,8 +722,10 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn
 		goto done;
 
 	err = ntfs_prepare_mft_record_io_units(ni, units);
-	if (err < 0)
+	if (err < 0) {
+		err = -EFSCORRUPTED;
 		goto err_out;
+	}
 	nr_units = err;
 
 	kaddr = kmap_local_folio(folio, 0);
@@ -727,6 +736,7 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn
 	err = pre_write_mst_fixup((struct ntfs_record *)fixup_m, vol->mft_record_size);
 	if (err) {
 		ntfs_error(vol->sb, "Failed to apply mst fixups!");
+		err = -EFSCORRUPTED;
 		goto unmap_err_out;
 	}
 
@@ -813,8 +823,13 @@ int write_mft_record_nolock(struct ntfs_inode *ni, struct mft_record *m, int syn
 		mark_mft_record_dirty(ni);
 		if (!sync)
 			err = 0;
-	} else
-		NVolSetErrors(vol);
+	} else {
+		struct ntfs_inode *base_ni = ntfs_inode_base(ni);
+
+		mapping_set_error(folio->mapping, err);
+		ntfs_report_file_metadata_error(VFS_I(base_ni),
+						err);
+	}
 	return err;
 }
 
@@ -3165,7 +3180,7 @@ static int ntfs_map_mft_io_locked(struct ntfs_inode *ni, u64 folio_byte,
 
 	lockdep_assert_held(&ni->runlist.lock);
 	if (!ni->runlist.rl)
-		return -EIO;
+		return -EFSCORRUPTED;
 
 	vcn = ntfs_bytes_to_cluster(vol, file_ofs);
 	/*
@@ -3177,10 +3192,10 @@ static int ntfs_map_mft_io_locked(struct ntfs_inode *ni, u64 folio_byte,
 	if (lcn == LCN_RL_NOT_MAPPED)
 		return -EAGAIN;
 	if (lcn < 0)
-		return -EIO;
+		return -EFSCORRUPTED;
 	if (ntfs_bytes_to_cluster_off(vol, file_ofs) + unit->len >
 	    vol->cluster_size)
-		return -EIO;
+		return -EFSCORRUPTED;
 
 	unit->sector = ntfs_bytes_to_bio_sector(
 		NTFS_CLU_TO_B(vol, lcn) +
@@ -3271,7 +3286,7 @@ static void ntfs_mft_write_error(struct ntfs_volume *vol,
 				 struct address_space *mapping, int err)
 {
 	mapping_set_error(mapping, err);
-	NVolSetErrors(vol);
+	ntfs_report_file_metadata_error(mapping->host, err);
 	ntfs_error(vol->sb, "Error while writing MFT folio: %d", err);
 }
 
@@ -3375,8 +3390,10 @@ static int ntfs_write_mft_block(struct folio *folio, struct writeback_control *w
 	err = ntfs_prepare_mft_folio_units(ni, folio_byte, file_limit,
 					   record_selected, units, &nr_units,
 					   max_units, &defer);
-	if (err)
+	if (err) {
+		err = -EFSCORRUPTED;
 		goto out_noio;
+	}
 	if (!nr_units) {
 		if (defer)
 			goto out_noio;
@@ -3429,6 +3446,7 @@ static int ntfs_write_mft_block(struct folio *folio, struct writeback_control *w
 						ctx->error = -EIO;
 					redirty = true;
 					err = -EIO;
+					ntfs_mft_write_error(vol, mapping, err);
 					break;
 				}
 			}
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 28/31] ntfs: report EA metadata and restoration errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (26 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 27/31] ntfs: report MFT writeback errors to fsnotify Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-27  5:51   ` Hyunchul Lee
  2026-10-20  8:44 ` [PATCH v4 29/31] ntfs: report named stream metadata and recovery errors Baolin Liu
                   ` (3 subsequent siblings)
  31 siblings, 1 reply; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Xuewen Wang <wangxuewen@kylinos.cn>

Report malformed EA chains, orphaned attributes and invalid Linux flag
sizes. Convert EINVAL from stored ACL decoding and inconsistent saved-ACL
read lengths to EFSCORRUPTED. Keep missing values, unsupported ACL versions
and allocation failures distinct from corruption.

Report EA and EA_INFORMATION restoration failures separately, and failed
ACL restoration after a WSL mode update. Even ENOMEM here can leave
metadata inconsistent. Preserve the initiating error and existing
shutdown decisions.

Preserve EFSCORRUPTED for missing $AttrDef definitions instead of masking
it as EFBIG. Keep non-ENOENT file-flag lookup errors. Report mapping-pairs
sizing errors only for EFSCORRUPTED; an unmapped cache returning EINVAL
does not prove corruption.

Signed-off-by: Xuewen Wang <wangxuewen@kylinos.cn>
---
 fs/ntfs/ea.c | 72 +++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 51 insertions(+), 21 deletions(-)

diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index 1198f3059c5d..ee4d7d082b2c 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -129,6 +129,7 @@ static int ntfs_get_ea(struct inode *inode, const char *name, size_t name_len,
 		return PTR_ERR(p_ea_info);
 	if (ea_info_size != sizeof(struct ea_information)) {
 		kvfree(p_ea_info);
+		ntfs_report_file_metadata_error(inode, -EUCLEAN);
 		return -EUCLEAN;
 	}
 
@@ -168,6 +169,8 @@ static int ntfs_get_ea(struct inode *inode, const char *name, size_t name_len,
 	if (err == -ENOENT)
 		err = -ENODATA;
 free_ea_buf:
+	if (err == -EUCLEAN)
+		ntfs_report_file_metadata_error(inode, err);
 	kvfree(ea_buf);
 	return err;
 }
@@ -253,8 +256,10 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
 	if (err && err != -ENOENT)
 		return err;
 	/* Do not silently discard an orphaned attribute. */
-	if (exists != !err)
+	if (exists != !err) {
+		ntfs_report_file_metadata_error(inode, -EUCLEAN);
 		return -EUCLEAN;
+	}
 
 	if (exists) {
 		old_info = ntfs_attr_readall(ni, AT_EA_INFORMATION, NULL, 0,
@@ -307,9 +312,13 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
 
 	add_size = remove ? 0 : ALIGN(sizeof(*entry) + name_len + 1 + val_size, 4);
 	new_size = size - entry_size + add_size;
-	if (new_size && ntfs_attr_size_bounds_check(ni->vol, AT_EA, new_size)) {
-		err = -EFBIG;
-		goto out;
+	if (new_size) {
+		err = ntfs_attr_size_bounds_check(ni->vol, AT_EA, new_size);
+		if (err) {
+			if (err == -ERANGE)
+				err = -EFBIG;
+			goto out;
+		}
 	}
 	if (new_size) {
 		ea = kvzalloc(new_size, GFP_NOFS);
@@ -366,7 +375,10 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
 		info_err = ntfs_ea_update_attr(ni, AT_EA_INFORMATION,
 					       (char *)old_info, sizeof(info));
 		if (ea_err || info_err) {
-			NVolSetErrors(ni->vol);
+			if (ea_err)
+				ntfs_report_file_metadata_error(inode, ea_err);
+			if (info_err)
+				ntfs_report_file_metadata_error(inode, info_err);
 			NVolSetShutdown(ni->vol);
 			ntfs_error(inode->i_sb,
 				   "Failed to restore EAs of inode 0x%llx (%d, %d); shutting down.",
@@ -385,6 +397,8 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
 	else
 		NInoClearHasEA(ni);
 out:
+	if (err == -EUCLEAN)
+		ntfs_report_file_metadata_error(inode, err);
 	kvfree(ea);
 	kvfree(old_ea);
 	kvfree(old_info);
@@ -492,10 +506,12 @@ int ntfs_ea_get_linuxflags(struct inode *inode)
 			  &value, sizeof(value));
 	if (err == -ENODATA)
 		return 0;
-	if (err < 0)
-		return err == -ERANGE ? -EUCLEAN : err;
-	if (err != sizeof(value))
+	if (err < 0 && err != -ERANGE)
+		return err;
+	if (err != sizeof(value)) {
+		ntfs_report_file_metadata_error(inode, -EUCLEAN);
 		return -EUCLEAN;
+	}
 
 	ni->linuxflags = le32_to_cpu(value);
 	if (ni->linuxflags & NTFS_LINUXFLAGS_IMMUTABLE)
@@ -605,6 +621,8 @@ ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size)
 	} while (next > 0 && offset < ea_info_qsize);
 
 out:
+	if (err == -EUCLEAN)
+		ntfs_report_file_metadata_error(inode, err);
 	mutex_unlock(&NTFS_I(inode)->mrec_lock);
 	kvfree(ea_info);
 	kvfree(ea_buf);
@@ -694,7 +712,8 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr)
 	err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len,
 			CASE_SENSITIVE, 0, NULL, 0, ctx);
 	if (err) {
-		err = -EINVAL;
+		if (err == -ENOENT)
+			err = -EINVAL;
 		goto err_out;
 	}
 
@@ -744,7 +763,11 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr)
 				CASE_SENSITIVE,
 				0, NULL, 0, ctx);
 		if (err) {
-			err = -EINVAL;
+			if (err == -ENOENT) {
+				err = -EFSCORRUPTED;
+				ntfs_report_file_metadata_error(VFS_I(ni),
+								-EFSCORRUPTED);
+			}
 			goto err_out;
 		}
 		a = ctx->attr;
@@ -770,6 +793,8 @@ static int ntfs_new_attr_flags(struct ntfs_inode *ni, __le32 fattr)
 	mp_size = ntfs_get_size_for_mapping_pairs(ni->vol, ni->runlist.rl, 0, -1, -1);
 	if (unlikely(mp_size < 0)) {
 		err = mp_size;
+		if (err == -EFSCORRUPTED)
+			ntfs_report_file_metadata_error(VFS_I(ni), err);
 		ntfs_debug("Failed to get size for mapping pairs array, error code %i.\n", err);
 		goto err_out;
 	}
@@ -1036,12 +1061,17 @@ struct posix_acl *ntfs_get_acl(struct mnt_idmap *idmap, struct dentry *dentry,
 	mutex_unlock(&ni->mrec_lock);
 
 	/* Translate extended attribute to acl. */
-	if (err >= 0)
+	if (err >= 0) {
 		acl = posix_acl_from_xattr(&init_user_ns, buf, err);
-	else if (err == -ENODATA)
+		if (IS_ERR(acl) && PTR_ERR(acl) == -EINVAL) {
+			acl = ERR_PTR(-EFSCORRUPTED);
+			ntfs_report_file_metadata_error(inode, -EFSCORRUPTED);
+		}
+	} else if (err == -ENODATA) {
 		acl = NULL;
-	else
+	} else {
 		acl = ERR_PTR(err);
+	}
 
 	if (!IS_ERR(acl))
 		set_cached_acl(inode, type, acl);
@@ -1118,8 +1148,11 @@ static noinline int ntfs_set_acl_ex(struct mnt_idmap *idmap,
 			if (err != old_len) {
 				kfree(old_value);
 				old_value = NULL;
-				if (err >= 0)
-					err = -EIO;
+				if (err >= 0) {
+					err = -EFSCORRUPTED;
+					ntfs_report_file_metadata_error(inode,
+									-EFSCORRUPTED);
+				}
 				goto out_unlock;
 			}
 		} else if (old_len != -ENODATA) {
@@ -1163,12 +1196,9 @@ static noinline int ntfs_set_acl_ex(struct mnt_idmap *idmap,
 							XATTR_REPLACE, NULL);
 				}
 				if (undo_err) {
-					/*
-					 * Record a failed rollback so the next
-					 * persistence point marks the volume
-					 * dirty.  Run chkdsk.
-					 */
-					NVolSetErrors(NTFS_I(inode)->vol);
+					/* The ACL and WSL mode may now disagree. */
+					ntfs_report_file_metadata_error(inode,
+									undo_err);
 					ntfs_error(inode->i_sb,
 						   "Failed to undo the ACL xattr: %d",
 						   undo_err);
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 29/31] ntfs: report named stream metadata and recovery errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (27 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 28/31] ntfs: report EA metadata and restoration errors Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 30/31] ntfs: report reparse and object ID metadata errors Baolin Liu
                   ` (2 subsequent siblings)
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Xuewen Wang <wangxuewen@kylinos.cn>

Report missing or stale MFT references after a successful base-name
lookup as EFSCORRUPTED only when the caller holds the directory i_rwsem.
Pass that locking state through the helper and its callers. Unlocked
revalidation can race with unlink or record reuse and must not report
those races as corruption.

Also report missing required stream attributes and cleanup failures.
For rollback failures that trigger shutdown, report the cleanup errno
and retain the EIO operation result. Preserve the existing ENOMEM and
interruption exceptions, and keep ordinary missing streams silent.

Signed-off-by: Xuewen Wang <wangxuewen@kylinos.cn>
---
 fs/ntfs/named_stream.c | 35 +++++++++++++++++++++++++++--------
 fs/ntfs/stream.h       |  2 +-
 2 files changed, 28 insertions(+), 9 deletions(-)

diff --git a/fs/ntfs/named_stream.c b/fs/ntfs/named_stream.c
index 6130359f616a..1073ff7e12f7 100644
--- a/fs/ntfs/named_stream.c
+++ b/fs/ntfs/named_stream.c
@@ -252,6 +252,7 @@ int ntfs_stream_path_check(struct ntfs_volume *vol,
  * @dir_ino: directory containing the base file or directory
  * @uname: UTF-16LE base name
  * @uname_len: Length of @uname in UTF-16 code units
+ * @dir_locked: caller holds the directory i_rwsem against namespace changes
  *
  * Resolve the directory entry and verify its MFT reference before returning
  * the inode.
@@ -259,7 +260,7 @@ int ntfs_stream_path_check(struct ntfs_volume *vol,
  * Return: Referenced inode on success, or ERR_PTR() on failure.
  */
 struct inode *ntfs_stream_lookup_inode_by_name(struct inode *dir_ino,
-		__le16 *uname, int uname_len)
+		__le16 *uname, int uname_len, bool dir_locked)
 {
 	struct ntfs_volume *vol = NTFS_SB(dir_ino->i_sb);
 	struct ntfs_name *name = NULL;
@@ -276,12 +277,20 @@ struct inode *ntfs_stream_lookup_inode_by_name(struct inode *dir_ino,
 		return ERR_PTR(MREF_ERR(mref));
 
 	inode = ntfs_iget(vol->sb, MREF(mref));
-	if (IS_ERR(inode))
+	if (IS_ERR(inode)) {
+		if (dir_locked && PTR_ERR(inode) == -ENOENT) {
+			ntfs_report_file_metadata_error(dir_ino, -EFSCORRUPTED);
+			inode = ERR_PTR(-EFSCORRUPTED);
+		}
 		return inode;
+	}
 	if (MSEQNO(mref) != NTFS_I(inode)->seq_no &&
 	    MREF(mref) != FILE_MFT) {
+		/* Unlocked dentry revalidation can race with unlink and reuse. */
+		if (dir_locked)
+			ntfs_report_file_metadata_error(dir_ino, -EFSCORRUPTED);
 		iput(inode);
-		return ERR_PTR(-EIO);
+		return ERR_PTR(dir_locked ? -EFSCORRUPTED : -EIO);
 	}
 	return inode;
 }
@@ -333,7 +342,7 @@ static int ntfs_stream_d_revalidate(struct inode *dir,
 			ni->vol->upcase_len))
 		goto out;
 
-	base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len);
+	base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len, false);
 	if (IS_ERR(base_vi))
 		goto out;
 	if (NTFS_I(base_vi) != ni->ext.base_ntfs_ino) {
@@ -441,7 +450,7 @@ struct dentry *ntfs_lookup_stream(struct inode *dir_ino,
 	}
 
 	base_vi = ntfs_stream_lookup_inode_by_name(dir_ino, base_uname,
-			base_len);
+			base_len, true);
 	kmem_cache_free(ntfs_name_cache, base_uname);
 	if (IS_ERR(base_vi)) {
 		err = PTR_ERR(base_vi);
@@ -564,7 +573,7 @@ struct inode *ntfs_create_named_stream(struct mnt_idmap *idmap,
 	bool stream_created = false;
 	int err, rollback_err;
 
-	base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len);
+	base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len, true);
 	if (IS_ERR(base_vi))
 		return base_vi;
 	if (!S_ISREG(base_vi->i_mode) && !S_ISDIR(base_vi->i_mode)) {
@@ -635,6 +644,10 @@ struct inode *ntfs_create_named_stream(struct mnt_idmap *idmap,
 	else
 		err = PTR_ERR(stream_vi);
 	if (err) {
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(base_vi, -EFSCORRUPTED);
+		}
 		if (stream_created) {
 			rollback_err = ntfs_attr_remove(base_ni, AT_DATA,
 					stream_name, stream_len, &rollback_vi);
@@ -646,7 +659,8 @@ struct inode *ntfs_create_named_stream(struct mnt_idmap *idmap,
 				    rollback_err == -ERESTARTSYS) {
 					err = rollback_err;
 				} else {
-					NVolSetErrors(base_ni->vol);
+					ntfs_report_file_metadata_error(base_vi,
+									rollback_err);
 					NVolSetShutdown(base_ni->vol);
 					err = -EIO;
 				}
@@ -698,7 +712,7 @@ int ntfs_unlink_named_stream(struct inode *dir, struct dentry *dentry)
 	if (!path_result)
 		return -EINVAL;
 
-	base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len);
+	base_vi = ntfs_stream_lookup_inode_by_name(dir, base_name, base_len, true);
 	if (IS_ERR(base_vi)) {
 		err = PTR_ERR(base_vi);
 		goto out_free;
@@ -1044,6 +1058,7 @@ static int ntfs_stream_finish_remove(struct inode *attr_vi)
 		remove_inode_hash(attr_vi);
 	} else {
 		NInoSetBeingDeleted(attr_ni);
+		ntfs_report_file_metadata_error(VFS_I(ni), err);
 		remove_inode_hash(attr_vi);
 	}
 
@@ -1279,6 +1294,10 @@ static int ntfs_stream_ioctl_get_inode(struct inode *base_vi, __le16 *sname,
 	attr_vi = ntfs_attr_iget(base_vi, AT_DATA, sname, sname_len);
 	if (IS_ERR(attr_vi)) {
 		err = PTR_ERR(attr_vi);
+		if (err == -ENOENT) {
+			err = -EFSCORRUPTED;
+			ntfs_report_file_metadata_error(base_vi, -EFSCORRUPTED);
+		}
 		attr_vi = NULL;
 		goto out_unlock;
 	}
diff --git a/fs/ntfs/stream.h b/fs/ntfs/stream.h
index 5cbf6e8f6b68..84929e755689 100644
--- a/fs/ntfs/stream.h
+++ b/fs/ntfs/stream.h
@@ -29,7 +29,7 @@ int ntfs_stream_path_names(struct ntfs_volume *vol,
 		__le16 **stream_name, int *stream_len);
 
 struct inode *ntfs_stream_lookup_inode_by_name(struct inode *dir,
-		__le16 *name, int name_len);
+		__le16 *name, int name_len, bool dir_locked);
 struct dentry *ntfs_lookup_stream(struct inode *dir, struct dentry *dentry,
 		const struct ntfs_stream_path *path);
 struct inode *ntfs_create_named_stream(struct mnt_idmap *idmap,
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 30/31] ntfs: report reparse and object ID metadata errors
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (28 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 29/31] ntfs: report named stream metadata and recovery errors Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  8:44 ` [PATCH v4 31/31] ntfs: report each shutdown transition once Baolin Liu
  2026-10-20  9:01 ` [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify liubaolin
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan

From: Xuewen Wang <wangxuewen@kylinos.cn>

Report missing required reparse/ObjectID indexes or entries, short stored
values and missing reparse attributes. Use EFSCORRUPTED for corruption
while preserving read errors. Keep new zero-tag reparse placeholders
valid before index insertion.

Return error pointers from reparse-index lookup to preserve specific
errors instead of replacing them with EINVAL. Retain best-effort handling
of index-removal lookup failures.

Report failed reparse writes when replacing existing data or after a
partial write, and failures in the existing index-insertion rollback.
Keep the initiating error separate from cleanup errors.

Signed-off-by: Xuewen Wang <wangxuewen@kylinos.cn>
---
 fs/ntfs/object_id.c | 40 +++++++++++++++-----
 fs/ntfs/reparse.c   | 89 ++++++++++++++++++++++++++++++++++++---------
 2 files changed, 101 insertions(+), 28 deletions(-)

diff --git a/fs/ntfs/object_id.c b/fs/ntfs/object_id.c
index 89002c3759df..2a1b0f6910fc 100644
--- a/fs/ntfs/object_id.c
+++ b/fs/ntfs/object_id.c
@@ -37,15 +37,16 @@ __le16 objid_index_name[] = {cpu_to_le16('$'), cpu_to_le16('O'), 0};
 
 /*
  * open_object_id_index - Open the $Extend/$ObjId file and its index
- * @vol: NTFS volume structure
+ * @ni: NTFS inode with an existing object ID attribute
  *
  * Opens the $ObjId system file and retrieves its index context.
  *
  * Return: The index context if opened successfully, or NULL if an error
  *	   occurred.
  */
-static struct ntfs_index_context *open_object_id_index(struct ntfs_volume *vol)
+static struct ntfs_index_context *open_object_id_index(struct ntfs_inode *ni)
 {
+	struct ntfs_volume *vol = ni->vol;
 	struct inode *dir_vi, *vi;
 	struct ntfs_inode *dir_ni;
 	struct ntfs_index_context *xo = NULL;
@@ -62,6 +63,8 @@ static struct ntfs_index_context *open_object_id_index(struct ntfs_volume *vol)
 	/* do not use path_name_to inode - could reopen root */
 	dir_vi = ntfs_iget(vol->sb, FILE_Extend);
 	if (IS_ERR(dir_vi)) {
+		if (vol->major_ver >= 3 && PTR_ERR(dir_vi) == -ENOENT)
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
 		kmem_cache_free(ntfs_name_cache, uname);
 		return NULL;
 	}
@@ -72,12 +75,19 @@ static struct ntfs_index_context *open_object_id_index(struct ntfs_volume *vol)
 	mutex_unlock(&dir_ni->mrec_lock);
 	kfree(name);
 	kmem_cache_free(ntfs_name_cache, uname);
-	if (IS_ERR_MREF(mref))
+	if (IS_ERR_MREF(mref)) {
+		if (MREF_ERR(mref) == -ENOENT)
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
 		goto put_dir_vi;
+	}
 
 	vi = ntfs_iget(vol->sb, MREF(mref));
-	if (IS_ERR(vi))
+	if (IS_ERR(vi)) {
+		if (PTR_ERR(vi) == -ENOENT)
+			ntfs_report_file_metadata_error(dir_vi, -EFSCORRUPTED);
 		goto put_dir_vi;
+	}
 
 	xo = ntfs_index_ctx_get(NTFS_I(vi), objid_index_name, 2);
 	if (!xo)
@@ -101,18 +111,28 @@ static int remove_object_id_index(struct ntfs_inode *ni, struct ntfs_index_conte
 {
 	struct object_id_index_key key = {0};
 	s64 size;
+	int err;
 
-	if (ni->data_size == 0)
-		return -ENODATA;
+	if (ni->data_size == 0) {
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
+		return -EFSCORRUPTED;
+	}
 
 	/* read the existing object id attribute */
 	size = ntfs_inode_attr_pread(VFS_I(ni), 0, sizeof(struct guid),
 				     (char *)&key);
-	if (size != sizeof(struct guid))
-		return -ENODATA;
+	if (size != sizeof(struct guid)) {
+		if (size >= 0 || size == -EIO)
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							size < 0 ? (int)size : -EFSCORRUPTED);
+		return size < 0 ? size : -EFSCORRUPTED;
+	}
 
-	if (!ntfs_index_lookup(&key, sizeof(struct object_id_index_key), xo))
+	err = ntfs_index_lookup(&key, sizeof(struct object_id_index_key), xo);
+	if (!err)
 		return ntfs_index_rm(xo);
+	if (err == -ENOENT)
+		ntfs_report_file_metadata_error(VFS_I(ni), -EFSCORRUPTED);
 
 	return 0;
 }
@@ -139,7 +159,7 @@ int ntfs_delete_object_id_index(struct ntfs_inode *ni)
 	/*
 	 * read the existing object id and un-index it
 	 */
-	xo = open_object_id_index(ni->vol);
+	xo = open_object_id_index(ni);
 	if (xo) {
 		xoni = xo->idx_ni;
 		mutex_lock_nested(&xoni->mrec_lock, NTFS_EXTEND_MUTEX_PARENT);
diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c
index c05768a49440..1d076ecc2f9a 100644
--- a/fs/ntfs/reparse.c
+++ b/fs/ntfs/reparse.c
@@ -598,26 +598,36 @@ static int remove_reparse_index(struct inode *rp, struct ntfs_index_context *xr,
 
 	/* read the existing reparse_tag */
 	size = ntfs_inode_attr_pread(rp, 0, 4, (char *)preparse_tag);
-	if (size != 4)
-		return -ENODATA;
+	if (size != 4) {
+		if (size >= 0 || size == -EIO)
+			ntfs_report_file_metadata_error(rp,
+							size < 0 ? (int)size : -EFSCORRUPTED);
+		return size < 0 ? size : -EFSCORRUPTED;
+	}
 
 	file_id_cpu = MK_MREF(ni->mft_no, ni->seq_no);
 	file_id = cpu_to_le64(file_id_cpu);
 	key.reparse_tag = *preparse_tag;
 	/* danger on processors which require proper alignment! */
 	memcpy(&key.file_id, &file_id, 8);
-	if (!ntfs_index_lookup(&key, sizeof(struct reparse_index_key), xr)) {
+	err = ntfs_index_lookup(&key, sizeof(struct reparse_index_key), xr);
+	if (!err) {
 		err = ntfs_index_rm(xr);
 		if (err)
 			ret = err;
+	} else if (err == -ENOENT && *preparse_tag) {
+		/* A newly created attribute has a zero tag and is not indexed yet. */
+		ntfs_report_file_metadata_error(rp, -EFSCORRUPTED);
 	}
 	return ret;
 }
 
 /*
  * Open the $Extend/$Reparse file and its index
+ * @inode is non-NULL when existing reparse metadata requires the index.
  */
-static struct ntfs_index_context *open_reparse_index(struct ntfs_volume *vol)
+static struct ntfs_index_context *open_reparse_index(struct ntfs_volume *vol,
+						     struct inode *inode)
 {
 	struct ntfs_index_context *xr = NULL;
 	u64 mref;
@@ -628,14 +638,19 @@ static struct ntfs_index_context *open_reparse_index(struct ntfs_volume *vol)
 
 	/* do not use path_name_to inode - could reopen root */
 	dir_vi = ntfs_iget(vol->sb, FILE_Extend);
-	if (IS_ERR(dir_vi))
-		return NULL;
+	if (IS_ERR(dir_vi)) {
+		if (vol->major_ver >= 3 && PTR_ERR(dir_vi) == -ENOENT) {
+			ntfs_report_metadata_error(vol, -EFSCORRUPTED);
+			dir_vi = ERR_PTR(-EFSCORRUPTED);
+		}
+		return ERR_CAST(dir_vi);
+	}
 
 	uname_len = ntfs_nlstoucs(vol, "$Reparse", 8, &uname,
 				  NTFS_MAX_NAME_LEN);
 	if (uname_len < 0) {
 		iput(dir_vi);
-		return NULL;
+		return ERR_PTR(uname_len);
 	}
 
 	mutex_lock_nested(&NTFS_I(dir_vi)->mrec_lock, NTFS_EXTEND_MUTEX_PARENT);
@@ -644,16 +659,30 @@ static struct ntfs_index_context *open_reparse_index(struct ntfs_volume *vol)
 	mutex_unlock(&NTFS_I(dir_vi)->mrec_lock);
 	kfree(name);
 	kmem_cache_free(ntfs_name_cache, uname);
-	if (IS_ERR_MREF(mref))
+	if (IS_ERR_MREF(mref)) {
+		if (MREF_ERR(mref) == -ENOENT && inode) {
+			ntfs_report_file_metadata_error(inode, -EFSCORRUPTED);
+			mref = ERR_MREF(-EFSCORRUPTED);
+		}
+		xr = ERR_PTR(MREF_ERR(mref));
 		goto put_dir_vi;
+	}
 
 	vi = ntfs_iget(vol->sb, MREF(mref));
-	if (IS_ERR(vi))
+	if (IS_ERR(vi)) {
+		if (PTR_ERR(vi) == -ENOENT) {
+			ntfs_report_file_metadata_error(dir_vi, -EFSCORRUPTED);
+			vi = ERR_PTR(-EFSCORRUPTED);
+		}
+		xr = ERR_CAST(vi);
 		goto put_dir_vi;
+	}
 
 	xr = ntfs_index_ctx_get(NTFS_I(vi), reparse_index_name, 2);
-	if (!xr)
+	if (!xr) {
 		iput(vi);
+		xr = ERR_PTR(-ENOMEM);
+	}
 put_dir_vi:
 	iput(dir_vi);
 	return xr;
@@ -678,8 +707,14 @@ static int update_reparse_data(struct ntfs_inode *ni, struct ntfs_index_context
 	struct ntfs_inode *rp_ni;
 
 	rp_inode = ntfs_attr_iget(VFS_I(ni), AT_REPARSE_POINT, AT_UNNAMED, 0);
-	if (IS_ERR(rp_inode))
+	if (IS_ERR(rp_inode)) {
+		if (PTR_ERR(rp_inode) == -ENOENT) {
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+			rp_inode = ERR_PTR(-EFSCORRUPTED);
+		}
 		return PTR_ERR(rp_inode);
+	}
 	rp_ni = NTFS_I(rp_inode);
 
 	/* remove the existing reparse data */
@@ -693,19 +728,26 @@ static int update_reparse_data(struct ntfs_inode *ni, struct ntfs_index_context
 	written = ntfs_inode_attr_pwrite(rp_inode, 0, size, value, false);
 	if (written != size) {
 		ntfs_error(ni->vol->sb, "Failed to update reparse data\n");
-		err = -EIO;
+		if (oldsize > 0 || written > 0)
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							written < 0 ? (int)written : -EIO);
+		err = written < 0 ? written : -EIO;
 		goto put_rp_inode;
 	}
 
 	err = set_reparse_index(ni, xr,
 				((const struct reparse_point *)value)->reparse_tag);
 	if (err && oldsize > 0) {
+		int rollback_err;
+
 		/*
 		 * If cannot index, try to remove the reparse
 		 * data and log the error. There will be an
 		 * inconsistency if removal fails.
 		 */
-		ntfs_attr_rm(rp_ni);
+		rollback_err = ntfs_attr_rm(rp_ni);
+		if (rollback_err)
+			ntfs_report_file_metadata_error(VFS_I(ni), rollback_err);
 		ntfs_error(ni->vol->sb,
 			   "Failed to index reparse data. Possible corruption.\n");
 	}
@@ -732,14 +774,24 @@ int ntfs_delete_reparse_index(struct ntfs_inode *ni)
 		return 0;
 
 	vi = ntfs_attr_iget(VFS_I(ni), AT_REPARSE_POINT, AT_UNNAMED, 0);
-	if (IS_ERR(vi))
+	if (IS_ERR(vi)) {
+		if (PTR_ERR(vi) == -ENOENT) {
+			ntfs_report_file_metadata_error(VFS_I(ni),
+							-EFSCORRUPTED);
+			vi = ERR_PTR(-EFSCORRUPTED);
+		}
 		return PTR_ERR(vi);
+	}
 
 	/*
 	 * read the existing reparse data (the tag is enough)
 	 * and un-index it
 	 */
-	xr = open_reparse_index(ni->vol);
+	xr = open_reparse_index(ni->vol, VFS_I(ni));
+	if (IS_ERR(xr)) {
+		err = PTR_ERR(xr);
+		goto out;
+	}
 	if (xr) {
 		xrni = xr->idx_ni;
 		mutex_lock_nested(&xrni->mrec_lock, NTFS_EXTEND_MUTEX_PARENT);
@@ -785,9 +837,10 @@ static int ntfs_set_ntfs_reparse_data(struct ntfs_inode *ni, char *value, size_t
 	if (valid_reparse_data(ni, (const struct reparse_point *)value, size) == false)
 		return -EINVAL;
 
-	xr = open_reparse_index(ni->vol);
-	if (!xr)
-		return -EINVAL;
+	xr = open_reparse_index(ni->vol,
+				ni->flags & FILE_ATTR_REPARSE_POINT ? VFS_I(ni) : NULL);
+	if (IS_ERR(xr))
+		return PTR_ERR(xr);
 	xrni = xr->idx_ni;
 
 	if (!ntfs_attr_exist(ni, AT_REPARSE_POINT, AT_UNNAMED, 0)) {
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* [PATCH v4 31/31] ntfs: report each shutdown transition once
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (29 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 30/31] ntfs: report reparse and object ID metadata errors Baolin Liu
@ 2026-10-20  8:44 ` Baolin Liu
  2026-10-20  9:01 ` [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify liubaolin
  31 siblings, 0 replies; 41+ messages in thread
From: Baolin Liu @ 2026-10-20  8:44 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu

From: Baolin Liu <liubaolin@kylinos.cn>

The existing shutdown sites set NV_Shutdown without notifying filesystem
error listeners. Add ntfs_report_shutdown() and use it for forced
shutdown, device removal, EA restoration and named-stream rollback.

Use test_and_set_bit(NV_Shutdown, &vol->flags) to set the flag atomically
and call the generic shutdown reporter only when the bit was previously
clear. Concurrent callers therefore initiate one report per transition.
Keep the existing conditions that trigger shutdown.

Signed-off-by: Baolin Liu <liubaolin@kylinos.cn>
---
 fs/ntfs/ea.c           |  2 +-
 fs/ntfs/named_stream.c |  2 +-
 fs/ntfs/super.c        | 12 +++++++++---
 fs/ntfs/volume.h       |  1 +
 4 files changed, 12 insertions(+), 5 deletions(-)

diff --git a/fs/ntfs/ea.c b/fs/ntfs/ea.c
index ee4d7d082b2c..fa8ed74791a9 100644
--- a/fs/ntfs/ea.c
+++ b/fs/ntfs/ea.c
@@ -379,7 +379,7 @@ static int ntfs_set_ea(struct inode *inode, const char *name, size_t name_len,
 				ntfs_report_file_metadata_error(inode, ea_err);
 			if (info_err)
 				ntfs_report_file_metadata_error(inode, info_err);
-			NVolSetShutdown(ni->vol);
+			ntfs_report_shutdown(ni->vol);
 			ntfs_error(inode->i_sb,
 				   "Failed to restore EAs of inode 0x%llx (%d, %d); shutting down.",
 				   ni->mft_no, ea_err, info_err);
diff --git a/fs/ntfs/named_stream.c b/fs/ntfs/named_stream.c
index 1073ff7e12f7..ae1e907e729b 100644
--- a/fs/ntfs/named_stream.c
+++ b/fs/ntfs/named_stream.c
@@ -661,7 +661,7 @@ struct inode *ntfs_create_named_stream(struct mnt_idmap *idmap,
 				} else {
 					ntfs_report_file_metadata_error(base_vi,
 									rollback_err);
-					NVolSetShutdown(base_ni->vol);
+					ntfs_report_shutdown(base_ni->vol);
 					err = -EIO;
 				}
 			}
diff --git a/fs/ntfs/super.c b/fs/ntfs/super.c
index 7c10a1b24b14..462173686a37 100644
--- a/fs/ntfs/super.c
+++ b/fs/ntfs/super.c
@@ -455,6 +455,12 @@ void ntfs_report_file_metadata_error(struct inode *inode, int error)
 		fserror_report_file_metadata(inode, error, GFP_ATOMIC);
 }
 
+void ntfs_report_shutdown(struct ntfs_volume *vol)
+{
+	if (!test_and_set_bit(NV_Shutdown, &vol->flags))
+		fserror_report_shutdown(vol->sb, GFP_ATOMIC);
+}
+
 void ntfs_handle_error(struct super_block *sb)
 {
 	struct ntfs_volume *vol = NTFS_SB(sb);
@@ -471,7 +477,7 @@ void ntfs_handle_error(struct super_block *sb)
 		      sb->s_id);
 	} else if (vol->on_errors == ON_ERRORS_CONTINUE) {
 		if (errseq_check(&sb->s_wb_err, vol->wb_err) == -ENODEV) {
-			NVolSetShutdown(vol);
+			ntfs_report_shutdown(vol);
 			vol->wb_err = sb->s_wb_err;
 		}
 	}
@@ -2352,10 +2358,10 @@ int ntfs_force_shutdown(struct super_block *sb, u32 flags)
 		if (ret)
 			return ret;
 		bdev_thaw(sb->s_bdev);
-		NVolSetShutdown(vol);
+		ntfs_report_shutdown(vol);
 		break;
 	case FS_SHUTDOWN_FLAGS_NOLOGFLUSH:
-		NVolSetShutdown(vol);
+		ntfs_report_shutdown(vol);
 		break;
 	default:
 		return -EINVAL;
diff --git a/fs/ntfs/volume.h b/fs/ntfs/volume.h
index 27f58beb4b2d..95ca0019906e 100644
--- a/fs/ntfs/volume.h
+++ b/fs/ntfs/volume.h
@@ -264,6 +264,7 @@ DEFINE_NVOL_BIT_OPS(StreamsWindows)
 
 void ntfs_report_metadata_error(struct ntfs_volume *vol, int error);
 void ntfs_report_file_metadata_error(struct inode *inode, int error);
+void ntfs_report_shutdown(struct ntfs_volume *vol);
 
 static inline void ntfs_inc_free_clusters(struct ntfs_volume *vol, s64 nr)
 {
-- 
2.51.0


^ permalink raw reply	[flat|nested] 41+ messages in thread

* Re: [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify
  2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
                   ` (30 preceding siblings ...)
  2026-10-20  8:44 ` [PATCH v4 31/31] ntfs: report each shutdown transition once Baolin Liu
@ 2026-10-20  9:01 ` liubaolin
  31 siblings, 0 replies; 41+ messages in thread
From: liubaolin @ 2026-10-20  9:01 UTC (permalink / raw)
  To: linkinjeon, hyc.lee; +Cc: ntfs, linux-kernel, wangxuewen, wudandan, Baolin Liu



在 2026/10/20 16:43, Baolin Liu 写道:
> From: Baolin Liu <liubaolin@kylinos.cn>
> 
> Report NTFS metadata corruption, failed recovery and shutdown through
> FAN_FS_ERROR. File events identify the affected base file; volume events
> cover errors without a usable file reference. Preserve the existing
> volume error bit and shutdown policy.
> 
> Confirmed metadata corruption uses -EFSCORRUPTED (-EUCLEAN) in both
> events and failing NTFS operations. This intentionally changes
> userspace-visible corruption errors previously returned as EIO or
> obscured as ENOMEM/EINVAL. Genuine I/O, allocation and unsupported-format
> errors retain their own errnos. Ordinary EOF and missing names or streams
> are not corruption. Partial-I/O and best-effort operations retain their
> existing return semantics; an independent recovery failure may produce
> an event with a different errno from the initiating operation.
> The generic open_by_handle_at() wrapper still converts decode errors
> other than ENOMEM to ESTALE.
> NFS handle decoding also maps corruption errors to STALE.
> 
> Based on ntfs-next commit 19dc08f1f2c5 ("ntfs: support persistent
> noatime attribute").
> 
> Changes since v3:
> - Extend reporting to previously uncovered corruption and recovery
>    failures in system metadata, attributes, runlists, directories/indexes,
>    MFT records and writeback, LZNT1/WOF, EA/ACLs, named streams, reparse
>    points and object IDs. This includes ntfs_ib_read(), nonresident iomap
>    and ntfs_lookup_inode_by_name(), the lookup path in this base; the
>    cited ntfs_lookup_inode_by_example() is absent.
> - Reuse ntfs_inode_base() for attribute and extent inode attribution.
>    Use volume reports during eviction; before SB_ACTIVE, record only
>    the volume error state.
> - Use test_and_set_bit(NV_Shutdown, &vol->flags) so concurrent shutdown
>    callers initiate only one shutdown report.
> - Preserve specific errors through attribute search contexts, index
>    traversal, buffered reads and mount initialization. Keep recoverable
>    resource failures distinct from failed recovery after metadata changes.
> - Stabilize export parent reconstruction against rename/rmdir and avoid
>    classifying unlocked named-stream revalidation races as corruption.
>    Preserve directory resume behavior after deleted keys and partial
>    reads, including retries after traversal errors.
> 
> Series organization:
> - 1-4: fix lost writeback errors and cluster-free count handling;
>    add reporting helpers and preserve search-context initialization errors
>    through the API and all callers.
> - 5-7: boot geometry/MFT bootstrap, required system files/volume
>    metadata, and logfile validation/emptying in separate patches.
> - 8-10: runlist/cluster allocation, iomap/buffered reads, and
>    LZNT1/WOF compressed streams in separate patches.
> - 11: handle source-record removal failure during attribute moves
>    before updating the attribute-list reference.
> - 12-14: attribute lookup/validation, record updates/cleanup, and
>    attribute-list updates/recovery.
> - 15-17: attribute conversion/mapping, resize/relocation recovery,
>    and range updates.
> - 18: inode metadata and eviction cleanup.
> - 19-22: index validation/block I/O, traversal errors/directory
>    retries, directory metadata, and index update/recovery failures.
> - 23-24: namespace/file recovery and export parent reconstruction.
> - 25-27: MFT record lifecycle, bitmap/data growth recovery, and
>    MFT writeback in separate patches.
> - 28-30: EA/ACLs, named streams, and reparse/ObjectID indexes.
> - 31: add the atomic shutdown helper and convert all callers.
> 
> Validation:
> - The final kernel passed 35 QEMU guest cases covering normal file and
>    namespace operations, streams, metadata corruption, directory resume,
>    compression/mapping gaps, AttrDef, mount, concurrent shutdown, export
>    handles, fsync/remount/readback and filesystem ustat selftests.
> - Compression-buffer allocation injection reproduced EIO before the
>    fix and ENOMEM afterward. A complete retry succeeded after disabling
>    injection, with no filesystem error event; the healthy control passed.
> - Six additional ACL guest cases passed with CONFIG_MULTIUSER=y.
>    On-disk invalid UID/GID values in access and default ACLs returned
>    EUCLEAN and generated EUCLEAN events identifying the affected inode.
>    Valid ACLs read back correctly; invalid userspace ACL submissions
>    returned EINVAL without error events. These tests used no kernel hooks.
> - Eleven additional NFSv3/TCP loopback guest cases passed using kernel
>    nfsd, a mounted Linux client and targeted RPC tests. Tracing confirmed
>    ntfs_get_parent() calls from nfsd for healthy and corrupt parents,
>    with subtree_check enabled and disabled. The tests also covered
>    stale handles and 320 rename/rmdir race iterations without false
>    corruption reports. Corruption events identified the affected directory.
> 
> - Sparse C=2 checked all 31 NTFS translation units, including WOF,
>    on both the final tree and its ntfs-next base, without diagnostics.
> - Seventeen guest cases passed with KCSAN and lockdep enabled, covering
>    normal I/O, namespace/streams, concurrent shutdown, ACLs and NFS races.
>    KCSAN selftests passed and both monitors remained active throughout;
>    no KCSAN or lockdep diagnostics were printed.
> - Eighteen targeted injection cases covered the two resident free-space
>    failure exits and six attribute/MFT errno branches, including moved
>    versus unmoved records, source-record restoration, ENOMEM controls,
>    event attribution and cluster cleanup.
> - Twenty-six directory-retry, EA/ACL and iomap/MFT injection cases were
>    rerun against the final source tree and passed.
> 
> Baolin Liu (11):
>    ntfs: preserve cluster-free counts in attribute cleanup
>    ntfs: add filesystem metadata error reporting helpers
>    ntfs: report runlist and cluster allocation metadata errors
>    ntfs: report attribute lookup and validation errors
>    ntfs: report attribute list update and recovery failures
>    ntfs: report attribute conversion and mapping update failures
>    ntfs: report inode metadata and eviction cleanup errors
>    ntfs: report MFT record validation and lifecycle errors
>    ntfs: report MFT bitmap and data growth recovery failures
>    ntfs: report MFT writeback errors to fsnotify
>    ntfs: report each shutdown transition once
> 
> Dandan Wu (11):
>    ntfs: report required system file and volume metadata errors
>    ntfs: report logfile metadata and emptying errors
>    ntfs: report iomap corruption and preserve buffered read errors
>    ntfs: report LZNT1 and WOF compressed stream errors
>    ntfs: report attribute resize and relocation recovery failures
>    ntfs: report incomplete attribute range updates
>    ntfs: report index validation and block I/O errors
>    ntfs: preserve index traversal errors and directory retries
>    ntfs: report directory lookup and metadata corruption
>    ntfs: report index update and recovery failures
>    ntfs: report namespace and file recovery errors
> 
> Xuewen Wang (9):
>    ntfs: propagate mapping-pairs update errors from inode writeback
>    ntfs: preserve attribute search context initialization errors
>    ntfs: report boot geometry and MFT bootstrap errors
>    ntfs: handle source record removal failure during attribute moves
>    ntfs: report attribute record update and cleanup failures
>    ntfs: serialize and report export parent reconstruction errors
>    ntfs: report EA metadata and restoration errors
>    ntfs: report named stream metadata and recovery errors
>    ntfs: report reparse and object ID metadata errors
> 
>   fs/ntfs/aops.c         |  24 +-
>   fs/ntfs/attrib.c       | 738 ++++++++++++++++++++++++++++++-----------
>   fs/ntfs/attrib.h       |   8 +-
>   fs/ntfs/attrlist.c     |  43 ++-
>   fs/ntfs/bitmap.c       |   2 +-
>   fs/ntfs/compress.c     |  30 +-
>   fs/ntfs/dir.c          | 108 ++++--
>   fs/ntfs/ea.c           |  84 +++--
>   fs/ntfs/file.c         |  33 +-
>   fs/ntfs/index.c        | 313 +++++++++++------
>   fs/ntfs/inode.c        | 265 ++++++++++-----
>   fs/ntfs/inode.h        |  10 +-
>   fs/ntfs/iomap.c        |  53 ++-
>   fs/ntfs/lcnalloc.c     |  17 +-
>   fs/ntfs/logfile.c      |  51 +--
>   fs/ntfs/mft.c          | 326 +++++++++++-------
>   fs/ntfs/named_stream.c |  55 ++-
>   fs/ntfs/namei.c        | 140 ++++++--
>   fs/ntfs/object_id.c    |  40 ++-
>   fs/ntfs/reparse.c      |  98 ++++--
>   fs/ntfs/reparse.h      |   2 +-
>   fs/ntfs/runlist.c      |  16 +-
>   fs/ntfs/runlist.h      |   1 +
>   fs/ntfs/stream.h       |   2 +-
>   fs/ntfs/super.c        | 279 +++++++++++-----
>   fs/ntfs/volume.h       |   4 +
>   fs/ntfs/wof.c          |  41 ++-
>   27 files changed, 1970 insertions(+), 813 deletions(-)
> 
> 
> base-commit: 19dc08f1f2c5563ba4da6afd5ecace8eba163aef

Hi Namjae and Hyunchul,
    Sorry for the delay in sending v4. It has been about a month since
I posted v3.

    Hyunchul raised three points in the v3 review:
    1. Some paths detect corruption without calling any report_*_error()
       helper, including ntfs_lookup_inode_by_example(), ntfs_ib_read(),
       and ntfs_read_iomap_begin_non_resident().
    2. Reuse ntfs_inode_base() instead of repeating the logic for finding
       the base inode at individual call sites.
    3. Use test_and_set_bit() when setting NV_Shutdown to avoid races
       and duplicate shutdown reports.

    We have addressed these points in v4. Xuewen, Dandan, and I spent
considerable time auditing the error paths, especially looking for
missing reports. During that work, we found further reporting gaps
and related error-handling issues that needed attention, so preparing
this revision took longer than expected.

    The series now contains 31 patches. We split the changes into 
smaller,focused patches to keep individual patches manageable and make 
review easier. Sorry for the length of the series, and thank you for 
your patience.

    We would appreciate another review of v4. Thank you both for your
time and feedback.

Best regards,
Baolin.


^ permalink raw reply	[flat|nested] 41+ messages in thread

* Re: [PATCH v4 04/31] ntfs: preserve attribute search context initialization errors
  2026-10-20  8:43 ` [PATCH v4 04/31] ntfs: preserve attribute search context initialization errors Baolin Liu
@ 2026-10-27  5:46   ` Hyunchul Lee
  2026-10-27  6:16     ` liubaolin
  0 siblings, 1 reply; 41+ messages in thread
From: Hyunchul Lee @ 2026-10-27  5:46 UTC (permalink / raw)
  To: Baolin Liu; +Cc: linkinjeon, ntfs, linux-kernel, wangxuewen, wudandan

Hi, 

> @@ -5476,7 +5480,7 @@ int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name,
>  	ntfs_debug("Entering\n");
>  
>  	ctx = ntfs_attr_get_search_ctx(ni, NULL);
> -	if (!ctx) {
> +	if (IS_ERR(ctx)) {
>  		ntfs_error(ni->vol->sb, "%s: Failed to get search context",
>  				__func__);
>  		return 0;

ntfs_attr_exist() still turns ntfs_attr_get_search_ctx() errors into
'attribute absent'.

> -- 
> 2.51.0
> 

-- 
Thanks,
Hyunchul

^ permalink raw reply	[flat|nested] 41+ messages in thread

* Re: [PATCH v4 05/31] ntfs: report boot geometry and MFT bootstrap errors
  2026-10-20  8:43 ` [PATCH v4 05/31] ntfs: report boot geometry and MFT bootstrap errors Baolin Liu
@ 2026-10-27  5:48   ` Hyunchul Lee
  2026-10-27  6:18     ` liubaolin
  0 siblings, 1 reply; 41+ messages in thread
From: Hyunchul Lee @ 2026-10-27  5:48 UTC (permalink / raw)
  To: Baolin Liu; +Cc: linkinjeon, ntfs, linux-kernel, wangxuewen, wudandan

> @@ -2802,7 +2822,8 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc)
>  
>  	tmp_ino->i_ino = FILE_MFT;
>  	insert_inode_hash(tmp_ino);
> -	if (ntfs_read_inode_mount(tmp_ino) < 0) {
> +	err = ntfs_read_inode_mount(tmp_ino);

There is still one error handling case where ntfs_read_inode_mount()
returns -1.

-- 
Thanks,
Hyunchul

^ permalink raw reply	[flat|nested] 41+ messages in thread

* Re: [PATCH v4 06/31] ntfs: report required system file and volume metadata errors
  2026-10-20  8:43 ` [PATCH v4 06/31] ntfs: report required system file and volume metadata errors Baolin Liu
@ 2026-10-27  5:50   ` Hyunchul Lee
  2026-10-27  6:19     ` liubaolin
  0 siblings, 1 reply; 41+ messages in thread
From: Hyunchul Lee @ 2026-10-27  5:50 UTC (permalink / raw)
  To: Baolin Liu; +Cc: linkinjeon, ntfs, linux-kernel, wangxuewen, wudandan

> @@ -1685,23 +1718,29 @@ static int load_and_check_badclus(struct ntfs_volume *vol)
>  	size_t i;
>  	int err;
>  
> -	base_ino = ntfs_iget(sb, FILE_BadClus);
> +	base_ino = ntfs_system_iget(vol, FILE_BadClus);
>  	if (IS_ERR(base_ino))
>  		return PTR_ERR(base_ino);
>  	if (!S_ISREG(base_ino->i_mode)) {
> -		err = -EIO;
> +		err = -EFSCORRUPTED;
> +		ntfs_report_metadata_error(vol, -EFSCORRUPTED);

ntfs_report_*_metadata_error() calls seem to be repeated across serveral
functions. Would it be possible to move and call them right before
returing from the fucntion? Also, could you apply the same change to the
other patches as well?

>  		goto iput_base;
>  	}
-- 
Thanks,
Hyunchul

^ permalink raw reply	[flat|nested] 41+ messages in thread

* Re: [PATCH v4 28/31] ntfs: report EA metadata and restoration errors
  2026-10-20  8:44 ` [PATCH v4 28/31] ntfs: report EA metadata and restoration errors Baolin Liu
@ 2026-10-27  5:51   ` Hyunchul Lee
  2026-10-27  6:20     ` liubaolin
  0 siblings, 1 reply; 41+ messages in thread
From: Hyunchul Lee @ 2026-10-27  5:51 UTC (permalink / raw)
  To: Baolin Liu; +Cc: linkinjeon, ntfs, linux-kernel, wangxuewen, wudandan

> @@ -605,6 +621,8 @@ ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size)
>  	} while (next > 0 && offset < ea_info_qsize);
>  
>  out:
> +	if (err == -EUCLEAN)
> +		ntfs_report_file_metadata_error(inode, err);

this function appears to reache here with -EIO or -ENOENT.

-- 
Thanks,
Hyunchul

^ permalink raw reply	[flat|nested] 41+ messages in thread

* Re: [PATCH v4 04/31] ntfs: preserve attribute search context initialization errors
  2026-10-27  5:46   ` Hyunchul Lee
@ 2026-10-27  6:16     ` liubaolin
  0 siblings, 0 replies; 41+ messages in thread
From: liubaolin @ 2026-10-27  6:16 UTC (permalink / raw)
  To: Hyunchul Lee; +Cc: linkinjeon, ntfs, linux-kernel, wangxuewen, wudandan



在 2026/10/27 13:46, Hyunchul Lee 写道:
> Hi,
> 
>> @@ -5476,7 +5480,7 @@ int ntfs_attr_exist(struct ntfs_inode *ni, const __le32 type, __le16 *name,
>>   	ntfs_debug("Entering\n");
>>   
>>   	ctx = ntfs_attr_get_search_ctx(ni, NULL);
>> -	if (!ctx) {
>> +	if (IS_ERR(ctx)) {
>>   		ntfs_error(ni->vol->sb, "%s: Failed to get search context",
>>   				__func__);
>>   		return 0;
> 
> ntfs_attr_exist() still turns ntfs_attr_get_search_ctx() errors into
> 'attribute absent'.

Hi HyunchulLee,
    You're right. In v5, I'll make ntfs_attr_exist() preserve errors and 
update its callers to distinguish errors from an absent attribute.

Thanks,
Baolin.

> 
>> -- 
>> 2.51.0
>>
> 


^ permalink raw reply	[flat|nested] 41+ messages in thread

* Re: [PATCH v4 05/31] ntfs: report boot geometry and MFT bootstrap errors
  2026-10-27  5:48   ` Hyunchul Lee
@ 2026-10-27  6:18     ` liubaolin
  0 siblings, 0 replies; 41+ messages in thread
From: liubaolin @ 2026-10-27  6:18 UTC (permalink / raw)
  To: Hyunchul Lee; +Cc: linkinjeon, ntfs, linux-kernel, wangxuewen, wudandan



在 2026/10/27 13:48, Hyunchul Lee 写道:
>> @@ -2802,7 +2822,8 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc)
>>   
>>   	tmp_ino->i_ino = FILE_MFT;
>>   	insert_inode_hash(tmp_ino);
>> -	if (ntfs_read_inode_mount(tmp_ino) < 0) {
>> +	err = ntfs_read_inode_mount(tmp_ino);
> 
> There is still one error handling case where ntfs_read_inode_mount()
> returns -1.
> 
Hi Hyunchul,

    You're right, I missed that return path. I'll return the actual 
errno there in v5.

Thanks,
Baolin.


^ permalink raw reply	[flat|nested] 41+ messages in thread

* Re: [PATCH v4 06/31] ntfs: report required system file and volume metadata errors
  2026-10-27  5:50   ` Hyunchul Lee
@ 2026-10-27  6:19     ` liubaolin
  0 siblings, 0 replies; 41+ messages in thread
From: liubaolin @ 2026-10-27  6:19 UTC (permalink / raw)
  To: Hyunchul Lee; +Cc: linkinjeon, ntfs, linux-kernel, wangxuewen, wudandan



在 2026/10/27 13:50, Hyunchul Lee 写道:
>> @@ -1685,23 +1718,29 @@ static int load_and_check_badclus(struct ntfs_volume *vol)
>>   	size_t i;
>>   	int err;
>>   
>> -	base_ino = ntfs_iget(sb, FILE_BadClus);
>> +	base_ino = ntfs_system_iget(vol, FILE_BadClus);
>>   	if (IS_ERR(base_ino))
>>   		return PTR_ERR(base_ino);
>>   	if (!S_ISREG(base_ino->i_mode)) {
>> -		err = -EIO;
>> +		err = -EFSCORRUPTED;
>> +		ntfs_report_metadata_error(vol, -EFSCORRUPTED);
> 
> ntfs_report_*_metadata_error() calls seem to be repeated across serveral
> functions. Would it be possible to move and call them right before
> returing from the fucntion? Also, could you apply the same change to the
> other patches as well?

Hi Hyunchul,
    Agreed. In v5, I'll consolidate repeated reporting at common error 
exits where appropriate, including in the other patches.

Thanks,
Baolin.

> 
>>   		goto iput_base;
>>   	}


^ permalink raw reply	[flat|nested] 41+ messages in thread

* Re: [PATCH v4 28/31] ntfs: report EA metadata and restoration errors
  2026-10-27  5:51   ` Hyunchul Lee
@ 2026-10-27  6:20     ` liubaolin
  0 siblings, 0 replies; 41+ messages in thread
From: liubaolin @ 2026-10-27  6:20 UTC (permalink / raw)
  To: Hyunchul Lee; +Cc: linkinjeon, ntfs, linux-kernel, wangxuewen, wudandan



在 2026/10/27 13:51, Hyunchul Lee 写道:
>> @@ -605,6 +621,8 @@ ssize_t ntfs_listxattr(struct dentry *dentry, char *buffer, size_t size)
>>   	} while (next > 0 && offset < ea_info_qsize);
>>   
>>   out:
>> +	if (err == -EUCLEAN)
>> +		ntfs_report_file_metadata_error(inode, err);
> 
> this function appears to reache here with -EIO or -ENOENT.
> 
Hi Hyunchul

    You're right. I'll check the -EIO and -ENOENT paths and fix missing 
reporting in v5, while keeping normal absence distinct from corruption.

Thanks,
Baolin.


^ permalink raw reply	[flat|nested] 41+ messages in thread

end of thread, other threads:[~2026-10-27  6:21 UTC | newest]

Thread overview: 41+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-20  8:43 [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify Baolin Liu
2026-10-20  8:43 ` [PATCH v4 01/31] ntfs: propagate mapping-pairs update errors from inode writeback Baolin Liu
2026-10-20  8:43 ` [PATCH v4 02/31] ntfs: preserve cluster-free counts in attribute cleanup Baolin Liu
2026-10-20  8:43 ` [PATCH v4 03/31] ntfs: add filesystem metadata error reporting helpers Baolin Liu
2026-10-20  8:43 ` [PATCH v4 04/31] ntfs: preserve attribute search context initialization errors Baolin Liu
2026-10-27  5:46   ` Hyunchul Lee
2026-10-27  6:16     ` liubaolin
2026-10-20  8:43 ` [PATCH v4 05/31] ntfs: report boot geometry and MFT bootstrap errors Baolin Liu
2026-10-27  5:48   ` Hyunchul Lee
2026-10-27  6:18     ` liubaolin
2026-10-20  8:43 ` [PATCH v4 06/31] ntfs: report required system file and volume metadata errors Baolin Liu
2026-10-27  5:50   ` Hyunchul Lee
2026-10-27  6:19     ` liubaolin
2026-10-20  8:43 ` [PATCH v4 07/31] ntfs: report logfile metadata and emptying errors Baolin Liu
2026-10-20  8:43 ` [PATCH v4 08/31] ntfs: report runlist and cluster allocation metadata errors Baolin Liu
2026-10-20  8:43 ` [PATCH v4 09/31] ntfs: report iomap corruption and preserve buffered read errors Baolin Liu
2026-10-20  8:44 ` [PATCH v4 10/31] ntfs: report LZNT1 and WOF compressed stream errors Baolin Liu
2026-10-20  8:44 ` [PATCH v4 11/31] ntfs: handle source record removal failure during attribute moves Baolin Liu
2026-10-20  8:44 ` [PATCH v4 12/31] ntfs: report attribute lookup and validation errors Baolin Liu
2026-10-20  8:44 ` [PATCH v4 13/31] ntfs: report attribute record update and cleanup failures Baolin Liu
2026-10-20  8:44 ` [PATCH v4 14/31] ntfs: report attribute list update and recovery failures Baolin Liu
2026-10-20  8:44 ` [PATCH v4 15/31] ntfs: report attribute conversion and mapping update failures Baolin Liu
2026-10-20  8:44 ` [PATCH v4 16/31] ntfs: report attribute resize and relocation recovery failures Baolin Liu
2026-10-20  8:44 ` [PATCH v4 17/31] ntfs: report incomplete attribute range updates Baolin Liu
2026-10-20  8:44 ` [PATCH v4 18/31] ntfs: report inode metadata and eviction cleanup errors Baolin Liu
2026-10-20  8:44 ` [PATCH v4 19/31] ntfs: report index validation and block I/O errors Baolin Liu
2026-10-20  8:44 ` [PATCH v4 20/31] ntfs: preserve index traversal errors and directory retries Baolin Liu
2026-10-20  8:44 ` [PATCH v4 21/31] ntfs: report directory lookup and metadata corruption Baolin Liu
2026-10-20  8:44 ` [PATCH v4 22/31] ntfs: report index update and recovery failures Baolin Liu
2026-10-20  8:44 ` [PATCH v4 23/31] ntfs: report namespace and file recovery errors Baolin Liu
2026-10-20  8:44 ` [PATCH v4 24/31] ntfs: serialize and report export parent reconstruction errors Baolin Liu
2026-10-20  8:44 ` [PATCH v4 25/31] ntfs: report MFT record validation and lifecycle errors Baolin Liu
2026-10-20  8:44 ` [PATCH v4 26/31] ntfs: report MFT bitmap and data growth recovery failures Baolin Liu
2026-10-20  8:44 ` [PATCH v4 27/31] ntfs: report MFT writeback errors to fsnotify Baolin Liu
2026-10-20  8:44 ` [PATCH v4 28/31] ntfs: report EA metadata and restoration errors Baolin Liu
2026-10-27  5:51   ` Hyunchul Lee
2026-10-27  6:20     ` liubaolin
2026-10-20  8:44 ` [PATCH v4 29/31] ntfs: report named stream metadata and recovery errors Baolin Liu
2026-10-20  8:44 ` [PATCH v4 30/31] ntfs: report reparse and object ID metadata errors Baolin Liu
2026-10-20  8:44 ` [PATCH v4 31/31] ntfs: report each shutdown transition once Baolin Liu
2026-10-20  9:01 ` [PATCH v4 00/31] ntfs: report filesystem errors through fsnotify liubaolin

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®