_._     _,-'""`-._
(,-.`._,'(       |\`-/|
    `-.-' \ )-`( , o o)
          `-    \`_`"'-

NTFS3 ntfs_dir_emit OOB-read: CVE-2023-45896, still alive

2026-05-01 · linuxkernelntfs3filesystemoobcve-2023-45896

Greg KH disputed CVE-2023-45896, CISA reasserted it, and nothing landed upstream. i pulled up fs/ntfs3/dir.c expecting stale code and found the same unit confusion still sitting there. ntfs_dir_emit() checks an attacker-controlled directory entry's fname->name_len against the entry's e->size and treats both as bytes. they aren't the same unit. fname->name_len counts UTF-16 code units (fs/ntfs3/ntfs.h:575), so the check passes on entries it shouldn't. then ntfs_utf16_to_nls() at :311 reads name_len × 2 bytes from e + 0x52 - up to ~510 of them - and runs straight off the entry into adjacent slab memory.

the leak path is annoyingly clean. those bytes leave the kernel and come back to userspace as the directory entry's name through dir_emit(ctx, name, name_len, ino, dt_type), which any unprivileged process can read with getdents64 on the mounted directory. the slab neighbour is a kmalloc-1024 object, so what you get is a controlled-length read out of adjacent kernel memory.

prior art. this is not novel

dfir.ru disclosed this on 2023-08-13 as CVE-2023-45896. Greg KH disputed the CVE, CISA reasserted it at CVSS 7.1 HIGH, and nothing landed upstream. mainline still carries the unit-confused bound check at fs/ntfs3/dir.c:308 today, and the same primitive still fires.

i'm not claiming the bug. i verified it's still alive on current Resolute 26.04 LTS production kernels and on mainline 7.1-rc1, found the sibling in cmp_fnames, and built a PoC image and harness. cite dfir.ru first.

root cause

/* fs/ntfs3/dir.c:308 */
if (fname->name_len + sizeof(struct NTFS_DE) > le16_to_cpu(e->size))
    return true;     /* "valid" -- skip the entry */

/* fs/ntfs3/dir.c:311 */
name_len = ntfs_utf16_to_nls(sbi, fname->name, fname->name_len, name, PATH_MAX);

fname->name_len is in UTF-16 code units (fs/ntfs3/ntfs.h:575). the byte size of the name field is what fname_full_size(fname) returns (ntfs.h:590). the check at :308 compares the code-unit count against a byte size, so an entry with e->size=0x111, key_size=0x44, name_len=0xFF sails through. then ntfs_utf16_to_nls() reads 510 bytes - 255 UTF-16 chars times 2 - starting at e + 0x52, well past the entry boundary and into whatever sits next in the slab.

there's a sibling in the same family. cmp_fnames() at fs/ntfs3/index.c:34 takes e->key_size as its l2 without checking it against e->size - sizeof(NTFS_DE) first. it's reachable through hdr_find_e() (index.c:715-793) on the lookup() path. same unit confusion, different entry point.

verification (KASAN, Resolute lab)

i built a KASAN lab kernel at 7.0.0-kasan-kcov #15.15.kasan. one ls /mnt/poc as uid 1000 against the included poc.img trips it:

BUG: KASAN: slab-out-of-bounds in ntfs_utf16_to_nls+0x24e/0x380 [ntfs3]
Read of size 2 at addr ffff88810d484400 by task ls/5057
CPU: 3 UID: 1000 PID: 5057 Comm: ls Not tainted 7.0.0-kasan-kcov #15.15.kasan PREEMPT(lazy)
Call Trace:
 ntfs_utf16_to_nls+0x24e/0x380 [ntfs3]
 ntfs_read_hdr+0x43f/0x670 [ntfs3]
 ntfs_readdir+0x875/0xab0 [ntfs3]
 iterate_dir
 __x64_sys_getdents64
The buggy address is located 0 bytes to the right of
allocated 1024-byte region [...]
Allocated by task ...:
  mi_init+0x4b/0x80 [ntfs3]   /* kmalloc(record_size, GFP_NOFS) */

the read lands in adjacent kmalloc-1024 slab memory. groom the heap and that neighbour slot holds what you want it to - attacker-chosen objects, or something sensitive you've steered into place. either way the bytes return to userspace as the directory entry's name. i didn't finish a full weaponization chain here; the read primitive is what i can stand behind.

reproducer

# image is pre-built and included as poc.img (64 MB NTFS).
# crafter source: craft_poc.py (re-buildable from base.img if needed).
sudo modprobe ntfs3
sudo mkdir -p /mnt/poc
sudo losetup -fP poc.img
sudo mount -t ntfs3 -o ro,iocharset=iso8859-1 /dev/loopN /mnt/poc
ls /mnt/poc                                # KASAN trip on KASAN kernel
gcc -O2 -o /tmp/harness harness.c
/tmp/harness /mnt/poc                      # dumps OOB bytes to stdout

the iocharset=iso8859-1 mount option is Resolute's default for ntfs3. nothing here is a contrived flag. it's how the filesystem comes up when udisks2 attaches a real Windows NTFS partition on a dual-boot box.

reach

fs/ntfs3/super.c doesn't set FS_USERNS_MOUNT, so mounting a hostile NTFS3 image wants init_user_ns CAP_SYS_ADMIN. that gate sounds like it closes the door. it doesn't. here's where the mount happens anyway, with no sudo on the attacker's side:

suggested fix

/* fs/ntfs3/dir.c:308 -- use the correct byte-size accessor */
if (fname_full_size(fname) + sizeof(struct NTFS_DE) > le16_to_cpu(e->size))
    return true;

and while you're in there, tighten hdr_find_e at index.c:741-744 so it checks e_key_len + sizeof(NTFS_DE) <= e_size before it calls cmp_fnames.

cross-distro applicability

distro / kernelstatus
Ubuntu Resolute 7.0.0-15vulnerable (live verified, KASAN trace above)
Arch Linux 7.0.3.arch1vulnerable (source verified)
Mainline 7.1-rc1vulnerable (source verified)

downloads

SHA-256:

d3bf890db593e0a27064c3b3b8c2b102082e0660d69bdd1ccf60fdab90616d43  poc.img.gz
7deba7ab5df37d82b8ce6da4939f5f7ccacf0da1b32b90c257b2dd65b346d7e0  craft_poc.py
42a7833e04a04f1e8b873aeddd4037693c33e5434a7a851cfa6d466e79bad846  harness.c
54a3926fe61773b2ec7952e02d1978603ecdfb7d7b30cdc7ada642ebf4680ca9  repro.sh
fbd84f8889f9d5ff52a46a72f46d41b362bd1e893dacde4fca5bba55d3516af1  kasan-output.txt
3ac909a283ec43172f68d90a5c0b5e4299e330d6f351347b77e6ba3c6e264c01  evidence.txt

credit

dfir.ru. original 2023-08-13 disclosure (CVE-2023-45896).

CISA. CVSS 7.1 HIGH assignment.

_SiCk <sick@afflicted.sh> (https://afflicted.sh). verification still alive on current kernels, sibling-bug identification (cmp_fnames), pre-built PoC image and dumper harness.

no warranty. no support. no apologies.

. _SiCk · afflicted.sh