From b5198fcdc195fa531adff7bbfbe40dd27c8d0e89 Mon Sep 17 00:00:00 2001 From: DaeMyung Kang Date: Sun, 26 Apr 2026 13:02:31 +0900 Subject: [PATCH 1/7] ntfs: fix NULL dereference in ntfs_index_walk_down() ntfs_index_walk_down() allocates ictx->ib when descending from the root into an index allocation block. If that allocation fails, the old code still passes the NULL buffer to ntfs_ib_read(), which can write through it via ntfs_inode_attr_pread(). Allocate the index block into a temporary pointer and return -ENOMEM before changing the index context on allocation failure. Also propagate ERR_PTR() through ntfs_index_next() and ntfs_readdir() so walk-down allocation or index block read failures are not mistaken for normal index iteration inside the filesystem. ntfs_readdir() keeps the existing userspace-visible behavior of suppressing readdir errors after marking end_in_iterate; this change only prevents the walk-down failure path from dereferencing NULL internally. The failure was reproduced with failslab fail-nth injection on getdents64; the original module hits a NULL pointer dereference in memcpy_orig through ntfs_ib_read(), while the patched module reaches the same ntfs_index_walk_down() allocation failure without crashing. Fixes: 0a8ac0c1fa0b ("ntfs: update directory operations") Signed-off-by: DaeMyung Kang Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/dir.c | 13 ++++++++++--- fs/ntfs/index.c | 17 +++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/fs/ntfs/dir.c b/fs/ntfs/dir.c index bfa904d2ce66..20f5c7074bdd 100644 --- a/fs/ntfs/dir.c +++ b/fs/ntfs/dir.c @@ -911,8 +911,8 @@ static int ntfs_readdir(struct file *file, struct dir_context *actor) if (next->flags & INDEX_ENTRY_NODE) { next = ntfs_index_walk_down(next, ictx); - if (!next) { - err = -EIO; + if (IS_ERR(next)) { + err = PTR_ERR(next); goto out; } } @@ -920,7 +920,14 @@ static int ntfs_readdir(struct file *file, struct dir_context *actor) if (next && !(next->flags & INDEX_ENTRY_END)) goto nextdir; - while ((next = ntfs_index_next(next, ictx)) != NULL) { + while (1) { + next = ntfs_index_next(next, ictx); + if (IS_ERR(next)) { + err = PTR_ERR(next); + goto out; + } + if (!next) + break; nextdir: /* Check the consistency of an index entry */ if (ntfs_index_entry_inconsistent(ictx, vol, next, COLLATION_FILE_NAME, diff --git a/fs/ntfs/index.c b/fs/ntfs/index.c index 2080f3969137..a547bdcfa456 100644 --- a/fs/ntfs/index.c +++ b/fs/ntfs/index.c @@ -1969,15 +1969,19 @@ int ntfs_index_remove(struct ntfs_inode *dir_ni, const void *key, const u32 keyl struct index_entry *ntfs_index_walk_down(struct index_entry *ie, struct ntfs_index_context *ictx) { struct index_entry *entry; + struct index_block *ib; s64 vcn; entry = ie; do { vcn = ntfs_ie_get_vcn(entry); if (ictx->is_in_root) { + ib = kvzalloc(ictx->block_size, GFP_NOFS); + if (!ib) + return ERR_PTR(-ENOMEM); /* down from level zero */ ictx->ir = NULL; - ictx->ib = kvzalloc(ictx->block_size, GFP_NOFS); + ictx->ib = ib; ictx->pindex = 1; ictx->is_in_root = false; } else { @@ -1991,8 +1995,8 @@ struct index_entry *ntfs_index_walk_down(struct index_entry *ie, struct ntfs_ind ictx->entry = ntfs_ie_get_first(&ictx->ib->index); entry = ictx->entry; } else - entry = NULL; - } while (entry && (entry->flags & INDEX_ENTRY_NODE)); + entry = ERR_PTR(-EIO); + } while (!IS_ERR(entry) && (entry->flags & INDEX_ENTRY_NODE)); return entry; } @@ -2097,10 +2101,15 @@ struct index_entry *ntfs_index_next(struct index_entry *ie, struct ntfs_index_co /* walk down if it has a subnode */ if (flags & INDEX_ENTRY_NODE) { - if (!ictx->ia_ni) + if (!ictx->ia_ni) { ictx->ia_ni = ntfs_ia_open(ictx, ictx->idx_ni); + if (!ictx->ia_ni) + return ERR_PTR(-EIO); + } next = ntfs_index_walk_down(next, ictx); + if (IS_ERR(next)) + return next; } else { /* walk up it has no subnode, nor data */ From 2dd8c1662e38f7bb68a102f1acad9b518c09aeab Mon Sep 17 00:00:00 2001 From: DaeMyung Kang Date: Sun, 26 Apr 2026 13:02:32 +0900 Subject: [PATCH 2/7] ntfs: fix WSL symlink target leak on reparse failure ntfs_reparse_set_wsl_symlink() converts the symlink target into an allocated NLS string and transfers ownership to ni->target only after ntfs_set_ntfs_reparse_data() succeeds. If setting the reparse data fails, the converted target is left unreferenced and leaks. Free the converted target on the reparse update failure path. Use kfree() for the other local failure path as well, matching the ntfs_ucstonls() allocation contract. Fixes: fc053f05ca28 ("ntfs: add reparse and ea operations") Signed-off-by: DaeMyung Kang Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/reparse.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/reparse.c b/fs/ntfs/reparse.c index 8f60ec6f66c1..74713716813f 100644 --- a/fs/ntfs/reparse.c +++ b/fs/ntfs/reparse.c @@ -505,7 +505,6 @@ int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni, struct reparse_point *reparse; struct wsl_link_reparse_data *data; - utarget = (char *)NULL; len = ntfs_ucstonls(ni->vol, target, target_len, &utarget, 0); if (len <= 0) return -EINVAL; @@ -514,7 +513,7 @@ int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni, reparse = kvzalloc(reparse_len, GFP_NOFS); if (!reparse) { err = -ENOMEM; - kvfree(utarget); + kfree(utarget); } else { data = (struct wsl_link_reparse_data *)reparse->reparse_data; reparse->reparse_tag = IO_REPARSE_TAG_LX_SYMLINK; @@ -528,6 +527,8 @@ int ntfs_reparse_set_wsl_symlink(struct ntfs_inode *ni, kvfree(reparse); if (!err) ni->target = utarget; + else + kfree(utarget); } return err; } From cad7c6f0a5147680dd2081256cf8da54fb445d94 Mon Sep 17 00:00:00 2001 From: Zhan Xusheng Date: Thu, 23 Apr 2026 12:52:26 +0800 Subject: [PATCH 3/7] ntfs: fix VCN overflow in ntfs_mapping_pairs_decompress() In ntfs_mapping_pairs_decompress(), lowest_vcn is read from on-disk metadata and used as the initial vcn without validation. A malformed value can introduce an invalid (e.g. negative) vcn, corrupting the runlist from the start. Additionally, the accumulation vcn += deltaxcn does not check for s64 overflow. A crafted mapping pairs array can wrap vcn to a negative value, breaking the monotonically- increasing invariant relied upon by ntfs_rl_vcn_to_lcn() and related helpers. Fix this by validating lowest_vcn and using check_add_overflow() for vcn accumulation. Signed-off-by: Zhan Xusheng Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/runlist.c | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/fs/ntfs/runlist.c b/fs/ntfs/runlist.c index b213b4976d2b..be6ca3d374bb 100644 --- a/fs/ntfs/runlist.c +++ b/fs/ntfs/runlist.c @@ -15,6 +15,8 @@ * Copyright (c) 2007-2022 Jean-Pierre Andre */ +#include + #include "ntfs.h" #include "attrib.h" @@ -739,6 +741,7 @@ struct runlist_element *ntfs_mapping_pairs_decompress(const struct ntfs_volume * int rlsize; /* Size of runlist buffer. */ u16 rlpos; /* Current runlist position in units of struct runlist_elements. */ u8 b; /* Current byte offset in buf. */ + u64 lowest_vcn; /* Raw on-disk lowest_vcn. */ #ifdef DEBUG /* Make sure attr exists and is non-resident. */ @@ -747,8 +750,14 @@ struct runlist_element *ntfs_mapping_pairs_decompress(const struct ntfs_volume * return ERR_PTR(-EINVAL); } #endif + lowest_vcn = le64_to_cpu(attr->data.non_resident.lowest_vcn); + /* 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."); + goto err_out; + } /* Start at vcn = lowest_vcn and lcn 0. */ - vcn = le64_to_cpu(attr->data.non_resident.lowest_vcn); + vcn = lowest_vcn; lcn = 0; /* Get start of the mapping pairs array. */ buf = (u8 *)attr + @@ -823,8 +832,17 @@ struct runlist_element *ntfs_mapping_pairs_decompress(const struct ntfs_volume * * element. */ rl[rlpos].length = deltaxcn; - /* Increment the current vcn by the current run length. */ - vcn += deltaxcn; + /* + * Increment the current vcn by the current run length. + * Guard against s64 overflow from a crafted mapping + * pairs array to preserve the monotonically-increasing + * vcn invariant. + */ + if (unlikely(check_add_overflow(vcn, deltaxcn, &vcn))) { + ntfs_error(vol->sb, "VCN overflow in mapping pairs array."); + goto err_out; + } + /* * There might be no lcn change at all, as is the case for * sparse clusters on NTFS 3.0+, in which case we set the lcn From 785bc568161d96fdbd4326294d427a48e66fe60f Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Mon, 27 Apr 2026 22:58:52 +0900 Subject: [PATCH 4/7] ntfs: fix error handling in ntfs_write_iomap_end_resident() When ntfs_attr_get_search_ctx() fails and returns NULL, the function returned early without calling put_page(ipage). Fix this by jumping to err_out label on error. The err_out path now properly releases the page and the mutex, with a NULL check for the search context. Reported-by: DaeMyung Kang Signed-off-by: Namjae Jeon --- fs/ntfs/iomap.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fs/ntfs/iomap.c b/fs/ntfs/iomap.c index 74a4d3e971f4..dc7d8c893a69 100644 --- a/fs/ntfs/iomap.c +++ b/fs/ntfs/iomap.c @@ -788,8 +788,7 @@ static int ntfs_write_iomap_end_resident(struct inode *inode, loff_t pos, ctx = ntfs_attr_get_search_ctx(ni, NULL); if (!ctx) { written = -ENOMEM; - mutex_unlock(&ni->mrec_lock); - return written; + goto err_out; } err = ntfs_attr_lookup(ni->type, ni->name, ni->name_len, @@ -810,7 +809,8 @@ static int ntfs_write_iomap_end_resident(struct inode *inode, loff_t pos, memcpy(kattr + pos, iomap_inline_data(iomap, pos), written); mark_mft_record_dirty(ctx->ntfs_ino); err_out: - ntfs_attr_put_search_ctx(ctx); + if (ctx) + ntfs_attr_put_search_ctx(ctx); put_page(ipage); mutex_unlock(&ni->mrec_lock); return written; From d986ba0329dcca102e227995371135c9bbcefb6b Mon Sep 17 00:00:00 2001 From: Namjae Jeon Date: Tue, 28 Apr 2026 21:59:30 +0900 Subject: [PATCH 5/7] ntfs: fix invalid PTR_ERR() usage in __ntfs_bitmap_set_bits_in_run() The Smatch reported a warning in __ntfs_bitmap_set_bits_in_run(): "warn: passing a valid pointer to 'PTR_ERR'" This occurs because the 'folio' variable might contain a valid pointer when jumping to the 'rollback' label, specifically when 'cnt <= 0' is detected during the subsequent page mapping loop. In such cases, calling PTR_ERR(folio) is incorrect as it does not contain an error code. Fix this by introducing an explicit 'err' variable to track the error status. This ensures that the rollback logic and the return value consistently use a proper error code regardless of the state of the folio pointer. Reported-by: Dan Carpenter Signed-off-by: Namjae Jeon --- fs/ntfs/bitmap.c | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/fs/ntfs/bitmap.c b/fs/ntfs/bitmap.c index 656d802333e3..b1436b3151b9 100644 --- a/fs/ntfs/bitmap.c +++ b/fs/ntfs/bitmap.c @@ -125,7 +125,7 @@ int __ntfs_bitmap_set_bits_in_run(struct inode *vi, const s64 start_bit, struct address_space *mapping; struct folio *folio; u8 *kaddr; - int pos, len; + int pos, len, err; u8 bit; struct ntfs_inode *ni = NTFS_I(vi); struct ntfs_volume *vol = ni->vol; @@ -201,8 +201,10 @@ int __ntfs_bitmap_set_bits_in_run(struct inode *vi, const s64 start_bit, /* If we are not in the last page, deal with all subsequent pages. */ while (index < end_index) { - if (cnt <= 0) + if (cnt <= 0) { + err = -EIO; goto rollback; + } /* Update @index and get the next folio. */ folio_mark_dirty(folio); @@ -214,6 +216,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 %li), aborting.", PTR_ERR(folio)); + err = PTR_ERR(folio); goto rollback; } @@ -265,7 +268,7 @@ int __ntfs_bitmap_set_bits_in_run(struct inode *vi, const s64 start_bit, * - @count - @cnt is the number of bits that have been modified */ if (is_rollback) - return PTR_ERR(folio); + return err; if (count != cnt) pos = __ntfs_bitmap_set_bits_in_run(vi, start_bit, count - cnt, value ? 0 : 1, true); @@ -274,14 +277,14 @@ int __ntfs_bitmap_set_bits_in_run(struct inode *vi, const s64 start_bit, if (!pos) { /* Rollback was successful. */ ntfs_error(vi->i_sb, - "Failed to map subsequent page (error %li), aborting.", - PTR_ERR(folio)); + "Failed to map subsequent page (error %i), aborting.", + err); } else { /* Rollback failed. */ ntfs_error(vi->i_sb, - "Failed to map subsequent page (error %li) and rollback failed (error %i). Aborting and leaving inconsistent metadata. Unmount and run chkdsk.", - PTR_ERR(folio), pos); + "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)); } - return PTR_ERR(folio); + return err; } From 4ebcf3f94924d54706de0d2492c80944d85410fd Mon Sep 17 00:00:00 2001 From: Hyunchul Lee Date: Tue, 28 Apr 2026 10:34:10 +0900 Subject: [PATCH 6/7] ntfs: drop nlink once for WIN32/DOS aliases NTFS could store a filename as paired WIN32 and DOS $FILE_NAME attributes for directories. But ntfs_delete() deleted both attributes for unlinking a directory, but it also called drop_nlink() for each attributes. This could trigger warnings when unlinking directories. Signed-off-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/namei.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/fs/ntfs/namei.c b/fs/ntfs/namei.c index 10894de519c3..96c450e62efc 100644 --- a/fs/ntfs/namei.c +++ b/fs/ntfs/namei.c @@ -945,7 +945,8 @@ static int ntfs_delete(struct ntfs_inode *ni, struct ntfs_inode *dir_ni, ni_mrec = actx->base_mrec ? actx->base_mrec : actx->mrec; ni_mrec->link_count = cpu_to_le16(le16_to_cpu(ni_mrec->link_count) - 1); - drop_nlink(VFS_I(ni)); + if (!S_ISDIR(VFS_I(ni)->i_mode)) + drop_nlink(VFS_I(ni)); mark_mft_record_dirty(ni); if (looking_for_dos_name) { @@ -955,6 +956,13 @@ static int ntfs_delete(struct ntfs_inode *ni, struct ntfs_inode *dir_ni, goto search; } + /* + * For directories, Drop VFS nlink only when mft record link count + * becomes zero. Because we fixes VFS nlink to 1 for directories. + */ + if (S_ISDIR(VFS_I(ni)->i_mode) && !le16_to_cpu(ni_mrec->link_count)) + drop_nlink(VFS_I(ni)); + /* * If hard link count is not equal to zero then we are done. In other * case there are no reference to this inode left, so we should free all @@ -1221,7 +1229,8 @@ static int __ntfs_link(struct ntfs_inode *ni, struct ntfs_inode *dir_ni, } /* Increment hard links count. */ ni_mrec->link_count = cpu_to_le16(le16_to_cpu(ni_mrec->link_count) + 1); - inc_nlink(VFS_I(ni)); + if (!S_ISDIR(vi->i_mode)) + inc_nlink(VFS_I(ni)); /* Done! */ mark_mft_record_dirty(ni); From 9e9354075d5a15cfc0aba965f3d0d77b7d4303e9 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Tue, 28 Apr 2026 15:21:38 -0400 Subject: [PATCH 7/7] ntfs: Use return instead of goto in ntfs_mapping_pairs_decompress() Clang warns (or errors with CONFIG_WERROR=y / W=e): fs/ntfs/runlist.c:755:6: error: variable 'rl' is used uninitialized whenever 'if' condition is true [-Werror,-Wsometimes-uninitialized] 755 | if (overflows_type(lowest_vcn, vcn)) { | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... fs/ntfs/runlist.c:971:9: note: uninitialized use occurs here 971 | kvfree(rl); | ^~ ... rl has not been allocated at this point so the 'goto err_out' should really just be a return of the error pointer -EIO. Signed-off-by: Nathan Chancellor Reviewed-by: Hyunchul Lee Signed-off-by: Namjae Jeon --- fs/ntfs/runlist.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs/runlist.c b/fs/ntfs/runlist.c index be6ca3d374bb..da21dbeaaf66 100644 --- a/fs/ntfs/runlist.c +++ b/fs/ntfs/runlist.c @@ -754,7 +754,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."); - goto err_out; + return ERR_PTR(-EIO); } /* Start at vcn = lowest_vcn and lcn 0. */ vcn = lowest_vcn;