cd9660_readdir leaks kernel heap bytes via uninitialized struct dirent padding (d_unused1/d_unused2/d_name pad)
Summary
cd9660_vnops.c:447 idp=kmalloc(sizeof(*idp),M_TEMP,M_WAITOK) NO M_ZERO. Three embedded struct dirent in isoreaddir: saveent/assocent/current. d_unused1 (offset 11 1B) d_unused2 (offset 12-15 4B) d_name padding NEVER initialized. iso_uiodir:360 uiomove(dp,_DIRENT_DIRSIZ(dp)) copies full record including uninit bytes to userspace. 5-12B heap leak per directory entry. UFS/MSDOSFS/NTFS all use vop_write_dirent which M_ZERO-allocates. CD9660 rolls its own and skips zeroing. Fix: add M_ZERO flag to kmalloc.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0866 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak_dirent.c | trigger-source | per-entry hexdump + leak count of d_unused1/d_unused2/d_name padding | 4.1 KB | view raw |
| build.sh | build-script | cc -O2 -o leak_dirent leak_dirent.c | 182 B | view raw |
| run.sh | run-script | ./leak_dirent <mounted-iso-dir>; exit 1 if leak | 566 B | view raw |
| fix.diff | suggested-fix | zero d_unused1/d_unused2/d_name padding in iso_uiodir + M_ZERO on kmalloc | 1.6 KB | view raw |
| build.log | build-log | final successful PoC build | 13 B | view raw |
| run.log | run-log | baseline run: subdir entry shows leaked 0x63 byte | 1.6 KB | view raw |
| run.2.log | run-log | baseline repeat run 2 | 1.6 KB | view raw |
| run.3.log | run-log | baseline repeat run 3 | 1.6 KB | view raw |
| env.txt | environment | uname, cc version, mount state | 349 B | view raw |
| fix_build.log | build-log | patched-kernel nativekernel build, rc=0 | 5.6 MB | β download |
| fix_run.log | run-log | patched-kernel run: 0 leaked bytes | 1.6 KB | view raw |
| fix_run.2.log | run-log | patched-kernel repeat run 2 | 1.6 KB | view raw |
| fix_run.3.log | run-log | patched-kernel repeat run 3 | 1.6 KB | view raw |
| fix_env.txt | environment | patched-kernel kern.version (#1) + sha256 | 305 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, repro, fix-validation | 5.0 KB | β raw |
| README.md | readme | claim summary, mechanism, repro steps, fix | 4.0 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0866 β cd9660_readdir uninitialized heap leak
Claim (Low / CWE-908, CWE-200)
sys/vfs/isofs/cd9660/cd9660_vnops.c:447 β
cd9660_readdir() allocates its struct isoreaddir working buffer with
idp = kmalloc(sizeof(*idp), M_TEMP, M_WAITOK); without M_ZERO. The
buffer contains three embedded struct iso_dirent (saveent, assocent,
current), each holding a struct dirent de whose
char d_name[256] and reserved d_unused1 (1 B) / d_unused2 (4 B) fields
are never explicitly initialized. The kernel then calls
iso_uiodir β uiomove(dp, _DIRENT_DIRSIZ(dp))
(cd9660_vnops.c:360) which copies the whole record β including the
uninitialized bytes between d_name[d_namlen+1] and the next 8-byte boundary
β to userspace. UFS / MSDOSFS / NTFS use vop_write_dirent (which
M_ZERO-allocates the dirent) β cd9660 rolls its own and skips the zeroing,
leaking 1β7 bytes of kernel heap per directory entry.
Mechanism (confirmed on master DEV #0)
kmalloc(sizeof(*idp), M_TEMP, M_WAITOK)returns an M_TEMP slab chunk whose 1.5+ KiB of bytes (sizeof(struct isoreaddir)) are uninitialized.- The readdir loop only sets
d_ino,d_namlen,d_typeand writesd_namlen+1bytes ofd_nameper entry. Everything else in thestruct direntretains the slab's previous contents. iso_uiodirβuiomove(_DIRENT_DIRSIZ(dp))copies the rounded-up record (16 + name + 8-aligned padding) to userspace, leaking the uninitialized tail ofd_name(and any non-zero bytes ind_unused1/d_unused2).
Reproduction
# (as root) craft an ISO and mount via cd9660 mkdir -p /tmp/isosrc/subdir : > /tmp/isosrc/a ; : > /tmp/isosrc/bb ; : > /tmp/isosrc/cccccccc : > /tmp/isosrc/this_is_a_long_filename_for_padding_test mkisofs -V DF0866 -J -r -o /root/df0866.iso /tmp/isosrc vnconfig -c -T vn0 /root/df0866.iso mkdir -p /mnt/iso && mount_cd9660 -o ro /dev/vn0 /mnt/iso chmod 755 /mnt # (as unprivileged maxx) build + run cc -O2 -o leak_dirent leak_dirent.c ./leak_dirent /mnt/iso # exits 1 if leak detected
Decisive evidence (run 1 on master DEV #0)
[run 1] entry 6 reclen= 24 namlen=6 type=0 name='subdir' bytes[0..23]: 00e800000000000006000000000000007375626469720063 d_unused1=00 d_unused2=00000000 leak(unused1=0 unused2=0 namepad=1)=1
Offset 22 of the subdir dirent is 0x63 ('c') β a non-zero byte. This is
residue from the previous entry's processing of cccccccc (8 chars + NUL
written to d_name); after subdir\0 is written (7 bytes), the remaining
d_name buffer at offset 22 still holds the prior content, and that byte is
copied verbatim to userspace via uiomove. In the very first run after a
fresh slab allocation we observed 28 leaked bytes per scan, including the
recognizable ASCII residue s_is_ (from this_is_a_long...) bleeding into
the padding of multiple short-name entries.
Impact ceiling
Information disclosure of kernel heap bytes (M_TEMP slab) to unprivileged users. Up to 7 bytes per directory entry; mounts of attacker-supplied ISO images by an admin expose the leak to any user able to list the mount point. No corruption, no escalation β pure info leak. Realistic ceiling is KASLR / slab-layout hints, low value on its own.
Fix
Add M_ZERO to the kmalloc so the whole struct isoreaddir (including
all three embedded struct dirents' d_name, d_unused1, d_unused2)
starts zeroed. One-line change, see fix.diff:
- idp = kmalloc(sizeof(*idp), M_TEMP, M_WAITOK);
+ idp = kmalloc(sizeof(*idp), M_TEMP, M_WAITOK | M_ZERO);
This matches the pattern used by vop_write_dirent (the helper the
UFS/MSDOSFS/NTFS filesystems go through) and the finding markdown's
recommended fix.
Files
leak_dirent.cβ minimal trigger; dumps every dirent's bytes and counts non-zero bytes ind_unused1/d_unused2/ trailingd_namepadding.build.sh,run.shβ exact repro.fix.diffβM_ZEROpatch.build.log,run.log,run.2.log,run.3.log,env.txtβ full logs.fix_build.log,fix_run.logβ patched-kernel build + re-run.
DF-0866 β Verdict
VERDICT: REPRODUCED (info leak β Low). FIX VALIDATED.
Mechanism (path:line)
- Trigger: an unprivileged user calls
getdirentries(2)on a cd9660-mounted directory. - Allocation:
sys/vfs/isofs/cd9660/cd9660_vnops.c:447idp = kmalloc(sizeof(*idp), M_TEMP, M_WAITOK);β noM_ZERO.struct isoreaddir(idp) embeds threestruct iso_dirent(saveent,assocent,current), each holding astruct dirent de. The full struct is ~1.5 KiB; none of it is zeroed by this allocation. - Per-entry reuse: the readdir loop (
cd9660_vnops.c:482-577) overwrites onlyd_ino,d_namlen,d_typeand writesd_namlen+1bytes ofd_nameper entry. The remainingd_namebytes, plusd_unused1(1 B at off 11) andd_unused2(4 B at off 12-15), are never touched. - Sink:
iso_uiodiratcd9660_vnops.c:360uiomove((caddr_t)dp, _DIRENT_DIRSIZ(dp), idp->uio)copies the whole 8-byte-aligned record (16 + name + 8-aligned padding) to userspace. The uninitialized tail ofd_nameand any non-zero bytes ind_unused1/d_unused2ride along to userspace.
This is a classic CWE-908 / CWE-200 info leak. UFS / MSDOSFS /
NTFS use the vop_write_dirent helper (sys/kern/vfs_subr.c:2570)
which kmalloc(M_WAITOK | M_ZERO)s a fresh, exactly-sized dirent per
entry β so they do not leak. cd9660 rolls its own zeroing-free path.
Reproduction
- Built a 6-entry ISO image (
a,bb,cccccccc,subdir, plus a 40-char long name) withmkisofs -J -r. - Mounted as
vn0viavnconfig+mount_cd9660. - Ran the unprivileged PoC
leak_dirent /mnt/iso(as usermaxx). - Baseline
6.5-DEVELOPMENT #0: thesubdirentry (aftercccccccc) shows byte0x63('c') at offset 22 of its dirent β a non-zero residue from the prior entry's name sitting in the unzeroedd_nametail. In the very first run after a fresh slab allocation we also saw 28 leaked bytes per scan including the ASCII residues_is_(fromthis_is_a_longβ¦) bleeding into the padding of multiple short-name entries. Decisive evidence (entry 6 on baseline):
[run 1] entry 6 reclen= 24 namlen=6 type=0 name='subdir'
bytes[0..23]: 00e800000000000006000000000000007375626469720063
^^ LEAKED 'c'
d_unused1=00 d_unused2=00000000 leak(unused1=0 unused2=0 namepad=1)=1
Why my first fix (M_ZERO only) was insufficient β and the corrected fix
My first attempt added M_ZERO to the initial kmalloc only. That
zeroes the idp buffer once at the start of cd9660_readdir(), but
the readdir loop then reuses idp->current.de.d_name for every
entry in the same call. So after entry 5 cccccccc writes 9 bytes to
d_name, entry 6 subdir overwrites only 7 bytes and leaves
d_name[7] = 'c' from entry 5. The leak persisted: 1 leaked byte
still appeared on the M_ZERO-only kernel.
The corrected fix adds explicit per-entry zeroing in iso_uiodir
itself β the single funnel through which every dirent record passes
on its way to uiomove. Right before the residual check it now sets
d_unused1 = 0, d_unused2 = 0, and bzero()s the d_name tail
between d_namlen+1 and the next 8-byte boundary. This is exactly
what UFS achieves per-call via a fresh M_ZERO'd kmalloc in
vop_write_dirent. We also keep M_ZERO on the outer kmalloc as
defense-in-depth (matches UFS, free insurance).
The corrected fix has two hunks (see fix.diff):
- iso_uiodir β zero reserved fields + d_name padding per entry.
- cd9660_readdir β M_WAITOK | M_ZERO on the initial kmalloc.
Fix validation (Phase 8)
| Kernel | Build sha256 (kernel) | Result |
|---|---|---|
6.5-DEVELOPMENT #0 (unpatched) |
(audit baseline) | 1 byte leaked |
6.5-DEVELOPMENT #1 (single-fix) |
9e471a74β¦d167376 |
0 bytes leaked |
Three back-to-back runs on the patched kernel (3Γ determinism) all
return non-zero leaked bytes: 0. The previously-leaked 0x63 byte
at offset 22 of subdir is now 0x00. PoC leak_dirent now exits
0 (no leak) instead of 1.
Impact ceiling
Information disclosure of kernel heap bytes (M_TEMP slab) β up to 7 B
per directory entry per getdirentries call, to any user with read
access to a mounted cd9660 directory. Realistic preconditions:
admin-mounted ISO image (cd9660 is root-only to mount, but any user
can read the mount point). No corruption, no privilege escalation.
Ceiling is KASLR / slab-layout fingerprinting β low standalone value.
Files in this evidence pack
leak_dirent.cβ trigger PoC; per-entry hexdump + leak count.build.sh,run.shβ exact reproduction commands.fix.diffβ corrected two-hunk patch (per-entry bzero + M_ZERO).build.log,run.log,run.2.log,run.3.log,env.txtβ baseline.fix_build.log,fix_run.log,fix_run.2.log,fix_run.3.log,fix_env.txtβ patched-kernel build + 3Γ clean runs.
Fix verification
fixedVALIDATED: baseline 1B leaked; patched 0B across 3 runs. Per-entry bzero + M_ZERO closes leak.
BEFORE: bytes[22]=0x63. AFTER: bytes[22]=0x00. 3/3 runs 0 leaked.
Confirmed kernel references
Detail
Exploit chain
none -- CWE-908/CWE-200 info leak, no write primitive.
Evidence (decisive lines)
BEFORE: bytes[22]=0x63 ('c' residue). AFTER: bytes[22]=0x00. 3/3 patched runs 0 leaked.
PoC changes
Authored from scratch: leak_dirent.c (getdirentries + raw dirent dump + leak detector), VERDICT.md, fix.diff (per-entry bzero in iso_uiodir + M_ZERO on outer kmalloc), manifest.json.
Verified recommended fix
Two hunks: (1) zero d_unused1/d_unused2/d_name padding per-entry in iso_uiodir before uiomove; (2) add M_ZERO to outer kmalloc in cd9660_readdir. Supersedes finding M_ZERO-only proposal (within-call d_name reuse leaves residue). Full diff in findings/poc/DF-0866/fix.diff.
Verdict
REPRODUCED. cd9660_readdir kmalloc without M_ZERO; struct dirent d_unused1/d_unused2/d_name padding never zeroed. iso_uiodir uiomoves full _DIRENT_DIRSIZ to userspace. Up to 7B leaked per entry. Baseline: 1B leaked (0x63 residue from prior entry). Patched: 0B.
No comments yet.