vn_read has no VDIR gate: read() on a directory fd is dispatched to VOP_READ, and ffs_read explicitly accepts VDIR, exposing raw directory blocks (deleted-entry residue) instead of returning EISDIR
| Field | Value |
|---|---|
| ID | DF-2756 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N |
| CWE | CWE-668 Exposure of Resource to Wrong Sphere |
| File | sys/kern/vfs_vnops.c |
| Lines | 749-754 (ffs acceptance ufs_readwrite.c:81-85) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
vn_read performs no v_type check, and ffs_read treats VDIR as legal
(else if (vp->v_type != VREG && vp->v_type != VDIR) panic), so on
UFS-family mounts read() on a directory fd returns raw on-disk
directory blocks β violating POSIX's mandatory EISDIR and disclosing
inter-entry slack space (d_reclen padding) containing residue of
removed entries (names, inode numbers) from before the reader existed.
On-disk data residue, not kernel memory. hammer2 and tmpfs reject dir
reads (verified), so impact requires a UFS/ext2 mount.
Recommended fix
Add if (vp->v_type == VDIR) return (EISDIR); at the common layer in
vn_read() before taking the vnode lock (mirrors FreeBSD), and/or make
ffs_read return EISDIR for VDIR.
Timeline
- 2026-08-30 Discovered during pass-2 audit of vfs_vnops.c (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2756 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dirread2.c | β | 1.7 KB | view raw | |
| dirread.c | β | 1.9 KB | view raw | |
| run.log | β | 872 B | view raw | |
| fix.diff | β | 1.3 KB | view raw |
Fix verification
fixedPatched kernel: vn_read returns EISDIR for VDIR descriptors - read() on /boot now returns 0 bytes (error) instead of raw directory blocks; the deleted-entry residue path is closed.
dirread2 on patched kernel: total read = 0, residue hits = 0 (baseline: 1024 bytes, 8 residue hits)
Confirmed kernel references
Detail
Exploit chain
unpriv open(/boot) -> read() -> raw UFS dir blocks incl. deleted-entry residue (names + inodes of removed files)
Evidence (decisive lines)
dirread2.c; df2756_dirread.log: read=1024, 8x ZZsecretsalt residue hits in dirent slack
PoC changes
first parser used 8-byte dirent header; UFS uses fileno u32/reclen u16/namlen u8/type u8 - fixed
Verified recommended fix
--- sys/kern/vfs_vnops.c 2026-06-29 12:51:19.000000000 +0000
Verdict
Reproduced as an unprivileged user on the stock guest. vn_read (sys/kern/vfs_vnops.c:729-759) performs no v_type check, and UFS has no native vop_nresolve (lookups go through vop_compat) while ffs_read accepts VDIR - so read() on a directory descriptor of the /boot UFS mount returns raw on-disk directory blocks instead of EISDIR (first run: read(/boot) = 512 bytes of dirent data; full drain 1024 bytes). After root created and deleted eight files named ZZsecretsalt[1-8].file, the unprivileged reader found the deleted names 8 times in the raw dirent slack (offsets 468..688), disclosing deleted-entry names and inode numbers of files that existed before the reader had any access. This is on-disk residue disclosure (POSIX EISDIR violation), not kernel memory. hammer2 and tmpfs gate directory read (verified by the audit); the bug is live on UFS-family mounts.
No comments yet.