ntfs3: lookup with nocase returns ENOENT when name case differs from $FILE_NAME and inode is not cached

From: sulamufor

Date: Mon Sep 14 2026 - 17:17:07 EST


Hi,

with ntfs3 mounted with the `nocase` option, looking up a file whose name differs only in case from the
on-disk name fails with ENOENT as long as the inode is not yet in the inode cache. Once the inode has been
loaded by some other path (e.g. readdir), the same lookup succeeds.

Kernel: 7.2.5 (CachyOS build, unmodified fs/ntfs3). The relevant code is identical in current mainline and
in the Paragon linux-ntfs3 tree. Without `nocase` the same lookup fails consistently (as expected), so the
inconsistency is specific to the `nocase` option.

Minimal reproducer (fresh image, no special content):

#!/bin/sh
set -e
IMG=$(mktemp /tmp/ntfs3-XXXX.img); MNT=$(mktemp -d /tmp/ntfs3-mnt-XXXX)
truncate -s 64M "$IMG"
mkfs.ntfs -F -q "$IMG" >/dev/null
mount -t ntfs3 -o loop,nocase "$IMG" "$MNT"
: > "$MNT/Foo.TXT"
umount "$MNT"
mount -t ntfs3 -o loop,nocase "$IMG" "$MNT"
stat -c '%n ino=%i' "$MNT/foo.txt" # -> ENOENT
stat -c '%n ino=%i' "$MNT/Foo.TXT" # exact case: OK, loads the inode
stat -c '%n ino=%i' "$MNT/foo.txt" # -> now OK, same inode
cat "$MNT/FOO.txt" # -> works

Output on 7.2.5:

stat: cannot statx '/mnt/foo.txt': No such file or directory
/mnt/Foo.TXT ino=27
/mnt/foo.txt ino=27
hello

Listing the directory (`ls`) instead of the exact-case access has the same effect.

The same happens on a long-running mount whenever the inode gets evicted (observed with a large read-only
game asset tree accessed by a Windows application under Wine: after a reboot or under memory pressure a
subset of files that the application references with different casing than on disk "disappears", and
listing the directory makes them come back).

Analysis:

The directory index lookup itself honours `nocase`: `cmp_fnames()` in fs/ntfs3/index.c compares with
`sbi->upcase` and `both_case = ... && !sbi->options->nocase`, so `dir_search_u()` finds the entry and
calls `ntfs_iget5_flags(sb, &e->ref, uni, ...)` with the *caller-supplied* name.

If the inode is not cached, `ntfs_read_mft()` in fs/ntfs3/inode.c then walks the $FILE_NAME attributes
and compares each against that name with

if (name && name->len == fname->name_len &&
!ntfs_cmp_names_cpu(name, (struct le_str *)&fname->name_len,
NULL, false))
is_match = true;

`ntfs_cmp_names_cpu()` with `upcase == NULL` never takes the case-insensitive path, so this comparison is
always case-sensitive regardless of the mount option. `is_match` stays false and the function returns
-ENOENT (`if (!is_match && name) { err = -ENOENT; ... }`).

If the inode is already cached, `iget5_locked()` returns it without calling `ntfs_read_mft()`, hence the
lookup succeeds — which is why readdir (which loads the inodes) or an exact-case access appears to "fix" it.