β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0866

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0866 Β· 18 files
FileTypeDescriptionSize
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
README.md readme claim summary, mechanism, repro steps, fix
↓ download 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)

  1. kmalloc(sizeof(*idp), M_TEMP, M_WAITOK) returns an M_TEMP slab chunk whose 1.5+ KiB of bytes (sizeof(struct isoreaddir)) are uninitialized.
  2. The readdir loop only sets d_ino, d_namlen, d_type and writes d_namlen+1 bytes of d_name per entry. Everything else in the struct dirent retains the slab's previous contents.
  3. iso_uiodir β†’ uiomove(_DIRENT_DIRSIZ(dp)) copies the rounded-up record (16 + name + 8-aligned padding) to userspace, leaking the uninitialized tail of d_name (and any non-zero bytes in d_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 in d_unused1 / d_unused2 / trailing d_name padding.
  • build.sh, run.sh β€” exact repro.
  • fix.diff β€” M_ZERO patch.
  • 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.
VERDICT.md verdict full narrative: mechanism, repro, fix-validation
↓ download raw

DF-0866 β€” Verdict

VERDICT: REPRODUCED (info leak β€” Low). FIX VALIDATED.

Mechanism (path:line)

  1. Trigger: an unprivileged user calls getdirentries(2) on a cd9660-mounted directory.
  2. Allocation: sys/vfs/isofs/cd9660/cd9660_vnops.c:447 idp = kmalloc(sizeof(*idp), M_TEMP, M_WAITOK); β€” no M_ZERO. struct isoreaddir (idp) embeds three struct iso_dirent (saveent, assocent, current), each holding a struct dirent de. The full struct is ~1.5 KiB; none of it is zeroed by this allocation.
  3. Per-entry reuse: the readdir loop (cd9660_vnops.c:482-577) overwrites only d_ino, d_namlen, d_type and writes d_namlen+1 bytes of d_name per entry. The remaining d_name bytes, plus d_unused1 (1 B at off 11) and d_unused2 (4 B at off 12-15), are never touched.
  4. Sink: iso_uiodir at cd9660_vnops.c:360 uiomove((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 of d_name and any non-zero bytes in d_unused1/d_unused2 ride 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) with mkisofs -J -r.
  • Mounted as vn0 via vnconfig + mount_cd9660.
  • Ran the unprivileged PoC leak_dirent /mnt/iso (as user maxx).
  • Baseline 6.5-DEVELOPMENT #0: the subdir entry (after cccccccc) shows byte 0x63 ('c') at offset 22 of its dirent β€” a non-zero residue from the prior entry's name sitting in the unzeroed d_name tail. In the very first run after a fresh slab allocation we also saw 28 leaked bytes per scan including the ASCII residue s_is_ (from this_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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 05:44:47 UTC 2026

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.