From 5180138604323895b5c291eca6aa7c20be494ade Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Mon, 1 Sep 2025 11:48:48 +0300 Subject: [PATCH 01/26] fs/ntfs3: Support timestamps prior to epoch Before it used an unsigned 64-bit type, which prevented proper handling of timestamps earlier than 1970-01-01. Switch to a signed 64-bit type to support pre-epoch timestamps. The issue was caught by xfstests. Signed-off-by: Konstantin Komarov --- fs/ntfs3/ntfs_fs.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h index 630128716ea7..2649fbe16669 100644 --- a/fs/ntfs3/ntfs_fs.h +++ b/fs/ntfs3/ntfs_fs.h @@ -979,11 +979,12 @@ static inline __le64 kernel2nt(const struct timespec64 *ts) */ static inline void nt2kernel(const __le64 tm, struct timespec64 *ts) { - u64 t = le64_to_cpu(tm) - _100ns2seconds * SecondsToStartOf1970; + s32 t32; + /* use signed 64 bit to support timestamps prior to epoch. xfstest 258. */ + s64 t = le64_to_cpu(tm) - _100ns2seconds * SecondsToStartOf1970; - // WARNING: do_div changes its first argument(!) - ts->tv_nsec = do_div(t, _100ns2seconds) * 100; - ts->tv_sec = t; + ts->tv_sec = div_s64_rem(t, _100ns2seconds, &t32); + ts->tv_nsec = t32 * 100; } static inline struct ntfs_sb_info *ntfs_sb(struct super_block *sb) From a846cd0d0a05364c6fa5c4988e75d3b639d6dae5 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Mon, 1 Sep 2025 12:00:43 +0300 Subject: [PATCH 02/26] fs/ntfs3: Reformat code and update terminology Reformatted the driver code according to the current .clang-format rules and updated description of used terminology. No functional changes intended. Signed-off-by: Konstantin Komarov --- fs/ntfs3/dir.c | 3 +-- fs/ntfs3/file.c | 8 +++----- fs/ntfs3/frecord.c | 7 ++++--- fs/ntfs3/inode.c | 19 +++++++++++-------- fs/ntfs3/namei.c | 6 +++--- fs/ntfs3/ntfs_fs.h | 6 +++--- fs/ntfs3/super.c | 31 +++++++++++++++++-------------- 7 files changed, 42 insertions(+), 38 deletions(-) diff --git a/fs/ntfs3/dir.c b/fs/ntfs3/dir.c index 1b5c865a0339..b98e95d6b4d9 100644 --- a/fs/ntfs3/dir.c +++ b/fs/ntfs3/dir.c @@ -332,8 +332,7 @@ static inline bool ntfs_dir_emit(struct ntfs_sb_info *sbi, * It does additional locks/reads just to get the type of name. * Should we use additional mount option to enable branch below? */ - if (fname->dup.extend_data && - ino != ni->mi.rno) { + if (fname->dup.extend_data && ino != ni->mi.rno) { struct inode *inode = ntfs_iget5(sbi->sb, &e->ref, NULL); if (!IS_ERR_OR_NULL(inode)) { dt_type = fs_umode_to_dtype(inode->i_mode); diff --git a/fs/ntfs3/file.c b/fs/ntfs3/file.c index 4c90ec2fa2ea..a9ba37758944 100644 --- a/fs/ntfs3/file.c +++ b/fs/ntfs3/file.c @@ -503,8 +503,6 @@ static int ntfs_truncate(struct inode *inode, loff_t new_size) if (dirty) mark_inode_dirty(inode); - /*ntfs_flush_inodes(inode->i_sb, inode, NULL);*/ - return 0; } @@ -1114,8 +1112,8 @@ static ssize_t ntfs_compress_write(struct kiocb *iocb, struct iov_iter *from) size_t cp, tail = PAGE_SIZE - off; folio = page_folio(pages[ip]); - cp = copy_folio_from_iter_atomic(folio, off, - min(tail, bytes), from); + cp = copy_folio_from_iter_atomic( + folio, off, min(tail, bytes), from); flush_dcache_folio(folio); copied += cp; @@ -1312,7 +1310,7 @@ static int ntfs_file_release(struct inode *inode, struct file *file) if (sbi->options->prealloc && ((file->f_mode & FMODE_WRITE) && atomic_read(&inode->i_writecount) == 1) - /* + /* * The only file when inode->i_fop = &ntfs_file_operations and * init_rwsem(&ni->file.run_lock) is not called explicitly is MFT. * diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index 8f9fe1d7a690..c1c2ddaeb1e7 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -3026,8 +3026,8 @@ int ni_rename(struct ntfs_inode *dir_ni, struct ntfs_inode *new_dir_ni, err = ni_add_name(new_dir_ni, ni, new_de); if (!err) { err = ni_remove_name(dir_ni, ni, de, &de2, &undo); - WARN_ON(err && ni_remove_name(new_dir_ni, ni, new_de, &de2, - &undo)); + WARN_ON(err && + ni_remove_name(new_dir_ni, ni, new_de, &de2, &undo)); } /* @@ -3127,7 +3127,8 @@ static bool ni_update_parent(struct ntfs_inode *ni, struct NTFS_DUP_INFO *dup, if (attr) { const struct REPARSE_POINT *rp; - rp = resident_data_ex(attr, sizeof(struct REPARSE_POINT)); + rp = resident_data_ex(attr, + sizeof(struct REPARSE_POINT)); /* If ATTR_REPARSE exists 'rp' can't be NULL. */ if (rp) dup->extend_data = rp->ReparseTag; diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 3959f23c487a..b741a697e572 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -975,9 +975,9 @@ int ntfs_write_begin(const struct kiocb *iocb, struct address_space *mapping, /* * ntfs_write_end - Address_space_operations::write_end. */ -int ntfs_write_end(const struct kiocb *iocb, - struct address_space *mapping, loff_t pos, - u32 len, u32 copied, struct folio *folio, void *fsdata) +int ntfs_write_end(const struct kiocb *iocb, struct address_space *mapping, + loff_t pos, u32 len, u32 copied, struct folio *folio, + void *fsdata) { struct inode *inode = mapping->host; struct ntfs_inode *ni = ntfs_i(inode); @@ -1099,7 +1099,7 @@ ntfs_create_reparse_buffer(struct ntfs_sb_info *sbi, const char *symname, typeof(rp->SymbolicLinkReparseBuffer) *rs; bool is_absolute; - is_absolute = (strlen(symname) > 1 && symname[1] == ':'); + is_absolute = symname[0] && symname[1] == ':'; rp = kzalloc(ntfs_reparse_bytes(2 * size + 2, is_absolute), GFP_NOFS); if (!rp) @@ -1136,17 +1136,19 @@ ntfs_create_reparse_buffer(struct ntfs_sb_info *sbi, const char *symname, /* PrintName + SubstituteName. */ rs->SubstituteNameOffset = cpu_to_le16(sizeof(short) * err); - rs->SubstituteNameLength = cpu_to_le16(sizeof(short) * err + (is_absolute ? 8 : 0)); + rs->SubstituteNameLength = + cpu_to_le16(sizeof(short) * err + (is_absolute ? 8 : 0)); rs->PrintNameLength = rs->SubstituteNameOffset; /* * TODO: Use relative path if possible to allow Windows to * parse this path. - * 0-absolute path 1- relative path (SYMLINK_FLAG_RELATIVE). + * 0-absolute path, 1- relative path (SYMLINK_FLAG_RELATIVE). */ rs->Flags = cpu_to_le32(is_absolute ? 0 : SYMLINK_FLAG_RELATIVE); - memmove(rp_name + err + (is_absolute ? 4 : 0), rp_name, sizeof(short) * err); + memmove(rp_name + err + (is_absolute ? 4 : 0), rp_name, + sizeof(short) * err); if (is_absolute) { /* Decorate SubstituteName. */ @@ -1635,7 +1637,8 @@ int ntfs_create_inode(struct mnt_idmap *idmap, struct inode *dir, * Use ni_find_attr cause layout of MFT record may be changed * in ntfs_init_acl and ntfs_save_wsl_perm. */ - attr = ni_find_attr(ni, NULL, NULL, ATTR_NAME, NULL, 0, NULL, NULL); + attr = ni_find_attr(ni, NULL, NULL, ATTR_NAME, NULL, 0, NULL, + NULL); if (attr) { struct ATTR_FILE_NAME *fn; diff --git a/fs/ntfs3/namei.c b/fs/ntfs3/namei.c index 82c8ae56beee..3b24ca02de61 100644 --- a/fs/ntfs3/namei.c +++ b/fs/ntfs3/namei.c @@ -207,13 +207,13 @@ static int ntfs_symlink(struct mnt_idmap *idmap, struct inode *dir, } /* - * ntfs_mkdir- inode_operations::mkdir + * ntfs_mkdir - inode_operations::mkdir */ static struct dentry *ntfs_mkdir(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode) { - return ERR_PTR(ntfs_create_inode(idmap, dir, dentry, NULL, S_IFDIR | mode, 0, - NULL, 0, NULL)); + return ERR_PTR(ntfs_create_inode(idmap, dir, dentry, NULL, + S_IFDIR | mode, 0, NULL, 0, NULL)); } /* diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h index 2649fbe16669..6a7594d3f3eb 100644 --- a/fs/ntfs3/ntfs_fs.h +++ b/fs/ntfs3/ntfs_fs.h @@ -584,7 +584,8 @@ int ni_add_name(struct ntfs_inode *dir_ni, struct ntfs_inode *ni, struct NTFS_DE *de); int ni_rename(struct ntfs_inode *dir_ni, struct ntfs_inode *new_dir_ni, - struct ntfs_inode *ni, struct NTFS_DE *de, struct NTFS_DE *new_de); + struct ntfs_inode *ni, struct NTFS_DE *de, + struct NTFS_DE *new_de); bool ni_is_dirty(struct inode *inode); @@ -709,8 +710,7 @@ int ntfs_set_size(struct inode *inode, u64 new_size); int ntfs_get_block(struct inode *inode, sector_t vbn, struct buffer_head *bh_result, int create); int ntfs_write_begin(const struct kiocb *iocb, struct address_space *mapping, - loff_t pos, u32 len, struct folio **foliop, - void **fsdata); + loff_t pos, u32 len, struct folio **foliop, void **fsdata); int ntfs_write_end(const struct kiocb *iocb, struct address_space *mapping, loff_t pos, u32 len, u32 copied, struct folio *folio, void *fsdata); diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index ddff94c091b8..9f69316d77b6 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -16,6 +16,13 @@ * mi - MFT inode - One MFT record(usually 1024 bytes or 4K), consists of attributes. * ni - NTFS inode - Extends linux inode. consists of one or more mft inodes. * index - unit inside directory - 2K, 4K, <=page size, does not depend on cluster size. + * resident attribute - Attribute with content stored directly in the MFT record + * non-resident attribute - Attribute with content stored in clusters + * data_size - Size of attribute content in bytes. Equal to inode->i_size + * valid_size - Number of bytes written to the non-resident attribute + * allocated_size - Total size of clusters allocated for non-resident content + * total_size - Actual size of allocated clusters for sparse or compressed attributes + * - Constraint: valid_size <= data_size <= allocated_size * * WSL - Windows Subsystem for Linux * https://docs.microsoft.com/en-us/windows/wsl/file-permissions @@ -288,10 +295,8 @@ static const struct fs_parameter_spec ntfs_fs_parameters[] = { /* * Load nls table or if @nls is utf8 then return NULL. * - * It is good idea to use here "const char *nls". - * But load_nls accepts "char*". */ -static struct nls_table *ntfs_load_nls(char *nls) +static struct nls_table *ntfs_load_nls(const char *nls) { struct nls_table *ret; @@ -566,10 +571,8 @@ static void ntfs_create_procdir(struct super_block *sb) if (e) { struct ntfs_sb_info *sbi = sb->s_fs_info; - proc_create_data("volinfo", 0444, e, - &ntfs3_volinfo_fops, sb); - proc_create_data("label", 0644, e, - &ntfs3_label_fops, sb); + proc_create_data("volinfo", 0444, e, &ntfs3_volinfo_fops, sb); + proc_create_data("label", 0644, e, &ntfs3_label_fops, sb); sbi->procdir = e; } } @@ -600,10 +603,12 @@ static void ntfs_remove_proc_root(void) } } #else -static void ntfs_create_procdir(struct super_block *sb) {} -static void ntfs_remove_procdir(struct super_block *sb) {} -static void ntfs_create_proc_root(void) {} -static void ntfs_remove_proc_root(void) {} +// clang-format off +static void ntfs_create_procdir(struct super_block *sb){} +static void ntfs_remove_procdir(struct super_block *sb){} +static void ntfs_create_proc_root(void){} +static void ntfs_remove_proc_root(void){} +// clang-format on #endif static struct kmem_cache *ntfs_inode_cachep; @@ -1223,8 +1228,7 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) sb->s_export_op = &ntfs_export_ops; sb->s_time_gran = NTFS_TIME_GRAN; // 100 nsec sb->s_xattr = ntfs_xattr_handlers; - if (options->nocase) - set_default_d_op(sb, &ntfs_dentry_ops); + set_default_d_op(sb, options->nocase ? &ntfs_dentry_ops : NULL); options->nls = ntfs_load_nls(options->nls_name); if (IS_ERR(options->nls)) { @@ -1643,7 +1647,6 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) out: ntfs3_put_sbi(sbi); kfree(boot2); - ntfs3_put_sbi(sbi); return err; } From 801f614ba263cb37624982b27b4c82f3c3c597a9 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 18 Sep 2025 13:35:24 +0300 Subject: [PATCH 03/26] fs/ntfs3: fix mount failure for sparse runs in run_unpack() Some NTFS volumes failed to mount because sparse data runs were not handled correctly during runlist unpacking. The code performed arithmetic on the special SPARSE_LCN64 marker, leading to invalid LCN values and mount errors. Add an explicit check for the case described above, marking the run as sparse without applying arithmetic. Fixes: 736fc7bf5f68 ("fs: ntfs3: Fix integer overflow in run_unpack()") Cc: stable@vger.kernel.org Signed-off-by: Konstantin Komarov --- fs/ntfs3/run.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fs/ntfs3/run.c b/fs/ntfs3/run.c index 88550085f745..5df55e4adbb1 100644 --- a/fs/ntfs3/run.c +++ b/fs/ntfs3/run.c @@ -984,8 +984,12 @@ int run_unpack(struct runs_tree *run, struct ntfs_sb_info *sbi, CLST ino, if (!dlcn) return -EINVAL; - if (check_add_overflow(prev_lcn, dlcn, &lcn)) + /* Check special combination: 0 + SPARSE_LCN64. */ + if (!prev_lcn && dlcn == SPARSE_LCN64) { + lcn = SPARSE_LCN64; + } else if (check_add_overflow(prev_lcn, dlcn, &lcn)) { return -EINVAL; + } prev_lcn = lcn; } else { /* The size of 'dlcn' can't be > 8. */ From 14656154d26cf244d063477a03606775eec19780 Mon Sep 17 00:00:00 2001 From: "Matthew Wilcox (Oracle)" Date: Fri, 18 Jul 2025 20:53:56 +0100 Subject: [PATCH 04/26] ntfs: Do not kmap pages used for reading from disk These pages are accessed through DMA and vmap; they are not accessed by calling page_address(), so they do not need to be kmapped. Signed-off-by: Matthew Wilcox (Oracle) Signed-off-by: Konstantin Komarov --- fs/ntfs3/frecord.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index c1c2ddaeb1e7..784291ae04e6 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -2590,7 +2590,6 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, } pages_disk[i] = pg; lock_page(pg); - kmap(pg); } /* Read 'ondisk_size' bytes from disk. */ @@ -2640,7 +2639,6 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, for (i = 0; i < npages_disk; i++) { pg = pages_disk[i]; if (pg) { - kunmap(pg); unlock_page(pg); put_page(pg); } @@ -2735,7 +2733,6 @@ int ni_write_frame(struct ntfs_inode *ni, struct page **pages, } pages_disk[i] = pg; lock_page(pg); - kmap(pg); } /* To simplify compress algorithm do vmap for source and target pages. */ @@ -2823,7 +2820,6 @@ int ni_write_frame(struct ntfs_inode *ni, struct page **pages, for (i = 0; i < pages_per_frame; i++) { pg = pages_disk[i]; if (pg) { - kunmap(pg); unlock_page(pg); put_page(pg); } From 953b79a7a124c3dae86544cea581a5bc7655aa13 Mon Sep 17 00:00:00 2001 From: "Matthew Wilcox (Oracle)" Date: Fri, 18 Jul 2025 20:53:57 +0100 Subject: [PATCH 05/26] ntfs: Do not kmap page cache pages for compression These pages are accessed through vmap; they are not accessed by calling page_address(), so they do not need to be kmapped. Signed-off-by: Matthew Wilcox (Oracle) Signed-off-by: Konstantin Komarov --- fs/ntfs3/frecord.c | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index 784291ae04e6..e1832b66718f 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -2407,9 +2407,6 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, * To simplify decompress algorithm do vmap for source * and target pages. */ - for (i = 0; i < pages_per_frame; i++) - kmap(pages[i]); - frame_size = pages_per_frame << PAGE_SHIFT; frame_mem = vmap(pages, pages_per_frame, VM_MAP, PAGE_KERNEL); if (!frame_mem) { @@ -2655,7 +2652,6 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, out: for (i = 0; i < pages_per_frame; i++) { pg = pages[i]; - kunmap(pg); SetPageUptodate(pg); } @@ -2742,9 +2738,6 @@ int ni_write_frame(struct ntfs_inode *ni, struct page **pages, goto out1; } - for (i = 0; i < pages_per_frame; i++) - kmap(pages[i]); - /* Map in-memory frame for read-only. */ frame_mem = vmap(pages, pages_per_frame, VM_MAP, PAGE_KERNEL_RO); if (!frame_mem) { @@ -2810,11 +2803,7 @@ int ni_write_frame(struct ntfs_inode *ni, struct page **pages, out3: vunmap(frame_mem); - out2: - for (i = 0; i < pages_per_frame; i++) - kunmap(pages[i]); - vunmap(frame_ondisk); out1: for (i = 0; i < pages_per_frame; i++) { From 68f6bd128e75a032432eda9d16676ed2969a1096 Mon Sep 17 00:00:00 2001 From: "Matthew Wilcox (Oracle)" Date: Fri, 18 Jul 2025 20:53:58 +0100 Subject: [PATCH 06/26] ntfs: Do not overwrite uptodate pages When reading a compressed file, we may read several pages in addition to the one requested. The current code will overwrite pages in the page cache with the data from disc which can definitely result in changes that have been made being lost. For example if we have four consecutie pages ABCD in the file compressed into a single extent, on first access, we'll bring in ABCD. Then we write to page B. Memory pressure results in the eviction of ACD. When we attempt to write to page C, we will overwrite the data in page B with the data currently on disk. I haven't investigated the decompression code to check whether it's OK to overwrite a clean page or whether it might be possible to see corrupt data. Out of an abundance of caution, decline to overwrite uptodate pages, not just dirty pages. Fixes: 4342306f0f0d (fs/ntfs3: Add file operations and implementation) Signed-off-by: Matthew Wilcox (Oracle) Cc: stable@vger.kernel.org Signed-off-by: Konstantin Komarov --- fs/ntfs3/frecord.c | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index e1832b66718f..e44181185526 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -2020,6 +2020,29 @@ int ni_fiemap(struct ntfs_inode *ni, struct fiemap_extent_info *fieinfo, return err; } +static struct page *ntfs_lock_new_page(struct address_space *mapping, + pgoff_t index, gfp_t gfp) +{ + struct folio *folio = __filemap_get_folio(mapping, index, + FGP_LOCK | FGP_ACCESSED | FGP_CREAT, gfp); + struct page *page; + + if (IS_ERR(folio)) + return ERR_CAST(folio); + + if (!folio_test_uptodate(folio)) + return folio_file_page(folio, index); + + /* Use a temporary page to avoid data corruption */ + folio_unlock(folio); + folio_put(folio); + page = alloc_page(gfp); + if (!page) + return ERR_PTR(-ENOMEM); + __SetPageLocked(page); + return page; +} + /* * ni_readpage_cmpr * @@ -2074,9 +2097,9 @@ int ni_readpage_cmpr(struct ntfs_inode *ni, struct folio *folio) if (i == idx) continue; - pg = find_or_create_page(mapping, index, gfp_mask); - if (!pg) { - err = -ENOMEM; + pg = ntfs_lock_new_page(mapping, index, gfp_mask); + if (IS_ERR(pg)) { + err = PTR_ERR(pg); goto out1; } pages[i] = pg; @@ -2175,13 +2198,13 @@ int ni_decompress_file(struct ntfs_inode *ni) for (i = 0; i < pages_per_frame; i++, index++) { struct page *pg; - pg = find_or_create_page(mapping, index, gfp_mask); - if (!pg) { + pg = ntfs_lock_new_page(mapping, index, gfp_mask); + if (IS_ERR(pg)) { while (i--) { unlock_page(pages[i]); put_page(pages[i]); } - err = -ENOMEM; + err = PTR_ERR(pg); goto out; } pages[i] = pg; From 02f312754c873efe076888a2fdca982e56617929 Mon Sep 17 00:00:00 2001 From: YangWen Date: Wed, 10 Sep 2025 23:17:08 +0800 Subject: [PATCH 07/26] ntfs3: fix use-after-free of sbi->options in cmp_fnames The root cause is that sbi->options points directly to fc->fs_private. If fc->fs_private is freed while sbi still exists, sbi->options becomes a dangling pointer. This patch ensures that sbi->options is a separate copy of fc->fs_private and duplicates nls_name if present. On superblock release or error, sbi->options->nls_name and sbi->options are freed and sbi->options is set to NULL to avoid any dangling pointer. Reported-by: syzbot+d77c546c60db651a389c@syzkaller.appspotmail.com Signed-off-by: YangWen [almaz.alexandrovich@paragon-software.com: remove syzbot logs from description] Signed-off-by: Konstantin Komarov --- fs/ntfs3/super.c | 47 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index 9f69316d77b6..aae1f32f4dab 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -702,6 +702,14 @@ static void ntfs_put_super(struct super_block *sb) /* Mark rw ntfs as clear, if possible. */ ntfs_set_state(sbi, NTFS_DIRTY_CLEAR); + + if (sbi->options) { + unload_nls(sbi->options->nls); + kfree(sbi->options->nls); + kfree(sbi->options); + sbi->options = NULL; + } + ntfs3_put_sbi(sbi); } @@ -1203,7 +1211,8 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) int err; struct ntfs_sb_info *sbi = sb->s_fs_info; struct block_device *bdev = sb->s_bdev; - struct ntfs_mount_options *options; + struct ntfs_mount_options *fc_opts; + struct ntfs_mount_options *options = NULL; struct inode *inode; struct ntfs_inode *ni; size_t i, tt, bad_len, bad_frags; @@ -1220,20 +1229,35 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) ref.high = 0; sbi->sb = sb; - sbi->options = options = fc->fs_private; - fc->fs_private = NULL; + fc_opts = fc->fs_private; + if (!fc_opts) { + errorf(fc, "missing mount options"); + return -EINVAL; + } + options = kmemdup(fc_opts, sizeof(*fc_opts), GFP_KERNEL); + if (!options) + return -ENOMEM; + + if (fc_opts->nls_name) { + options->nls_name = kstrdup(fc_opts->nls_name, GFP_KERNEL); + if (!options->nls_name) { + kfree(options); + return -ENOMEM; + } + } + sbi->options = options; sb->s_flags |= SB_NODIRATIME; sb->s_magic = 0x7366746e; // "ntfs" sb->s_op = &ntfs_sops; sb->s_export_op = &ntfs_export_ops; sb->s_time_gran = NTFS_TIME_GRAN; // 100 nsec sb->s_xattr = ntfs_xattr_handlers; - set_default_d_op(sb, options->nocase ? &ntfs_dentry_ops : NULL); + set_default_d_op(sb, sbi->options->nocase ? &ntfs_dentry_ops : NULL); - options->nls = ntfs_load_nls(options->nls_name); - if (IS_ERR(options->nls)) { - options->nls = NULL; - errorf(fc, "Cannot load nls %s", options->nls_name); + sbi->options->nls = ntfs_load_nls(sbi->options->nls_name); + if (IS_ERR(sbi->options->nls)) { + sbi->options->nls = NULL; + errorf(fc, "Cannot load nls %s", fc_opts->nls_name); err = -EINVAL; goto out; } @@ -1645,6 +1669,13 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) put_inode_out: iput(inode); out: + if (sbi && sbi->options) { + unload_nls(sbi->options->nls); + kfree(sbi->options->nls); + kfree(sbi->options); + sbi->options = NULL; + } + ntfs3_put_sbi(sbi); kfree(boot2); return err; From 73e6b9dacf72a1e7a4265eacca46f8f33e0997d6 Mon Sep 17 00:00:00 2001 From: Raphael Pinsonneault-Thibeault Date: Sun, 12 Oct 2025 16:16:34 -0400 Subject: [PATCH 08/26] ntfs3: fix uninit memory after failed mi_read in mi_format_new MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix a KMSAN un-init bug found by syzkaller. ntfs_get_bh() expects a buffer from sb_getblk(), that buffer may not be uptodate. We do not bring the buffer uptodate before setting it as uptodate. If the buffer were to not be uptodate, it could mean adding a buffer with un-init data to the mi record. Attempting to load that record will trigger KMSAN. Avoid this by setting the buffer as uptodate, if it’s not already, by overwriting it. Reported-by: syzbot+7a2ba6b7b66340cff225@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=7a2ba6b7b66340cff225 Tested-by: syzbot+7a2ba6b7b66340cff225@syzkaller.appspotmail.com Fixes: 4342306f0f0d5 ("fs/ntfs3: Add file operations and implementation") Signed-off-by: Raphael Pinsonneault-Thibeault Signed-off-by: Konstantin Komarov --- fs/ntfs3/fsntfs.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fs/ntfs3/fsntfs.c b/fs/ntfs3/fsntfs.c index c7a2f191254d..5ae910e9ecbd 100644 --- a/fs/ntfs3/fsntfs.c +++ b/fs/ntfs3/fsntfs.c @@ -1349,7 +1349,14 @@ int ntfs_get_bh(struct ntfs_sb_info *sbi, const struct runs_tree *run, u64 vbo, } if (buffer_locked(bh)) __wait_on_buffer(bh); - set_buffer_uptodate(bh); + + lock_buffer(bh); + if (!buffer_uptodate(bh)) + { + memset(bh->b_data, 0, blocksize); + set_buffer_uptodate(bh); + } + unlock_buffer(bh); } else { bh = ntfs_bread(sb, block); if (!bh) { From 9948dcb2f7b5a1bf8e8710eafaf6016e00be3ad6 Mon Sep 17 00:00:00 2001 From: Sidharth Seela Date: Tue, 23 Sep 2025 12:10:16 +0530 Subject: [PATCH 09/26] ntfs3: Fix uninit buffer allocated by __getname() Fix uninit errors caused after buffer allocation given to 'de'; by initializing the buffer with zeroes. The fix was found by using KMSAN. Reported-by: syzbot+332bd4e9d148f11a87dc@syzkaller.appspotmail.com Fixes: 78ab59fee07f2 ("fs/ntfs3: Rework file operations") Signed-off-by: Sidharth Seela Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index b741a697e572..439078106cc6 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -1725,6 +1725,7 @@ int ntfs_link_inode(struct inode *inode, struct dentry *dentry) de = __getname(); if (!de) return -ENOMEM; + memset(de, 0, PATH_MAX); /* Mark rw ntfs as dirty. It will be cleared at umount. */ ntfs_set_state(sbi, NTFS_DIRTY_DIRTY); From 1ff28f36eb2f3b22b07c4f233f01b3a166108d1c Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Tue, 14 Oct 2025 20:17:25 +0300 Subject: [PATCH 10/26] fs/ntfs3: disable readahead for compressed files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading large compressed files is extremely slow when readahead is enabled. For example, reading a 4 GB XPRESS-4K compressed file (compression ratio ≈ 4:1) takes about 230 minutes with readahead enabled, but only around 3 minutes when readahead is disabled. The issue was first observed in January 2025 and is reproducible with large compressed NTFS files. Disabling readahead for compressed files avoids this performance regression, although this may not be the ideal long-term fix. Signed-off-by: Konstantin Komarov --- fs/ntfs3/file.c | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/fs/ntfs3/file.c b/fs/ntfs3/file.c index a9ba37758944..7471a4bbb438 100644 --- a/fs/ntfs3/file.c +++ b/fs/ntfs3/file.c @@ -325,9 +325,14 @@ static int ntfs_file_mmap_prepare(struct vm_area_desc *desc) return -EOPNOTSUPP; } - if (is_compressed(ni) && rw) { - ntfs_inode_warn(inode, "mmap(write) compressed not supported"); - return -EOPNOTSUPP; + if (is_compressed(ni)) { + if (rw) { + ntfs_inode_warn(inode, + "mmap(write) compressed not supported"); + return -EOPNOTSUPP; + } + /* Turn off readahead for compressed files. */ + file->f_ra.ra_pages = 0; } if (rw) { @@ -884,9 +889,14 @@ static ssize_t ntfs_file_read_iter(struct kiocb *iocb, struct iov_iter *iter) if (err) return err; - if (is_compressed(ni) && (iocb->ki_flags & IOCB_DIRECT)) { - ntfs_inode_warn(inode, "direct i/o + compressed not supported"); - return -EOPNOTSUPP; + if (is_compressed(ni)) { + if (iocb->ki_flags & IOCB_DIRECT) { + ntfs_inode_warn( + inode, "direct i/o + compressed not supported"); + return -EOPNOTSUPP; + } + /* Turn off readahead for compressed files. */ + file->f_ra.ra_pages = 0; } return generic_file_read_iter(iocb, iter); @@ -906,6 +916,11 @@ static ssize_t ntfs_file_splice_read(struct file *in, loff_t *ppos, if (err) return err; + if (is_compressed(ntfs_i(inode))) { + /* Turn off readahead for compressed files. */ + in->f_ra.ra_pages = 0; + } + return filemap_splice_read(in, ppos, pipe, len, flags); } From d1693a7d5a38acf6424235a6070bcf5b186a360d Mon Sep 17 00:00:00 2001 From: Pedro Demarchi Gomes Date: Fri, 3 Oct 2025 12:38:50 -0300 Subject: [PATCH 11/26] ntfs: set dummy blocksize to read boot_block when mounting When mounting, sb->s_blocksize is used to read the boot_block without being defined or validated. Set a dummy blocksize before attempting to read the boot_block. The issue can be triggered with the following syz reproducer: mkdirat(0xffffffffffffff9c, &(0x7f0000000080)='./file1\x00', 0x0) r4 = openat$nullb(0xffffffffffffff9c, &(0x7f0000000040), 0x121403, 0x0) ioctl$FS_IOC_SETFLAGS(r4, 0x40081271, &(0x7f0000000980)=0x4000) mount(&(0x7f0000000140)=@nullb, &(0x7f0000000040)='./cgroup\x00', &(0x7f0000000000)='ntfs3\x00', 0x2208004, 0x0) syz_clone(0x88200200, 0x0, 0x0, 0x0, 0x0, 0x0) Here, the ioctl sets the bdev block size to 16384. During mount, get_tree_bdev_flags() calls sb_set_blocksize(sb, block_size(bdev)), but since block_size(bdev) > PAGE_SIZE, sb_set_blocksize() leaves sb->s_blocksize at zero. Later, ntfs_init_from_boot() attempts to read the boot_block while sb->s_blocksize is still zero, which triggers the bug. Reported-by: syzbot+f4f84b57a01d6b8364ad@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=f4f84b57a01d6b8364ad Signed-off-by: Pedro Demarchi Gomes [almaz.alexandrovich@paragon-software.com: changed comment style, added return value handling] Signed-off-by: Konstantin Komarov --- fs/ntfs3/super.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index aae1f32f4dab..a478ec510640 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -946,6 +946,11 @@ static int ntfs_init_from_boot(struct super_block *sb, u32 sector_size, sbi->volume.blocks = dev_size >> PAGE_SHIFT; + /* Set dummy blocksize to read boot_block. */ + if (!sb_min_blocksize(sb, PAGE_SIZE)) { + return -EINVAL; + } + read_boot: bh = ntfs_bread(sb, boot_block); if (!bh) From be99c62ac7e7af514e4b13f83c891a3cccefaa48 Mon Sep 17 00:00:00 2001 From: Edward Adam Davis Date: Tue, 16 Sep 2025 13:50:13 +0800 Subject: [PATCH 12/26] ntfs3: init run lock for extend inode After setting the inode mode of $Extend to a regular file, executing the truncate system call will enter the do_truncate() routine, causing the run_lock uninitialized error reported by syzbot. Prior to patch 4e8011ffec79, if the inode mode of $Extend was not set to a regular file, the do_truncate() routine would not be entered. Add the run_lock initialization when loading $Extend. syzbot reported: INFO: trying to register non-static key. Call Trace: dump_stack_lvl+0x189/0x250 lib/dump_stack.c:120 assign_lock_key+0x133/0x150 kernel/locking/lockdep.c:984 register_lock_class+0x105/0x320 kernel/locking/lockdep.c:1299 __lock_acquire+0x99/0xd20 kernel/locking/lockdep.c:5112 lock_acquire+0x120/0x360 kernel/locking/lockdep.c:5868 down_write+0x96/0x1f0 kernel/locking/rwsem.c:1590 ntfs_set_size+0x140/0x200 fs/ntfs3/inode.c:860 ntfs_extend+0x1d9/0x970 fs/ntfs3/file.c:387 ntfs_setattr+0x2e8/0xbe0 fs/ntfs3/file.c:808 Fixes: 4e8011ffec79 ("ntfs3: pretend $Extend records as regular files") Reported-by: syzbot+bdeb22a4b9a09ab9aa45@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=bdeb22a4b9a09ab9aa45 Tested-by: syzbot+bdeb22a4b9a09ab9aa45@syzkaller.appspotmail.com Signed-off-by: Edward Adam Davis Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 439078106cc6..b0c557a6c115 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -472,6 +472,7 @@ static struct inode *ntfs_read_mft(struct inode *inode, /* Records in $Extend are not a files or general directories. */ inode->i_op = &ntfs_file_inode_operations; mode = S_IFREG; + init_rwsem(&ni->file.run_lock); } else { err = -EINVAL; goto out; From 5f33da04e6ceee849e76e6592cc283c72fef7af9 Mon Sep 17 00:00:00 2001 From: Nirbhay Sharma Date: Tue, 7 Oct 2025 04:08:04 +0530 Subject: [PATCH 13/26] fs/ntfs3: fix KMSAN uninit-value in ni_create_attr_list The call to kmalloc() to allocate the attribute list buffer is given a size of al_aligned(rs). This size can be larger than the data subsequently copied into the buffer, leaving trailing bytes uninitialized. This can trigger a KMSAN "uninit-value" warning if that memory is later accessed. Fix this by using kzalloc() instead, which ensures the entire allocated buffer is zero-initialized, preventing the warning. Reported-by: syzbot+83c9dd5c0dcf6184fdbf@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=83c9dd5c0dcf6184fdbf Signed-off-by: Nirbhay Sharma Signed-off-by: Konstantin Komarov --- fs/ntfs3/frecord.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index e44181185526..c3638f482393 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -767,7 +767,7 @@ int ni_create_attr_list(struct ntfs_inode *ni) * Skip estimating exact memory requirement. * Looks like one record_size is always enough. */ - le = kmalloc(al_aligned(rs), GFP_NOFS); + le = kzalloc(al_aligned(rs), GFP_NOFS); if (!le) return -ENOMEM; From c3856bb499eae0cd5773b609ee9aa8a0e6e42b6c Mon Sep 17 00:00:00 2001 From: Lizhi Xu Date: Thu, 9 Oct 2025 10:37:33 +0800 Subject: [PATCH 14/26] ntfs3: avoid memcpy size warning There are more entries after the structure, use unsafe_memcpy() to avoid this warning. syzbot reported: memcpy: detected field-spanning write (size 3656) of single field "hdr1" at fs/ntfs3/index.c:1927 (size 16) Call Trace: indx_insert_entry+0x1a0/0x460 fs/ntfs3/index.c:1996 ni_add_name+0x4dd/0x820 fs/ntfs3/frecord.c:2995 ni_rename+0x98/0x170 fs/ntfs3/frecord.c:3026 ntfs_rename+0xab9/0xf00 fs/ntfs3/namei.c:332 Reported-by: syzbot+3a1878433bc1cb97b42a@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=3a1878433bc1cb97b42a Signed-off-by: Lizhi Xu Signed-off-by: Konstantin Komarov --- fs/ntfs3/index.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/ntfs3/index.c b/fs/ntfs3/index.c index 6d1bf890929d..7157cfd70fdc 100644 --- a/fs/ntfs3/index.c +++ b/fs/ntfs3/index.c @@ -1924,7 +1924,8 @@ indx_insert_into_buffer(struct ntfs_index *indx, struct ntfs_inode *ni, * Undo critical operations. */ indx_mark_free(indx, ni, new_vbn >> indx->idx2vbn_bits); - memcpy(hdr1, hdr1_saved, used1); + unsafe_memcpy(hdr1, hdr1_saved, used1, + "There are entries after the structure"); indx_write(indx, ni, n1, 0); } From f35590ee26f5722bfe12cdff14396c4c057a8f74 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Tue, 14 Oct 2025 20:36:17 +0300 Subject: [PATCH 15/26] fs/ntfs3: remove ntfs_bio_pages and use page cache for compressed I/O Replace the use of ntfs_bio_pages with the disk page cache for reading and writing compressed files. This slightly improves performance when reading compressed data and simplifies the I/O logic. When an XPRESS or LZX compressed file is opened for writing, it is now decompressed into a normal file before modification. A new argument (`int copy`) is added to ni_read_frame() to handle writing of decompressed and mapped data. Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 4 +- fs/ntfs3/file.c | 6 +- fs/ntfs3/frecord.c | 152 ++++++++++++--------------------------------- fs/ntfs3/fsntfs.c | 127 +++++++++++++++++-------------------- fs/ntfs3/inode.c | 1 - fs/ntfs3/ntfs_fs.h | 20 ++++-- 6 files changed, 116 insertions(+), 194 deletions(-) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index eced9013a881..d0373254f82a 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -1457,7 +1457,6 @@ int attr_wof_frame_info(struct ntfs_inode *ni, struct ATTRIB *attr, pgoff_t index = vbo[i] >> PAGE_SHIFT; if (index != folio->index) { - struct page *page = &folio->page; u64 from = vbo[i] & ~(u64)(PAGE_SIZE - 1); u64 to = min(from + PAGE_SIZE, wof_size); @@ -1467,8 +1466,7 @@ int attr_wof_frame_info(struct ntfs_inode *ni, struct ATTRIB *attr, if (err) goto out1; - err = ntfs_bio_pages(sbi, run, &page, 1, from, - to - from, REQ_OP_READ); + err = ntfs_read_run(sbi, run, addr, from, to - from); if (err) { folio->index = -1; goto out1; diff --git a/fs/ntfs3/file.c b/fs/ntfs3/file.c index 7471a4bbb438..60eb90bff955 100644 --- a/fs/ntfs3/file.c +++ b/fs/ntfs3/file.c @@ -59,7 +59,7 @@ static int ntfs_ioctl_get_volume_label(struct ntfs_sb_info *sbi, u8 __user *buf) static int ntfs_ioctl_set_volume_label(struct ntfs_sb_info *sbi, u8 __user *buf) { - u8 user[FSLABEL_MAX] = {0}; + u8 user[FSLABEL_MAX] = { 0 }; int len; if (!capable(CAP_SYS_ADMIN)) @@ -1039,7 +1039,7 @@ static ssize_t ntfs_compress_write(struct kiocb *iocb, struct iov_iter *from) if (!frame_uptodate && off) { err = ni_read_frame(ni, frame_vbo, pages, - pages_per_frame); + pages_per_frame, 0); if (err) { for (ip = 0; ip < pages_per_frame; ip++) { folio = page_folio(pages[ip]); @@ -1104,7 +1104,7 @@ static ssize_t ntfs_compress_write(struct kiocb *iocb, struct iov_iter *from) if (off || (to < i_size && (to & (frame_size - 1)))) { err = ni_read_frame(ni, frame_vbo, pages, - pages_per_frame); + pages_per_frame, 0); if (err) { for (ip = 0; ip < pages_per_frame; ip++) { diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index c3638f482393..87609a381ce5 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -2105,7 +2105,7 @@ int ni_readpage_cmpr(struct ntfs_inode *ni, struct folio *folio) pages[i] = pg; } - err = ni_read_frame(ni, frame_vbo, pages, pages_per_frame); + err = ni_read_frame(ni, frame_vbo, pages, pages_per_frame, 0); out1: for (i = 0; i < pages_per_frame; i++) { @@ -2175,17 +2175,9 @@ int ni_decompress_file(struct ntfs_inode *ni) */ index = 0; for (vbo = 0; vbo < i_size; vbo += bytes) { - u32 nr_pages; bool new; - if (vbo + frame_size > i_size) { - bytes = i_size - vbo; - nr_pages = (bytes + PAGE_SIZE - 1) >> PAGE_SHIFT; - } else { - nr_pages = pages_per_frame; - bytes = frame_size; - } - + bytes = vbo + frame_size > i_size ? (i_size - vbo) : frame_size; end = bytes_to_cluster(sbi, vbo + bytes); for (vcn = vbo >> sbi->cluster_bits; vcn < end; vcn += clen) { @@ -2210,15 +2202,7 @@ int ni_decompress_file(struct ntfs_inode *ni) pages[i] = pg; } - err = ni_read_frame(ni, vbo, pages, pages_per_frame); - - if (!err) { - down_read(&ni->file.run_lock); - err = ntfs_bio_pages(sbi, &ni->file.run, pages, - nr_pages, vbo, bytes, - REQ_OP_WRITE); - up_read(&ni->file.run_lock); - } + err = ni_read_frame(ni, vbo, pages, pages_per_frame, 1); for (i = 0; i < pages_per_frame; i++) { unlock_page(pages[i]); @@ -2408,20 +2392,19 @@ static int decompress_lzx_xpress(struct ntfs_sb_info *sbi, const char *cmpr, * Pages - Array of locked pages. */ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, - u32 pages_per_frame) + u32 pages_per_frame, int copy) { int err; struct ntfs_sb_info *sbi = ni->mi.sbi; u8 cluster_bits = sbi->cluster_bits; char *frame_ondisk = NULL; char *frame_mem = NULL; - struct page **pages_disk = NULL; struct ATTR_LIST_ENTRY *le = NULL; struct runs_tree *run = &ni->file.run; u64 valid_size = ni->i_valid; u64 vbo_disk; size_t unc_size; - u32 frame_size, i, npages_disk, ondisk_size; + u32 frame_size, i, ondisk_size; struct page *pg; struct ATTRIB *attr; CLST frame, clst_data; @@ -2513,7 +2496,7 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, err = attr_wof_frame_info(ni, attr, run, frame64, frames, frame_bits, &ondisk_size, &vbo_data); if (err) - goto out2; + goto out1; if (frame64 == frames) { unc_size = 1 + ((i_size - 1) & (frame_size - 1)); @@ -2524,7 +2507,7 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, if (ondisk_size > frame_size) { err = -EINVAL; - goto out2; + goto out1; } if (!attr->non_res) { @@ -2545,10 +2528,7 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, ARRAY_SIZE(WOF_NAME), run, vbo_disk, vbo_data + ondisk_size); if (err) - goto out2; - npages_disk = (ondisk_size + (vbo_disk & (PAGE_SIZE - 1)) + - PAGE_SIZE - 1) >> - PAGE_SHIFT; + goto out1; #endif } else if (is_attr_compressed(attr)) { /* LZNT compression. */ @@ -2582,60 +2562,37 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, if (clst_data >= NTFS_LZNT_CLUSTERS) { /* Frame is not compressed. */ down_read(&ni->file.run_lock); - err = ntfs_bio_pages(sbi, run, pages, pages_per_frame, - frame_vbo, ondisk_size, - REQ_OP_READ); + err = ntfs_read_run(sbi, run, frame_mem, frame_vbo, + ondisk_size); up_read(&ni->file.run_lock); goto out1; } vbo_disk = frame_vbo; - npages_disk = (ondisk_size + PAGE_SIZE - 1) >> PAGE_SHIFT; } else { __builtin_unreachable(); err = -EINVAL; goto out1; } - pages_disk = kcalloc(npages_disk, sizeof(*pages_disk), GFP_NOFS); - if (!pages_disk) { + /* Allocate memory to read compressed data to. */ + frame_ondisk = kvmalloc(ondisk_size, GFP_KERNEL); + if (!frame_ondisk) { err = -ENOMEM; - goto out2; - } - - for (i = 0; i < npages_disk; i++) { - pg = alloc_page(GFP_KERNEL); - if (!pg) { - err = -ENOMEM; - goto out3; - } - pages_disk[i] = pg; - lock_page(pg); + goto out1; } /* Read 'ondisk_size' bytes from disk. */ down_read(&ni->file.run_lock); - err = ntfs_bio_pages(sbi, run, pages_disk, npages_disk, vbo_disk, - ondisk_size, REQ_OP_READ); + err = ntfs_read_run(sbi, run, frame_ondisk, vbo_disk, ondisk_size); up_read(&ni->file.run_lock); if (err) - goto out3; + goto out2; - /* - * To simplify decompress algorithm do vmap for source and target pages. - */ - frame_ondisk = vmap(pages_disk, npages_disk, VM_MAP, PAGE_KERNEL_RO); - if (!frame_ondisk) { - err = -ENOMEM; - goto out3; - } - - /* Decompress: Frame_ondisk -> frame_mem. */ #ifdef CONFIG_NTFS3_LZX_XPRESS if (run != &ni->file.run) { /* LZX or XPRESS */ - err = decompress_lzx_xpress( - sbi, frame_ondisk + (vbo_disk & (PAGE_SIZE - 1)), - ondisk_size, frame_mem, unc_size, frame_size); + err = decompress_lzx_xpress(sbi, frame_ondisk, ondisk_size, + frame_mem, unc_size, frame_size); } else #endif { @@ -2653,24 +2610,21 @@ int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, memset(frame_mem + ok, 0, frame_size - ok); } - vunmap(frame_ondisk); - -out3: - for (i = 0; i < npages_disk; i++) { - pg = pages_disk[i]; - if (pg) { - unlock_page(pg); - put_page(pg); - } - } - kfree(pages_disk); - out2: + kvfree(frame_ondisk); +out1: #ifdef CONFIG_NTFS3_LZX_XPRESS if (run != &ni->file.run) run_free(run); + if (!err && copy) { + /* We are called from 'ni_decompress_file' */ + /* Copy decompressed LZX or XPRESS data into new place. */ + down_read(&ni->file.run_lock); + err = ntfs_write_run(sbi, &ni->file.run, frame_mem, frame_vbo, + frame_size); + up_read(&ni->file.run_lock); + } #endif -out1: vunmap(frame_mem); out: for (i = 0; i < pages_per_frame; i++) { @@ -2697,13 +2651,10 @@ int ni_write_frame(struct ntfs_inode *ni, struct page **pages, u64 frame_vbo = folio_pos(folio); CLST frame = frame_vbo >> frame_bits; char *frame_ondisk = NULL; - struct page **pages_disk = NULL; struct ATTR_LIST_ENTRY *le = NULL; char *frame_mem; struct ATTRIB *attr; struct mft_inode *mi; - u32 i; - struct page *pg; size_t compr_size, ondisk_size; struct lznt *lznt; @@ -2738,34 +2689,18 @@ int ni_write_frame(struct ntfs_inode *ni, struct page **pages, goto out; } - pages_disk = kcalloc(pages_per_frame, sizeof(struct page *), GFP_NOFS); - if (!pages_disk) { - err = -ENOMEM; - goto out; - } - - for (i = 0; i < pages_per_frame; i++) { - pg = alloc_page(GFP_KERNEL); - if (!pg) { - err = -ENOMEM; - goto out1; - } - pages_disk[i] = pg; - lock_page(pg); - } - - /* To simplify compress algorithm do vmap for source and target pages. */ - frame_ondisk = vmap(pages_disk, pages_per_frame, VM_MAP, PAGE_KERNEL); + /* Allocate memory to write compressed data to. */ + frame_ondisk = kvmalloc(frame_size, GFP_KERNEL); if (!frame_ondisk) { err = -ENOMEM; - goto out1; + goto out; } /* Map in-memory frame for read-only. */ frame_mem = vmap(pages, pages_per_frame, VM_MAP, PAGE_KERNEL_RO); if (!frame_mem) { err = -ENOMEM; - goto out2; + goto out1; } mutex_lock(&sbi->compress.mtx_lznt); @@ -2781,7 +2716,7 @@ int ni_write_frame(struct ntfs_inode *ni, struct page **pages, if (!lznt) { mutex_unlock(&sbi->compress.mtx_lznt); err = -ENOMEM; - goto out3; + goto out2; } sbi->compress.lznt = lznt; @@ -2818,25 +2753,16 @@ int ni_write_frame(struct ntfs_inode *ni, struct page **pages, goto out2; down_read(&ni->file.run_lock); - err = ntfs_bio_pages(sbi, &ni->file.run, - ondisk_size < frame_size ? pages_disk : pages, - pages_per_frame, frame_vbo, ondisk_size, - REQ_OP_WRITE); + err = ntfs_write_run(sbi, &ni->file.run, + ondisk_size < frame_size ? frame_ondisk : + frame_mem, + frame_vbo, ondisk_size); up_read(&ni->file.run_lock); -out3: - vunmap(frame_mem); out2: - vunmap(frame_ondisk); + vunmap(frame_mem); out1: - for (i = 0; i < pages_per_frame; i++) { - pg = pages_disk[i]; - if (pg) { - unlock_page(pg); - put_page(pg); - } - } - kfree(pages_disk); + kvfree(frame_ondisk); out: return err; } diff --git a/fs/ntfs3/fsntfs.c b/fs/ntfs3/fsntfs.c index 5ae910e9ecbd..5f138f715835 100644 --- a/fs/ntfs3/fsntfs.c +++ b/fs/ntfs3/fsntfs.c @@ -1479,99 +1479,86 @@ int ntfs_write_bh(struct ntfs_sb_info *sbi, struct NTFS_RECORD_HEADER *rhdr, } /* - * ntfs_bio_pages - Read/write pages from/to disk. + * ntfs_read_write_run - Read/Write disk's page cache. */ -int ntfs_bio_pages(struct ntfs_sb_info *sbi, const struct runs_tree *run, - struct page **pages, u32 nr_pages, u64 vbo, u32 bytes, - enum req_op op) +int ntfs_read_write_run(struct ntfs_sb_info *sbi, const struct runs_tree *run, + void *buf, u64 vbo, size_t bytes, int wr) { - int err = 0; - struct bio *new, *bio = NULL; struct super_block *sb = sbi->sb; - struct block_device *bdev = sb->s_bdev; - struct page *page; + struct address_space *mapping = sb->s_bdev->bd_mapping; u8 cluster_bits = sbi->cluster_bits; - CLST lcn, clen, vcn, vcn_next; - u32 add, off, page_idx; + CLST vcn_next, vcn = vbo >> cluster_bits; + CLST lcn, clen; u64 lbo, len; - size_t run_idx; - struct blk_plug plug; + size_t idx; + u32 off, op; + struct folio *folio; + char *kaddr; if (!bytes) return 0; - blk_start_plug(&plug); + if (!run_lookup_entry(run, vcn, &lcn, &clen, &idx)) + return -ENOENT; - /* Align vbo and bytes to be 512 bytes aligned. */ - lbo = (vbo + bytes + 511) & ~511ull; - vbo = vbo & ~511ull; - bytes = lbo - vbo; + if (lcn == SPARSE_LCN) + return -EINVAL; - vcn = vbo >> cluster_bits; - if (!run_lookup_entry(run, vcn, &lcn, &clen, &run_idx)) { - err = -ENOENT; - goto out; - } off = vbo & sbi->cluster_mask; - page_idx = 0; - page = pages[0]; + lbo = ((u64)lcn << cluster_bits) + off; + len = ((u64)clen << cluster_bits) - off; for (;;) { - lbo = ((u64)lcn << cluster_bits) + off; - len = ((u64)clen << cluster_bits) - off; -new_bio: - new = bio_alloc(bdev, nr_pages - page_idx, op, GFP_NOFS); - if (bio) { - bio_chain(bio, new); - submit_bio(bio); + /* Read range [lbo, lbo+len). */ + folio = read_mapping_folio(mapping, lbo >> PAGE_SHIFT, NULL); + + if (IS_ERR(folio)) + return PTR_ERR(folio); + + off = offset_in_page(lbo); + op = PAGE_SIZE - off; + + if (op > len) + op = len; + if (op > bytes) + op = bytes; + + kaddr = kmap_local_folio(folio, 0); + if (wr) { + memcpy(kaddr + off, buf, op); + folio_mark_dirty(folio); + } else { + memcpy(buf, kaddr + off, op); + flush_dcache_folio(folio); } - bio = new; - bio->bi_iter.bi_sector = lbo >> 9; + kunmap_local(kaddr); + folio_put(folio); - while (len) { - off = vbo & (PAGE_SIZE - 1); - add = off + len > PAGE_SIZE ? (PAGE_SIZE - off) : len; + bytes -= op; + if (!bytes) + return 0; - if (bio_add_page(bio, page, add, off) < add) - goto new_bio; - - if (bytes <= add) - goto out; - bytes -= add; - vbo += add; - - if (add + off == PAGE_SIZE) { - page_idx += 1; - if (WARN_ON(page_idx >= nr_pages)) { - err = -EINVAL; - goto out; - } - page = pages[page_idx]; - } - - if (len <= add) - break; - len -= add; - lbo += add; + buf += op; + len -= op; + if (len) { + /* next volume's page. */ + lbo += op; + continue; } + /* get next range. */ vcn_next = vcn + clen; - if (!run_get_entry(run, ++run_idx, &vcn, &lcn, &clen) || + if (!run_get_entry(run, ++idx, &vcn, &lcn, &clen) || vcn != vcn_next) { - err = -ENOENT; - goto out; + return -ENOENT; } - off = 0; - } -out: - if (bio) { - if (!err) - err = submit_bio_wait(bio); - bio_put(bio); - } - blk_finish_plug(&plug); - return err; + if (lcn == SPARSE_LCN) + return -EINVAL; + + lbo = ((u64)lcn << cluster_bits); + len = ((u64)clen << cluster_bits); + } } /* diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index b0c557a6c115..74de82b8efe1 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -2107,7 +2107,6 @@ const struct address_space_operations ntfs_aops = { const struct address_space_operations ntfs_aops_cmpr = { .read_folio = ntfs_read_folio, - .readahead = ntfs_readahead, .dirty_folio = block_dirty_folio, .direct_IO = ntfs_direct_IO, }; diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h index 6a7594d3f3eb..86f825cf1c29 100644 --- a/fs/ntfs3/ntfs_fs.h +++ b/fs/ntfs3/ntfs_fs.h @@ -570,7 +570,7 @@ int ni_fiemap(struct ntfs_inode *ni, struct fiemap_extent_info *fieinfo, int ni_readpage_cmpr(struct ntfs_inode *ni, struct folio *folio); int ni_decompress_file(struct ntfs_inode *ni); int ni_read_frame(struct ntfs_inode *ni, u64 frame_vbo, struct page **pages, - u32 pages_per_frame); + u32 pages_per_frame, int copy); int ni_write_frame(struct ntfs_inode *ni, struct page **pages, u32 pages_per_frame); int ni_remove_name(struct ntfs_inode *dir_ni, struct ntfs_inode *ni, @@ -633,9 +633,21 @@ int ntfs_get_bh(struct ntfs_sb_info *sbi, const struct runs_tree *run, u64 vbo, u32 bytes, struct ntfs_buffers *nb); int ntfs_write_bh(struct ntfs_sb_info *sbi, struct NTFS_RECORD_HEADER *rhdr, struct ntfs_buffers *nb, int sync); -int ntfs_bio_pages(struct ntfs_sb_info *sbi, const struct runs_tree *run, - struct page **pages, u32 nr_pages, u64 vbo, u32 bytes, - enum req_op op); +int ntfs_read_write_run(struct ntfs_sb_info *sbi, const struct runs_tree *run, + void *buf, u64 vbo, size_t bytes, int wr); +static inline int ntfs_read_run(struct ntfs_sb_info *sbi, + const struct runs_tree *run, void *buf, u64 vbo, + size_t bytes) +{ + return ntfs_read_write_run(sbi, run, buf, vbo, bytes, 0); +} +static inline int ntfs_write_run(struct ntfs_sb_info *sbi, + const struct runs_tree *run, void *buf, + u64 vbo, size_t bytes) +{ + return ntfs_read_write_run(sbi, run, buf, vbo, bytes, 1); +} + int ntfs_bio_fill_1(struct ntfs_sb_info *sbi, const struct runs_tree *run); int ntfs_vbo_to_lbo(struct ntfs_sb_info *sbi, const struct runs_tree *run, u64 vbo, u64 *lbo, u64 *bytes); From a8a3ca23bbd9d849308a7921a049330dc6c91398 Mon Sep 17 00:00:00 2001 From: Bartlomiej Kubik Date: Wed, 5 Nov 2025 22:18:08 +0100 Subject: [PATCH 16/26] fs/ntfs3: Initialize allocated memory before use KMSAN reports: Multiple uninitialized values detected: - KMSAN: uninit-value in ntfs_read_hdr (3) - KMSAN: uninit-value in bcmp (3) Memory is allocated by __getname(), which is a wrapper for kmem_cache_alloc(). This memory is used before being properly cleared. Change kmem_cache_alloc() to kmem_cache_zalloc() to properly allocate and clear memory before use. Fixes: 82cae269cfa9 ("fs/ntfs3: Add initialization of super block") Fixes: 78ab59fee07f ("fs/ntfs3: Rework file operations") Tested-by: syzbot+332bd4e9d148f11a87dc@syzkaller.appspotmail.com Reported-by: syzbot+332bd4e9d148f11a87dc@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=332bd4e9d148f11a87dc Fixes: 82cae269cfa9 ("fs/ntfs3: Add initialization of super block") Fixes: 78ab59fee07f ("fs/ntfs3: Rework file operations") Tested-by: syzbot+0399100e525dd9696764@syzkaller.appspotmail.com Reported-by: syzbot+0399100e525dd9696764@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=0399100e525dd9696764 Reviewed-by: Khalid Aziz Signed-off-by: Bartlomiej Kubik Signed-off-by: Konstantin Komarov --- fs/ntfs3/inode.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/fs/ntfs3/inode.c b/fs/ntfs3/inode.c index 74de82b8efe1..9989c3592a04 100644 --- a/fs/ntfs3/inode.c +++ b/fs/ntfs3/inode.c @@ -1281,7 +1281,7 @@ int ntfs_create_inode(struct mnt_idmap *idmap, struct inode *dir, fa |= FILE_ATTRIBUTE_READONLY; /* Allocate PATH_MAX bytes. */ - new_de = __getname(); + new_de = kmem_cache_zalloc(names_cachep, GFP_KERNEL); if (!new_de) { err = -ENOMEM; goto out1; @@ -1723,10 +1723,9 @@ int ntfs_link_inode(struct inode *inode, struct dentry *dentry) struct NTFS_DE *de; /* Allocate PATH_MAX bytes. */ - de = __getname(); + de = kmem_cache_zalloc(names_cachep, GFP_KERNEL); if (!de) return -ENOMEM; - memset(de, 0, PATH_MAX); /* Mark rw ntfs as dirty. It will be cleared at umount. */ ntfs_set_state(sbi, NTFS_DIRTY_DIRTY); @@ -1762,7 +1761,7 @@ int ntfs_unlink_inode(struct inode *dir, const struct dentry *dentry) return -EINVAL; /* Allocate PATH_MAX bytes. */ - de = __getname(); + de = kmem_cache_zalloc(names_cachep, GFP_KERNEL); if (!de) return -ENOMEM; From aee4d5a521e94f658e46c904e08a473daa9c8fc0 Mon Sep 17 00:00:00 2001 From: YangWen Date: Fri, 31 Oct 2025 00:20:45 +0800 Subject: [PATCH 17/26] ntfs3: fix double free of sbi->options->nls and clarify ownership of fc->fs_private commit 02f312754c87 ("ntfs3: fix use-after-free of sbi->options in cmp_fnames") introduced a use-after-free bug due to improper handling of sbi->options in error paths. This resulted in crashes when superblock cleanup is performed in ntfs_put_super. This patch ensures that the options structure and its subfields are properly freed, preventing the memory corruption and use-after-free errors. Fixes: 02f312754c87 ("ntfs3: fix use-after-free of sbi->options in cmp_fnames") Reported-by: syzbot+cc433e4cd6d54736bf80@syzkaller.appspotmail.com Closes: https://syzkaller.appspot.com/bug?extid=cc433e4cd6d54736bf80 Signed-off-by: YangWen [almaz.alexandrovich@paragon-software.com: added fixes and closes tags] Signed-off-by: Konstantin Komarov --- fs/ntfs3/super.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index a478ec510640..96f56333cf99 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -705,7 +705,7 @@ static void ntfs_put_super(struct super_block *sb) if (sbi->options) { unload_nls(sbi->options->nls); - kfree(sbi->options->nls); + kfree(sbi->options->nls_name); kfree(sbi->options); sbi->options = NULL; } @@ -1251,6 +1251,7 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) } } sbi->options = options; + fc->fs_private = NULL; sb->s_flags |= SB_NODIRATIME; sb->s_magic = 0x7366746e; // "ntfs" sb->s_op = &ntfs_sops; @@ -1676,7 +1677,7 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) out: if (sbi && sbi->options) { unload_nls(sbi->options->nls); - kfree(sbi->options->nls); + kfree(sbi->options->nls_name); kfree(sbi->options); sbi->options = NULL; } From 2109b080240ca0e1a3ebe28cf7577ebb00a5d887 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 30 Oct 2025 23:35:24 +0300 Subject: [PATCH 18/26] fs/ntfs3: correct attr_collapse_range when file is too fragmented Fix incorrect VCN adjustments in attr_collapse_range() that caused filesystem errors or corruption on very fragmented NTFS files when performing collapse-range operations. Signed-off-by: Konstantin Komarov --- fs/ntfs3/attrib.c | 84 ++++++++++++++++++++++------------------------ fs/ntfs3/ntfs_fs.h | 4 +-- fs/ntfs3/record.c | 2 +- fs/ntfs3/run.c | 11 ++++-- 4 files changed, 53 insertions(+), 48 deletions(-) diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c index d0373254f82a..980ae9157248 100644 --- a/fs/ntfs3/attrib.c +++ b/fs/ntfs3/attrib.c @@ -1860,7 +1860,7 @@ int attr_collapse_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) struct ATTRIB *attr = NULL, *attr_b; struct ATTR_LIST_ENTRY *le, *le_b; struct mft_inode *mi, *mi_b; - CLST svcn, evcn1, len, dealloc, alen; + CLST svcn, evcn1, len, dealloc, alen, done; CLST vcn, end; u64 valid_size, data_size, alloc_size, total_size; u32 mask; @@ -1923,6 +1923,7 @@ int attr_collapse_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) len = bytes >> sbi->cluster_bits; end = vcn + len; dealloc = 0; + done = 0; svcn = le64_to_cpu(attr_b->nres.svcn); evcn1 = le64_to_cpu(attr_b->nres.evcn) + 1; @@ -1931,23 +1932,28 @@ int attr_collapse_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) attr = attr_b; le = le_b; mi = mi_b; - } else if (!le_b) { + goto check_seg; + } + + if (!le_b) { err = -EINVAL; goto out; - } else { - le = le_b; - attr = ni_find_attr(ni, attr_b, &le, ATTR_DATA, NULL, 0, &vcn, - &mi); - if (!attr) { - err = -EINVAL; - goto out; - } + } - svcn = le64_to_cpu(attr->nres.svcn); - evcn1 = le64_to_cpu(attr->nres.evcn) + 1; + le = le_b; + attr = ni_find_attr(ni, attr_b, &le, ATTR_DATA, NULL, 0, &vcn, &mi); + if (!attr) { + err = -EINVAL; + goto out; } for (;;) { + CLST vcn1, eat, next_svcn; + + svcn = le64_to_cpu(attr->nres.svcn); + evcn1 = le64_to_cpu(attr->nres.evcn) + 1; + +check_seg: if (svcn >= end) { /* Shift VCN- */ attr->nres.svcn = cpu_to_le64(svcn - len); @@ -1957,22 +1963,25 @@ int attr_collapse_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) ni->attr_list.dirty = true; } mi->dirty = true; - } else if (svcn < vcn || end < evcn1) { - CLST vcn1, eat, next_svcn; + goto next_attr; + } + run_truncate(run, 0); + err = attr_load_runs(attr, ni, run, &svcn); + if (err) + goto out; + + vcn1 = vcn + done; /* original vcn in attr/run. */ + eat = min(end, evcn1) - vcn1; + + err = run_deallocate_ex(sbi, run, vcn1, eat, &dealloc, true); + if (err) + goto out; + + if (svcn + eat < evcn1) { /* Collapse a part of this attribute segment. */ - err = attr_load_runs(attr, ni, run, &svcn); - if (err) - goto out; - vcn1 = max(vcn, svcn); - eat = min(end, evcn1) - vcn1; - err = run_deallocate_ex(sbi, run, vcn1, eat, &dealloc, - true); - if (err) - goto out; - - if (!run_collapse_range(run, vcn1, eat)) { + if (!run_collapse_range(run, vcn1, eat, done)) { err = -ENOMEM; goto out; } @@ -1980,7 +1989,7 @@ int attr_collapse_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) if (svcn >= vcn) { /* Shift VCN */ attr->nres.svcn = cpu_to_le64(vcn); - if (le) { + if (le && attr->nres.svcn != le->vcn) { le->vcn = attr->nres.svcn; ni->attr_list.dirty = true; } @@ -1991,7 +2000,7 @@ int attr_collapse_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) goto out; next_svcn = le64_to_cpu(attr->nres.evcn) + 1; - if (next_svcn + eat < evcn1) { + if (next_svcn + eat + done < evcn1) { err = ni_insert_nonresident( ni, ATTR_DATA, NULL, 0, run, next_svcn, evcn1 - eat - next_svcn, a_flags, &attr, @@ -2005,18 +2014,9 @@ int attr_collapse_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) /* Free all allocated memory. */ run_truncate(run, 0); + done += eat; } else { u16 le_sz; - u16 roff = le16_to_cpu(attr->nres.run_off); - - if (roff > le32_to_cpu(attr->size)) { - err = -EINVAL; - goto out; - } - - run_unpack_ex(RUN_DEALLOCATE, sbi, ni->mi.rno, svcn, - evcn1 - 1, svcn, Add2Ptr(attr, roff), - le32_to_cpu(attr->size) - roff); /* Delete this attribute segment. */ mi_remove_attr(NULL, mi, attr); @@ -2029,6 +2029,7 @@ int attr_collapse_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) goto out; } + done += evcn1 - svcn; if (evcn1 >= alen) break; @@ -2046,11 +2047,12 @@ int attr_collapse_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) err = -EINVAL; goto out; } - goto next_attr; + continue; } le = (struct ATTR_LIST_ENTRY *)((u8 *)le - le_sz); } +next_attr: if (evcn1 >= alen) break; @@ -2059,10 +2061,6 @@ int attr_collapse_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) err = -EINVAL; goto out; } - -next_attr: - svcn = le64_to_cpu(attr->nres.svcn); - evcn1 = le64_to_cpu(attr->nres.evcn) + 1; } if (!attr_b) { @@ -2552,7 +2550,7 @@ int attr_insert_range(struct ntfs_inode *ni, u64 vbo, u64 bytes) if (attr_load_runs(attr, ni, run, NULL)) goto bad_inode; - if (!run_collapse_range(run, vcn, len)) + if (!run_collapse_range(run, vcn, len, 0)) goto bad_inode; if (mi_pack_runs(mi, attr, run, evcn1 + len - svcn)) diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h index 86f825cf1c29..8ff49c5a2973 100644 --- a/fs/ntfs3/ntfs_fs.h +++ b/fs/ntfs3/ntfs_fs.h @@ -777,7 +777,7 @@ bool mi_remove_attr(struct ntfs_inode *ni, struct mft_inode *mi, struct ATTRIB *attr); bool mi_resize_attr(struct mft_inode *mi, struct ATTRIB *attr, int bytes); int mi_pack_runs(struct mft_inode *mi, struct ATTRIB *attr, - struct runs_tree *run, CLST len); + const struct runs_tree *run, CLST len); static inline bool mi_is_ref(const struct mft_inode *mi, const struct MFT_REF *ref) { @@ -812,7 +812,7 @@ void run_truncate_head(struct runs_tree *run, CLST vcn); void run_truncate_around(struct runs_tree *run, CLST vcn); bool run_add_entry(struct runs_tree *run, CLST vcn, CLST lcn, CLST len, bool is_mft); -bool run_collapse_range(struct runs_tree *run, CLST vcn, CLST len); +bool run_collapse_range(struct runs_tree *run, CLST vcn, CLST len, CLST sub); bool run_insert_range(struct runs_tree *run, CLST vcn, CLST len); bool run_get_entry(const struct runs_tree *run, size_t index, CLST *vcn, CLST *lcn, CLST *len); diff --git a/fs/ntfs3/record.c b/fs/ntfs3/record.c index 714c7ecedca8..167093e8d287 100644 --- a/fs/ntfs3/record.c +++ b/fs/ntfs3/record.c @@ -621,7 +621,7 @@ bool mi_resize_attr(struct mft_inode *mi, struct ATTRIB *attr, int bytes) * If failed record is not changed. */ int mi_pack_runs(struct mft_inode *mi, struct ATTRIB *attr, - struct runs_tree *run, CLST len) + const struct runs_tree *run, CLST len) { int err = 0; struct ntfs_sb_info *sbi = mi->sbi; diff --git a/fs/ntfs3/run.c b/fs/ntfs3/run.c index 5df55e4adbb1..395b20492525 100644 --- a/fs/ntfs3/run.c +++ b/fs/ntfs3/run.c @@ -487,7 +487,7 @@ bool run_add_entry(struct runs_tree *run, CLST vcn, CLST lcn, CLST len, * Helper for attr_collapse_range(), * which is helper for fallocate(collapse_range). */ -bool run_collapse_range(struct runs_tree *run, CLST vcn, CLST len) +bool run_collapse_range(struct runs_tree *run, CLST vcn, CLST len, CLST sub) { size_t index, eat; struct ntfs_run *r, *e, *eat_start, *eat_end; @@ -511,7 +511,7 @@ bool run_collapse_range(struct runs_tree *run, CLST vcn, CLST len) /* Collapse a middle part of normal run, split. */ if (!run_add_entry(run, vcn, SPARSE_LCN, len, false)) return false; - return run_collapse_range(run, vcn, len); + return run_collapse_range(run, vcn, len, sub); } r += 1; @@ -545,6 +545,13 @@ bool run_collapse_range(struct runs_tree *run, CLST vcn, CLST len) memmove(eat_start, eat_end, (e - eat_end) * sizeof(*r)); run->count -= eat; + if (sub) { + e -= eat; + for (r = run->runs; r < e; r++) { + r->vcn -= sub; + } + } + return true; } From ae91dfe38966fa30d713e705a69bf6c5c7f4c2aa Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 30 Oct 2025 23:41:01 +0300 Subject: [PATCH 19/26] fs/ntfs3: implement NTFS3_IOC_SHUTDOWN ioctl Add support for the NTFS3_IOC_SHUTDOWN ioctl, allowing userspace to request a filesystem shutdown. The ioctl number is shared with other filesystems such as ext4, exfat, and f2fs. Signed-off-by: Konstantin Komarov --- fs/ntfs3/file.c | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/fs/ntfs3/file.c b/fs/ntfs3/file.c index 60eb90bff955..b9484f48db34 100644 --- a/fs/ntfs3/file.c +++ b/fs/ntfs3/file.c @@ -19,6 +19,12 @@ #include "ntfs.h" #include "ntfs_fs.h" +/* + * cifx, btrfs, exfat, ext4, f2fs use this constant. + * Hope this value will become common to all fs. + */ +#define NTFS3_IOC_SHUTDOWN _IOR('X', 125, __u32) + static int ntfs_ioctl_fitrim(struct ntfs_sb_info *sbi, unsigned long arg) { struct fstrim_range __user *user_range; @@ -73,13 +79,47 @@ static int ntfs_ioctl_set_volume_label(struct ntfs_sb_info *sbi, u8 __user *buf) return ntfs_set_label(sbi, user, len); } +/* + * ntfs_force_shutdown - helper function. Called from ioctl + */ +static int ntfs_force_shutdown(struct super_block *sb, u32 flags) +{ + int err; + struct ntfs_sb_info *sbi = sb->s_fs_info; + + if (unlikely(ntfs3_forced_shutdown(sb))) + return 0; + + /* No additional options yet (flags). */ + err = bdev_freeze(sb->s_bdev); + if (err) + return err; + set_bit(NTFS_FLAGS_SHUTDOWN_BIT, &sbi->flags); + bdev_thaw(sb->s_bdev); + return 0; +} + +static int ntfs_ioctl_shutdown(struct super_block *sb, unsigned long arg) +{ + u32 flags; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (get_user(flags, (__u32 __user *)arg)) + return -EFAULT; + + return ntfs_force_shutdown(sb, flags); +} + /* * ntfs_ioctl - file_operations::unlocked_ioctl */ long ntfs_ioctl(struct file *filp, u32 cmd, unsigned long arg) { struct inode *inode = file_inode(filp); - struct ntfs_sb_info *sbi = inode->i_sb->s_fs_info; + struct super_block *sb = inode->i_sb; + struct ntfs_sb_info *sbi = sb->s_fs_info; /* Avoid any operation if inode is bad. */ if (unlikely(is_bad_ni(ntfs_i(inode)))) @@ -92,6 +132,8 @@ long ntfs_ioctl(struct file *filp, u32 cmd, unsigned long arg) return ntfs_ioctl_get_volume_label(sbi, (u8 __user *)arg); case FS_IOC_SETFSLABEL: return ntfs_ioctl_set_volume_label(sbi, (u8 __user *)arg); + case NTFS3_IOC_SHUTDOWN: + return ntfs_ioctl_shutdown(sb, arg); } return -ENOTTY; /* Inappropriate ioctl for device. */ } From d8e1e0d33d975952e65945c9dd68c76c7f946435 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 30 Oct 2025 23:45:33 +0300 Subject: [PATCH 20/26] fs/ntfs3: check minimum alignment for direct I/O Add a check for minimum alignment when performing direct I/O reads. If the file offset or user buffer is not aligned to the device's logical block size, fall back to buffered I/O instead of continuing with unaligned direct I/O. Signed-off-by: Konstantin Komarov --- fs/ntfs3/file.c | 10 ++++++++++ fs/ntfs3/ntfs_fs.h | 1 + fs/ntfs3/super.c | 1 + 3 files changed, 12 insertions(+) diff --git a/fs/ntfs3/file.c b/fs/ntfs3/file.c index b9484f48db34..3b22c7375616 100644 --- a/fs/ntfs3/file.c +++ b/fs/ntfs3/file.c @@ -941,6 +941,16 @@ static ssize_t ntfs_file_read_iter(struct kiocb *iocb, struct iov_iter *iter) file->f_ra.ra_pages = 0; } + /* Check minimum alignment for dio. */ + if (iocb->ki_flags & IOCB_DIRECT) { + struct super_block *sb = inode->i_sb; + struct ntfs_sb_info *sbi = sb->s_fs_info; + if ((iocb->ki_pos | iov_iter_alignment(iter)) & + sbi->bdev_blocksize_mask) { + iocb->ki_flags &= ~IOCB_DIRECT; + } + } + return generic_file_read_iter(iocb, iter); } diff --git a/fs/ntfs3/ntfs_fs.h b/fs/ntfs3/ntfs_fs.h index 8ff49c5a2973..a4559c9f64e6 100644 --- a/fs/ntfs3/ntfs_fs.h +++ b/fs/ntfs3/ntfs_fs.h @@ -212,6 +212,7 @@ struct ntfs_sb_info { u32 discard_granularity; u64 discard_granularity_mask_inv; // ~(discard_granularity_mask_inv-1) + u32 bdev_blocksize_mask; // bdev_logical_block_size(bdev) - 1; u32 cluster_size; // bytes per cluster u32 cluster_mask; // == cluster_size - 1 diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index 96f56333cf99..f481e9df0237 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -1075,6 +1075,7 @@ static int ntfs_init_from_boot(struct super_block *sb, u32 sector_size, dev_size += sector_size - 1; } + sbi->bdev_blocksize_mask = max(boot_sector_size, sector_size) - 1; sbi->mft.lbo = mlcn << cluster_bits; sbi->mft.lbo2 = mlcn2 << cluster_bits; From 266ab6d02aa34586baac68e2f986085f48efd96f Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 30 Oct 2025 23:49:27 +0300 Subject: [PATCH 21/26] fs/ntfs3: update mode in xattr when ACL can be reduced to mode If a file's ACL can be reduced to standard mode bits, update mode accordingly, persist the change, and update the cached ACL. This keeps mode and ACL consistent and avoids redundant xattrs. Signed-off-by: Konstantin Komarov --- fs/ntfs3/xattr.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/fs/ntfs3/xattr.c b/fs/ntfs3/xattr.c index e519e21596a7..c93df55e98d0 100644 --- a/fs/ntfs3/xattr.c +++ b/fs/ntfs3/xattr.c @@ -654,12 +654,22 @@ static noinline int ntfs_set_acl_ex(struct mnt_idmap *idmap, err = ntfs_set_ea(inode, name, name_len, value, size, flags, 0, NULL); if (err == -ENODATA && !size) err = 0; /* Removing non existed xattr. */ - if (!err) { - set_cached_acl(inode, type, acl); + if (err) + goto out; + + if (inode->i_mode != mode) { + umode_t old_mode = inode->i_mode; + inode->i_mode = mode; + err = ntfs_save_wsl_perm(inode, NULL); + if (err) { + inode->i_mode = old_mode; + goto out; + } inode->i_mode = mode; - inode_set_ctime_current(inode); - mark_inode_dirty(inode); } + set_cached_acl(inode, type, acl); + inode_set_ctime_current(inode); + mark_inode_dirty(inode); out: kfree(value); From 2469f2e78d074bf2d416ea82c32b154f5632effe Mon Sep 17 00:00:00 2001 From: Colin Ian King Date: Tue, 9 May 2023 08:28:55 +0100 Subject: [PATCH 22/26] fs/ntfs3: Fix spelling mistake "recommened" -> "recommended" There is a spelling mistake in a ntfs_info message. Fix it. Signed-off-by: Colin Ian King Signed-off-by: Konstantin Komarov --- fs/ntfs3/super.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index f481e9df0237..344217ab513c 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -1329,7 +1329,7 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) sbi->volume.ni = ni; if (info->flags & VOLUME_FLAG_DIRTY) { sbi->volume.real_dirty = true; - ntfs_info(sb, "It is recommened to use chkdsk."); + ntfs_info(sb, "It is recommended to use chkdsk."); } /* Load $MFTMirr to estimate recs_mirr. */ From 4d78d1173a653acdaf7500a32b8dc530ca4ad075 Mon Sep 17 00:00:00 2001 From: Edward Adam Davis Date: Tue, 11 Nov 2025 19:13:56 +0800 Subject: [PATCH 23/26] fs/ntfs3: out1 also needs to put mi After ntfs_look_free_mft() executes successfully, all subsequent code that fails to execute must put mi. Fixes: 4342306f0f0d ("fs/ntfs3: Add file operations and implementation") Signed-off-by: Edward Adam Davis Signed-off-by: Konstantin Komarov --- fs/ntfs3/frecord.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index 87609a381ce5..9881d7dce93d 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -1015,9 +1015,9 @@ static int ni_ins_attr_ext(struct ntfs_inode *ni, struct ATTR_LIST_ENTRY *le, out2: ni_remove_mi(ni, mi); - mi_put(mi); out1: + mi_put(mi); ntfs_mark_rec_free(sbi, rno, is_mft); out: From ccc4e86d1c24260c18ae94541198c3711c140da6 Mon Sep 17 00:00:00 2001 From: Edward Adam Davis Date: Tue, 11 Nov 2025 19:05:42 +0800 Subject: [PATCH 24/26] fs/ntfs3: Prevent memory leaks in add sub record If a rb node with the same ino already exists in the rb tree, the newly alloced mft_inode in ni_add_subrecord() will not have its memory cleaned up, which leads to the memory leak issue reported by syzbot. The best option to avoid this issue is to put the newly alloced mft node when a rb node with the same ino already exists in the rb tree and return the rb node found in the rb tree to the parent layer. syzbot reported: BUG: memory leak unreferenced object 0xffff888110bef280 (size 128): backtrace (crc 126a088f): ni_add_subrecord+0x31/0x180 fs/ntfs3/frecord.c:317 ntfs_look_free_mft+0xf0/0x790 fs/ntfs3/fsntfs.c:715 BUG: memory leak unreferenced object 0xffff888109093400 (size 1024): backtrace (crc 7197c55e): mi_init+0x2b/0x50 fs/ntfs3/record.c:105 mi_format_new+0x40/0x220 fs/ntfs3/record.c:422 Fixes: 4342306f0f0d ("fs/ntfs3: Add file operations and implementation") Reported-by: syzbot+3932ccb896e06f7414c9@syzkaller.appspotmail.com Signed-off-by: Edward Adam Davis Signed-off-by: Konstantin Komarov --- fs/ntfs3/frecord.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fs/ntfs3/frecord.c b/fs/ntfs3/frecord.c index 9881d7dce93d..641ddaf8d4a0 100644 --- a/fs/ntfs3/frecord.c +++ b/fs/ntfs3/frecord.c @@ -325,8 +325,10 @@ bool ni_add_subrecord(struct ntfs_inode *ni, CLST rno, struct mft_inode **mi) mi_get_ref(&ni->mi, &m->mrec->parent_ref); - ni_add_mi(ni, m); - *mi = m; + *mi = ni_ins_mi(ni, &ni->mi_tree, m->rno, &m->node); + if (*mi != m) + mi_put(m); + return true; } From bcbb8d0afd94927215e8ae97c40cfefa807cfe0b Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 30 Oct 2025 23:51:09 +0300 Subject: [PATCH 25/26] fs/ntfs3: change the default mount options for "acl" and "prealloc" Switch the "acl" and "prealloc" mount parameters to fsparam_flag_no(), making them enabled by default and allowing users to disable them with "noacl" and "noprealloc". Signed-off-by: Konstantin Komarov --- fs/ntfs3/super.c | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/fs/ntfs3/super.c b/fs/ntfs3/super.c index 344217ab513c..c74e25eb66a5 100644 --- a/fs/ntfs3/super.c +++ b/fs/ntfs3/super.c @@ -284,9 +284,9 @@ static const struct fs_parameter_spec ntfs_fs_parameters[] = { fsparam_flag("hide_dot_files", Opt_hide_dot_files), fsparam_flag("windows_names", Opt_windows_names), fsparam_flag("showmeta", Opt_showmeta), - fsparam_flag("acl", Opt_acl), + fsparam_flag_no("acl", Opt_acl), fsparam_string("iocharset", Opt_iocharset), - fsparam_flag("prealloc", Opt_prealloc), + fsparam_flag_no("prealloc", Opt_prealloc), fsparam_flag("nocase", Opt_nocase), {} }; @@ -395,7 +395,7 @@ static int ntfs_fs_parse_param(struct fs_context *fc, param->string = NULL; break; case Opt_prealloc: - opts->prealloc = 1; + opts->prealloc = !result.negated; break; case Opt_nocase: opts->nocase = 1; @@ -1259,12 +1259,12 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) sb->s_export_op = &ntfs_export_ops; sb->s_time_gran = NTFS_TIME_GRAN; // 100 nsec sb->s_xattr = ntfs_xattr_handlers; - set_default_d_op(sb, sbi->options->nocase ? &ntfs_dentry_ops : NULL); + set_default_d_op(sb, options->nocase ? &ntfs_dentry_ops : NULL); - sbi->options->nls = ntfs_load_nls(sbi->options->nls_name); - if (IS_ERR(sbi->options->nls)) { - sbi->options->nls = NULL; - errorf(fc, "Cannot load nls %s", fc_opts->nls_name); + options->nls = ntfs_load_nls(options->nls_name); + if (IS_ERR(options->nls)) { + options->nls = NULL; + errorf(fc, "Cannot load nls %s", options->nls_name); err = -EINVAL; goto out; } @@ -1676,10 +1676,11 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc) put_inode_out: iput(inode); out: - if (sbi && sbi->options) { - unload_nls(sbi->options->nls); - kfree(sbi->options->nls_name); - kfree(sbi->options); + /* sbi->options == options */ + if (options) { + unload_nls(options->nls); + kfree(options->nls_name); + kfree(options); sbi->options = NULL; } @@ -1808,6 +1809,12 @@ static int __ntfs_init_fs_context(struct fs_context *fc) opts->fs_gid = current_gid(); opts->fs_fmask_inv = ~current_umask(); opts->fs_dmask_inv = ~current_umask(); + opts->prealloc = 1; + +#ifdef CONFIG_NTFS3_FS_POSIX_ACL + /* Set the default value 'acl' */ + fc->sb_flags |= SB_POSIXACL; +#endif if (fc->purpose == FS_CONTEXT_FOR_RECONFIGURE) goto ok; From 1b2ae190ea43bebb8c73d21f076addc8a8c71849 Mon Sep 17 00:00:00 2001 From: Konstantin Komarov Date: Thu, 6 Nov 2025 16:17:19 +0300 Subject: [PATCH 26/26] fs/ntfs3: check for shutdown in fsync Ensure fsync() returns -EIO when the ntfs3 filesystem is in forced shutdown, instead of silently succeeding via generic_file_fsync(). Signed-off-by: Konstantin Komarov --- fs/ntfs3/file.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/fs/ntfs3/file.c b/fs/ntfs3/file.c index 3b22c7375616..5016bccc2ac5 100644 --- a/fs/ntfs3/file.c +++ b/fs/ntfs3/file.c @@ -1440,6 +1440,18 @@ static ssize_t ntfs_file_splice_write(struct pipe_inode_info *pipe, return iter_file_splice_write(pipe, file, ppos, len, flags); } +/* + * ntfs_file_fsync - file_operations::fsync + */ +static int ntfs_file_fsync(struct file *file, loff_t start, loff_t end, int datasync) +{ + struct inode *inode = file_inode(file); + if (unlikely(ntfs3_forced_shutdown(inode->i_sb))) + return -EIO; + + return generic_file_fsync(file, start, end, datasync); +} + // clang-format off const struct inode_operations ntfs_file_inode_operations = { .getattr = ntfs_getattr, @@ -1462,7 +1474,7 @@ const struct file_operations ntfs_file_operations = { .splice_write = ntfs_file_splice_write, .mmap_prepare = ntfs_file_mmap_prepare, .open = ntfs_file_open, - .fsync = generic_file_fsync, + .fsync = ntfs_file_fsync, .fallocate = ntfs_fallocate, .release = ntfs_file_release, };