]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
ntfs: fix default_upcase refcount underflow and UAF on fs_context teardown
authorDaeMyung Kang <charsyam@gmail.com>
Tue, 5 May 2026 13:07:52 +0000 (22:07 +0900)
committerNamjae Jeon <linkinjeon@kernel.org>
Fri, 8 May 2026 14:51:01 +0000 (23:51 +0900)
ntfs_init_fs_context() allocates a fresh ntfs_volume with vol->upcase
left as NULL. ntfs_free_fs_context() unconditionally calls
ntfs_volume_free() during fs_context teardown, even when ntfs_fill_super()
never ran or already cleaned up. ntfs_volume_free() then executes:

mutex_lock(&ntfs_lock);
if (vol->upcase == default_upcase) {
ntfs_nr_upcase_users--;
vol->upcase = NULL;
}

When the global default_upcase is also NULL (very first mount attempt,
or all prior mounts have released the table), the comparison is
NULL == NULL, and ntfs_nr_upcase_users is decremented even though this
volume never claimed a reference. ntfs_nr_upcase_users is unsigned long,
so the decrement wraps to ULONG_MAX.

A subsequent successful mount can then free the shared table while the
mounted volume still points at it:

  1. ntfs_fill_super() does the temporary ntfs_nr_upcase_users++ at the
     "Generate the global default upcase table if necessary" block. With
     the prior wraparound this brings the counter back to 0.
  2. If the volume's $UpCase matches the default, the match path does
     ntfs_nr_upcase_users++ and sets vol->upcase = default_upcase. The
     counter is now 1.
  3. On the success path, !--ntfs_nr_upcase_users evaluates true and
     default_upcase is kvfree()'d while vol->upcase still points at it.
     Subsequent upcase comparisons through that mount touch freed
     memory.

This was reproduced with KASAN by closing a fresh fsopen("ntfs") context,
then mounting an NTFS image whose $UpCase table matches
generate_default_upcase(), and finally doing a case-insensitive lookup.
KASAN reports the dangling vol->upcase access:

  BUG: KASAN: use-after-free in ntfs_collate_names+0x3b4/0x420
  Read of size 2 at addr ffff888008d40048 by task init/1
   ntfs_collate_names+0x3b4/0x420
   ntfs_lookup_inode_by_name+0x1921/0x3130
   ntfs_lookup+0x193/0xc40
   vfs_statx+0xc7/0x190
   vfs_fstatat+0x4b/0xa0
   __do_sys_newfstatat+0x92/0xf0

The same QEMU reproducer was rerun after this change with KASAN
enabled. It reached "reproducer finished", and the log contained no
KASAN, use-after-free, Oops, or panic signatures.

Guard each comparison with an explicit vol->upcase non-NULL check so a
volume that never took a reference cannot decrement the global users
counter. Apply the same guard to the other default_upcase release sites
so all cleanup paths follow the same ownership rule: only volumes that
actually hold a default_upcase reference may drop one.

Fixes: 1e9ea7e04472 ("Revert "fs: Remove NTFS classic"")
Signed-off-by: DaeMyung Kang <charsyam@gmail.com>
Reviewed-by: Hyunchul Lee <hyc.lee@gmail.com>
Signed-off-by: Namjae Jeon <linkinjeon@kernel.org>
fs/ntfs/super.c

index 22dc7865eca79e45d44f7c25c14fc7bac2aedfb0..e9de84fb82973f003669b8e8ba59ab3e2d96b39e 100644 (file)
@@ -1671,7 +1671,7 @@ iput_attrdef_err_out:
 iput_upcase_err_out:
        vol->upcase_len = 0;
        mutex_lock(&ntfs_lock);
-       if (vol->upcase == default_upcase) {
+       if (vol->upcase && vol->upcase == default_upcase) {
                ntfs_nr_upcase_users--;
                vol->upcase = NULL;
        }
@@ -1701,7 +1701,7 @@ static void ntfs_volume_free(struct ntfs_volume *vol)
         * the number of upcase users if we are a user.
         */
        mutex_lock(&ntfs_lock);
-       if (vol->upcase == default_upcase) {
+       if (vol->upcase && vol->upcase == default_upcase) {
                ntfs_nr_upcase_users--;
                vol->upcase = NULL;
        }
@@ -2494,7 +2494,7 @@ static int ntfs_fill_super(struct super_block *sb, struct fs_context *fc)
        }
        vol->upcase_len = 0;
        mutex_lock(&ntfs_lock);
-       if (vol->upcase == default_upcase) {
+       if (vol->upcase && vol->upcase == default_upcase) {
                ntfs_nr_upcase_users--;
                vol->upcase = NULL;
        }